mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 10:26:08 +01:00 
			
		
		
		
	Compare commits
	
		
			111 Commits
		
	
	
		
			v0.5.3-bet
			...
			v0.9.0-bet
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | a5c9180533 | ||
|  | e86f1e0d05 | ||
|  | b6277049f3 | ||
|  | c831221cc4 | ||
|  | 577a168714 | ||
|  | b0bd27321a | ||
|  | 90c5348ca7 | ||
|  | 8e95b080da | ||
|  | 766a567a32 | ||
|  | 6d0218cb36 | ||
|  | d26170762b | ||
|  | b3209a9bbf | ||
|  | 61c2456cf6 | ||
|  | 1c6fc9029f | ||
|  | 5c91e38dfe | ||
|  | 07bf075894 | ||
|  | ddce5c959e | ||
|  | 3b9d1df05c | ||
|  | d239ef2956 | ||
|  | 7a865a9081 | ||
|  | 83d6c2970f | ||
|  | 8c7d159012 | ||
|  | d169f67901 | ||
|  | 982b723647 | ||
|  | 31d5ac05ff | ||
|  | 72d91d1571 | ||
|  | f4b57f4c57 | ||
|  | ee0833390a | ||
|  | 2acff07368 | ||
|  | bea1d24f07 | ||
|  | adc270c59f | ||
|  | 66064f7a94 | ||
|  | 1501fa8dbf | ||
|  | 60bba46d80 | ||
|  | 12c06ae97e | ||
|  | f0bea9cf71 | ||
|  | a555b6319c | ||
|  | 5dd93e4cdc | ||
|  | 3b4509d833 | ||
|  | 19308bbfbd | ||
|  | 4acc5432c3 | ||
|  | 08b8141fdf | ||
|  | e1200aa308 | ||
|  | 89666eb078 | ||
|  | d5605aa64d | ||
|  | 2582b016f9 | ||
|  | e8c52e25f0 | ||
|  | a149c6a105 | ||
|  | 131af9ab12 | ||
|  | aa2bbc6575 | ||
|  | 78e8c15786 | ||
|  | fda4146150 | ||
|  | ddc885066e | ||
|  | 08bc2afb49 | ||
|  | 1d0220b03d | ||
|  | 3033f7cc08 | ||
|  | 6b9ff47c88 | ||
|  | fd02c6102d | ||
|  | 30c712a6be | ||
|  | 3928c96640 | ||
|  | d86f655658 | ||
|  | abdad1c3ae | ||
|  | 9e5f1a0a87 | ||
|  | cdde6a4d8e | ||
|  | 8028b09351 | ||
|  | ebe66eaed9 | ||
|  | 5bce9a5f94 | ||
|  | dfd9927310 | ||
|  | 9bf1735bde | ||
|  | 2e8eeda5ab | ||
|  | 1cef0ce5f9 | ||
|  | 1efac99828 | ||
|  | 0e9473119e | ||
|  | 7bbfef7af3 | ||
|  | 5cb93509c1 | ||
|  | 89e89e04d8 | ||
|  | 72df0d8861 | ||
|  | 9910aebf45 | ||
|  | f9f8ecb2b1 | ||
|  | 438f7c5b0b | ||
|  | 4b1d1aba74 | ||
|  | 6dea73cfe2 | ||
|  | 58f5d0cf6e | ||
|  | 7b77e40514 | ||
|  | 660908c54b | ||
|  | e970564036 | ||
|  | b3038487f8 | ||
|  | cac98392a6 | ||
|  | dbd28377e3 | ||
|  | c76e4faf5d | ||
|  | e011b9ae63 | ||
|  | 7c74c77a2c | ||
|  | c2a2f195aa | ||
|  | 85d32c66f2 | ||
|  | 4e70cebf70 | ||
|  | 214d2e7659 | ||
|  | f380bb7f65 | ||
|  | 0a9a032daa | ||
|  | 23a2b58b24 | ||
|  | aee64b2522 | ||
|  | 02e07ec03a | ||
|  | 3d2dc8e699 | ||
|  | c84e15c9be | ||
|  | e18d0b9fd4 | ||
|  | 52817504d1 | ||
|  | a3b31fab54 | ||
|  | bc4aa3e40a | ||
|  | 873ea67e9c | ||
|  | 2c5115003b | ||
|  | e8ed913374 | ||
|  | 5bffba4e2f | 
| @@ -10,6 +10,7 @@ Trilium Notes is a hierarchical note taking application. Picture tells a thousan | |||||||
| * WYSIWYG (What You See Is What You Get) editing | * WYSIWYG (What You See Is What You Get) editing | ||||||
| * Fast and easy [navigation between notes](https://github.com/zadam/trilium/wiki/Note-navigation) | * Fast and easy [navigation between notes](https://github.com/zadam/trilium/wiki/Note-navigation) | ||||||
| * Seamless note versioning | * Seamless note versioning | ||||||
|  | * Note attributes can be used to tag/label notes as an alternative note organization and querying | ||||||
| * Can be deployed as web application and / or desktop application with offline access (electron based) | * Can be deployed as web application and / or desktop application with offline access (electron based) | ||||||
| * [Synchronization with](https://github.com/zadam/trilium/wiki/Synchronization) self-hosted sync server | * [Synchronization with](https://github.com/zadam/trilium/wiki/Synchronization) 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) | ||||||
| @@ -34,6 +35,7 @@ List of documentation pages: | |||||||
| * [Installation as webapp](https://github.com/zadam/trilium/wiki/Installation-as-webapp) | * [Installation as webapp](https://github.com/zadam/trilium/wiki/Installation-as-webapp) | ||||||
| * [Note navigation](https://github.com/zadam/trilium/wiki/Note-navigation) | * [Note navigation](https://github.com/zadam/trilium/wiki/Note-navigation) | ||||||
| * [Tree manipulation](https://github.com/zadam/trilium/wiki/Tree-manipulation) | * [Tree manipulation](https://github.com/zadam/trilium/wiki/Tree-manipulation) | ||||||
|  | * [Attributes](https://github.com/zadam/trilium/wiki/Attributes) | ||||||
| * [Links](https://github.com/zadam/trilium/wiki/Links) | * [Links](https://github.com/zadam/trilium/wiki/Links) | ||||||
| * [Cloning notes](https://github.com/zadam/trilium/wiki/Cloning-notes) | * [Cloning notes](https://github.com/zadam/trilium/wiki/Cloning-notes) | ||||||
| * [Protected notes](https://github.com/zadam/trilium/wiki/Protected-notes) | * [Protected notes](https://github.com/zadam/trilium/wiki/Protected-notes) | ||||||
|   | |||||||
| @@ -1,3 +1,7 @@ | |||||||
|  | [General] | ||||||
|  | # Instance name can be used to distinguish between different instances | ||||||
|  | instanceName= | ||||||
|  |  | ||||||
| [Network] | [Network] | ||||||
| port=8080 | port=8080 | ||||||
| # true for TLS/SSL/HTTPS (secure), false for HTTP (unsecure). | # true for TLS/SSL/HTTPS (secure), false for HTTP (unsecure). | ||||||
|   | |||||||
| @@ -0,0 +1 @@ | |||||||
|  | DROP INDEX IDX_attributes_noteId_name; | ||||||
							
								
								
									
										1
									
								
								db/migrations/0073__add_isDeleted_to_attributes.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								db/migrations/0073__add_isDeleted_to_attributes.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | ALTER TABLE attributes ADD COLUMN isDeleted INT NOT NULL DEFAULT 0; | ||||||
							
								
								
									
										1
									
								
								db/migrations/0074__add_position_to_attribute.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								db/migrations/0074__add_position_to_attribute.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | ALTER TABLE attributes ADD COLUMN position INT NOT NULL DEFAULT 0; | ||||||
							
								
								
									
										7
									
								
								db/migrations/0075__add_api_token.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								db/migrations/0075__add_api_token.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | |||||||
|  | CREATE TABLE IF NOT EXISTS "api_tokens" | ||||||
|  | ( | ||||||
|  |   apiTokenId TEXT PRIMARY KEY NOT NULL, | ||||||
|  |   token TEXT NOT NULL, | ||||||
|  |   dateCreated TEXT NOT NULL, | ||||||
|  |   isDeleted INT NOT NULL DEFAULT 0 | ||||||
|  | ); | ||||||
							
								
								
									
										1
									
								
								db/migrations/0076__add_attribute_name_index.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								db/migrations/0076__add_attribute_name_index.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | CREATE INDEX IDX_attributes_name_value ON attributes (name, value); | ||||||
							
								
								
									
										23
									
								
								db/migrations/0077__non_null_attribute_value.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								db/migrations/0077__non_null_attribute_value.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | |||||||
|  | UPDATE attributes SET value = '' WHERE value IS NULL; | ||||||
|  |  | ||||||
|  | CREATE TABLE IF NOT EXISTS "attributes_mig" | ||||||
|  | ( | ||||||
|  |   attributeId TEXT PRIMARY KEY NOT NULL, | ||||||
|  |   noteId TEXT NOT NULL, | ||||||
|  |   name TEXT NOT NULL, | ||||||
|  |   value TEXT NOT NULL DEFAULT '', | ||||||
|  |   position INT NOT NULL DEFAULT 0, | ||||||
|  |   dateCreated TEXT NOT NULL, | ||||||
|  |   dateModified TEXT NOT NULL, | ||||||
|  |   isDeleted INT NOT NULL | ||||||
|  | ); | ||||||
|  |  | ||||||
|  | INSERT INTO attributes_mig (attributeId, noteId, name, value, position, dateCreated, dateModified, isDeleted) | ||||||
|  |     SELECT attributeId, noteId, name, value, position, dateCreated, dateModified, isDeleted FROM attributes; | ||||||
|  |  | ||||||
|  | DROP TABLE attributes; | ||||||
|  |  | ||||||
|  | ALTER TABLE attributes_mig RENAME TO attributes; | ||||||
|  |  | ||||||
|  | CREATE INDEX IDX_attributes_noteId ON attributes (noteId); | ||||||
|  | CREATE INDEX IDX_attributes_name_value ON attributes (name, value); | ||||||
							
								
								
									
										1
									
								
								db/migrations/0078__javascript_type.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								db/migrations/0078__javascript_type.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | UPDATE notes SET mime = 'application/javascript;env=frontend' WHERE type = 'code' AND mime = 'application/javascript'; | ||||||
| @@ -85,8 +85,10 @@ CREATE TABLE IF NOT EXISTS "attributes" | |||||||
|   noteId TEXT NOT NULL, |   noteId TEXT NOT NULL, | ||||||
|   name TEXT NOT NULL, |   name TEXT NOT NULL, | ||||||
|   value TEXT, |   value TEXT, | ||||||
|  |   position INT NOT NULL DEFAULT 0, | ||||||
|   dateCreated TEXT NOT NULL, |   dateCreated TEXT NOT NULL, | ||||||
|   dateModified TEXT NOT NULL |   dateModified TEXT NOT NULL, | ||||||
|  |   isDeleted INT NOT NULL | ||||||
| ); | ); | ||||||
| CREATE UNIQUE INDEX `IDX_sync_entityName_entityId` ON `sync` ( | CREATE UNIQUE INDEX `IDX_sync_entityName_entityId` ON `sync` ( | ||||||
|   `entityName`, |   `entityName`, | ||||||
| @@ -118,4 +120,12 @@ CREATE INDEX IDX_note_images_noteId ON note_images (noteId); | |||||||
| CREATE INDEX IDX_note_images_imageId ON note_images (imageId); | CREATE INDEX IDX_note_images_imageId ON note_images (imageId); | ||||||
| CREATE INDEX IDX_note_images_noteId_imageId ON note_images (noteId, imageId); | CREATE INDEX IDX_note_images_noteId_imageId ON note_images (noteId, imageId); | ||||||
| CREATE INDEX IDX_attributes_noteId ON attributes (noteId); | CREATE INDEX IDX_attributes_noteId ON attributes (noteId); | ||||||
| CREATE UNIQUE INDEX IDX_attributes_noteId_name ON attributes (noteId, name); | CREATE INDEX IDX_attributes_name_value ON attributes (name, value); | ||||||
|  |  | ||||||
|  | CREATE TABLE IF NOT EXISTS "api_tokens" | ||||||
|  | ( | ||||||
|  |   apiTokenId TEXT PRIMARY KEY NOT NULL, | ||||||
|  |   token TEXT NOT NULL, | ||||||
|  |   dateCreated TEXT NOT NULL, | ||||||
|  |   isDeleted INT NOT NULL DEFAULT 0 | ||||||
|  | ); | ||||||
							
								
								
									
										24
									
								
								electron.js
									
									
									
									
									
								
							
							
						
						
									
										24
									
								
								electron.js
									
									
									
									
									
								
							| @@ -3,9 +3,11 @@ | |||||||
| const electron = require('electron'); | const electron = require('electron'); | ||||||
| const path = require('path'); | const path = require('path'); | ||||||
| const config = require('./src/services/config'); | const config = require('./src/services/config'); | ||||||
|  | const log = require('./src/services/log'); | ||||||
| const url = require("url"); | const url = require("url"); | ||||||
|  |  | ||||||
| const app = electron.app; | const app = electron.app; | ||||||
|  | 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')(); | ||||||
| @@ -13,6 +15,8 @@ require('electron-debug')(); | |||||||
| // Prevent window being garbage collected | // Prevent window being garbage collected | ||||||
| let mainWindow; | let mainWindow; | ||||||
|  |  | ||||||
|  | require('electron-dl')({ saveAs: true }); | ||||||
|  |  | ||||||
| function onClosed() { | function onClosed() { | ||||||
|     // Dereference the window |     // Dereference the window | ||||||
|     // For multiple windows store them in an array |     // For multiple windows store them in an array | ||||||
| @@ -67,6 +71,26 @@ app.on('activate', () => { | |||||||
|  |  | ||||||
| app.on('ready', () => { | app.on('ready', () => { | ||||||
|     mainWindow = createMainWindow(); |     mainWindow = createMainWindow(); | ||||||
|  |  | ||||||
|  |     const result = globalShortcut.register('CommandOrControl+Alt+P', async () => { | ||||||
|  |         const date_notes = require('./src/services/date_notes'); | ||||||
|  |         const utils = require('./src/services/utils'); | ||||||
|  |  | ||||||
|  |         const parentNoteId = await date_notes.getDateNoteId(utils.nowDate()); | ||||||
|  |  | ||||||
|  |         // window may be hidden / not in focus | ||||||
|  |         mainWindow.focus(); | ||||||
|  |  | ||||||
|  |         mainWindow.webContents.send('create-day-sub-note', parentNoteId); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     if (!result) { | ||||||
|  |         log.error("Could not register global shortcut CTRL+ALT+P"); | ||||||
|  |     } | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | app.on('will-quit', () => { | ||||||
|  |     globalShortcut.unregisterAll(); | ||||||
| }); | }); | ||||||
|  |  | ||||||
| require('./src/www'); | require('./src/www'); | ||||||
|   | |||||||
							
								
								
									
										148
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										148
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @@ -1,6 +1,6 @@ | |||||||
| { | { | ||||||
|   "name": "trilium", |   "name": "trilium", | ||||||
|   "version": "0.4.1", |   "version": "0.7.0-beta", | ||||||
|   "lockfileVersion": 1, |   "lockfileVersion": 1, | ||||||
|   "requires": true, |   "requires": true, | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
| @@ -3061,19 +3061,19 @@ | |||||||
|       "integrity": "sha1-zIcsFoiArjxxiXYv1f/ACJbJUYo=" |       "integrity": "sha1-zIcsFoiArjxxiXYv1f/ACJbJUYo=" | ||||||
|     }, |     }, | ||||||
|     "electron": { |     "electron": { | ||||||
|       "version": "1.8.2-beta.4", |       "version": "1.8.2", | ||||||
|       "resolved": "https://registry.npmjs.org/electron/-/electron-1.8.2-beta.4.tgz", |       "resolved": "https://registry.npmjs.org/electron/-/electron-1.8.2.tgz", | ||||||
|       "integrity": "sha1-GDayBO6s6dx3Bi7Ugg/bxsvZoZU=", |       "integrity": "sha512-0TV5Hy92g8ACnPn+PVol6a/2uk+khzmRtWxhah/FcKs6StCytm5hD14QqOdZxEdJN8HljXIVCayN/wJX+0wDiQ==", | ||||||
|       "requires": { |       "requires": { | ||||||
|         "@types/node": "8.5.9", |         "@types/node": "8.9.4", | ||||||
|         "electron-download": "3.3.0", |         "electron-download": "3.3.0", | ||||||
|         "extract-zip": "1.6.5" |         "extract-zip": "1.6.5" | ||||||
|       }, |       }, | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
|         "@types/node": { |         "@types/node": { | ||||||
|           "version": "8.5.9", |           "version": "8.9.4", | ||||||
|           "resolved": "https://registry.npmjs.org/@types/node/-/node-8.5.9.tgz", |           "resolved": "https://registry.npmjs.org/@types/node/-/node-8.9.4.tgz", | ||||||
|           "integrity": "sha512-s+c3AjymyAccTI4hcgNFK4mToH8l+hyPDhu4LIkn71lRy56FLijGu00fyLgldjM/846Pmk9N4KFUs2P8GDs0pA==" |           "integrity": "sha512-dSvD36qnQs78G1BPsrZFdPpvLgMW/dnvr5+nTW2csMs5TiP9MOXrjUbnMZOEwnIuBklXtn7b6TPA2Cuq07bDHA==" | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
| @@ -3206,6 +3206,16 @@ | |||||||
|         "electron-localshortcut": "3.1.0" |         "electron-localshortcut": "3.1.0" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "electron-dl": { | ||||||
|  |       "version": "1.11.0", | ||||||
|  |       "resolved": "https://registry.npmjs.org/electron-dl/-/electron-dl-1.11.0.tgz", | ||||||
|  |       "integrity": "sha512-iL9qHzzWOuL9bus+UT+P72SwrDQcFTV6QHqcbhwgqjCC9/K5jhdRzG0dIMB3TzYlk6rmApanPqh9DvWykwIH1Q==", | ||||||
|  |       "requires": { | ||||||
|  |         "ext-name": "5.0.0", | ||||||
|  |         "pupa": "1.0.0", | ||||||
|  |         "unused-filename": "1.0.0" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "electron-download": { |     "electron-download": { | ||||||
|       "version": "3.3.0", |       "version": "3.3.0", | ||||||
|       "resolved": "https://registry.npmjs.org/electron-download/-/electron-download-3.3.0.tgz", |       "resolved": "https://registry.npmjs.org/electron-download/-/electron-download-3.3.0.tgz", | ||||||
| @@ -3325,9 +3335,9 @@ | |||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "electron-packager": { |     "electron-packager": { | ||||||
|       "version": "10.1.1", |       "version": "11.0.1", | ||||||
|       "resolved": "https://registry.npmjs.org/electron-packager/-/electron-packager-10.1.1.tgz", |       "resolved": "https://registry.npmjs.org/electron-packager/-/electron-packager-11.0.1.tgz", | ||||||
|       "integrity": "sha1-MWp/ossf/CYz9YBcn8IJE8vAnZQ=", |       "integrity": "sha1-wtH/nsqBEL6evIGCbiqSHATRIA4=", | ||||||
|       "dev": true, |       "dev": true, | ||||||
|       "requires": { |       "requires": { | ||||||
|         "asar": "0.14.0", |         "asar": "0.14.0", | ||||||
| @@ -3343,13 +3353,19 @@ | |||||||
|         "pify": "3.0.0", |         "pify": "3.0.0", | ||||||
|         "plist": "2.1.0", |         "plist": "2.1.0", | ||||||
|         "pruner": "0.0.7", |         "pruner": "0.0.7", | ||||||
|         "rcedit": "0.9.0", |         "rcedit": "1.0.0", | ||||||
|         "resolve": "1.4.0", |         "resolve": "1.4.0", | ||||||
|         "sanitize-filename": "1.6.1", |         "sanitize-filename": "1.6.1", | ||||||
|         "semver": "5.4.1", |         "semver": "5.4.1", | ||||||
|         "yargs-parser": "8.1.0" |         "yargs-parser": "9.0.2" | ||||||
|       }, |       }, | ||||||
|       "dependencies": { |       "dependencies": { | ||||||
|  |         "camelcase": { | ||||||
|  |           "version": "4.1.0", | ||||||
|  |           "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", | ||||||
|  |           "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", | ||||||
|  |           "dev": true | ||||||
|  |         }, | ||||||
|         "electron-download": { |         "electron-download": { | ||||||
|           "version": "4.1.0", |           "version": "4.1.0", | ||||||
|           "resolved": "https://registry.npmjs.org/electron-download/-/electron-download-4.1.0.tgz", |           "resolved": "https://registry.npmjs.org/electron-download/-/electron-download-4.1.0.tgz", | ||||||
| @@ -3437,6 +3453,12 @@ | |||||||
|           "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", |           "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", | ||||||
|           "dev": true |           "dev": true | ||||||
|         }, |         }, | ||||||
|  |         "rcedit": { | ||||||
|  |           "version": "1.0.0", | ||||||
|  |           "resolved": "https://registry.npmjs.org/rcedit/-/rcedit-1.0.0.tgz", | ||||||
|  |           "integrity": "sha512-W7DNa34x/3OgWyDHsI172AG/Lr/lZ+PkavFkHj0QhhkBRcV9QTmRJE1tDKrWkx8XHPSBsmZkNv9OKue6pncLFQ==", | ||||||
|  |           "dev": true | ||||||
|  |         }, | ||||||
|         "sumchecker": { |         "sumchecker": { | ||||||
|           "version": "2.0.2", |           "version": "2.0.2", | ||||||
|           "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-2.0.2.tgz", |           "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-2.0.2.tgz", | ||||||
| @@ -3456,20 +3478,29 @@ | |||||||
|               } |               } | ||||||
|             } |             } | ||||||
|           } |           } | ||||||
|  |         }, | ||||||
|  |         "yargs-parser": { | ||||||
|  |           "version": "9.0.2", | ||||||
|  |           "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-9.0.2.tgz", | ||||||
|  |           "integrity": "sha1-nM9qQ0YP5O1Aqbto9I1DuKaMwHc=", | ||||||
|  |           "dev": true, | ||||||
|  |           "requires": { | ||||||
|  |             "camelcase": "4.1.0" | ||||||
|  |           } | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "electron-prebuilt-compile": { |     "electron-prebuilt-compile": { | ||||||
|       "version": "1.8.2-beta.4", |       "version": "1.8.2", | ||||||
|       "resolved": "https://registry.npmjs.org/electron-prebuilt-compile/-/electron-prebuilt-compile-1.8.2-beta.4.tgz", |       "resolved": "https://registry.npmjs.org/electron-prebuilt-compile/-/electron-prebuilt-compile-1.8.2.tgz", | ||||||
|       "integrity": "sha512-whVdRgFEDovWSFrAsbMXIiush6RQ8IV3XhYdL59zShck4U1eXGmdkaBCy+2tlkGmUGr0fRu+S4FpUx2ebBkRhQ==", |       "integrity": "sha512-wiDVjy8S0PA/K/TUM0lw5gzZ+SmyVVGQ0qt9iFYXHJc6t8TzDXFY3DsoK37H3A7nWnkvXvoPdpJ5/h9KbTMoAw==", | ||||||
|       "dev": true, |       "dev": true, | ||||||
|       "requires": { |       "requires": { | ||||||
|         "babel-plugin-array-includes": "2.0.3", |         "babel-plugin-array-includes": "2.0.3", | ||||||
|         "babel-plugin-transform-async-to-generator": "6.24.1", |         "babel-plugin-transform-async-to-generator": "6.24.1", | ||||||
|         "babel-preset-es2016-node5": "1.1.2", |         "babel-preset-es2016-node5": "1.1.2", | ||||||
|         "babel-preset-react": "6.24.1", |         "babel-preset-react": "6.24.1", | ||||||
|         "electron": "1.8.2-beta.4", |         "electron": "1.8.2", | ||||||
|         "electron-compile": "6.4.2", |         "electron-compile": "6.4.2", | ||||||
|         "electron-compilers": "5.9.0", |         "electron-compilers": "5.9.0", | ||||||
|         "yargs": "6.6.0" |         "yargs": "6.6.0" | ||||||
| @@ -4353,6 +4384,23 @@ | |||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "ext-list": { | ||||||
|  |       "version": "2.2.2", | ||||||
|  |       "resolved": "https://registry.npmjs.org/ext-list/-/ext-list-2.2.2.tgz", | ||||||
|  |       "integrity": "sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA==", | ||||||
|  |       "requires": { | ||||||
|  |         "mime-db": "1.30.0" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     "ext-name": { | ||||||
|  |       "version": "5.0.0", | ||||||
|  |       "resolved": "https://registry.npmjs.org/ext-name/-/ext-name-5.0.0.tgz", | ||||||
|  |       "integrity": "sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ==", | ||||||
|  |       "requires": { | ||||||
|  |         "ext-list": "2.2.2", | ||||||
|  |         "sort-keys-length": "1.0.1" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "extend": { |     "extend": { | ||||||
|       "version": "3.0.1", |       "version": "3.0.1", | ||||||
|       "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", |       "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", | ||||||
| @@ -5901,8 +5949,7 @@ | |||||||
|     "is-plain-obj": { |     "is-plain-obj": { | ||||||
|       "version": "1.1.0", |       "version": "1.1.0", | ||||||
|       "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", |       "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", | ||||||
|       "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", |       "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=" | ||||||
|       "dev": true |  | ||||||
|     }, |     }, | ||||||
|     "is-png": { |     "is-png": { | ||||||
|       "version": "1.1.0", |       "version": "1.1.0", | ||||||
| @@ -7131,6 +7178,11 @@ | |||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "modify-filename": { | ||||||
|  |       "version": "1.1.0", | ||||||
|  |       "resolved": "https://registry.npmjs.org/modify-filename/-/modify-filename-1.1.0.tgz", | ||||||
|  |       "integrity": "sha1-mi3sg4Bvuy2XXyK+7IWcoms5OqE=" | ||||||
|  |     }, | ||||||
|     "moment": { |     "moment": { | ||||||
|       "version": "2.20.1", |       "version": "2.20.1", | ||||||
|       "resolved": "https://registry.npmjs.org/moment/-/moment-2.20.1.tgz", |       "resolved": "https://registry.npmjs.org/moment/-/moment-2.20.1.tgz", | ||||||
| @@ -7543,6 +7595,11 @@ | |||||||
|         "mimic-fn": "1.1.0" |         "mimic-fn": "1.1.0" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "open": { | ||||||
|  |       "version": "0.0.5", | ||||||
|  |       "resolved": "https://registry.npmjs.org/open/-/open-0.0.5.tgz", | ||||||
|  |       "integrity": "sha1-QsPhjslUZra/DcQvOilFw/DK2Pw=" | ||||||
|  |     }, | ||||||
|     "optimist": { |     "optimist": { | ||||||
|       "version": "0.6.1", |       "version": "0.6.1", | ||||||
|       "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", |       "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", | ||||||
| @@ -8370,6 +8427,11 @@ | |||||||
|       "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", |       "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", | ||||||
|       "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" |       "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" | ||||||
|     }, |     }, | ||||||
|  |     "pupa": { | ||||||
|  |       "version": "1.0.0", | ||||||
|  |       "resolved": "https://registry.npmjs.org/pupa/-/pupa-1.0.0.tgz", | ||||||
|  |       "integrity": "sha1-mpVopa9+ZXuEYqbp1TKHQ1YM7/Y=" | ||||||
|  |     }, | ||||||
|     "q": { |     "q": { | ||||||
|       "version": "1.5.1", |       "version": "1.5.1", | ||||||
|       "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", |       "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", | ||||||
| @@ -8472,12 +8534,6 @@ | |||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "rcedit": { |  | ||||||
|       "version": "0.9.0", |  | ||||||
|       "resolved": "https://registry.npmjs.org/rcedit/-/rcedit-0.9.0.tgz", |  | ||||||
|       "integrity": "sha1-ORDfVzRTmeKwMl9KUZAH+J5V7xw=", |  | ||||||
|       "dev": true |  | ||||||
|     }, |  | ||||||
|     "read-all-stream": { |     "read-all-stream": { | ||||||
|       "version": "3.1.0", |       "version": "3.1.0", | ||||||
|       "resolved": "https://registry.npmjs.org/read-all-stream/-/read-all-stream-3.1.0.tgz", |       "resolved": "https://registry.npmjs.org/read-all-stream/-/read-all-stream-3.1.0.tgz", | ||||||
| @@ -9171,11 +9227,18 @@ | |||||||
|       "version": "1.1.2", |       "version": "1.1.2", | ||||||
|       "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", |       "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", | ||||||
|       "integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=", |       "integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=", | ||||||
|       "dev": true, |  | ||||||
|       "requires": { |       "requires": { | ||||||
|         "is-plain-obj": "1.1.0" |         "is-plain-obj": "1.1.0" | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|  |     "sort-keys-length": { | ||||||
|  |       "version": "1.0.1", | ||||||
|  |       "resolved": "https://registry.npmjs.org/sort-keys-length/-/sort-keys-length-1.0.1.tgz", | ||||||
|  |       "integrity": "sha1-nLb09OnkgVWmqgZx7dM2/xR5oYg=", | ||||||
|  |       "requires": { | ||||||
|  |         "sort-keys": "1.1.2" | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "source-map": { |     "source-map": { | ||||||
|       "version": "0.5.7", |       "version": "0.5.7", | ||||||
|       "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", |       "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", | ||||||
| @@ -10948,6 +11011,22 @@ | |||||||
|       "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", |       "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", | ||||||
|       "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" |       "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" | ||||||
|     }, |     }, | ||||||
|  |     "unused-filename": { | ||||||
|  |       "version": "1.0.0", | ||||||
|  |       "resolved": "https://registry.npmjs.org/unused-filename/-/unused-filename-1.0.0.tgz", | ||||||
|  |       "integrity": "sha1-00CID3GuIRXrqhMlvvBcxmhEacY=", | ||||||
|  |       "requires": { | ||||||
|  |         "modify-filename": "1.1.0", | ||||||
|  |         "path-exists": "3.0.0" | ||||||
|  |       }, | ||||||
|  |       "dependencies": { | ||||||
|  |         "path-exists": { | ||||||
|  |           "version": "3.0.0", | ||||||
|  |           "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", | ||||||
|  |           "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     "unzip-response": { |     "unzip-response": { | ||||||
|       "version": "2.0.1", |       "version": "2.0.1", | ||||||
|       "resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-2.0.1.tgz", |       "resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-2.0.1.tgz", | ||||||
| @@ -11694,23 +11773,6 @@ | |||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "yargs-parser": { |  | ||||||
|       "version": "8.1.0", |  | ||||||
|       "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-8.1.0.tgz", |  | ||||||
|       "integrity": "sha512-yP+6QqN8BmrgW2ggLtTbdrOyBNSI7zBa4IykmiV5R1wl1JWNxQvWhMfMdmzIYtKU7oP3OOInY/tl2ov3BDjnJQ==", |  | ||||||
|       "dev": true, |  | ||||||
|       "requires": { |  | ||||||
|         "camelcase": "4.1.0" |  | ||||||
|       }, |  | ||||||
|       "dependencies": { |  | ||||||
|         "camelcase": { |  | ||||||
|           "version": "4.1.0", |  | ||||||
|           "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", |  | ||||||
|           "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", |  | ||||||
|           "dev": true |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     }, |  | ||||||
|     "yauzl": { |     "yauzl": { | ||||||
|       "version": "2.4.1", |       "version": "2.4.1", | ||||||
|       "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.4.1.tgz", |       "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.4.1.tgz", | ||||||
|   | |||||||
							
								
								
									
										11
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								package.json
									
									
									
									
									
								
							| @@ -1,7 +1,7 @@ | |||||||
| { | { | ||||||
|   "name": "trilium", |   "name": "trilium", | ||||||
|   "description": "Trilium Notes", |   "description": "Trilium Notes", | ||||||
|   "version": "0.5.3-beta", |   "version": "0.9.0-beta", | ||||||
|   "license": "AGPL-3.0-only", |   "license": "AGPL-3.0-only", | ||||||
|   "main": "electron.js", |   "main": "electron.js", | ||||||
|   "repository": { |   "repository": { | ||||||
| @@ -27,8 +27,9 @@ | |||||||
|     "debug": "~3.1.0", |     "debug": "~3.1.0", | ||||||
|     "devtron": "^1.4.0", |     "devtron": "^1.4.0", | ||||||
|     "ejs": "~2.5.7", |     "ejs": "~2.5.7", | ||||||
|     "electron": "^1.8.2-beta.4", |     "electron": "^1.8.2", | ||||||
|     "electron-debug": "^1.5.0", |     "electron-debug": "^1.5.0", | ||||||
|  |     "electron-dl": "^1.11.0", | ||||||
|     "electron-in-page-search": "^1.2.4", |     "electron-in-page-search": "^1.2.4", | ||||||
|     "express": "~4.16.2", |     "express": "~4.16.2", | ||||||
|     "express-promise-wrap": "^0.2.2", |     "express-promise-wrap": "^0.2.2", | ||||||
| @@ -45,6 +46,7 @@ | |||||||
|     "jimp": "^0.2.28", |     "jimp": "^0.2.28", | ||||||
|     "moment": "^2.20.1", |     "moment": "^2.20.1", | ||||||
|     "multer": "^1.3.0", |     "multer": "^1.3.0", | ||||||
|  |     "open": "0.0.5", | ||||||
|     "rand-token": "^0.4.0", |     "rand-token": "^0.4.0", | ||||||
|     "request": "^2.83.0", |     "request": "^2.83.0", | ||||||
|     "request-promise": "^4.2.2", |     "request-promise": "^4.2.2", | ||||||
| @@ -55,13 +57,14 @@ | |||||||
|     "session-file-store": "^1.1.2", |     "session-file-store": "^1.1.2", | ||||||
|     "simple-node-logger": "^0.93.30", |     "simple-node-logger": "^0.93.30", | ||||||
|     "sqlite": "^2.9.0", |     "sqlite": "^2.9.0", | ||||||
|  |     "tar-stream": "^1.5.5", | ||||||
|     "unescape": "^1.0.1", |     "unescape": "^1.0.1", | ||||||
|     "ws": "^3.3.2" |     "ws": "^3.3.2" | ||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|     "electron-compile": "^6.4.2", |     "electron-compile": "^6.4.2", | ||||||
|     "electron-packager": "^10.1.1", |     "electron-packager": "^11.0.1", | ||||||
|     "electron-prebuilt-compile": "1.8.2-beta.4", |     "electron-prebuilt-compile": "1.8.2", | ||||||
|     "electron-rebuild": "^1.7.3", |     "electron-rebuild": "^1.7.3", | ||||||
|     "tape": "^4.8.0", |     "tape": "^4.8.0", | ||||||
|     "xo": "^0.18.0" |     "xo": "^0.18.0" | ||||||
|   | |||||||
| @@ -73,7 +73,7 @@ require('./services/backup'); | |||||||
| // trigger consistency checks timer | // trigger consistency checks timer | ||||||
| require('./services/consistency_checks'); | require('./services/consistency_checks'); | ||||||
|  |  | ||||||
| require('./plugins/reddit'); | require('./services/scheduler'); | ||||||
|  |  | ||||||
| module.exports = { | module.exports = { | ||||||
|     app, |     app, | ||||||
|   | |||||||
| @@ -23,10 +23,53 @@ class Note extends Entity { | |||||||
|         return this.type === "code" && this.mime === "application/json"; |         return this.type === "code" && this.mime === "application/json"; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     async getAttributes() { |     isJavaScript() { | ||||||
|         return this.repository.getEntities("SELECT * FROM attributes WHERE noteId = ?", [this.noteId]); |         return (this.type === "code" || this.type === "file") | ||||||
|  |             && (this.mime.startsWith("application/javascript") || this.mime === "application/x-javascript"); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     isHtml() { | ||||||
|  |         return (this.type === "code" || this.type === "file") && this.mime === "text/html"; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     getScriptEnv() { | ||||||
|  |         if (this.isHtml() || (this.isJavaScript() && this.mime.endsWith('env=frontend'))) { | ||||||
|  |             return "frontend"; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (this.type === 'render') { | ||||||
|  |             return "frontend"; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         if (this.isJavaScript() && this.mime.endsWith('env=backend')) { | ||||||
|  |             return "backend"; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return null; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async getAttributes() { | ||||||
|  |         return this.repository.getEntities("SELECT * FROM attributes WHERE noteId = ? AND isDeleted = 0", [this.noteId]); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // WARNING: this doesn't take into account the possibility to have multi-valued attributes! | ||||||
|  |     async getAttributeMap() { | ||||||
|  |         const map = {}; | ||||||
|  |  | ||||||
|  |         for (const attr of await this.getAttributes()) { | ||||||
|  |             map[attr.name] = attr.value; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return map; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async hasAttribute(name) { | ||||||
|  |         const map = await this.getAttributeMap(); | ||||||
|  |  | ||||||
|  |         return map.hasOwnProperty(name); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // WARNING: this doesn't take into account the possibility to have multi-valued attributes! | ||||||
|     async getAttribute(name) { |     async getAttribute(name) { | ||||||
|         return this.repository.getEntity("SELECT * FROM attributes WHERE noteId = ? AND name = ?", [this.noteId, name]); |         return this.repository.getEntity("SELECT * FROM attributes WHERE noteId = ? AND name = ?", [this.noteId, name]); | ||||||
|     } |     } | ||||||
| @@ -39,6 +82,49 @@ class Note extends Entity { | |||||||
|         return this.repository.getEntities("SELECT * FROM note_tree WHERE isDeleted = 0 AND noteId = ?", [this.noteId]); |         return this.repository.getEntities("SELECT * FROM note_tree WHERE isDeleted = 0 AND noteId = ?", [this.noteId]); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     async getChild(name) { | ||||||
|  |         return this.repository.getEntity(` | ||||||
|  |           SELECT notes.*  | ||||||
|  |           FROM note_tree  | ||||||
|  |             JOIN notes USING(noteId)  | ||||||
|  |           WHERE notes.isDeleted = 0 | ||||||
|  |                 AND note_tree.isDeleted = 0 | ||||||
|  |                 AND note_tree.parentNoteId = ? | ||||||
|  |                 AND notes.title = ?`, [this.noteId, name]); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async getChildren() { | ||||||
|  |         return this.repository.getEntities(` | ||||||
|  |           SELECT notes.*  | ||||||
|  |           FROM note_tree  | ||||||
|  |             JOIN notes USING(noteId)  | ||||||
|  |           WHERE notes.isDeleted = 0 | ||||||
|  |                 AND note_tree.isDeleted = 0 | ||||||
|  |                 AND note_tree.parentNoteId = ? | ||||||
|  |           ORDER BY note_tree.notePosition`, [this.noteId]); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async getParents() { | ||||||
|  |         return this.repository.getEntities(` | ||||||
|  |           SELECT parent_notes.*  | ||||||
|  |           FROM  | ||||||
|  |             note_tree AS child_tree  | ||||||
|  |             JOIN notes AS parent_notes ON parent_notes.noteId = child_tree.parentNoteId  | ||||||
|  |           WHERE child_tree.noteId = ? | ||||||
|  |                 AND child_tree.isDeleted = 0 | ||||||
|  |                 AND parent_notes.isDeleted = 0`, [this.noteId]); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async getNoteTree() { | ||||||
|  |         return this.repository.getEntities(` | ||||||
|  |           SELECT note_tree.*  | ||||||
|  |           FROM note_tree  | ||||||
|  |             JOIN notes USING(noteId)  | ||||||
|  |           WHERE notes.isDeleted = 0 | ||||||
|  |                 AND note_tree.isDeleted = 0 | ||||||
|  |                 AND note_tree.noteId = ?`, [this.noteId]); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     beforeSaving() { |     beforeSaving() { | ||||||
|         this.content = JSON.stringify(this.jsonContent, null, '\t'); |         this.content = JSON.stringify(this.jsonContent, null, '\t'); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,143 +0,0 @@ | |||||||
| "use strict"; |  | ||||||
|  |  | ||||||
| const sql = require('../services/sql'); |  | ||||||
| const notes = require('../services/notes'); |  | ||||||
| const axios = require('axios'); |  | ||||||
| const log = require('../services/log'); |  | ||||||
| const utils = require('../services/utils'); |  | ||||||
| const unescape = require('unescape'); |  | ||||||
| const attributes = require('../services/attributes'); |  | ||||||
| const sync_mutex = require('../services/sync_mutex'); |  | ||||||
| const config = require('../services/config'); |  | ||||||
| const date_notes = require('../services/date_notes'); |  | ||||||
|  |  | ||||||
| // "reddit" date note is subnote of date note which contains all reddit comments from that date |  | ||||||
| const REDDIT_DATE_ATTRIBUTE = 'reddit_date_note'; |  | ||||||
|  |  | ||||||
| async function createNote(parentNoteId, noteTitle, noteText) { |  | ||||||
|     return (await notes.createNewNote(parentNoteId, { |  | ||||||
|         title: noteTitle, |  | ||||||
|         content: noteText, |  | ||||||
|         target: 'into', |  | ||||||
|         isProtected: false |  | ||||||
|     })).noteId; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function redditId(kind, id) { |  | ||||||
|     return kind + "_" + id; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| async function getDateNoteIdForReddit(dateTimeStr, rootNoteId) { |  | ||||||
|     const dateStr = dateTimeStr.substr(0, 10); |  | ||||||
|  |  | ||||||
|     let redditDateNoteId = await attributes.getNoteIdWithAttribute(REDDIT_DATE_ATTRIBUTE, dateStr); |  | ||||||
|  |  | ||||||
|     if (!redditDateNoteId) { |  | ||||||
|         const dateNoteId = await date_notes.getDateNoteId(dateTimeStr, rootNoteId); |  | ||||||
|  |  | ||||||
|         redditDateNoteId = await createNote(dateNoteId, "Reddit"); |  | ||||||
|  |  | ||||||
|         await attributes.createAttribute(redditDateNoteId, REDDIT_DATE_ATTRIBUTE, dateStr); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return redditDateNoteId; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| async function importComments(rootNoteId, accountName, afterId = null) { |  | ||||||
|     let url = `https://www.reddit.com/user/${accountName}.json`; |  | ||||||
|  |  | ||||||
|     if (afterId) { |  | ||||||
|         url += "?after=" + afterId; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const response = await axios.get(url); |  | ||||||
|     const listing = response.data; |  | ||||||
|  |  | ||||||
|     if (listing.kind !== 'Listing') { |  | ||||||
|         log.info(`Reddit: Unknown object kind ${listing.kind}`); |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const children = listing.data.children; |  | ||||||
|  |  | ||||||
|     let importedComments = 0; |  | ||||||
|  |  | ||||||
|     for (const child of children) { |  | ||||||
|         const comment = child.data; |  | ||||||
|  |  | ||||||
|         let commentNoteId = await attributes.getNoteIdWithAttribute('reddit_id', redditId(child.kind, comment.id)); |  | ||||||
|  |  | ||||||
|         if (commentNoteId) { |  | ||||||
|             continue; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         const dateTimeStr = utils.dateStr(new Date(comment.created_utc * 1000)); |  | ||||||
|  |  | ||||||
|         const permaLink = 'https://reddit.com' + comment.permalink; |  | ||||||
|  |  | ||||||
|         const noteText = |  | ||||||
| `<p><a href="${permaLink}">${permaLink}</a></p> |  | ||||||
| <p>author: <a href="https://reddit.com/u/${comment.author}">${comment.author}</a>,  |  | ||||||
| subreddit: <a href="https://reddit.com/r/${comment.subreddit}">${comment.subreddit}</a>,  |  | ||||||
| karma: ${comment.score}, created at ${dateTimeStr}</p><p></p>` |  | ||||||
|             + unescape(comment.body_html); |  | ||||||
|  |  | ||||||
|         let parentNoteId = await getDateNoteIdForReddit(dateTimeStr, rootNoteId); |  | ||||||
|  |  | ||||||
|         await sql.doInTransaction(async () => { |  | ||||||
|             commentNoteId = await createNote(parentNoteId, comment.link_title, noteText); |  | ||||||
|  |  | ||||||
|             log.info("Reddit: Imported comment to note " + commentNoteId); |  | ||||||
|             importedComments++; |  | ||||||
|  |  | ||||||
|             await attributes.createAttribute(commentNoteId, "reddit_kind", child.kind); |  | ||||||
|             await attributes.createAttribute(commentNoteId, "reddit_id", redditId(child.kind, comment.id)); |  | ||||||
|             await attributes.createAttribute(commentNoteId, "reddit_created_utc", comment.created_utc); |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     // if there have been no imported comments on this page, there shouldn't be any to import |  | ||||||
|     // on the next page since those are older |  | ||||||
|     if (listing.data.after && importedComments > 0) { |  | ||||||
|         importedComments += await importComments(rootNoteId, accountName, listing.data.after); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return importedComments; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| let redditAccounts = []; |  | ||||||
|  |  | ||||||
| async function runImport() { |  | ||||||
|     const rootNoteId = await date_notes.getRootNoteId(); |  | ||||||
|  |  | ||||||
|     // technically mutex shouldn't be necessary but we want to avoid doing potentially expensive import |  | ||||||
|     // concurrently with sync |  | ||||||
|     await sync_mutex.doExclusively(async () => { |  | ||||||
|         let importedComments = 0; |  | ||||||
|  |  | ||||||
|         for (const account of redditAccounts) { |  | ||||||
|             importedComments += await importComments(rootNoteId, account); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         log.info(`Reddit: Imported ${importedComments} comments.`); |  | ||||||
|     }); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| sql.dbReady.then(async () => { |  | ||||||
|     if (!config['Reddit'] || config['Reddit']['enabled'] !== true) { |  | ||||||
|         return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const redditAccountsStr = config['Reddit']['accounts']; |  | ||||||
|  |  | ||||||
|     if (!redditAccountsStr) { |  | ||||||
|         log.info("Reddit: No reddit accounts defined in option 'reddit_accounts'"); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     redditAccounts = redditAccountsStr.split(",").map(s => s.trim()); |  | ||||||
|  |  | ||||||
|     const pollingIntervalInSeconds = config['Reddit']['pollingIntervalInSeconds'] || (4 * 3600); |  | ||||||
|  |  | ||||||
|     setInterval(runImport, pollingIntervalInSeconds * 1000); |  | ||||||
|     setTimeout(runImport, 10000); // 10 seconds after startup - intentionally after initial sync |  | ||||||
| }); |  | ||||||
							
								
								
									
										
											BIN
										
									
								
								src/public/images/icons/paperclip.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/public/images/icons/paperclip.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 358 B | 
							
								
								
									
										
											BIN
										
									
								
								src/public/images/icons/play.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/public/images/icons/play.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 252 B | 
							
								
								
									
										62
									
								
								src/public/javascripts/api.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								src/public/javascripts/api.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,62 @@ | |||||||
|  | function ScriptContext(startNote, allNotes) { | ||||||
|  |     return { | ||||||
|  |         modules: {}, | ||||||
|  |         notes: toObject(allNotes, note => [note.noteId, note]), | ||||||
|  |         apis: toObject(allNotes, note => [note.noteId, ScriptApi(startNote, note)]), | ||||||
|  |     }; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function ScriptApi(startNote, currentNote) { | ||||||
|  |     const $pluginButtons = $("#plugin-buttons"); | ||||||
|  |  | ||||||
|  |     async function activateNote(notePath) { | ||||||
|  |         await noteTree.activateNode(notePath); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     function addButtonToToolbar(buttonId, button) { | ||||||
|  |         $("#" + buttonId).remove(); | ||||||
|  |  | ||||||
|  |         button.attr('id', buttonId); | ||||||
|  |  | ||||||
|  |         $pluginButtons.append(button); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     function prepareParams(params) { | ||||||
|  |         if (!params) { | ||||||
|  |             return params; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return params.map(p => { | ||||||
|  |             if (typeof p === "function") { | ||||||
|  |                 return "!@#Function: " + p.toString(); | ||||||
|  |             } | ||||||
|  |             else { | ||||||
|  |                 return p; | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async function runOnServer(script, params = []) { | ||||||
|  |         if (typeof script === "function") { | ||||||
|  |             script = script.toString(); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         const ret = await server.post('script/exec', { | ||||||
|  |             script: script, | ||||||
|  |             params: prepareParams(params), | ||||||
|  |             startNoteId: startNote.noteId, | ||||||
|  |             currentNoteId: currentNote.noteId | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         return ret.executionResult; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return { | ||||||
|  |         startNote: startNote, | ||||||
|  |         currentNote: currentNote, | ||||||
|  |         addButtonToToolbar, | ||||||
|  |         activateNote, | ||||||
|  |         getInstanceName: noteTree.getInstanceName, | ||||||
|  |         runOnServer | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -1,7 +1,7 @@ | |||||||
| "use strict"; | "use strict"; | ||||||
|  |  | ||||||
| const contextMenu = (function() { | const contextMenu = (function() { | ||||||
|     const treeEl = $("#tree"); |     const $tree = $("#tree"); | ||||||
|  |  | ||||||
|     let clipboardIds = []; |     let clipboardIds = []; | ||||||
|     let clipboardMode = null; |     let clipboardMode = null; | ||||||
| @@ -85,16 +85,19 @@ const contextMenu = (function() { | |||||||
|             {title: "Paste into <kbd>Ctrl+V</kbd>", cmd: "pasteInto", uiIcon: "ui-icon-clipboard"}, |             {title: "Paste into <kbd>Ctrl+V</kbd>", cmd: "pasteInto", uiIcon: "ui-icon-clipboard"}, | ||||||
|             {title: "Paste after", cmd: "pasteAfter", uiIcon: "ui-icon-clipboard"}, |             {title: "Paste after", cmd: "pasteAfter", uiIcon: "ui-icon-clipboard"}, | ||||||
|             {title: "----"}, |             {title: "----"}, | ||||||
|             {title: "Collapse sub-tree <kbd>Alt+-</kbd>", cmd: "collapse-sub-tree", uiIcon: "ui-icon-minus"}, |             {title: "Export sub-tree", cmd: "exportSubTree", uiIcon: " ui-icon-arrowthick-1-ne"}, | ||||||
|             {title: "Force note sync", cmd: "force-note-sync", uiIcon: "ui-icon-refresh"}, |             {title: "Import sub-tree into", cmd: "importSubTree", uiIcon: "ui-icon-arrowthick-1-sw"}, | ||||||
|             {title: "Sort alphabetically <kbd>Alt+S</kbd>", cmd: "sort-alphabetically", uiIcon: " ui-icon-arrowthick-2-n-s"} |             {title: "----"}, | ||||||
|  |             {title: "Collapse sub-tree <kbd>Alt+-</kbd>", cmd: "collapseSubTree", uiIcon: "ui-icon-minus"}, | ||||||
|  |             {title: "Force note sync", cmd: "forceNoteSync", uiIcon: "ui-icon-refresh"}, | ||||||
|  |             {title: "Sort alphabetically <kbd>Alt+S</kbd>", cmd: "sortAlphabetically", uiIcon: " ui-icon-arrowthick-2-n-s"} | ||||||
|  |  | ||||||
|         ], |         ], | ||||||
|         beforeOpen: (event, ui) => { |         beforeOpen: (event, ui) => { | ||||||
|             const node = $.ui.fancytree.getNode(ui.target); |             const node = $.ui.fancytree.getNode(ui.target); | ||||||
|             // Modify menu entries depending on node status |             // Modify menu entries depending on node status | ||||||
|             treeEl.contextmenu("enableEntry", "pasteAfter", clipboardIds.length > 0); |             $tree.contextmenu("enableEntry", "pasteAfter", clipboardIds.length > 0); | ||||||
|             treeEl.contextmenu("enableEntry", "pasteInto", clipboardIds.length > 0); |             $tree.contextmenu("enableEntry", "pasteInto", clipboardIds.length > 0); | ||||||
|  |  | ||||||
|             // Activate node on right-click |             // Activate node on right-click | ||||||
|             node.setActive(); |             node.setActive(); | ||||||
| @@ -139,13 +142,19 @@ const contextMenu = (function() { | |||||||
|             else if (ui.cmd === "delete") { |             else if (ui.cmd === "delete") { | ||||||
|                 treeChanges.deleteNodes(noteTree.getSelectedNodes(true)); |                 treeChanges.deleteNodes(noteTree.getSelectedNodes(true)); | ||||||
|             } |             } | ||||||
|             else if (ui.cmd === "collapse-sub-tree") { |             else if (ui.cmd === "exportSubTree") { | ||||||
|  |                 exportSubTree(node.data.noteId); | ||||||
|  |             } | ||||||
|  |             else if (ui.cmd === "importSubTree") { | ||||||
|  |                 importSubTree(node.data.noteId); | ||||||
|  |             } | ||||||
|  |             else if (ui.cmd === "collapseSubTree") { | ||||||
|                 noteTree.collapseTree(node); |                 noteTree.collapseTree(node); | ||||||
|             } |             } | ||||||
|             else if (ui.cmd === "force-note-sync") { |             else if (ui.cmd === "forceNoteSync") { | ||||||
|                 forceNoteSync(node.data.noteId); |                 forceNoteSync(node.data.noteId); | ||||||
|             } |             } | ||||||
|             else if (ui.cmd === "sort-alphabetically") { |             else if (ui.cmd === "sortAlphabetically") { | ||||||
|                 noteTree.sortAlphabetically(node.data.noteId); |                 noteTree.sortAlphabetically(node.data.noteId); | ||||||
|             } |             } | ||||||
|             else { |             else { | ||||||
|   | |||||||
| @@ -1,18 +1,18 @@ | |||||||
| "use strict"; | "use strict"; | ||||||
|  |  | ||||||
| const addLink = (function() { | const addLink = (function() { | ||||||
|     const dialogEl = $("#add-link-dialog"); |     const $dialog = $("#add-link-dialog"); | ||||||
|     const formEl = $("#add-link-form"); |     const $form = $("#add-link-form"); | ||||||
|     const autoCompleteEl = $("#note-autocomplete"); |     const $autoComplete = $("#note-autocomplete"); | ||||||
|     const linkTitleEl = $("#link-title"); |     const $linkTitle = $("#link-title"); | ||||||
|     const clonePrefixEl = $("#clone-prefix"); |     const $clonePrefix = $("#clone-prefix"); | ||||||
|     const linkTitleFormGroup = $("#add-link-title-form-group"); |     const $linkTitleFormGroup = $("#add-link-title-form-group"); | ||||||
|     const prefixFormGroup = $("#add-link-prefix-form-group"); |     const $prefixFormGroup = $("#add-link-prefix-form-group"); | ||||||
|     const linkTypeEls = $("input[name='add-link-type']"); |     const $linkTypes = $("input[name='add-link-type']"); | ||||||
|     const linkTypeHtmlEl = linkTypeEls.filter('input[value="html"]'); |     const $linkTypeHtml = $linkTypes.filter('input[value="html"]'); | ||||||
|  |  | ||||||
|     function setLinkType(linkType) { |     function setLinkType(linkType) { | ||||||
|         linkTypeEls.each(function () { |         $linkTypes.each(function () { | ||||||
|             $(this).prop('checked', $(this).val() === linkType); |             $(this).prop('checked', $(this).val() === linkType); | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
| @@ -20,39 +20,39 @@ const addLink = (function() { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     function showDialog() { |     function showDialog() { | ||||||
|         glob.activeDialog = dialogEl; |         glob.activeDialog = $dialog; | ||||||
|  |  | ||||||
|         if (noteEditor.getCurrentNoteType() === 'text') { |         if (noteEditor.getCurrentNoteType() === 'text') { | ||||||
|             linkTypeHtmlEl.prop('disabled', false); |             $linkTypeHtml.prop('disabled', false); | ||||||
|  |  | ||||||
|             setLinkType('html'); |             setLinkType('html'); | ||||||
|         } |         } | ||||||
|         else { |         else { | ||||||
|             linkTypeHtmlEl.prop('disabled', true); |             $linkTypeHtml.prop('disabled', true); | ||||||
|  |  | ||||||
|             setLinkType('selected-to-current'); |             setLinkType('selected-to-current'); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         dialogEl.dialog({ |         $dialog.dialog({ | ||||||
|             modal: true, |             modal: true, | ||||||
|             width: 700 |             width: 700 | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|         autoCompleteEl.val('').focus(); |         $autoComplete.val('').focus(); | ||||||
|         clonePrefixEl.val(''); |         $clonePrefix.val(''); | ||||||
|         linkTitleEl.val(''); |         $linkTitle.val(''); | ||||||
|  |  | ||||||
|         function setDefaultLinkTitle(noteId) { |         function setDefaultLinkTitle(noteId) { | ||||||
|             const noteTitle = noteTree.getNoteTitle(noteId); |             const noteTitle = noteTree.getNoteTitle(noteId); | ||||||
|  |  | ||||||
|             linkTitleEl.val(noteTitle); |             $linkTitle.val(noteTitle); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         autoCompleteEl.autocomplete({ |         $autoComplete.autocomplete({ | ||||||
|             source: noteTree.getAutocompleteItems(), |             source: noteTree.getAutocompleteItems(), | ||||||
|             minLength: 0, |             minLength: 0, | ||||||
|             change: () => { |             change: () => { | ||||||
|                 const val = autoCompleteEl.val(); |                 const val = $autoComplete.val(); | ||||||
|                 const notePath = link.getNodePathFromLabel(val); |                 const notePath = link.getNodePathFromLabel(val); | ||||||
|                 if (!notePath) { |                 if (!notePath) { | ||||||
|                     return; |                     return; | ||||||
| @@ -75,8 +75,8 @@ const addLink = (function() { | |||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     formEl.submit(() => { |     $form.submit(() => { | ||||||
|         const value = autoCompleteEl.val(); |         const value = $autoComplete.val(); | ||||||
|  |  | ||||||
|         const notePath = link.getNodePathFromLabel(value); |         const notePath = link.getNodePathFromLabel(value); | ||||||
|         const noteId = treeUtils.getNoteIdFromNotePath(notePath); |         const noteId = treeUtils.getNoteIdFromNotePath(notePath); | ||||||
| @@ -85,25 +85,25 @@ const addLink = (function() { | |||||||
|             const linkType = $("input[name='add-link-type']:checked").val(); |             const linkType = $("input[name='add-link-type']:checked").val(); | ||||||
|  |  | ||||||
|             if (linkType === 'html') { |             if (linkType === 'html') { | ||||||
|                 const linkTitle = linkTitleEl.val(); |                 const linkTitle = $linkTitle.val(); | ||||||
|  |  | ||||||
|                 dialogEl.dialog("close"); |                 $dialog.dialog("close"); | ||||||
|  |  | ||||||
|                 link.addLinkToEditor(linkTitle, '#' + notePath); |                 link.addLinkToEditor(linkTitle, '#' + notePath); | ||||||
|             } |             } | ||||||
|             else if (linkType === 'selected-to-current') { |             else if (linkType === 'selected-to-current') { | ||||||
|                 const prefix = clonePrefixEl.val(); |                 const prefix = $clonePrefix.val(); | ||||||
|  |  | ||||||
|                 cloning.cloneNoteTo(noteId, noteEditor.getCurrentNoteId(), prefix); |                 cloning.cloneNoteTo(noteId, noteEditor.getCurrentNoteId(), prefix); | ||||||
|  |  | ||||||
|                 dialogEl.dialog("close"); |                 $dialog.dialog("close"); | ||||||
|             } |             } | ||||||
|             else if (linkType === 'current-to-selected') { |             else if (linkType === 'current-to-selected') { | ||||||
|                 const prefix = clonePrefixEl.val(); |                 const prefix = $clonePrefix.val(); | ||||||
|  |  | ||||||
|                 cloning.cloneNoteTo(noteEditor.getCurrentNoteId(), noteId, prefix); |                 cloning.cloneNoteTo(noteEditor.getCurrentNoteId(), noteId, prefix); | ||||||
|  |  | ||||||
|                 dialogEl.dialog("close"); |                 $dialog.dialog("close"); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
| @@ -111,19 +111,19 @@ const addLink = (function() { | |||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     function linkTypeChanged() { |     function linkTypeChanged() { | ||||||
|         const value = linkTypeEls.filter(":checked").val(); |         const value = $linkTypes.filter(":checked").val(); | ||||||
|  |  | ||||||
|         if (value === 'html') { |         if (value === 'html') { | ||||||
|             linkTitleFormGroup.show(); |             $linkTitleFormGroup.show(); | ||||||
|             prefixFormGroup.hide(); |             $prefixFormGroup.hide(); | ||||||
|         } |         } | ||||||
|         else { |         else { | ||||||
|             linkTitleFormGroup.hide(); |             $linkTitleFormGroup.hide(); | ||||||
|             prefixFormGroup.show(); |             $prefixFormGroup.show(); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     linkTypeEls.change(linkTypeChanged); |     $linkTypes.change(linkTypeChanged); | ||||||
|  |  | ||||||
|     $(document).bind('keydown', 'ctrl+l', e => { |     $(document).bind('keydown', 'ctrl+l', e => { | ||||||
|         showDialog(); |         showDialog(); | ||||||
|   | |||||||
| @@ -1,8 +1,12 @@ | |||||||
| "use strict"; | "use strict"; | ||||||
|  |  | ||||||
| const attributesDialog = (function() { | const attributesDialog = (function() { | ||||||
|     const dialogEl = $("#attributes-dialog"); |     const $dialog = $("#attributes-dialog"); | ||||||
|  |     const $saveAttributesButton = $("#save-attributes-button"); | ||||||
|  |     const $attributesBody = $('#attributes-table tbody'); | ||||||
|  |  | ||||||
|     const attributesModel = new AttributesModel(); |     const attributesModel = new AttributesModel(); | ||||||
|  |     let attributeNames = []; | ||||||
|  |  | ||||||
|     function AttributesModel() { |     function AttributesModel() { | ||||||
|         const self = this; |         const self = this; | ||||||
| @@ -14,38 +18,148 @@ const attributesDialog = (function() { | |||||||
|  |  | ||||||
|             const attributes = await server.get('notes/' + noteId + '/attributes'); |             const attributes = await server.get('notes/' + noteId + '/attributes'); | ||||||
|  |  | ||||||
|             this.attributes(attributes); |             self.attributes(attributes.map(ko.observable)); | ||||||
|         }; |  | ||||||
|  |  | ||||||
|         this.addNewRow = function() { |             addLastEmptyRow(); | ||||||
|             self.attributes.push({ |  | ||||||
|                 attributeId: '', |             attributeNames = await server.get('attributes/names'); | ||||||
|                 name: '', |  | ||||||
|                 value: '' |             // attribute might not be rendered immediatelly so could not focus | ||||||
|  |             setTimeout(() => $(".attribute-name:last").focus(), 100); | ||||||
|  |  | ||||||
|  |             $attributesBody.sortable({ | ||||||
|  |                 handle: '.handle', | ||||||
|  |                 containment: $attributesBody, | ||||||
|  |                 update: function() { | ||||||
|  |                     let position = 0; | ||||||
|  |  | ||||||
|  |                     // we need to update positions by searching in the DOM, because order of the | ||||||
|  |                     // attributes in the viewmodel (self.attributes()) stays the same | ||||||
|  |                     $attributesBody.find('input[name="position"]').each(function() { | ||||||
|  |                         const attr = self.getTargetAttribute(this); | ||||||
|  |  | ||||||
|  |                         attr().position = position++; | ||||||
|  |                     }); | ||||||
|  |                 } | ||||||
|             }); |             }); | ||||||
|         }; |         }; | ||||||
|  |  | ||||||
|  |         this.deleteAttribute = function(data, event) { | ||||||
|  |             const attr = self.getTargetAttribute(event.target); | ||||||
|  |             const attrData = attr(); | ||||||
|  |  | ||||||
|  |             if (attrData) { | ||||||
|  |                 attrData.isDeleted = 1; | ||||||
|  |  | ||||||
|  |                 attr(attrData); | ||||||
|  |  | ||||||
|  |                 addLastEmptyRow(); | ||||||
|  |             } | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         function isValid() { | ||||||
|  |             for (let attrs = self.attributes(), i = 0; i < attrs.length; i++) { | ||||||
|  |                 if (self.isEmptyName(i)) { | ||||||
|  |                     return false; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             return true; | ||||||
|  |         } | ||||||
|  |  | ||||||
|         this.save = async function() { |         this.save = async function() { | ||||||
|  |             // we need to defocus from input (in case of enter-triggered save) because value is updated | ||||||
|  |             // on blur event (because of conflict with jQuery UI Autocomplete). Without this, input would | ||||||
|  |             // stay in focus, blur wouldn't be triggered and change wouldn't be updated in the viewmodel. | ||||||
|  |             $saveAttributesButton.focus(); | ||||||
|  |  | ||||||
|  |             if (!isValid()) { | ||||||
|  |                 alert("Please fix all validation errors and try saving again."); | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  |  | ||||||
|             const noteId = noteEditor.getCurrentNoteId(); |             const noteId = noteEditor.getCurrentNoteId(); | ||||||
|  |  | ||||||
|             const attributes = await server.put('notes/' + noteId + '/attributes', this.attributes()); |             const attributesToSave = self.attributes() | ||||||
|  |                 .map(attr => attr()) | ||||||
|  |                 .filter(attr => attr.attributeId !== "" || attr.name !== ""); | ||||||
|  |  | ||||||
|             self.attributes(attributes); |             const attributes = await server.put('notes/' + noteId + '/attributes', attributesToSave); | ||||||
|  |  | ||||||
|  |             self.attributes(attributes.map(ko.observable)); | ||||||
|  |  | ||||||
|  |             addLastEmptyRow(); | ||||||
|  |  | ||||||
|             showMessage("Attributes have been saved."); |             showMessage("Attributes have been saved."); | ||||||
|  |  | ||||||
|  |             noteEditor.loadAttributeList(); | ||||||
|         }; |         }; | ||||||
|  |  | ||||||
|  |         function addLastEmptyRow() { | ||||||
|  |             const attrs = self.attributes().filter(attr => attr().isDeleted === 0); | ||||||
|  |             const last = attrs.length === 0 ? null : attrs[attrs.length - 1](); | ||||||
|  |  | ||||||
|  |             if (!last || last.name.trim() !== "" || last.value !== "") { | ||||||
|  |                 self.attributes.push(ko.observable({ | ||||||
|  |                     attributeId: '', | ||||||
|  |                     name: '', | ||||||
|  |                     value: '', | ||||||
|  |                     isDeleted: 0, | ||||||
|  |                     position: 0 | ||||||
|  |                 })); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         this.attributeChanged = function (data, event) { | ||||||
|  |             addLastEmptyRow(); | ||||||
|  |  | ||||||
|  |             const attr = self.getTargetAttribute(event.target); | ||||||
|  |  | ||||||
|  |             attr.valueHasMutated(); | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         this.isNotUnique = function(index) { | ||||||
|  |             const cur = self.attributes()[index](); | ||||||
|  |  | ||||||
|  |             if (cur.name.trim() === "") { | ||||||
|  |                 return false; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             for (let attrs = self.attributes(), i = 0; i < attrs.length; i++) { | ||||||
|  |                 const attr = attrs[i](); | ||||||
|  |  | ||||||
|  |                 if (index !== i && cur.name === attr.name) { | ||||||
|  |                     return true; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             return false; | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         this.isEmptyName = function(index) { | ||||||
|  |             const cur = self.attributes()[index](); | ||||||
|  |  | ||||||
|  |             return cur.name.trim() === "" && (cur.attributeId !== "" || cur.value !== ""); | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         this.getTargetAttribute = function(target) { | ||||||
|  |             const context = ko.contextFor(target); | ||||||
|  |             const index = context.$index(); | ||||||
|  |  | ||||||
|  |             return self.attributes()[index]; | ||||||
|  |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     async function showDialog() { |     async function showDialog() { | ||||||
|         glob.activeDialog = dialogEl; |         glob.activeDialog = $dialog; | ||||||
|  |  | ||||||
|         dialogEl.dialog({ |         await attributesModel.loadAttributes(); | ||||||
|  |  | ||||||
|  |         $dialog.dialog({ | ||||||
|             modal: true, |             modal: true, | ||||||
|             width: 800, |             width: 800, | ||||||
|             height: 700 |             height: 500 | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|         attributesModel.loadAttributes(); |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     $(document).bind('keydown', 'alt+a', e => { |     $(document).bind('keydown', 'alt+a', e => { | ||||||
| @@ -56,6 +170,54 @@ const attributesDialog = (function() { | |||||||
|  |  | ||||||
|     ko.applyBindings(attributesModel, document.getElementById('attributes-dialog')); |     ko.applyBindings(attributesModel, document.getElementById('attributes-dialog')); | ||||||
|  |  | ||||||
|  |     $(document).on('focus', '.attribute-name', function (e) { | ||||||
|  |         if (!$(this).hasClass("ui-autocomplete-input")) { | ||||||
|  |             $(this).autocomplete({ | ||||||
|  |                 // shouldn't be required and autocomplete should just accept array of strings, but that fails | ||||||
|  |                 // because we have overriden filter() function in init.js | ||||||
|  |                 source: attributeNames.map(attr => { | ||||||
|  |                     return { | ||||||
|  |                         label: attr, | ||||||
|  |                         value: attr | ||||||
|  |                     } | ||||||
|  |                 }), | ||||||
|  |                 minLength: 0 | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         $(this).autocomplete("search", $(this).val()); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     $(document).on('focus', '.attribute-value', async function (e) { | ||||||
|  |         if (!$(this).hasClass("ui-autocomplete-input")) { | ||||||
|  |             const attributeName = $(this).parent().parent().find('.attribute-name').val(); | ||||||
|  |  | ||||||
|  |             if (attributeName.trim() === "") { | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             const attributeValues = await server.get('attributes/values/' + encodeURIComponent(attributeName)); | ||||||
|  |  | ||||||
|  |             if (attributeValues.length === 0) { | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             $(this).autocomplete({ | ||||||
|  |                 // shouldn't be required and autocomplete should just accept array of strings, but that fails | ||||||
|  |                 // because we have overriden filter() function in init.js | ||||||
|  |                 source: attributeValues.map(attr => { | ||||||
|  |                     return { | ||||||
|  |                         label: attr, | ||||||
|  |                         value: attr | ||||||
|  |                     } | ||||||
|  |                 }), | ||||||
|  |                 minLength: 0 | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         $(this).autocomplete("search", $(this).val()); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|     return { |     return { | ||||||
|         showDialog |         showDialog | ||||||
|     }; |     }; | ||||||
|   | |||||||
| @@ -1,17 +1,17 @@ | |||||||
| "use strict"; | "use strict"; | ||||||
|  |  | ||||||
| const editTreePrefix = (function() { | const editTreePrefix = (function() { | ||||||
|     const dialogEl = $("#edit-tree-prefix-dialog"); |     const $dialog = $("#edit-tree-prefix-dialog"); | ||||||
|     const formEl = $("#edit-tree-prefix-form"); |     const $form = $("#edit-tree-prefix-form"); | ||||||
|     const treePrefixInputEl = $("#tree-prefix-input"); |     const $treePrefixInput = $("#tree-prefix-input"); | ||||||
|     const noteTitleEl = $('#tree-prefix-note-title'); |     const $noteTitle = $('#tree-prefix-note-title'); | ||||||
|  |  | ||||||
|     let noteTreeId; |     let noteTreeId; | ||||||
|  |  | ||||||
|     async function showDialog() { |     async function showDialog() { | ||||||
|         glob.activeDialog = dialogEl; |         glob.activeDialog = $dialog; | ||||||
|  |  | ||||||
|         await dialogEl.dialog({ |         await $dialog.dialog({ | ||||||
|             modal: true, |             modal: true, | ||||||
|             width: 500 |             width: 500 | ||||||
|         }); |         }); | ||||||
| @@ -20,21 +20,21 @@ const editTreePrefix = (function() { | |||||||
|  |  | ||||||
|         noteTreeId = currentNode.data.noteTreeId; |         noteTreeId = currentNode.data.noteTreeId; | ||||||
|  |  | ||||||
|         treePrefixInputEl.val(currentNode.data.prefix).focus(); |         $treePrefixInput.val(currentNode.data.prefix).focus(); | ||||||
|  |  | ||||||
|         const noteTitle = noteTree.getNoteTitle(currentNode.data.noteId); |         const noteTitle = noteTree.getNoteTitle(currentNode.data.noteId); | ||||||
|  |  | ||||||
|         noteTitleEl.html(noteTitle); |         $noteTitle.html(noteTitle); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     formEl.submit(() => { |     $form.submit(() => { | ||||||
|         const prefix = treePrefixInputEl.val(); |         const prefix = $treePrefixInput.val(); | ||||||
|  |  | ||||||
|         server.put('tree/' + noteTreeId + '/set-prefix', { |         server.put('tree/' + noteTreeId + '/set-prefix', { | ||||||
|             prefix: prefix |             prefix: prefix | ||||||
|         }).then(() => noteTree.setPrefix(noteTreeId, prefix)); |         }).then(() => noteTree.setPrefix(noteTreeId, prefix)); | ||||||
|  |  | ||||||
|         dialogEl.dialog("close"); |         $dialog.dialog("close"); | ||||||
|  |  | ||||||
|         return false; |         return false; | ||||||
|     }); |     }); | ||||||
|   | |||||||
| @@ -1,13 +1,13 @@ | |||||||
| "use strict"; | "use strict"; | ||||||
|  |  | ||||||
| const eventLog = (function() { | const eventLog = (function() { | ||||||
|     const dialogEl = $("#event-log-dialog"); |     const $dialog = $("#event-log-dialog"); | ||||||
|     const listEl = $("#event-log-list"); |     const $list = $("#event-log-list"); | ||||||
|  |  | ||||||
|     async function showDialog() { |     async function showDialog() { | ||||||
|         glob.activeDialog = dialogEl; |         glob.activeDialog = $dialog; | ||||||
|  |  | ||||||
|         dialogEl.dialog({ |         $dialog.dialog({ | ||||||
|             modal: true, |             modal: true, | ||||||
|             width: 800, |             width: 800, | ||||||
|             height: 700 |             height: 700 | ||||||
| @@ -15,7 +15,7 @@ const eventLog = (function() { | |||||||
|  |  | ||||||
|         const result = await server.get('event-log'); |         const result = await server.get('event-log'); | ||||||
|  |  | ||||||
|         listEl.html(''); |         $list.html(''); | ||||||
|  |  | ||||||
|         for (const event of result) { |         for (const event of result) { | ||||||
|             const dateTime = formatDateTime(parseDate(event.dateAdded)); |             const dateTime = formatDateTime(parseDate(event.dateAdded)); | ||||||
| @@ -28,7 +28,7 @@ const eventLog = (function() { | |||||||
|  |  | ||||||
|             const eventEl = $('<li>').html(dateTime + " - " + event.comment); |             const eventEl = $('<li>').html(dateTime + " - " + event.comment); | ||||||
|  |  | ||||||
|             listEl.append(eventEl); |             $list.append(eventEl); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,28 +1,28 @@ | |||||||
| "use strict"; | "use strict"; | ||||||
|  |  | ||||||
| const jumpToNote = (function() { | const jumpToNote = (function() { | ||||||
|     const dialogEl = $("#jump-to-note-dialog"); |     const $dialog = $("#jump-to-note-dialog"); | ||||||
|     const autoCompleteEl = $("#jump-to-note-autocomplete"); |     const $autoComplete = $("#jump-to-note-autocomplete"); | ||||||
|     const formEl = $("#jump-to-note-form"); |     const $form = $("#jump-to-note-form"); | ||||||
|  |  | ||||||
|     async function showDialog() { |     async function showDialog() { | ||||||
|         glob.activeDialog = dialogEl; |         glob.activeDialog = $dialog; | ||||||
|  |  | ||||||
|         autoCompleteEl.val(''); |         $autoComplete.val(''); | ||||||
|  |  | ||||||
|         dialogEl.dialog({ |         $dialog.dialog({ | ||||||
|             modal: true, |             modal: true, | ||||||
|             width: 800 |             width: 800 | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|         await autoCompleteEl.autocomplete({ |         await $autoComplete.autocomplete({ | ||||||
|             source: await stopWatch("building autocomplete", noteTree.getAutocompleteItems), |             source: await stopWatch("building autocomplete", noteTree.getAutocompleteItems), | ||||||
|             minLength: 0 |             minLength: 0 | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     function getSelectedNotePath() { |     function getSelectedNotePath() { | ||||||
|         const val = autoCompleteEl.val(); |         const val = $autoComplete.val(); | ||||||
|         return link.getNodePathFromLabel(val); |         return link.getNodePathFromLabel(val); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -32,7 +32,7 @@ const jumpToNote = (function() { | |||||||
|         if (notePath) { |         if (notePath) { | ||||||
|             noteTree.activateNode(notePath); |             noteTree.activateNode(notePath); | ||||||
|  |  | ||||||
|             dialogEl.dialog('close'); |             $dialog.dialog('close'); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -42,8 +42,8 @@ const jumpToNote = (function() { | |||||||
|         e.preventDefault(); |         e.preventDefault(); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     formEl.submit(() => { |     $form.submit(() => { | ||||||
|         const action = dialogEl.find("button:focus").val(); |         const action = $dialog.find("button:focus").val(); | ||||||
|  |  | ||||||
|         goToNote(); |         goToNote(); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,10 +1,10 @@ | |||||||
| "use strict"; | "use strict"; | ||||||
|  |  | ||||||
| const noteHistory = (function() { | const noteHistory = (function() { | ||||||
|     const dialogEl = $("#note-history-dialog"); |     const $dialog = $("#note-history-dialog"); | ||||||
|     const listEl = $("#note-history-list"); |     const $list = $("#note-history-list"); | ||||||
|     const contentEl = $("#note-history-content"); |     const $content = $("#note-history-content"); | ||||||
|     const titleEl = $("#note-history-title"); |     const $title = $("#note-history-title"); | ||||||
|  |  | ||||||
|     let historyItems = []; |     let historyItems = []; | ||||||
|  |  | ||||||
| @@ -13,23 +13,23 @@ const noteHistory = (function() { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     async function showNoteHistoryDialog(noteId, noteRevisionId) { |     async function showNoteHistoryDialog(noteId, noteRevisionId) { | ||||||
|         glob.activeDialog = dialogEl; |         glob.activeDialog = $dialog; | ||||||
|  |  | ||||||
|         dialogEl.dialog({ |         $dialog.dialog({ | ||||||
|             modal: true, |             modal: true, | ||||||
|             width: 800, |             width: 800, | ||||||
|             height: 700 |             height: 700 | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|         listEl.empty(); |         $list.empty(); | ||||||
|         contentEl.empty(); |         $content.empty(); | ||||||
|  |  | ||||||
|         historyItems = await server.get('notes-history/' + noteId); |         historyItems = await server.get('notes-history/' + noteId); | ||||||
|  |  | ||||||
|         for (const item of historyItems) { |         for (const item of historyItems) { | ||||||
|             const dateModified = parseDate(item.dateModifiedFrom); |             const dateModified = parseDate(item.dateModifiedFrom); | ||||||
|  |  | ||||||
|             listEl.append($('<option>', { |             $list.append($('<option>', { | ||||||
|                 value: item.noteRevisionId, |                 value: item.noteRevisionId, | ||||||
|                 text: formatDateTime(dateModified) |                 text: formatDateTime(dateModified) | ||||||
|             })); |             })); | ||||||
| @@ -37,13 +37,13 @@ const noteHistory = (function() { | |||||||
|  |  | ||||||
|         if (historyItems.length > 0) { |         if (historyItems.length > 0) { | ||||||
|             if (!noteRevisionId) { |             if (!noteRevisionId) { | ||||||
|                 noteRevisionId = listEl.find("option:first").val(); |                 noteRevisionId = $list.find("option:first").val(); | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             listEl.val(noteRevisionId).trigger('change'); |             $list.val(noteRevisionId).trigger('change'); | ||||||
|         } |         } | ||||||
|         else { |         else { | ||||||
|             titleEl.text("No history for this note yet..."); |             $title.text("No history for this note yet..."); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -53,13 +53,13 @@ const noteHistory = (function() { | |||||||
|         e.preventDefault(); |         e.preventDefault(); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     listEl.on('change', () => { |     $list.on('change', () => { | ||||||
|         const optVal = listEl.find(":selected").val(); |         const optVal = $list.find(":selected").val(); | ||||||
|  |  | ||||||
|         const historyItem = historyItems.find(r => r.noteRevisionId === optVal); |         const historyItem = historyItems.find(r => r.noteRevisionId === optVal); | ||||||
|  |  | ||||||
|         titleEl.html(historyItem.title); |         $title.html(historyItem.title); | ||||||
|         contentEl.html(historyItem.content); |         $content.html(historyItem.content); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     $(document).on('click', "a[action='note-history']", event => { |     $(document).on('click', "a[action='note-history']", event => { | ||||||
|   | |||||||
| @@ -1,13 +1,13 @@ | |||||||
| "use strict"; | "use strict"; | ||||||
|  |  | ||||||
| const noteSource = (function() { | const noteSource = (function() { | ||||||
|     const dialogEl = $("#note-source-dialog"); |     const $dialog = $("#note-source-dialog"); | ||||||
|     const noteSourceEl = $("#note-source"); |     const $noteSource = $("#note-source"); | ||||||
|  |  | ||||||
|     function showDialog() { |     function showDialog() { | ||||||
|         glob.activeDialog = dialogEl; |         glob.activeDialog = $dialog; | ||||||
|  |  | ||||||
|         dialogEl.dialog({ |         $dialog.dialog({ | ||||||
|             modal: true, |             modal: true, | ||||||
|             width: 800, |             width: 800, | ||||||
|             height: 500 |             height: 500 | ||||||
| @@ -15,7 +15,7 @@ const noteSource = (function() { | |||||||
|  |  | ||||||
|         const noteText = noteEditor.getCurrentNote().detail.content; |         const noteText = noteEditor.getCurrentNote().detail.content; | ||||||
|  |  | ||||||
|         noteSourceEl.text(formatHtml(noteText)); |         $noteSource.text(formatHtml(noteText)); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     function formatHtml(str) { |     function formatHtml(str) { | ||||||
|   | |||||||
| @@ -1,12 +1,12 @@ | |||||||
| "use strict"; | "use strict"; | ||||||
|  |  | ||||||
| const recentChanges = (function() { | const recentChanges = (function() { | ||||||
|     const dialogEl = $("#recent-changes-dialog"); |     const $dialog = $("#recent-changes-dialog"); | ||||||
|  |  | ||||||
|     async function showDialog() { |     async function showDialog() { | ||||||
|         glob.activeDialog = dialogEl; |         glob.activeDialog = $dialog; | ||||||
|  |  | ||||||
|         dialogEl.dialog({ |         $dialog.dialog({ | ||||||
|             modal: true, |             modal: true, | ||||||
|             width: 800, |             width: 800, | ||||||
|             height: 700 |             height: 700 | ||||||
| @@ -14,7 +14,7 @@ const recentChanges = (function() { | |||||||
|  |  | ||||||
|         const result = await server.get('recent-changes/'); |         const result = await server.get('recent-changes/'); | ||||||
|  |  | ||||||
|         dialogEl.html(''); |         $dialog.html(''); | ||||||
|  |  | ||||||
|         const groupedByDate = groupByDate(result); |         const groupedByDate = groupByDate(result); | ||||||
|  |  | ||||||
| @@ -48,7 +48,7 @@ const recentChanges = (function() { | |||||||
|                     .append(' (').append(revLink).append(')')); |                     .append(' (').append(revLink).append(')')); | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             dialogEl.append(dayEl); |             $dialog.append(dayEl); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,13 +1,9 @@ | |||||||
| "use strict"; | "use strict"; | ||||||
|  |  | ||||||
| const recentNotes = (function() { | const recentNotes = (function() { | ||||||
|     const dialogEl = $("#recent-notes-dialog"); |     const $dialog = $("#recent-notes-dialog"); | ||||||
|     const selectBoxEl = $('#recent-notes-select-box'); |     const $searchInput = $('#recent-notes-search-input'); | ||||||
|     const jumpToButtonEl = $('#recent-notes-jump-to'); |  | ||||||
|     const addLinkButtonEl = $('#recent-notes-add-link'); |  | ||||||
|     const addCurrentAsChildEl = $("#recent-notes-add-current-as-child"); |  | ||||||
|     const addRecentAsChildEl = $("#recent-notes-add-recent-as-child"); |  | ||||||
|     const noteDetailEl = $('#note-detail'); |  | ||||||
|     // list of recent note paths |     // list of recent note paths | ||||||
|     let list = []; |     let list = []; | ||||||
|  |  | ||||||
| @@ -29,98 +25,67 @@ const recentNotes = (function() { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     function showDialog() { |     function showDialog() { | ||||||
|         glob.activeDialog = dialogEl; |         glob.activeDialog = $dialog; | ||||||
|  |  | ||||||
|         dialogEl.dialog({ |         $dialog.dialog({ | ||||||
|             modal: true, |             modal: true, | ||||||
|             width: 800 |             width: 800, | ||||||
|  |             height: 100, | ||||||
|  |             position: { my: "center top+100", at: "top", of: window } | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|         selectBoxEl.find('option').remove(); |         $searchInput.val(''); | ||||||
|  |  | ||||||
|         // remove the current note |         // remove the current note | ||||||
|         const recNotes = list.filter(note => note !== noteTree.getCurrentNotePath()); |         const recNotes = list.filter(note => note !== noteTree.getCurrentNotePath()); | ||||||
|  |  | ||||||
|         $.each(recNotes, (key, valueNotePath) => { |         $searchInput.autocomplete({ | ||||||
|             const noteTitle = noteTree.getNotePathTitle(valueNotePath); |             source: recNotes.map(notePath => { | ||||||
|  |                 let noteTitle; | ||||||
|  |  | ||||||
|             const option = $("<option></option>") |                 try { | ||||||
|                 .attr("value", valueNotePath) |                     noteTitle = noteTree.getNotePathTitle(notePath); | ||||||
|                 .text(noteTitle); |                 } | ||||||
|  |                 catch (e) { | ||||||
|  |                     noteTitle = "[error - can't find note title]"; | ||||||
|  |  | ||||||
|             // select the first one (most recent one) by default |                     messaging.logError("Could not find title for notePath=" + notePath + ", stack=" + e.stack); | ||||||
|             if (key === 0) { |                 } | ||||||
|                 option.attr("selected", "selected"); |  | ||||||
|  |                 return { | ||||||
|  |                     label: noteTitle, | ||||||
|  |                     value: notePath | ||||||
|  |                 } | ||||||
|  |             }), | ||||||
|  |             minLength: 0, | ||||||
|  |             autoFocus: true, | ||||||
|  |             select: function (event, ui) { | ||||||
|  |                 noteTree.activateNode(ui.item.value); | ||||||
|  |  | ||||||
|  |                 $searchInput.autocomplete('destroy'); | ||||||
|  |                 $dialog.dialog('close'); | ||||||
|  |             }, | ||||||
|  |             focus: function (event, ui) { | ||||||
|  |                 event.preventDefault(); | ||||||
|  |             }, | ||||||
|  |             close: function (event, ui) { | ||||||
|  |                 if (event.keyCode === 27) { // escape closes dialog | ||||||
|  |                     $searchInput.autocomplete('destroy'); | ||||||
|  |                     $dialog.dialog('close'); | ||||||
|  |                 } | ||||||
|  |                 else { | ||||||
|  |                     // keep autocomplete open | ||||||
|  |                     // we're kind of abusing autocomplete to work in a way which it's not designed for | ||||||
|  |                     $searchInput.autocomplete("search", ""); | ||||||
|  |                 } | ||||||
|  |             }, | ||||||
|  |             create: () => $searchInput.autocomplete("search", ""), | ||||||
|  |             classes: { | ||||||
|  |                 "ui-autocomplete": "recent-notes-autocomplete" | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             selectBoxEl.append(option); |  | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     function getSelectedNotePath() { |  | ||||||
|         return selectBoxEl.find("option:selected").val(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     function getSelectedNoteId() { |  | ||||||
|         const notePath = getSelectedNotePath(); |  | ||||||
|         return treeUtils.getNoteIdFromNotePath(notePath); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     function setActiveNoteBasedOnRecentNotes() { |  | ||||||
|         const notePath = getSelectedNotePath(); |  | ||||||
|  |  | ||||||
|         noteTree.activateNode(notePath); |  | ||||||
|  |  | ||||||
|         dialogEl.dialog('close'); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     function addLinkBasedOnRecentNotes() { |  | ||||||
|         const notePath = getSelectedNotePath(); |  | ||||||
|         const noteId = treeUtils.getNoteIdFromNotePath(notePath); |  | ||||||
|  |  | ||||||
|         const linkTitle = noteTree.getNoteTitle(noteId); |  | ||||||
|  |  | ||||||
|         dialogEl.dialog("close"); |  | ||||||
|  |  | ||||||
|         link.addLinkToEditor(linkTitle, '#' + notePath); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     async function addCurrentAsChild() { |  | ||||||
|         await cloning.cloneNoteTo(noteEditor.getCurrentNoteId(), getSelectedNoteId()); |  | ||||||
|  |  | ||||||
|         dialogEl.dialog("close"); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     async function addRecentAsChild() { |  | ||||||
|         await cloning.cloneNoteTo(getSelectedNoteId(), noteEditor.getCurrentNoteId()); |  | ||||||
|  |  | ||||||
|         dialogEl.dialog("close"); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     selectBoxEl.keydown(e => { |  | ||||||
|         const key = e.which; |  | ||||||
|  |  | ||||||
|         // to get keycodes use http://keycode.info/ |  | ||||||
|         if (key === 13)// the enter key code |  | ||||||
|         { |  | ||||||
|             setActiveNoteBasedOnRecentNotes(); |  | ||||||
|         } |  | ||||||
|         else if (key === 76 /* l */) { |  | ||||||
|             addLinkBasedOnRecentNotes(); |  | ||||||
|         } |  | ||||||
|         else if (key === 67 /* c */) { |  | ||||||
|             addCurrentAsChild(); |  | ||||||
|         } |  | ||||||
|         else if (key === 82 /* r */) { |  | ||||||
|             addRecentAsChild() |  | ||||||
|         } |  | ||||||
|         else { |  | ||||||
|             return; // avoid prevent default |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         e.preventDefault(); |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     reload(); |     reload(); | ||||||
|  |  | ||||||
|     $(document).bind('keydown', 'ctrl+e', e => { |     $(document).bind('keydown', 'ctrl+e', e => { | ||||||
| @@ -129,15 +94,6 @@ const recentNotes = (function() { | |||||||
|         e.preventDefault(); |         e.preventDefault(); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     selectBoxEl.dblclick(e => { |  | ||||||
|         setActiveNoteBasedOnRecentNotes(); |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     jumpToButtonEl.click(setActiveNoteBasedOnRecentNotes); |  | ||||||
|     addLinkButtonEl.click(addLinkBasedOnRecentNotes); |  | ||||||
|     addCurrentAsChildEl.click(addCurrentAsChild); |  | ||||||
|     addRecentAsChildEl.click(addRecentAsChild); |  | ||||||
|  |  | ||||||
|     return { |     return { | ||||||
|         showDialog, |         showDialog, | ||||||
|         addRecentNote, |         addRecentNote, | ||||||
|   | |||||||
| @@ -1,8 +1,8 @@ | |||||||
| "use strict"; | "use strict"; | ||||||
|  |  | ||||||
| const settings = (function() { | const settings = (function() { | ||||||
|     const dialogEl = $("#settings-dialog"); |     const $dialog = $("#settings-dialog"); | ||||||
|     const tabsEl = $("#settings-tabs"); |     const $tabs = $("#settings-tabs"); | ||||||
|  |  | ||||||
|     const settingModules = []; |     const settingModules = []; | ||||||
|  |  | ||||||
| @@ -11,16 +11,16 @@ const settings = (function() { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     async function showDialog() { |     async function showDialog() { | ||||||
|         glob.activeDialog = dialogEl; |         glob.activeDialog = $dialog; | ||||||
|  |  | ||||||
|         const settings = await server.get('settings'); |         const settings = await server.get('settings'); | ||||||
|  |  | ||||||
|         dialogEl.dialog({ |         $dialog.dialog({ | ||||||
|             modal: true, |             modal: true, | ||||||
|             width: 900 |             width: 900 | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|         tabsEl.tabs(); |         $tabs.tabs(); | ||||||
|  |  | ||||||
|         for (const module of settingModules) { |         for (const module of settingModules) { | ||||||
|             if (module.settingsLoaded) { |             if (module.settingsLoaded) { | ||||||
| @@ -46,22 +46,22 @@ const settings = (function() { | |||||||
| })(); | })(); | ||||||
|  |  | ||||||
| settings.addModule((function() { | settings.addModule((function() { | ||||||
|     const formEl = $("#change-password-form"); |     const $form = $("#change-password-form"); | ||||||
|     const oldPasswordEl = $("#old-password"); |     const $oldPassword = $("#old-password"); | ||||||
|     const newPassword1El = $("#new-password1"); |     const $newPassword1 = $("#new-password1"); | ||||||
|     const newPassword2El = $("#new-password2"); |     const $newPassword2 = $("#new-password2"); | ||||||
|  |  | ||||||
|     function settingsLoaded(settings) { |     function settingsLoaded(settings) { | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     formEl.submit(() => { |     $form.submit(() => { | ||||||
|         const oldPassword = oldPasswordEl.val(); |         const oldPassword = $oldPassword.val(); | ||||||
|         const newPassword1 = newPassword1El.val(); |         const newPassword1 = $newPassword1.val(); | ||||||
|         const newPassword2 = newPassword2El.val(); |         const newPassword2 = $newPassword2.val(); | ||||||
|  |  | ||||||
|         oldPasswordEl.val(''); |         $oldPassword.val(''); | ||||||
|         newPassword1El.val(''); |         $newPassword1.val(''); | ||||||
|         newPassword2El.val(''); |         $newPassword2.val(''); | ||||||
|  |  | ||||||
|         if (newPassword1 !== newPassword2) { |         if (newPassword1 !== newPassword2) { | ||||||
|             alert("New passwords are not the same."); |             alert("New passwords are not the same."); | ||||||
| @@ -92,16 +92,16 @@ settings.addModule((function() { | |||||||
| })()); | })()); | ||||||
|  |  | ||||||
| settings.addModule((function() { | settings.addModule((function() { | ||||||
|     const formEl = $("#protected-session-timeout-form"); |     const $form = $("#protected-session-timeout-form"); | ||||||
|     const protectedSessionTimeoutEl = $("#protected-session-timeout-in-seconds"); |     const $protectedSessionTimeout = $("#protected-session-timeout-in-seconds"); | ||||||
|     const settingName = 'protected_session_timeout'; |     const settingName = 'protected_session_timeout'; | ||||||
|  |  | ||||||
|     function settingsLoaded(settings) { |     function settingsLoaded(settings) { | ||||||
|         protectedSessionTimeoutEl.val(settings[settingName]); |         $protectedSessionTimeout.val(settings[settingName]); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     formEl.submit(() => { |     $form.submit(() => { | ||||||
|         const protectedSessionTimeout = protectedSessionTimeoutEl.val(); |         const protectedSessionTimeout = $protectedSessionTimeout.val(); | ||||||
|  |  | ||||||
|         settings.saveSettings(settingName, protectedSessionTimeout).then(() => { |         settings.saveSettings(settingName, protectedSessionTimeout).then(() => { | ||||||
|             protected_session.setProtectedSessionTimeout(protectedSessionTimeout); |             protected_session.setProtectedSessionTimeout(protectedSessionTimeout); | ||||||
| @@ -116,16 +116,16 @@ settings.addModule((function() { | |||||||
| })()); | })()); | ||||||
|  |  | ||||||
| settings.addModule((function () { | settings.addModule((function () { | ||||||
|     const formEl = $("#history-snapshot-time-interval-form"); |     const $form = $("#history-snapshot-time-interval-form"); | ||||||
|     const timeIntervalEl = $("#history-snapshot-time-interval-in-seconds"); |     const $timeInterval = $("#history-snapshot-time-interval-in-seconds"); | ||||||
|     const settingName = 'history_snapshot_time_interval'; |     const settingName = 'history_snapshot_time_interval'; | ||||||
|  |  | ||||||
|     function settingsLoaded(settings) { |     function settingsLoaded(settings) { | ||||||
|         timeIntervalEl.val(settings[settingName]); |         $timeInterval.val(settings[settingName]); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     formEl.submit(() => { |     $form.submit(() => { | ||||||
|         settings.saveSettings(settingName, timeIntervalEl.val()); |         settings.saveSettings(settingName, $timeInterval.val()); | ||||||
|  |  | ||||||
|         return false; |         return false; | ||||||
|     }); |     }); | ||||||
| @@ -136,50 +136,50 @@ settings.addModule((function () { | |||||||
| })()); | })()); | ||||||
|  |  | ||||||
| settings.addModule((async function () { | settings.addModule((async function () { | ||||||
|     const appVersionEl = $("#app-version"); |     const $appVersion = $("#app-version"); | ||||||
|     const dbVersionEl = $("#db-version"); |     const $dbVersion = $("#db-version"); | ||||||
|     const buildDateEl = $("#build-date"); |     const $buildDate = $("#build-date"); | ||||||
|     const buildRevisionEl = $("#build-revision"); |     const $buildRevision = $("#build-revision"); | ||||||
|  |  | ||||||
|     const appInfo = await server.get('app-info'); |     const appInfo = await server.get('app-info'); | ||||||
|  |  | ||||||
|     appVersionEl.html(appInfo.app_version); |     $appVersion.html(appInfo.app_version); | ||||||
|     dbVersionEl.html(appInfo.db_version); |     $dbVersion.html(appInfo.db_version); | ||||||
|     buildDateEl.html(appInfo.build_date); |     $buildDate.html(appInfo.build_date); | ||||||
|     buildRevisionEl.html(appInfo.build_revision); |     $buildRevision.html(appInfo.build_revision); | ||||||
|     buildRevisionEl.attr('href', 'https://github.com/zadam/trilium/commit/' + appInfo.build_revision); |     $buildRevision.attr('href', 'https://github.com/zadam/trilium/commit/' + appInfo.build_revision); | ||||||
|  |  | ||||||
|     return {}; |     return {}; | ||||||
| })()); | })()); | ||||||
|  |  | ||||||
| settings.addModule((async function () { | settings.addModule((async function () { | ||||||
|     const forceFullSyncButton = $("#force-full-sync-button"); |     const $forceFullSyncButton = $("#force-full-sync-button"); | ||||||
|     const fillSyncRowsButton = $("#fill-sync-rows-button"); |     const $fillSyncRowsButton = $("#fill-sync-rows-button"); | ||||||
|     const anonymizeButton = $("#anonymize-button"); |     const $anonymizeButton = $("#anonymize-button"); | ||||||
|     const cleanupSoftDeletedButton = $("#cleanup-soft-deleted-items-button"); |     const $cleanupSoftDeletedButton = $("#cleanup-soft-deleted-items-button"); | ||||||
|     const cleanupUnusedImagesButton = $("#cleanup-unused-images-button"); |     const $cleanupUnusedImagesButton = $("#cleanup-unused-images-button"); | ||||||
|     const vacuumDatabaseButton = $("#vacuum-database-button"); |     const $vacuumDatabaseButton = $("#vacuum-database-button"); | ||||||
|  |  | ||||||
|     forceFullSyncButton.click(async () => { |     $forceFullSyncButton.click(async () => { | ||||||
|         await server.post('sync/force-full-sync'); |         await server.post('sync/force-full-sync'); | ||||||
|  |  | ||||||
|         showMessage("Full sync triggered"); |         showMessage("Full sync triggered"); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     fillSyncRowsButton.click(async () => { |     $fillSyncRowsButton.click(async () => { | ||||||
|         await server.post('sync/fill-sync-rows'); |         await server.post('sync/fill-sync-rows'); | ||||||
|  |  | ||||||
|         showMessage("Sync rows filled successfully"); |         showMessage("Sync rows filled successfully"); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|  |  | ||||||
|     anonymizeButton.click(async () => { |     $anonymizeButton.click(async () => { | ||||||
|         await server.post('anonymization/anonymize'); |         await server.post('anonymization/anonymize'); | ||||||
|  |  | ||||||
|         showMessage("Created anonymized database"); |         showMessage("Created anonymized database"); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     cleanupSoftDeletedButton.click(async () => { |     $cleanupSoftDeletedButton.click(async () => { | ||||||
|         if (confirm("Do you really want to clean up soft-deleted items?")) { |         if (confirm("Do you really want to clean up soft-deleted items?")) { | ||||||
|             await server.post('cleanup/cleanup-soft-deleted-items'); |             await server.post('cleanup/cleanup-soft-deleted-items'); | ||||||
|  |  | ||||||
| @@ -187,7 +187,7 @@ settings.addModule((async function () { | |||||||
|         } |         } | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     cleanupUnusedImagesButton.click(async () => { |     $cleanupUnusedImagesButton.click(async () => { | ||||||
|         if (confirm("Do you really want to clean up unused images?")) { |         if (confirm("Do you really want to clean up unused images?")) { | ||||||
|             await server.post('cleanup/cleanup-unused-images'); |             await server.post('cleanup/cleanup-unused-images'); | ||||||
|  |  | ||||||
| @@ -195,7 +195,7 @@ settings.addModule((async function () { | |||||||
|         } |         } | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     vacuumDatabaseButton.click(async () => { |     $vacuumDatabaseButton.click(async () => { | ||||||
|         await server.post('cleanup/vacuum-database'); |         await server.post('cleanup/vacuum-database'); | ||||||
|  |  | ||||||
|         showMessage("Database has been vacuumed"); |         showMessage("Database has been vacuumed"); | ||||||
|   | |||||||
| @@ -1,24 +1,59 @@ | |||||||
| "use strict"; | "use strict"; | ||||||
|  |  | ||||||
| const sqlConsole = (function() { | const sqlConsole = (function() { | ||||||
|     const dialogEl = $("#sql-console-dialog"); |     const $dialog = $("#sql-console-dialog"); | ||||||
|     const queryEl = $('#sql-console-query'); |     const $query = $('#sql-console-query'); | ||||||
|     const executeButton = $('#sql-console-execute'); |     const $executeButton = $('#sql-console-execute'); | ||||||
|     const resultHeadEl = $('#sql-console-results thead'); |     const $resultHead = $('#sql-console-results thead'); | ||||||
|     const resultBodyEl = $('#sql-console-results tbody'); |     const $resultBody = $('#sql-console-results tbody'); | ||||||
|  |  | ||||||
|  |     let codeEditor; | ||||||
|  |  | ||||||
|     function showDialog() { |     function showDialog() { | ||||||
|         glob.activeDialog = dialogEl; |         glob.activeDialog = $dialog; | ||||||
|  |  | ||||||
|         dialogEl.dialog({ |         $dialog.dialog({ | ||||||
|             modal: true, |             modal: true, | ||||||
|             width: $(window).width(), |             width: $(window).width(), | ||||||
|             height: $(window).height() |             height: $(window).height(), | ||||||
|  |             open: function() { | ||||||
|  |                 initEditor(); | ||||||
|  |             } | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     async function execute() { |     async function initEditor() { | ||||||
|         const sqlQuery = queryEl.val(); |         if (!codeEditor) { | ||||||
|  |             await requireLibrary(CODE_MIRROR); | ||||||
|  |  | ||||||
|  |             CodeMirror.keyMap.default["Shift-Tab"] = "indentLess"; | ||||||
|  |             CodeMirror.keyMap.default["Tab"] = "indentMore"; | ||||||
|  |  | ||||||
|  |             // removing Escape binding so that Escape will propagate to the dialog (which will close on escape) | ||||||
|  |             delete CodeMirror.keyMap.basic["Esc"]; | ||||||
|  |  | ||||||
|  |             CodeMirror.modeURL = 'libraries/codemirror/mode/%N/%N.js'; | ||||||
|  |  | ||||||
|  |             codeEditor = CodeMirror($query[0], { | ||||||
|  |                 value: "", | ||||||
|  |                 viewportMargin: Infinity, | ||||||
|  |                 indentUnit: 4, | ||||||
|  |                 highlightSelectionMatches: {showToken: /\w/, annotateScrollbar: false} | ||||||
|  |             }); | ||||||
|  |  | ||||||
|  |             codeEditor.setOption("mode", "text/x-sqlite"); | ||||||
|  |             CodeMirror.autoLoadMode(codeEditor, "sql"); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         codeEditor.focus(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async function execute(e) { | ||||||
|  |         // stop from propagating upwards (dangerous especially with ctrl+enter executable javascript notes) | ||||||
|  |         e.preventDefault(); | ||||||
|  |         e.stopPropagation(); | ||||||
|  |  | ||||||
|  |         const sqlQuery = codeEditor.getValue(); | ||||||
|  |  | ||||||
|         const result = await server.post("sql/execute", { |         const result = await server.post("sql/execute", { | ||||||
|             query: sqlQuery |             query: sqlQuery | ||||||
| @@ -34,8 +69,8 @@ const sqlConsole = (function() { | |||||||
|  |  | ||||||
|         const rows = result.rows; |         const rows = result.rows; | ||||||
|  |  | ||||||
|         resultHeadEl.empty(); |         $resultHead.empty(); | ||||||
|         resultBodyEl.empty(); |         $resultBody.empty(); | ||||||
|  |  | ||||||
|         if (rows.length > 0) { |         if (rows.length > 0) { | ||||||
|             const result = rows[0]; |             const result = rows[0]; | ||||||
| @@ -45,7 +80,7 @@ const sqlConsole = (function() { | |||||||
|                 rowEl.append($("<th>").html(key)); |                 rowEl.append($("<th>").html(key)); | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             resultHeadEl.append(rowEl); |             $resultHead.append(rowEl); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         for (const result of rows) { |         for (const result of rows) { | ||||||
| @@ -55,15 +90,15 @@ const sqlConsole = (function() { | |||||||
|                 rowEl.append($("<td>").html(result[key])); |                 rowEl.append($("<td>").html(result[key])); | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             resultBodyEl.append(rowEl); |             $resultBody.append(rowEl); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     $(document).bind('keydown', 'alt+o', showDialog); |     $(document).bind('keydown', 'alt+o', showDialog); | ||||||
|  |  | ||||||
|     queryEl.bind('keydown', 'ctrl+return', execute); |     $query.bind('keydown', 'ctrl+return', execute); | ||||||
|  |  | ||||||
|     executeButton.click(execute); |     $executeButton.click(execute); | ||||||
|  |  | ||||||
|     return { |     return { | ||||||
|         showDialog |         showDialog | ||||||
|   | |||||||
							
								
								
									
										32
									
								
								src/public/javascripts/export.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/public/javascripts/export.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | |||||||
|  | "use strict"; | ||||||
|  |  | ||||||
|  | function exportSubTree(noteId) { | ||||||
|  |     const url = getHost() + "/api/export/" + noteId + "?protectedSessionId=" | ||||||
|  |         + encodeURIComponent(protected_session.getProtectedSessionId()); | ||||||
|  |  | ||||||
|  |     download(url); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | let importNoteId; | ||||||
|  |  | ||||||
|  | function importSubTree(noteId) { | ||||||
|  |     importNoteId = noteId; | ||||||
|  |  | ||||||
|  |     $("#import-upload").trigger('click'); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | $("#import-upload").change(async function() { | ||||||
|  |     const formData = new FormData(); | ||||||
|  |     formData.append('upload', this.files[0]); | ||||||
|  |  | ||||||
|  |     await $.ajax({ | ||||||
|  |         url: baseApiUrl + 'import/' + importNoteId, | ||||||
|  |         headers: server.getHeaders(), | ||||||
|  |         data: formData, | ||||||
|  |         type: 'POST', | ||||||
|  |         contentType: false, // NEEDED, DON'T OMIT THIS | ||||||
|  |         processData: false, // NEEDED, DON'T OMIT THIS | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     await noteTree.reload(); | ||||||
|  | }); | ||||||
| @@ -54,24 +54,6 @@ $(document).bind('keydown', 'ctrl+f', () => { | |||||||
|     } |     } | ||||||
| }); | }); | ||||||
|  |  | ||||||
| $(document).bind('keydown', "ctrl+shift+left", () => { |  | ||||||
|     const node = noteTree.getCurrentNode(); |  | ||||||
|     node.navigate($.ui.keyCode.LEFT, true); |  | ||||||
|  |  | ||||||
|     $("#note-detail").focus(); |  | ||||||
|  |  | ||||||
|     return false; |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| $(document).bind('keydown', "ctrl+shift+right", () => { |  | ||||||
|     const node = noteTree.getCurrentNode(); |  | ||||||
|     node.navigate($.ui.keyCode.RIGHT, true); |  | ||||||
|  |  | ||||||
|     $("#note-detail").focus(); |  | ||||||
|  |  | ||||||
|     return false; |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| $(document).bind('keydown', "ctrl+shift+up", () => { | $(document).bind('keydown', "ctrl+shift+up", () => { | ||||||
|     const node = noteTree.getCurrentNode(); |     const node = noteTree.getCurrentNode(); | ||||||
|     node.navigate($.ui.keyCode.UP, true); |     node.navigate($.ui.keyCode.UP, true); | ||||||
| @@ -123,7 +105,7 @@ $(window).on('beforeunload', () => { | |||||||
| // Overrides the default autocomplete filter function to search for matched on atleast 1 word in each of the input term's words | // Overrides the default autocomplete filter function to search for matched on atleast 1 word in each of the input term's words | ||||||
| $.ui.autocomplete.filter = (array, terms) => { | $.ui.autocomplete.filter = (array, terms) => { | ||||||
|     if (!terms) { |     if (!terms) { | ||||||
|         return []; |         return array; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const startDate = new Date(); |     const startDate = new Date(); | ||||||
| @@ -132,18 +114,32 @@ $.ui.autocomplete.filter = (array, terms) => { | |||||||
|     const tokens = terms.toLowerCase().split(" "); |     const tokens = terms.toLowerCase().split(" "); | ||||||
|  |  | ||||||
|     for (const item of array) { |     for (const item of array) { | ||||||
|         let found = true; |  | ||||||
|         const lcLabel = item.label.toLowerCase(); |         const lcLabel = item.label.toLowerCase(); | ||||||
|  |  | ||||||
|         for (const token of tokens) { |         const found = tokens.every(token => lcLabel.indexOf(token) !== -1); | ||||||
|             if (lcLabel.indexOf(token) === -1) { |         if (!found) { | ||||||
|                 found = false; |             continue; | ||||||
|                 break; |         } | ||||||
|  |  | ||||||
|  |         // this is not completely correct and might cause minor problems with note with names containing this " / " | ||||||
|  |         const lastSegmentIndex = lcLabel.lastIndexOf(" / "); | ||||||
|  |  | ||||||
|  |         if (lastSegmentIndex !== -1) { | ||||||
|  |             const lastSegment = lcLabel.substr(lastSegmentIndex + 3); | ||||||
|  |  | ||||||
|  |             // at least some token needs to be in the last segment (leaf note), otherwise this | ||||||
|  |             // particular note is not that interesting (query is satisfied by parent note) | ||||||
|  |             const foundInLastSegment = tokens.some(token => lastSegment.indexOf(token) !== -1); | ||||||
|  |  | ||||||
|  |             if (!foundInLastSegment) { | ||||||
|  |                 continue; | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         if (found) { |         results.push(item); | ||||||
|             results.push(item); |  | ||||||
|  |         if (results.length > 100) { | ||||||
|  |             break; | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -205,9 +201,48 @@ window.onerror = function (msg, url, lineNo, columnNo, error) { | |||||||
| $("#logout-button").toggle(!isElectron()); | $("#logout-button").toggle(!isElectron()); | ||||||
|  |  | ||||||
| $(document).ready(() => { | $(document).ready(() => { | ||||||
|     server.get("script/startup").then(scripts => { |     server.get("script/startup").then(scriptBundles => { | ||||||
|         for (const script of scripts) { |         for (const bundle of scriptBundles) { | ||||||
|             executeScript(script); |             executeBundle(bundle); | ||||||
|         } |         } | ||||||
|     }); |     }); | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | if (isElectron()) { | ||||||
|  |     require('electron').ipcRenderer.on('create-day-sub-note', async function(event, parentNoteId) { | ||||||
|  |         // this might occur when day note had to be created | ||||||
|  |         if (!noteTree.noteExists(parentNoteId)) { | ||||||
|  |             await noteTree.reload(); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         await noteTree.activateNode(parentNoteId); | ||||||
|  |  | ||||||
|  |         setTimeout(() => { | ||||||
|  |             const node = noteTree.getCurrentNode(); | ||||||
|  |  | ||||||
|  |             noteTree.createNote(node, node.data.noteId, 'into', node.data.isProtected); | ||||||
|  |         }, 500); | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function uploadAttachment() { | ||||||
|  |     $("#attachment-upload").trigger('click'); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | $("#attachment-upload").change(async function() { | ||||||
|  |     const formData = new FormData(); | ||||||
|  |     formData.append('upload', this.files[0]); | ||||||
|  |  | ||||||
|  |     const resp = await $.ajax({ | ||||||
|  |         url: baseApiUrl + 'attachments/upload/' + noteEditor.getCurrentNoteId(), | ||||||
|  |         headers: server.getHeaders(), | ||||||
|  |         data: formData, | ||||||
|  |         type: 'POST', | ||||||
|  |         contentType: false, // NEEDED, DON'T OMIT THIS | ||||||
|  |         processData: false, // NEEDED, DON'T OMIT THIS | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     await noteTree.reload(); | ||||||
|  |  | ||||||
|  |     await noteTree.activateNode(resp.noteId); | ||||||
| }); | }); | ||||||
| @@ -41,11 +41,11 @@ const link = (function() { | |||||||
|     function goToLink(e) { |     function goToLink(e) { | ||||||
|         e.preventDefault(); |         e.preventDefault(); | ||||||
|  |  | ||||||
|         const linkEl = $(e.target); |         const $link = $(e.target); | ||||||
|         let notePath = linkEl.attr("note-path"); |         let notePath = $link.attr("note-path"); | ||||||
|  |  | ||||||
|         if (!notePath) { |         if (!notePath) { | ||||||
|             const address = linkEl.attr("note-path") ? linkEl.attr("note-path") : linkEl.attr('href'); |             const address = $link.attr("note-path") ? $link.attr("note-path") : $link.attr('href'); | ||||||
|  |  | ||||||
|             if (!address) { |             if (!address) { | ||||||
|                 return; |                 return; | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| "use strict"; | "use strict"; | ||||||
|  |  | ||||||
| const messaging = (function() { | const messaging = (function() { | ||||||
|     const changesToPushCountEl = $("#changes-to-push-count"); |     const $changesToPushCount = $("#changes-to-push-count"); | ||||||
|  |  | ||||||
|     function logError(message) { |     function logError(message) { | ||||||
|         console.log(now(), message); // needs to be separate from .trace() |         console.log(now(), message); // needs to be separate from .trace() | ||||||
| @@ -52,7 +52,7 @@ const messaging = (function() { | |||||||
|             // we don't detect image changes here since images themselves are immutable and references should be |             // we don't detect image changes here since images themselves are immutable and references should be | ||||||
|             // updated in note detail as well |             // updated in note detail as well | ||||||
|  |  | ||||||
|             changesToPushCountEl.html(message.changesToPushCount); |             $changesToPushCount.html(message.changesToPushCount); | ||||||
|         } |         } | ||||||
|         else if (message.type === 'sync-hash-check-failed') { |         else if (message.type === 'sync-hash-check-failed') { | ||||||
|             showError("Sync check failed!", 60000); |             showError("Sync check failed!", 60000); | ||||||
| @@ -84,7 +84,7 @@ const messaging = (function() { | |||||||
|     let connectionBrokenNotification = null; |     let connectionBrokenNotification = null; | ||||||
|  |  | ||||||
|     setInterval(async () => { |     setInterval(async () => { | ||||||
|         if (new Date().getTime() - lastPingTs > 5000) { |         if (new Date().getTime() - lastPingTs > 30000) { | ||||||
|             if (!connectionBrokenNotification) { |             if (!connectionBrokenNotification) { | ||||||
|                 connectionBrokenNotification = $.notify({ |                 connectionBrokenNotification = $.notify({ | ||||||
|                     // options |                     // options | ||||||
|   | |||||||
| @@ -1,14 +1,24 @@ | |||||||
| "use strict"; | "use strict"; | ||||||
|  |  | ||||||
| const noteEditor = (function() { | const noteEditor = (function() { | ||||||
|     const noteTitleEl = $("#note-title"); |     const $noteTitle = $("#note-title"); | ||||||
|     const noteDetailEl = $('#note-detail'); |  | ||||||
|     const noteDetailCodeEl = $('#note-detail-code'); |     const $noteDetail = $('#note-detail'); | ||||||
|     const noteDetailRenderEl = $('#note-detail-render'); |     const $noteDetailCode = $('#note-detail-code'); | ||||||
|     const protectButton = $("#protect-button"); |     const $noteDetailRender = $('#note-detail-render'); | ||||||
|     const unprotectButton = $("#unprotect-button"); |     const $noteDetailAttachment = $('#note-detail-attachment'); | ||||||
|     const noteDetailWrapperEl = $("#note-detail-wrapper"); |  | ||||||
|     const noteIdDisplayEl = $("#note-id-display"); |     const $protectButton = $("#protect-button"); | ||||||
|  |     const $unprotectButton = $("#unprotect-button"); | ||||||
|  |     const $noteDetailWrapper = $("#note-detail-wrapper"); | ||||||
|  |     const $noteIdDisplay = $("#note-id-display"); | ||||||
|  |     const $attributeList = $("#attribute-list"); | ||||||
|  |     const $attributeListInner = $("#attribute-list-inner"); | ||||||
|  |     const $attachmentFileName = $("#attachment-filename"); | ||||||
|  |     const $attachmentFileType = $("#attachment-filetype"); | ||||||
|  |     const $attachmentFileSize = $("#attachment-filesize"); | ||||||
|  |     const $attachmentDownload = $("#attachment-download"); | ||||||
|  |     const $attachmentOpen = $("#attachment-open"); | ||||||
|  |  | ||||||
|     let editor = null; |     let editor = null; | ||||||
|     let codeEditor = null; |     let codeEditor = null; | ||||||
| @@ -67,25 +77,27 @@ const noteEditor = (function() { | |||||||
|  |  | ||||||
|     function updateNoteFromInputs(note) { |     function updateNoteFromInputs(note) { | ||||||
|         if (note.detail.type === 'text') { |         if (note.detail.type === 'text') { | ||||||
|             note.detail.content = editor.getData(); |             let content = editor.getData(); | ||||||
|  |  | ||||||
|             // if content is only tags/whitespace (typically <p> </p>), then just make it empty |             // if content is only tags/whitespace (typically <p> </p>), then just make it empty | ||||||
|             // this is important when setting new note to code |             // this is important when setting new note to code | ||||||
|             if (jQuery(note.detail.content).text().trim() === '') { |             if (jQuery(content).text().trim() === '' && !content.includes("<img")) { | ||||||
|                 note.detail.content = '' |                 content = ''; | ||||||
|             } |             } | ||||||
|  |  | ||||||
|  |             note.detail.content = content; | ||||||
|         } |         } | ||||||
|         else if (note.detail.type === 'code') { |         else if (note.detail.type === 'code') { | ||||||
|             note.detail.content = codeEditor.getValue(); |             note.detail.content = codeEditor.getValue(); | ||||||
|         } |         } | ||||||
|         else if (note.detail.type === 'render') { |         else if (note.detail.type === 'render' || note.detail.type === 'file') { | ||||||
|             // nothing |             // nothing | ||||||
|         } |         } | ||||||
|         else { |         else { | ||||||
|             throwError("Unrecognized type: " + note.detail.type); |             throwError("Unrecognized type: " + note.detail.type); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         const title = noteTitleEl.val(); |         const title = $noteTitle.val(); | ||||||
|  |  | ||||||
|         note.detail.title = title; |         note.detail.title = title; | ||||||
|  |  | ||||||
| @@ -103,9 +115,9 @@ const noteEditor = (function() { | |||||||
|     function setNoteBackgroundIfProtected(note) { |     function setNoteBackgroundIfProtected(note) { | ||||||
|         const isProtected = !!note.detail.isProtected; |         const isProtected = !!note.detail.isProtected; | ||||||
|  |  | ||||||
|         noteDetailWrapperEl.toggleClass("protected", isProtected); |         $noteDetailWrapper.toggleClass("protected", isProtected); | ||||||
|         protectButton.toggle(!isProtected); |         $protectButton.toggle(!isProtected); | ||||||
|         unprotectButton.toggle(isProtected); |         $unprotectButton.toggle(isProtected); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     let isNewNoteCreated = false; |     let isNewNoteCreated = false; | ||||||
| @@ -114,16 +126,71 @@ const noteEditor = (function() { | |||||||
|         isNewNoteCreated = true; |         isNewNoteCreated = true; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     async function setContent(content) { | ||||||
|  |         if (currentNote.detail.type === 'text') { | ||||||
|  |             if (!editor) { | ||||||
|  |                 await requireLibrary(CKEDITOR); | ||||||
|  |  | ||||||
|  |                 editor = await BalloonEditor.create($noteDetail[0], {}); | ||||||
|  |  | ||||||
|  |                 editor.document.on('change', noteChanged); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             // temporary workaround for https://github.com/ckeditor/ckeditor5-enter/issues/49 | ||||||
|  |             editor.setData(content ? content : "<p></p>"); | ||||||
|  |  | ||||||
|  |             $noteDetail.show(); | ||||||
|  |         } | ||||||
|  |         else if (currentNote.detail.type === 'code') { | ||||||
|  |             if (!codeEditor) { | ||||||
|  |                 await requireLibrary(CODE_MIRROR); | ||||||
|  |  | ||||||
|  |                 CodeMirror.keyMap.default["Shift-Tab"] = "indentLess"; | ||||||
|  |                 CodeMirror.keyMap.default["Tab"] = "indentMore"; | ||||||
|  |  | ||||||
|  |                 CodeMirror.modeURL = 'libraries/codemirror/mode/%N/%N.js'; | ||||||
|  |  | ||||||
|  |                 codeEditor = CodeMirror($("#note-detail-code")[0], { | ||||||
|  |                     value: "", | ||||||
|  |                     viewportMargin: Infinity, | ||||||
|  |                     indentUnit: 4, | ||||||
|  |                     matchBrackets: true, | ||||||
|  |                     matchTags: { bothTags: true }, | ||||||
|  |                     highlightSelectionMatches: { showToken: /\w/, annotateScrollbar: false }, | ||||||
|  |                     lint: true, | ||||||
|  |                     gutters: ["CodeMirror-lint-markers"], | ||||||
|  |                     lineNumbers: true | ||||||
|  |                 }); | ||||||
|  |  | ||||||
|  |                 codeEditor.on('change', noteChanged); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             $noteDetailCode.show(); | ||||||
|  |  | ||||||
|  |             // this needs to happen after the element is shown, otherwise the editor won't be refresheds | ||||||
|  |             codeEditor.setValue(content); | ||||||
|  |  | ||||||
|  |             const info = CodeMirror.findModeByMIME(currentNote.detail.mime); | ||||||
|  |  | ||||||
|  |             if (info) { | ||||||
|  |                 codeEditor.setOption("mode", info.mime); | ||||||
|  |                 CodeMirror.autoLoadMode(codeEditor, info.mode); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             codeEditor.refresh(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|     async function loadNoteToEditor(noteId) { |     async function loadNoteToEditor(noteId) { | ||||||
|         currentNote = await loadNote(noteId); |         currentNote = await loadNote(noteId); | ||||||
|  |  | ||||||
|         if (isNewNoteCreated) { |         if (isNewNoteCreated) { | ||||||
|             isNewNoteCreated = false; |             isNewNoteCreated = false; | ||||||
|  |  | ||||||
|             noteTitleEl.focus().select(); |             $noteTitle.focus().select(); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         noteIdDisplayEl.html(noteId); |         $noteIdDisplay.html(noteId); | ||||||
|  |  | ||||||
|         await protected_session.ensureProtectedSession(currentNote.detail.isProtected, false); |         await protected_session.ensureProtectedSession(currentNote.detail.isProtected, false); | ||||||
|  |  | ||||||
| @@ -135,49 +202,38 @@ const noteEditor = (function() { | |||||||
|         // to login, but we chose instead to come to another node - at that point the dialog is still visible and this will close it. |         // to login, but we chose instead to come to another node - at that point the dialog is still visible and this will close it. | ||||||
|         protected_session.ensureDialogIsClosed(); |         protected_session.ensureDialogIsClosed(); | ||||||
|  |  | ||||||
|         noteDetailWrapperEl.show(); |         $noteDetailWrapper.show(); | ||||||
|  |  | ||||||
|         noteChangeDisabled = true; |         noteChangeDisabled = true; | ||||||
|  |  | ||||||
|         noteTitleEl.val(currentNote.detail.title); |         $noteTitle.val(currentNote.detail.title); | ||||||
|  |  | ||||||
|         noteType.setNoteType(currentNote.detail.type); |         noteType.setNoteType(currentNote.detail.type); | ||||||
|         noteType.setNoteMime(currentNote.detail.mime); |         noteType.setNoteMime(currentNote.detail.mime); | ||||||
|  |  | ||||||
|         if (currentNote.detail.type === 'text') { |         $noteDetail.hide(); | ||||||
|             // temporary workaround for https://github.com/ckeditor/ckeditor5-enter/issues/49 |         $noteDetailCode.hide(); | ||||||
|             editor.setData(currentNote.detail.content ? currentNote.detail.content : "<p></p>"); |         $noteDetailRender.html('').hide(); | ||||||
|  |         $noteDetailAttachment.hide(); | ||||||
|  |  | ||||||
|             noteDetailEl.show(); |         if (currentNote.detail.type === 'render') { | ||||||
|             noteDetailCodeEl.hide(); |             $noteDetailRender.show(); | ||||||
|             noteDetailRenderEl.html('').hide(); |  | ||||||
|  |             const bundle = await server.get('script/bundle/' + getCurrentNoteId()); | ||||||
|  |  | ||||||
|  |             $noteDetailRender.html(bundle.html); | ||||||
|  |  | ||||||
|  |             executeBundle(bundle); | ||||||
|         } |         } | ||||||
|         else if (currentNote.detail.type === 'code') { |         else if (currentNote.detail.type === 'file') { | ||||||
|             noteDetailEl.hide(); |             $noteDetailAttachment.show(); | ||||||
|             noteDetailCodeEl.show(); |  | ||||||
|             noteDetailRenderEl.html('').hide(); |  | ||||||
|  |  | ||||||
|             // this needs to happen after the element is shown, otherwise the editor won't be refresheds |             $attachmentFileName.text(currentNote.attributes.original_file_name); | ||||||
|             codeEditor.setValue(currentNote.detail.content); |             $attachmentFileSize.text(currentNote.attributes.file_size + " bytes"); | ||||||
|  |             $attachmentFileType.text(currentNote.detail.mime); | ||||||
|             const info = CodeMirror.findModeByMIME(currentNote.detail.mime); |  | ||||||
|  |  | ||||||
|             if (info) { |  | ||||||
|                 codeEditor.setOption("mode", info.mime); |  | ||||||
|                 CodeMirror.autoLoadMode(codeEditor, info.mode); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|         else if (currentNote.detail.type === 'render') { |  | ||||||
|             noteDetailEl.hide(); |  | ||||||
|             noteDetailCodeEl.hide(); |  | ||||||
|             noteDetailRenderEl.html('').show(); |  | ||||||
|  |  | ||||||
|             const subTree = await server.get('script/subtree/' + getCurrentNoteId()); |  | ||||||
|  |  | ||||||
|             noteDetailRenderEl.html(subTree); |  | ||||||
|         } |         } | ||||||
|         else { |         else { | ||||||
|             throwError("Unrecognized type " + currentNote.detail.type); |             await setContent(currentNote.detail.content); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         noteChangeDisabled = false; |         noteChangeDisabled = false; | ||||||
| @@ -186,7 +242,28 @@ const noteEditor = (function() { | |||||||
|         noteTree.setNoteTreeBackgroundBasedOnProtectedStatus(noteId); |         noteTree.setNoteTreeBackgroundBasedOnProtectedStatus(noteId); | ||||||
|  |  | ||||||
|         // after loading new note make sure editor is scrolled to the top |         // after loading new note make sure editor is scrolled to the top | ||||||
|         noteDetailWrapperEl.scrollTop(0); |         $noteDetailWrapper.scrollTop(0); | ||||||
|  |  | ||||||
|  |         loadAttributeList(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async function loadAttributeList() { | ||||||
|  |         const noteId = getCurrentNoteId(); | ||||||
|  |  | ||||||
|  |         const attributes = await server.get('notes/' + noteId + '/attributes'); | ||||||
|  |  | ||||||
|  |         $attributeListInner.html(''); | ||||||
|  |  | ||||||
|  |         if (attributes.length > 0) { | ||||||
|  |             for (const attr of attributes) { | ||||||
|  |                 $attributeListInner.append(formatAttribute(attr) + " "); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             $attributeList.show(); | ||||||
|  |         } | ||||||
|  |         else { | ||||||
|  |             $attributeList.hide(); | ||||||
|  |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     async function loadNote(noteId) { |     async function loadNote(noteId) { | ||||||
| @@ -201,12 +278,12 @@ const noteEditor = (function() { | |||||||
|         const note = getCurrentNote(); |         const note = getCurrentNote(); | ||||||
|  |  | ||||||
|         if (note.detail.type === 'text') { |         if (note.detail.type === 'text') { | ||||||
|             noteDetailEl.focus(); |             $noteDetail.focus(); | ||||||
|         } |         } | ||||||
|         else if (note.detail.type === 'code') { |         else if (note.detail.type === 'code') { | ||||||
|             codeEditor.focus(); |             codeEditor.focus(); | ||||||
|         } |         } | ||||||
|         else if (note.detail.type === 'render') { |         else if (note.detail.type === 'render' || note.detail.type === 'file') { | ||||||
|             // do nothing |             // do nothing | ||||||
|         } |         } | ||||||
|         else { |         else { | ||||||
| @@ -225,51 +302,50 @@ const noteEditor = (function() { | |||||||
|             // make sure note is saved so we load latest changes |             // make sure note is saved so we load latest changes | ||||||
|             await saveNoteIfChanged(); |             await saveNoteIfChanged(); | ||||||
|  |  | ||||||
|             const script = await server.get('script/subtree/' + getCurrentNoteId()); |             if (currentNote.detail.mime.endsWith("env=frontend")) { | ||||||
|  |                 const bundle = await server.get('script/bundle/' + getCurrentNoteId()); | ||||||
|  |  | ||||||
|             executeScript(script); |                 executeBundle(bundle); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             if (currentNote.detail.mime.endsWith("env=backend")) { | ||||||
|  |                 await server.post('script/run/' + getCurrentNoteId()); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             showMessage("Note executed"); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     $attachmentDownload.click(() => download(getAttachmentUrl())); | ||||||
|  |  | ||||||
|  |     $attachmentOpen.click(() => { | ||||||
|  |         if (isElectron()) { | ||||||
|  |             const open = require("open"); | ||||||
|  |  | ||||||
|  |             open(getAttachmentUrl()); | ||||||
|  |         } | ||||||
|  |         else { | ||||||
|  |             window.location.href = getAttachmentUrl(); | ||||||
|  |         } | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     function getAttachmentUrl() { | ||||||
|  |         // electron needs absolute URL so we extract current host, port, protocol | ||||||
|  |         return getHost() + "/api/attachments/download/" + getCurrentNoteId() | ||||||
|  |             + "?protectedSessionId=" + encodeURIComponent(protected_session.getProtectedSessionId()); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     $(document).ready(() => { |     $(document).ready(() => { | ||||||
|         noteTitleEl.on('input', () => { |         $noteTitle.on('input', () => { | ||||||
|             noteChanged(); |             noteChanged(); | ||||||
|  |  | ||||||
|             const title = noteTitleEl.val(); |             const title = $noteTitle.val(); | ||||||
|  |  | ||||||
|             noteTree.setNoteTitle(getCurrentNoteId(), title); |             noteTree.setNoteTitle(getCurrentNoteId(), title); | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|         BalloonEditor |  | ||||||
|             .create(document.querySelector('#note-detail'), { |  | ||||||
|             }) |  | ||||||
|             .then(edit => { |  | ||||||
|                 editor = edit; |  | ||||||
|  |  | ||||||
|                 editor.document.on('change', noteChanged); |  | ||||||
|             }) |  | ||||||
|             .catch(error => { |  | ||||||
|                 console.error(error); |  | ||||||
|             }); |  | ||||||
|  |  | ||||||
|         CodeMirror.keyMap.default["Shift-Tab"] = "indentLess"; |  | ||||||
|         CodeMirror.keyMap.default["Tab"] = "indentMore"; |  | ||||||
|  |  | ||||||
|         CodeMirror.modeURL = 'libraries/codemirror/mode/%N/%N.js'; |  | ||||||
|  |  | ||||||
|         codeEditor = CodeMirror($("#note-detail-code")[0], { |  | ||||||
|             value: "", |  | ||||||
|             viewportMargin: Infinity, |  | ||||||
|             indentUnit: 4, |  | ||||||
|             matchBrackets: true, |  | ||||||
|             matchTags: { bothTags: true }, |  | ||||||
|             highlightSelectionMatches: { showToken: /\w/, annotateScrollbar: false } |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|         codeEditor.on('change', noteChanged); |  | ||||||
|  |  | ||||||
|         // so that tab jumps from note title (which has tabindex 1) |         // so that tab jumps from note title (which has tabindex 1) | ||||||
|         noteDetailEl.attr("tabindex", 2); |         $noteDetail.attr("tabindex", 2); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     $(document).bind('keydown', "ctrl+return", executeCurrentNote); |     $(document).bind('keydown', "ctrl+return", executeCurrentNote); | ||||||
| @@ -290,6 +366,8 @@ const noteEditor = (function() { | |||||||
|         newNoteCreated, |         newNoteCreated, | ||||||
|         getEditor, |         getEditor, | ||||||
|         focus, |         focus, | ||||||
|         executeCurrentNote |         executeCurrentNote, | ||||||
|  |         loadAttributeList, | ||||||
|  |         setContent | ||||||
|     }; |     }; | ||||||
| })(); | })(); | ||||||
| @@ -1,9 +1,11 @@ | |||||||
| "use strict"; | "use strict"; | ||||||
|  |  | ||||||
| const noteTree = (function() { | const noteTree = (function() { | ||||||
|     const treeEl = $("#tree"); |     const $tree = $("#tree"); | ||||||
|     const parentListEl = $("#parent-list"); |     const $parentList = $("#parent-list"); | ||||||
|     const parentListListEl = $("#parent-list-list"); |     const $parentListList = $("#parent-list-inner"); | ||||||
|  |  | ||||||
|  |     let instanceName = null; // should have better place | ||||||
|  |  | ||||||
|     let startNotePath = null; |     let startNotePath = null; | ||||||
|     let notesTreeMap = {}; |     let notesTreeMap = {}; | ||||||
| @@ -14,6 +16,8 @@ const noteTree = (function() { | |||||||
|     let parentChildToNoteTreeId = {}; |     let parentChildToNoteTreeId = {}; | ||||||
|     let noteIdToTitle = {}; |     let noteIdToTitle = {}; | ||||||
|  |  | ||||||
|  |     let hiddenInAutocomplete = {}; | ||||||
|  |  | ||||||
|     function getNoteTreeId(parentNoteId, childNoteId) { |     function getNoteTreeId(parentNoteId, childNoteId) { | ||||||
|         assertArguments(parentNoteId, childNoteId); |         assertArguments(parentNoteId, childNoteId); | ||||||
|  |  | ||||||
| @@ -50,7 +54,7 @@ const noteTree = (function() { | |||||||
|  |  | ||||||
|     // note that if you want to access data like noteId or isProtected, you need to go into "data" property |     // note that if you want to access data like noteId or isProtected, you need to go into "data" property | ||||||
|     function getCurrentNode() { |     function getCurrentNode() { | ||||||
|         return treeEl.fancytree("getActiveNode"); |         return $tree.fancytree("getActiveNode"); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     function getCurrentNotePath() { |     function getCurrentNotePath() { | ||||||
| @@ -153,6 +157,12 @@ const noteTree = (function() { | |||||||
|         if (note.type === 'code') { |         if (note.type === 'code') { | ||||||
|             extraClasses.push("code"); |             extraClasses.push("code"); | ||||||
|         } |         } | ||||||
|  |         else if (note.type === 'render') { | ||||||
|  |             extraClasses.push('render'); | ||||||
|  |         } | ||||||
|  |         else if (note.type === 'file') { | ||||||
|  |             extraClasses.push('attachment'); | ||||||
|  |         } | ||||||
|  |  | ||||||
|         return extraClasses.join(" "); |         return extraClasses.join(" "); | ||||||
|     } |     } | ||||||
| @@ -312,11 +322,11 @@ const noteTree = (function() { | |||||||
|         } |         } | ||||||
|  |  | ||||||
|         if (parents.length <= 1) { |         if (parents.length <= 1) { | ||||||
|             parentListEl.hide(); |             $parentList.hide(); | ||||||
|         } |         } | ||||||
|         else { |         else { | ||||||
|             parentListEl.show(); |             $parentList.show(); | ||||||
|             parentListListEl.empty(); |             $parentListList.empty(); | ||||||
|  |  | ||||||
|             for (const parentNoteId of parents) { |             for (const parentNoteId of parents) { | ||||||
|                 const parentNotePath = getSomeNotePath(parentNoteId); |                 const parentNotePath = getSomeNotePath(parentNoteId); | ||||||
| @@ -333,7 +343,7 @@ const noteTree = (function() { | |||||||
|                     item = link.createNoteLink(notePath, title); |                     item = link.createNoteLink(notePath, title); | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|                 parentListListEl.append($("<li/>").append(item)); |                 $parentListList.append($("<li/>").append(item)); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| @@ -541,7 +551,7 @@ const noteTree = (function() { | |||||||
|             } |             } | ||||||
|         }; |         }; | ||||||
|  |  | ||||||
|         treeEl.fancytree({ |         $tree.fancytree({ | ||||||
|             autoScroll: true, |             autoScroll: true, | ||||||
|             keyboard: false, // we takover keyboard handling in the hotkeys plugin |             keyboard: false, // we takover keyboard handling in the hotkeys plugin | ||||||
|             extensions: ["hotkeys", "filter", "dnd", "clones"], |             extensions: ["hotkeys", "filter", "dnd", "clones"], | ||||||
| @@ -622,11 +632,11 @@ const noteTree = (function() { | |||||||
|             } |             } | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|         treeEl.contextmenu(contextMenu.contextMenuSettings); |         $tree.contextmenu(contextMenu.contextMenuSettings); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     function getTree() { |     function getTree() { | ||||||
|         return treeEl.fancytree('getTree'); |         return $tree.fancytree('getTree'); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     async function reload() { |     async function reload() { | ||||||
| @@ -640,23 +650,29 @@ const noteTree = (function() { | |||||||
|         return document.location.hash.substr(1); // strip initial # |         return document.location.hash.substr(1); // strip initial # | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     function loadTree() { |     async function loadTree() { | ||||||
|         return server.get('tree').then(resp => { |         const resp = await server.get('tree'); | ||||||
|             startNotePath = resp.start_note_path; |         startNotePath = resp.start_note_path; | ||||||
|  |         instanceName = resp.instanceName; | ||||||
|  |  | ||||||
|             if (document.location.hash) { |         if (document.location.hash) { | ||||||
|                 startNotePath = getNotePathFromAddress(); |             startNotePath = getNotePathFromAddress(); | ||||||
|             } |         } | ||||||
|  |  | ||||||
|             return prepareNoteTree(resp.notes); |         hiddenInAutocomplete = {}; | ||||||
|         }); |  | ||||||
|  |         for (const noteId of resp.hiddenInAutocomplete) { | ||||||
|  |             hiddenInAutocomplete[noteId] = true; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return prepareNoteTree(resp.notes); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     $(() => loadTree().then(noteTree => initFancyTree(noteTree))); |     $(() => loadTree().then(noteTree => initFancyTree(noteTree))); | ||||||
|  |  | ||||||
|     function collapseTree(node = null) { |     function collapseTree(node = null) { | ||||||
|         if (!node) { |         if (!node) { | ||||||
|             node = treeEl.fancytree("getRootNode"); |             node = $tree.fancytree("getRootNode"); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         node.setExpanded(false); |         node.setExpanded(false); | ||||||
| @@ -703,9 +719,16 @@ const noteTree = (function() { | |||||||
|             titlePath = ''; |             titlePath = ''; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         // https://github.com/zadam/trilium/issues/46 | ||||||
|  |         // unfortunately not easy to implement because we don't have an easy access to note's isProtected property | ||||||
|  |  | ||||||
|         const autocompleteItems = []; |         const autocompleteItems = []; | ||||||
|  |  | ||||||
|         for (const childNoteId of parentToChildren[parentNoteId]) { |         for (const childNoteId of parentToChildren[parentNoteId]) { | ||||||
|  |             if (hiddenInAutocomplete[childNoteId]) { | ||||||
|  |                 continue; | ||||||
|  |             } | ||||||
|  |  | ||||||
|             const childNotePath = (notePath ? (notePath + '/') : '') + childNoteId; |             const childNotePath = (notePath ? (notePath + '/') : '') + childNoteId; | ||||||
|             const childTitlePath = (titlePath ? (titlePath + ' / ') : '') + getNoteTitle(childNoteId, parentNoteId); |             const childTitlePath = (titlePath ? (titlePath + ' / ') : '') + getNoteTitle(childNoteId, parentNoteId); | ||||||
|  |  | ||||||
| @@ -733,7 +756,7 @@ const noteTree = (function() { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     async function createNewTopLevelNote() { |     async function createNewTopLevelNote() { | ||||||
|         const rootNode = treeEl.fancytree("getRootNode"); |         const rootNode = $tree.fancytree("getRootNode"); | ||||||
|  |  | ||||||
|         await createNote(rootNode, "root", "into"); |         await createNote(rootNode, "root", "into"); | ||||||
|     } |     } | ||||||
| @@ -775,7 +798,7 @@ const noteTree = (function() { | |||||||
|         }; |         }; | ||||||
|  |  | ||||||
|         if (target === 'after') { |         if (target === 'after') { | ||||||
|             node.appendSibling(newNode).setActive(true); |             await node.appendSibling(newNode).setActive(true); | ||||||
|         } |         } | ||||||
|         else if (target === 'into') { |         else if (target === 'into') { | ||||||
|             if (!node.getChildren() && node.isFolder()) { |             if (!node.getChildren() && node.isFolder()) { | ||||||
| @@ -785,7 +808,7 @@ const noteTree = (function() { | |||||||
|                 node.addChildren(newNode); |                 node.addChildren(newNode); | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             node.getLastChild().setActive(true); |             await node.getLastChild().setActive(true); | ||||||
|  |  | ||||||
|             node.folder = true; |             node.folder = true; | ||||||
|             node.renderTitle(); |             node.renderTitle(); | ||||||
| @@ -794,6 +817,8 @@ const noteTree = (function() { | |||||||
|             throwError("Unrecognized target: " + target); |             throwError("Unrecognized target: " + target); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         clearSelectedNodes(); // to unmark previously active node | ||||||
|  |  | ||||||
|         showMessage("Created!"); |         showMessage("Created!"); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -803,6 +828,14 @@ const noteTree = (function() { | |||||||
|         await reload(); |         await reload(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     function noteExists(noteId) { | ||||||
|  |         return !!childToParents[noteId]; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     function getInstanceName() { | ||||||
|  |         return instanceName; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     $(document).bind('keydown', 'ctrl+o', e => { |     $(document).bind('keydown', 'ctrl+o', e => { | ||||||
|         const node = getCurrentNode(); |         const node = getCurrentNode(); | ||||||
|         const parentNoteId = node.data.parentNoteId; |         const parentNoteId = node.data.parentNoteId; | ||||||
| @@ -876,6 +909,8 @@ const noteTree = (function() { | |||||||
|         removeParentChildRelation, |         removeParentChildRelation, | ||||||
|         setParentChildRelation, |         setParentChildRelation, | ||||||
|         getSelectedNodes, |         getSelectedNodes, | ||||||
|         sortAlphabetically |         sortAlphabetically, | ||||||
|  |         noteExists, | ||||||
|  |         getInstanceName | ||||||
|     }; |     }; | ||||||
| })(); | })(); | ||||||
| @@ -1,7 +1,7 @@ | |||||||
| "use strict"; | "use strict"; | ||||||
|  |  | ||||||
| const noteType = (function() { | const noteType = (function() { | ||||||
|     const executeScriptButton = $("#execute-script-button"); |     const $executeScriptButton = $("#execute-script-button"); | ||||||
|     const noteTypeModel = new NoteTypeModel(); |     const noteTypeModel = new NoteTypeModel(); | ||||||
|  |  | ||||||
|     function NoteTypeModel() { |     function NoteTypeModel() { | ||||||
| @@ -25,7 +25,8 @@ const noteType = (function() { | |||||||
|             { mime: 'text/html', title: 'HTML' }, |             { mime: 'text/html', title: 'HTML' }, | ||||||
|             { mime: 'message/http', title: 'HTTP' }, |             { mime: 'message/http', title: 'HTTP' }, | ||||||
|             { mime: 'text/x-java', title: 'Java' }, |             { mime: 'text/x-java', title: 'Java' }, | ||||||
|             { mime: 'application/javascript', title: 'JavaScript' }, |             { mime: 'application/javascript;env=frontend', title: 'JavaScript frontend' }, | ||||||
|  |             { mime: 'application/javascript;env=backend', title: 'JavaScript backend' }, | ||||||
|             { mime: 'application/json', title: 'JSON' }, |             { mime: 'application/json', title: 'JSON' }, | ||||||
|             { mime: 'text/x-kotlin', title: 'Kotlin' }, |             { mime: 'text/x-kotlin', title: 'Kotlin' }, | ||||||
|             { mime: 'text/x-lua', title: 'Lua' }, |             { mime: 'text/x-lua', title: 'Lua' }, | ||||||
| @@ -65,11 +66,18 @@ const noteType = (function() { | |||||||
|             else if (type === 'render') { |             else if (type === 'render') { | ||||||
|                 return 'Render HTML note'; |                 return 'Render HTML note'; | ||||||
|             } |             } | ||||||
|  |             else if (type === 'file') { | ||||||
|  |                 return 'Attachment'; | ||||||
|  |             } | ||||||
|             else { |             else { | ||||||
|                 throwError('Unrecognized type: ' + type); |                 throwError('Unrecognized type: ' + type); | ||||||
|             } |             } | ||||||
|         }; |         }; | ||||||
|  |  | ||||||
|  |         this.isDisabled = function() { | ||||||
|  |             return self.type() === "file"; | ||||||
|  |         }; | ||||||
|  |  | ||||||
|         async function save() { |         async function save() { | ||||||
|             const note = noteEditor.getCurrentNote(); |             const note = noteEditor.getCurrentNote(); | ||||||
|  |  | ||||||
| @@ -114,7 +122,7 @@ const noteType = (function() { | |||||||
|         }; |         }; | ||||||
|  |  | ||||||
|         this.updateExecuteScriptButtonVisibility = function() { |         this.updateExecuteScriptButtonVisibility = function() { | ||||||
|             executeScriptButton.toggle(self.mime() === 'application/javascript'); |             $executeScriptButton.toggle(self.mime().startsWith('application/javascript')); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,10 +1,10 @@ | |||||||
| "use strict"; | "use strict"; | ||||||
|  |  | ||||||
| const protected_session = (function() { | const protected_session = (function() { | ||||||
|     const dialogEl = $("#protected-session-password-dialog"); |     const $dialog = $("#protected-session-password-dialog"); | ||||||
|     const passwordFormEl = $("#protected-session-password-form"); |     const $passwordForm = $("#protected-session-password-form"); | ||||||
|     const passwordEl = $("#protected-session-password"); |     const $password = $("#protected-session-password"); | ||||||
|     const noteDetailWrapperEl = $("#note-detail-wrapper"); |     const $noteDetailWrapper = $("#note-detail-wrapper"); | ||||||
|  |  | ||||||
|     let protectedSessionDeferred = null; |     let protectedSessionDeferred = null; | ||||||
|     let lastProtectedSessionOperationDate = null; |     let lastProtectedSessionOperationDate = null; | ||||||
| @@ -25,9 +25,11 @@ const protected_session = (function() { | |||||||
|         if (requireProtectedSession && !isProtectedSessionAvailable()) { |         if (requireProtectedSession && !isProtectedSessionAvailable()) { | ||||||
|             protectedSessionDeferred = dfd; |             protectedSessionDeferred = dfd; | ||||||
|  |  | ||||||
|             noteDetailWrapperEl.hide(); |             if (noteTree.getCurrentNode().data.isProtected) { | ||||||
|  |                 $noteDetailWrapper.hide(); | ||||||
|  |             } | ||||||
|  |  | ||||||
|             dialogEl.dialog({ |             $dialog.dialog({ | ||||||
|                 modal: modal, |                 modal: modal, | ||||||
|                 width: 400, |                 width: 400, | ||||||
|                 open: () => { |                 open: () => { | ||||||
| @@ -46,8 +48,8 @@ const protected_session = (function() { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     async function setupProtectedSession() { |     async function setupProtectedSession() { | ||||||
|         const password = passwordEl.val(); |         const password = $password.val(); | ||||||
|         passwordEl.val(""); |         $password.val(""); | ||||||
|  |  | ||||||
|         const response = await enterProtectedSession(password); |         const response = await enterProtectedSession(password); | ||||||
|  |  | ||||||
| @@ -58,15 +60,15 @@ const protected_session = (function() { | |||||||
|  |  | ||||||
|         protectedSessionId = response.protectedSessionId; |         protectedSessionId = response.protectedSessionId; | ||||||
|  |  | ||||||
|         dialogEl.dialog("close"); |         $dialog.dialog("close"); | ||||||
|  |  | ||||||
|         noteEditor.reload(); |         noteEditor.reload(); | ||||||
|         noteTree.reload(); |         noteTree.reload(); | ||||||
|  |  | ||||||
|         if (protectedSessionDeferred !== null) { |         if (protectedSessionDeferred !== null) { | ||||||
|             ensureDialogIsClosed(dialogEl, passwordEl); |             ensureDialogIsClosed($dialog, $password); | ||||||
|  |  | ||||||
|             noteDetailWrapperEl.show(); |             $noteDetailWrapper.show(); | ||||||
|  |  | ||||||
|             protectedSessionDeferred.resolve(); |             protectedSessionDeferred.resolve(); | ||||||
|  |  | ||||||
| @@ -77,11 +79,11 @@ const protected_session = (function() { | |||||||
|     function ensureDialogIsClosed() { |     function ensureDialogIsClosed() { | ||||||
|         // this may fal if the dialog has not been previously opened |         // this may fal if the dialog has not been previously opened | ||||||
|         try { |         try { | ||||||
|             dialogEl.dialog('close'); |             $dialog.dialog('close'); | ||||||
|         } |         } | ||||||
|         catch (e) {} |         catch (e) {} | ||||||
|  |  | ||||||
|         passwordEl.val(''); |         $password.val(''); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     async function enterProtectedSession(password) { |     async function enterProtectedSession(password) { | ||||||
| @@ -155,7 +157,7 @@ const protected_session = (function() { | |||||||
|         noteEditor.reload(); |         noteEditor.reload(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     passwordFormEl.submit(() => { |     $passwordForm.submit(() => { | ||||||
|         setupProtectedSession(); |         setupProtectedSession(); | ||||||
|  |  | ||||||
|         return false; |         return false; | ||||||
|   | |||||||
| @@ -1,40 +1,40 @@ | |||||||
| "use strict"; | "use strict"; | ||||||
|  |  | ||||||
| const searchTree = (function() { | const searchTree = (function() { | ||||||
|     const treeEl = $("#tree"); |     const $tree = $("#tree"); | ||||||
|     const searchInputEl = $("input[name='search-text']"); |     const $searchInput = $("input[name='search-text']"); | ||||||
|     const resetSearchButton = $("button#reset-search-button"); |     const $resetSearchButton = $("button#reset-search-button"); | ||||||
|     const searchBoxEl = $("#search-box"); |     const $searchBox = $("#search-box"); | ||||||
|  |  | ||||||
|     resetSearchButton.click(resetSearch); |     $resetSearchButton.click(resetSearch); | ||||||
|  |  | ||||||
|     function toggleSearch() { |     function toggleSearch() { | ||||||
|         if (searchBoxEl.is(":hidden")) { |         if ($searchBox.is(":hidden")) { | ||||||
|             searchBoxEl.show(); |             $searchBox.show(); | ||||||
|             searchInputEl.focus(); |             $searchInput.focus(); | ||||||
|         } |         } | ||||||
|         else { |         else { | ||||||
|             resetSearch(); |             resetSearch(); | ||||||
|  |  | ||||||
|             searchBoxEl.hide(); |             $searchBox.hide(); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     function resetSearch() { |     function resetSearch() { | ||||||
|         searchInputEl.val(""); |         $searchInput.val(""); | ||||||
|  |  | ||||||
|         getTree().clearFilter(); |         getTree().clearFilter(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     function getTree() { |     function getTree() { | ||||||
|         return treeEl.fancytree('getTree'); |         return $tree.fancytree('getTree'); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     searchInputEl.keyup(async e => { |     $searchInput.keyup(async e => { | ||||||
|         const searchText = searchInputEl.val(); |         const searchText = $searchInput.val(); | ||||||
|  |  | ||||||
|         if (e && e.which === $.ui.keyCode.ESCAPE || $.trim(searchText) === "") { |         if (e && e.which === $.ui.keyCode.ESCAPE || $.trim(searchText) === "") { | ||||||
|             resetSearchButton.click(); |             $resetSearchButton.click(); | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -31,16 +31,6 @@ const server = (function() { | |||||||
|         return await call('DELETE', url); |         return await call('DELETE', url); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     async function exec(params, script) { |  | ||||||
|         if (typeof script === "function") { |  | ||||||
|             script = script.toString(); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         const ret = await post('script/exec', { script: script, params: params }); |  | ||||||
|  |  | ||||||
|         return ret.executionResult; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     let i = 1; |     let i = 1; | ||||||
|     const reqResolves = {}; |     const reqResolves = {}; | ||||||
|  |  | ||||||
| @@ -104,6 +94,8 @@ const server = (function() { | |||||||
|         post, |         post, | ||||||
|         put, |         put, | ||||||
|         remove, |         remove, | ||||||
|         exec |         ajax, | ||||||
|  |         // don't remove, used from CKEditor image upload! | ||||||
|  |         getHeaders | ||||||
|     } |     } | ||||||
| })(); | })(); | ||||||
| @@ -1,14 +1,14 @@ | |||||||
| "use strict"; | "use strict"; | ||||||
|  |  | ||||||
| const treeUtils = (function() { | const treeUtils = (function() { | ||||||
|     const treeEl = $("#tree"); |     const $tree = $("#tree"); | ||||||
|  |  | ||||||
|     function getParentProtectedStatus(node) { |     function getParentProtectedStatus(node) { | ||||||
|         return isTopLevelNode(node) ? 0 : node.getParent().data.isProtected; |         return isTopLevelNode(node) ? 0 : node.getParent().data.isProtected; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     function getNodeByKey(key) { |     function getNodeByKey(key) { | ||||||
|         return treeEl.fancytree('getNodeByKey', key); |         return $tree.fancytree('getNodeByKey', key); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     function getNoteIdFromNotePath(notePath) { |     function getNoteIdFromNotePath(notePath) { | ||||||
|   | |||||||
| @@ -115,6 +115,108 @@ async function stopWatch(what, func) { | |||||||
|     return ret; |     return ret; | ||||||
| } | } | ||||||
|  |  | ||||||
| function executeScript(script) { | async function executeBundle(bundle) { | ||||||
|     eval("(async function() {" + script + "})()"); |     const apiContext = ScriptContext(bundle.note, bundle.allNotes); | ||||||
|  |  | ||||||
|  |     return await (function() { return eval(`const apiContext = this; (async function() { ${bundle.script}\r\n})()`); }.call(apiContext)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function formatValueWithWhitespace(val) { | ||||||
|  |     return /[^\w_-]/.test(val) ? '"' + val + '"' : val; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function formatAttribute(attr) { | ||||||
|  |     let str = "@" + formatValueWithWhitespace(attr.name); | ||||||
|  |  | ||||||
|  |     if (attr.value !== "") { | ||||||
|  |         str += "=" + formatValueWithWhitespace(attr.value); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return str; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const CKEDITOR = { "js": ["libraries/ckeditor/ckeditor.js"] }; | ||||||
|  |  | ||||||
|  | const CODE_MIRROR = { | ||||||
|  |     js: [ | ||||||
|  |         "libraries/codemirror/codemirror.js", | ||||||
|  |         "libraries/codemirror/addon/mode/loadmode.js", | ||||||
|  |         "libraries/codemirror/addon/fold/xml-fold.js", | ||||||
|  |         "libraries/codemirror/addon/edit/matchbrackets.js", | ||||||
|  |         "libraries/codemirror/addon/edit/matchtags.js", | ||||||
|  |         "libraries/codemirror/addon/search/match-highlighter.js", | ||||||
|  |         "libraries/codemirror/mode/meta.js", | ||||||
|  |         "libraries/codemirror/addon/lint/lint.js", | ||||||
|  |         "libraries/codemirror/addon/lint/eslint.js" | ||||||
|  |     ], | ||||||
|  |     css: [ | ||||||
|  |         "libraries/codemirror/codemirror.css", | ||||||
|  |         "libraries/codemirror/addon/lint/lint.css" | ||||||
|  |     ] | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const ESLINT = { js: [ "libraries/eslint.js" ] }; | ||||||
|  |  | ||||||
|  | async function requireLibrary(library) { | ||||||
|  |     if (library.css) { | ||||||
|  |         library.css.map(cssUrl => requireCss(cssUrl)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (library.js) { | ||||||
|  |         for (const scriptUrl of library.js) { | ||||||
|  |             await requireScript(scriptUrl); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const dynamicallyLoadedScripts = []; | ||||||
|  |  | ||||||
|  | async function requireScript(url) { | ||||||
|  |     if (!dynamicallyLoadedScripts.includes(url)) { | ||||||
|  |         dynamicallyLoadedScripts.push(url); | ||||||
|  |  | ||||||
|  |         return await $.ajax({ | ||||||
|  |             url: url, | ||||||
|  |             dataType: "script", | ||||||
|  |             cache: true | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function requireCss(url) { | ||||||
|  |     const css = Array | ||||||
|  |         .from(document.querySelectorAll('link')) | ||||||
|  |         .map(scr => scr.href); | ||||||
|  |  | ||||||
|  |     if (!css.includes(url)) { | ||||||
|  |         $('head').append($('<link rel="stylesheet" type="text/css" />').attr('href', url)); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getHost() { | ||||||
|  |     const url = new URL(window.location.href); | ||||||
|  |     return url.protocol + "//" + url.hostname + ":" + url.port; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function download(url) { | ||||||
|  |     if (isElectron()) { | ||||||
|  |         const remote = require('electron').remote; | ||||||
|  |  | ||||||
|  |         remote.getCurrentWebContents().downloadURL(url); | ||||||
|  |     } | ||||||
|  |     else { | ||||||
|  |         window.location.href = url; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function toObject(array, fn) { | ||||||
|  |     const obj = {}; | ||||||
|  |  | ||||||
|  |     for (const item of array) { | ||||||
|  |         const ret = fn(item); | ||||||
|  |  | ||||||
|  |         obj[ret[0]] = ret[1]; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return obj; | ||||||
| } | } | ||||||
							
								
								
									
										2
									
								
								src/public/libraries/ckeditor/ckeditor.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								src/public/libraries/ckeditor/ckeditor.js
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @@ -102,18 +102,23 @@ | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   var currentlyHighlighted = null; |  | ||||||
|   function doMatchBrackets(cm) { |   function doMatchBrackets(cm) { | ||||||
|     cm.operation(function() { |     cm.operation(function() { | ||||||
|       if (currentlyHighlighted) {currentlyHighlighted(); currentlyHighlighted = null;} |       if (cm.state.matchBrackets.currentlyHighlighted) { | ||||||
|       currentlyHighlighted = matchBrackets(cm, false, cm.state.matchBrackets); |         cm.state.matchBrackets.currentlyHighlighted(); | ||||||
|  |         cm.state.matchBrackets.currentlyHighlighted = null; | ||||||
|  |       } | ||||||
|  |       cm.state.matchBrackets.currentlyHighlighted = matchBrackets(cm, false, cm.state.matchBrackets); | ||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   CodeMirror.defineOption("matchBrackets", false, function(cm, val, old) { |   CodeMirror.defineOption("matchBrackets", false, function(cm, val, old) { | ||||||
|     if (old && old != CodeMirror.Init) { |     if (old && old != CodeMirror.Init) { | ||||||
|       cm.off("cursorActivity", doMatchBrackets); |       cm.off("cursorActivity", doMatchBrackets); | ||||||
|       if (currentlyHighlighted) {currentlyHighlighted(); currentlyHighlighted = null;} |       if (cm.state.matchBrackets && cm.state.matchBrackets.currentlyHighlighted) { | ||||||
|  |         cm.state.matchBrackets.currentlyHighlighted(); | ||||||
|  |         cm.state.matchBrackets.currentlyHighlighted = null; | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|     if (val) { |     if (val) { | ||||||
|       cm.state.matchBrackets = typeof val == "object" ? val : {}; |       cm.state.matchBrackets = typeof val == "object" ? val : {}; | ||||||
|   | |||||||
| @@ -138,7 +138,7 @@ | |||||||
|     var iter = new Iter(cm, start.line, 0); |     var iter = new Iter(cm, start.line, 0); | ||||||
|     for (;;) { |     for (;;) { | ||||||
|       var openTag = toNextTag(iter), end; |       var openTag = toNextTag(iter), end; | ||||||
|       if (!openTag || iter.line != start.line || !(end = toTagEnd(iter))) return; |       if (!openTag || !(end = toTagEnd(iter)) || iter.line != start.line) return; | ||||||
|       if (!openTag[1] && end != "selfClose") { |       if (!openTag[1] && end != "selfClose") { | ||||||
|         var startPos = Pos(iter.line, iter.ch); |         var startPos = Pos(iter.line, iter.ch); | ||||||
|         var endPos = findMatchingClose(iter, openTag[2]); |         var endPos = findMatchingClose(iter, openTag[2]); | ||||||
|   | |||||||
							
								
								
									
										92
									
								
								src/public/libraries/codemirror/addon/lint/eslint.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								src/public/libraries/codemirror/addon/lint/eslint.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,92 @@ | |||||||
|  | // CodeMirror, copyright (c) by Marijn Haverbeke and others | ||||||
|  | // Distributed under an MIT license: http://codemirror.net/LICENSE | ||||||
|  |  | ||||||
|  | (function(mod) { | ||||||
|  |   if (typeof exports == "object" && typeof module == "object") // CommonJS | ||||||
|  |     mod(require("../../lib/codemirror")); | ||||||
|  |   else if (typeof define == "function" && define.amd) // AMD | ||||||
|  |     define(["../../lib/codemirror"], mod); | ||||||
|  |   else // Plain browser env | ||||||
|  |     mod(CodeMirror); | ||||||
|  | })(function(CodeMirror) { | ||||||
|  |     "use strict"; | ||||||
|  |  | ||||||
|  |     async function validatorHtml(text, options) { | ||||||
|  |         const result = /<script[^>]*>([\s\S]+)<\/script>/ig.exec(text); | ||||||
|  |  | ||||||
|  |         if (result !== null) { | ||||||
|  |             // preceding code is copied over but any (non-newline) character is replaced with space | ||||||
|  |             // this will preserve line numbers etc. | ||||||
|  |             const prefix = text.substr(0, result.index).replace(/./g, " "); | ||||||
|  |  | ||||||
|  |             const js = prefix + result[1]; | ||||||
|  |  | ||||||
|  |             return await validatorJavaScript(js, options); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return []; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     async function validatorJavaScript(text, options) { | ||||||
|  |         if (noteEditor.getCurrentNote().detail.mime === 'application/json') { | ||||||
|  |             // eslint doesn't seem to validate pure JSON well | ||||||
|  |             return []; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         await requireLibrary(ESLINT); | ||||||
|  |  | ||||||
|  |         if (text.length > 20000) { | ||||||
|  |             console.log("Skipping linting because of large size: ", text.length); | ||||||
|  |  | ||||||
|  |             return []; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         const errors = new eslint().verify(text, { | ||||||
|  |             root: true, | ||||||
|  |             parserOptions: { | ||||||
|  |                 ecmaVersion: 2017 | ||||||
|  |             }, | ||||||
|  |             extends: ['eslint:recommended', 'airbnb-base'], | ||||||
|  |             env: { | ||||||
|  |                 'node': true | ||||||
|  |             }, | ||||||
|  |             rules: { | ||||||
|  |                 'import/no-unresolved': 'off', | ||||||
|  |                 'func-names': 'off', | ||||||
|  |                 'comma-dangle': ['warn'], | ||||||
|  |                 'padded-blocks': 'off', | ||||||
|  |                 'linebreak-style': 'off', | ||||||
|  |                 'class-methods-use-this': 'off', | ||||||
|  |                 'no-unused-vars': ['warn', { vars: 'local', args: 'after-used' }], | ||||||
|  |                 'no-nested-ternary': 'off', | ||||||
|  |                 'no-underscore-dangle': ['error', {'allow': ['_super', '_lookupFactory']}] | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         const result = []; | ||||||
|  |         if (errors) { | ||||||
|  |             parseErrors(errors, result); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         return result; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     CodeMirror.registerHelper("lint", "javascript", validatorJavaScript); | ||||||
|  |     CodeMirror.registerHelper("lint", "html", validatorHtml); | ||||||
|  |  | ||||||
|  |     function parseErrors(errors, output) { | ||||||
|  |         for (const error of errors) { | ||||||
|  |             const startLine = error.line - 1; | ||||||
|  |             const endLine = error.endLine !== undefined ? error.endLine - 1 : startLine; | ||||||
|  |             const startCol = error.column - 1; | ||||||
|  |             const endCol = error.endColumn !== undefined ? error.endColumn - 1 : startCol + 1; | ||||||
|  |  | ||||||
|  |             output.push({ | ||||||
|  |                 message: error.message, | ||||||
|  |                 severity: error.severity === 1 ? "warning" : "error", | ||||||
|  |                 from: CodeMirror.Pos(startLine, startCol), | ||||||
|  |                 to: CodeMirror.Pos(endLine, endCol) | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | }); | ||||||
							
								
								
									
										73
									
								
								src/public/libraries/codemirror/addon/lint/lint.css
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								src/public/libraries/codemirror/addon/lint/lint.css
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,73 @@ | |||||||
|  | /* The lint marker gutter */ | ||||||
|  | .CodeMirror-lint-markers { | ||||||
|  |   width: 16px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .CodeMirror-lint-tooltip { | ||||||
|  |   background-color: #ffd; | ||||||
|  |   border: 1px solid black; | ||||||
|  |   border-radius: 4px 4px 4px 4px; | ||||||
|  |   color: black; | ||||||
|  |   font-family: monospace; | ||||||
|  |   font-size: 10pt; | ||||||
|  |   overflow: hidden; | ||||||
|  |   padding: 2px 5px; | ||||||
|  |   position: fixed; | ||||||
|  |   white-space: pre; | ||||||
|  |   white-space: pre-wrap; | ||||||
|  |   z-index: 100; | ||||||
|  |   max-width: 600px; | ||||||
|  |   opacity: 0; | ||||||
|  |   transition: opacity .4s; | ||||||
|  |   -moz-transition: opacity .4s; | ||||||
|  |   -webkit-transition: opacity .4s; | ||||||
|  |   -o-transition: opacity .4s; | ||||||
|  |   -ms-transition: opacity .4s; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .CodeMirror-lint-mark-error, .CodeMirror-lint-mark-warning { | ||||||
|  |   background-position: left bottom; | ||||||
|  |   background-repeat: repeat-x; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .CodeMirror-lint-mark-error { | ||||||
|  |   background-image: | ||||||
|  |   url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJDw4cOCW1/KIAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAHElEQVQI12NggIL/DAz/GdA5/xkY/qPKMDAwAADLZwf5rvm+LQAAAABJRU5ErkJggg==") | ||||||
|  |   ; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .CodeMirror-lint-mark-warning { | ||||||
|  |   background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJFhQXEbhTg7YAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAMklEQVQI12NkgIIvJ3QXMjAwdDN+OaEbysDA4MPAwNDNwMCwiOHLCd1zX07o6kBVGQEAKBANtobskNMAAAAASUVORK5CYII="); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .CodeMirror-lint-marker-error, .CodeMirror-lint-marker-warning { | ||||||
|  |   background-position: center center; | ||||||
|  |   background-repeat: no-repeat; | ||||||
|  |   cursor: pointer; | ||||||
|  |   display: inline-block; | ||||||
|  |   height: 16px; | ||||||
|  |   width: 16px; | ||||||
|  |   vertical-align: middle; | ||||||
|  |   position: relative; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .CodeMirror-lint-message-error, .CodeMirror-lint-message-warning { | ||||||
|  |   padding-left: 18px; | ||||||
|  |   background-position: top left; | ||||||
|  |   background-repeat: no-repeat; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .CodeMirror-lint-marker-error, .CodeMirror-lint-message-error { | ||||||
|  |   background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAHlBMVEW7AAC7AACxAAC7AAC7AAAAAAC4AAC5AAD///+7AAAUdclpAAAABnRSTlMXnORSiwCK0ZKSAAAATUlEQVR42mWPOQ7AQAgDuQLx/z8csYRmPRIFIwRGnosRrpamvkKi0FTIiMASR3hhKW+hAN6/tIWhu9PDWiTGNEkTtIOucA5Oyr9ckPgAWm0GPBog6v4AAAAASUVORK5CYII="); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .CodeMirror-lint-marker-warning, .CodeMirror-lint-message-warning { | ||||||
|  |   background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAANlBMVEX/uwDvrwD/uwD/uwD/uwD/uwD/uwD/uwD/uwD6twD/uwAAAADurwD2tQD7uAD+ugAAAAD/uwDhmeTRAAAADHRSTlMJ8mN1EYcbmiixgACm7WbuAAAAVklEQVR42n3PUQqAIBBFUU1LLc3u/jdbOJoW1P08DA9Gba8+YWJ6gNJoNYIBzAA2chBth5kLmG9YUoG0NHAUwFXwO9LuBQL1giCQb8gC9Oro2vp5rncCIY8L8uEx5ZkAAAAASUVORK5CYII="); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .CodeMirror-lint-marker-multiple { | ||||||
|  |   background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAMAAADzjKfhAAAACVBMVEUAAAAAAAC/v7914kyHAAAAAXRSTlMAQObYZgAAACNJREFUeNo1ioEJAAAIwmz/H90iFFSGJgFMe3gaLZ0od+9/AQZ0ADosbYraAAAAAElFTkSuQmCC"); | ||||||
|  |   background-repeat: no-repeat; | ||||||
|  |   background-position: right bottom; | ||||||
|  |   width: 100%; height: 100%; | ||||||
|  | } | ||||||
							
								
								
									
										252
									
								
								src/public/libraries/codemirror/addon/lint/lint.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										252
									
								
								src/public/libraries/codemirror/addon/lint/lint.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,252 @@ | |||||||
|  | // CodeMirror, copyright (c) by Marijn Haverbeke and others | ||||||
|  | // Distributed under an MIT license: http://codemirror.net/LICENSE | ||||||
|  |  | ||||||
|  | (function(mod) { | ||||||
|  |   if (typeof exports == "object" && typeof module == "object") // CommonJS | ||||||
|  |     mod(require("../../lib/codemirror")); | ||||||
|  |   else if (typeof define == "function" && define.amd) // AMD | ||||||
|  |     define(["../../lib/codemirror"], mod); | ||||||
|  |   else // Plain browser env | ||||||
|  |     mod(CodeMirror); | ||||||
|  | })(function(CodeMirror) { | ||||||
|  |   "use strict"; | ||||||
|  |   var GUTTER_ID = "CodeMirror-lint-markers"; | ||||||
|  |  | ||||||
|  |   function showTooltip(e, content) { | ||||||
|  |     var tt = document.createElement("div"); | ||||||
|  |     tt.className = "CodeMirror-lint-tooltip"; | ||||||
|  |     tt.appendChild(content.cloneNode(true)); | ||||||
|  |     document.body.appendChild(tt); | ||||||
|  |  | ||||||
|  |     function position(e) { | ||||||
|  |       if (!tt.parentNode) return CodeMirror.off(document, "mousemove", position); | ||||||
|  |       tt.style.top = Math.max(0, e.clientY - tt.offsetHeight - 5) + "px"; | ||||||
|  |       tt.style.left = (e.clientX + 5) + "px"; | ||||||
|  |     } | ||||||
|  |     CodeMirror.on(document, "mousemove", position); | ||||||
|  |     position(e); | ||||||
|  |     if (tt.style.opacity != null) tt.style.opacity = 1; | ||||||
|  |     return tt; | ||||||
|  |   } | ||||||
|  |   function rm(elt) { | ||||||
|  |     if (elt.parentNode) elt.parentNode.removeChild(elt); | ||||||
|  |   } | ||||||
|  |   function hideTooltip(tt) { | ||||||
|  |     if (!tt.parentNode) return; | ||||||
|  |     if (tt.style.opacity == null) rm(tt); | ||||||
|  |     tt.style.opacity = 0; | ||||||
|  |     setTimeout(function() { rm(tt); }, 600); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function showTooltipFor(e, content, node) { | ||||||
|  |     var tooltip = showTooltip(e, content); | ||||||
|  |     function hide() { | ||||||
|  |       CodeMirror.off(node, "mouseout", hide); | ||||||
|  |       if (tooltip) { hideTooltip(tooltip); tooltip = null; } | ||||||
|  |     } | ||||||
|  |     var poll = setInterval(function() { | ||||||
|  |       if (tooltip) for (var n = node;; n = n.parentNode) { | ||||||
|  |         if (n && n.nodeType == 11) n = n.host; | ||||||
|  |         if (n == document.body) return; | ||||||
|  |         if (!n) { hide(); break; } | ||||||
|  |       } | ||||||
|  |       if (!tooltip) return clearInterval(poll); | ||||||
|  |     }, 400); | ||||||
|  |     CodeMirror.on(node, "mouseout", hide); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function LintState(cm, options, hasGutter) { | ||||||
|  |     this.marked = []; | ||||||
|  |     this.options = options; | ||||||
|  |     this.timeout = null; | ||||||
|  |     this.hasGutter = hasGutter; | ||||||
|  |     this.onMouseOver = function(e) { onMouseOver(cm, e); }; | ||||||
|  |     this.waitingFor = 0 | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function parseOptions(_cm, options) { | ||||||
|  |     if (options instanceof Function) return {getAnnotations: options}; | ||||||
|  |     if (!options || options === true) options = {}; | ||||||
|  |     return options; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function clearMarks(cm) { | ||||||
|  |     var state = cm.state.lint; | ||||||
|  |     if (state.hasGutter) cm.clearGutter(GUTTER_ID); | ||||||
|  |     for (var i = 0; i < state.marked.length; ++i) | ||||||
|  |       state.marked[i].clear(); | ||||||
|  |     state.marked.length = 0; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function makeMarker(labels, severity, multiple, tooltips) { | ||||||
|  |     var marker = document.createElement("div"), inner = marker; | ||||||
|  |     marker.className = "CodeMirror-lint-marker-" + severity; | ||||||
|  |     if (multiple) { | ||||||
|  |       inner = marker.appendChild(document.createElement("div")); | ||||||
|  |       inner.className = "CodeMirror-lint-marker-multiple"; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (tooltips != false) CodeMirror.on(inner, "mouseover", function(e) { | ||||||
|  |       showTooltipFor(e, labels, inner); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     return marker; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function getMaxSeverity(a, b) { | ||||||
|  |     if (a == "error") return a; | ||||||
|  |     else return b; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function groupByLine(annotations) { | ||||||
|  |     var lines = []; | ||||||
|  |     for (var i = 0; i < annotations.length; ++i) { | ||||||
|  |       var ann = annotations[i], line = ann.from.line; | ||||||
|  |       (lines[line] || (lines[line] = [])).push(ann); | ||||||
|  |     } | ||||||
|  |     return lines; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function annotationTooltip(ann) { | ||||||
|  |     var severity = ann.severity; | ||||||
|  |     if (!severity) severity = "error"; | ||||||
|  |     var tip = document.createElement("div"); | ||||||
|  |     tip.className = "CodeMirror-lint-message-" + severity; | ||||||
|  |     if (typeof ann.messageHTML != 'undefined') { | ||||||
|  |         tip.innerHTML = ann.messageHTML; | ||||||
|  |     } else { | ||||||
|  |         tip.appendChild(document.createTextNode(ann.message)); | ||||||
|  |     } | ||||||
|  |     return tip; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function lintAsync(cm, getAnnotations, passOptions) { | ||||||
|  |     var state = cm.state.lint | ||||||
|  |     var id = ++state.waitingFor | ||||||
|  |     function abort() { | ||||||
|  |       id = -1 | ||||||
|  |       cm.off("change", abort) | ||||||
|  |     } | ||||||
|  |     cm.on("change", abort) | ||||||
|  |     getAnnotations(cm.getValue(), function(annotations, arg2) { | ||||||
|  |       cm.off("change", abort) | ||||||
|  |       if (state.waitingFor != id) return | ||||||
|  |       if (arg2 && annotations instanceof CodeMirror) annotations = arg2 | ||||||
|  |       cm.operation(function() {updateLinting(cm, annotations)}) | ||||||
|  |     }, passOptions, cm); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function startLinting(cm) { | ||||||
|  |     var state = cm.state.lint, options = state.options; | ||||||
|  |     /* | ||||||
|  |      * Passing rules in `options` property prevents JSHint (and other linters) from complaining | ||||||
|  |      * about unrecognized rules like `onUpdateLinting`, `delay`, `lintOnChange`, etc. | ||||||
|  |      */ | ||||||
|  |     var passOptions = options.options || options; | ||||||
|  |     var getAnnotations = options.getAnnotations || cm.getHelper(CodeMirror.Pos(0, 0), "lint"); | ||||||
|  |     if (!getAnnotations) return; | ||||||
|  |     if (options.async || getAnnotations.async) { | ||||||
|  |       lintAsync(cm, getAnnotations, passOptions) | ||||||
|  |     } else { | ||||||
|  |       var annotations = getAnnotations(cm.getValue(), passOptions, cm); | ||||||
|  |       if (!annotations) return; | ||||||
|  |       if (annotations.then) annotations.then(function(issues) { | ||||||
|  |         cm.operation(function() {updateLinting(cm, issues)}) | ||||||
|  |       }); | ||||||
|  |       else cm.operation(function() {updateLinting(cm, annotations)}) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function updateLinting(cm, annotationsNotSorted) { | ||||||
|  |     clearMarks(cm); | ||||||
|  |     var state = cm.state.lint, options = state.options; | ||||||
|  |  | ||||||
|  |     var annotations = groupByLine(annotationsNotSorted); | ||||||
|  |  | ||||||
|  |     for (var line = 0; line < annotations.length; ++line) { | ||||||
|  |       var anns = annotations[line]; | ||||||
|  |       if (!anns) continue; | ||||||
|  |  | ||||||
|  |       var maxSeverity = null; | ||||||
|  |       var tipLabel = state.hasGutter && document.createDocumentFragment(); | ||||||
|  |  | ||||||
|  |       for (var i = 0; i < anns.length; ++i) { | ||||||
|  |         var ann = anns[i]; | ||||||
|  |         var severity = ann.severity; | ||||||
|  |         if (!severity) severity = "error"; | ||||||
|  |         maxSeverity = getMaxSeverity(maxSeverity, severity); | ||||||
|  |  | ||||||
|  |         if (options.formatAnnotation) ann = options.formatAnnotation(ann); | ||||||
|  |         if (state.hasGutter) tipLabel.appendChild(annotationTooltip(ann)); | ||||||
|  |  | ||||||
|  |         if (ann.to) state.marked.push(cm.markText(ann.from, ann.to, { | ||||||
|  |           className: "CodeMirror-lint-mark-" + severity, | ||||||
|  |           __annotation: ann | ||||||
|  |         })); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       if (state.hasGutter) | ||||||
|  |         cm.setGutterMarker(line, GUTTER_ID, makeMarker(tipLabel, maxSeverity, anns.length > 1, | ||||||
|  |                                                        state.options.tooltips)); | ||||||
|  |     } | ||||||
|  |     if (options.onUpdateLinting) options.onUpdateLinting(annotationsNotSorted, annotations, cm); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function onChange(cm) { | ||||||
|  |     var state = cm.state.lint; | ||||||
|  |     if (!state) return; | ||||||
|  |     clearTimeout(state.timeout); | ||||||
|  |     state.timeout = setTimeout(function(){startLinting(cm);}, state.options.delay || 500); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function popupTooltips(annotations, e) { | ||||||
|  |     var target = e.target || e.srcElement; | ||||||
|  |     var tooltip = document.createDocumentFragment(); | ||||||
|  |     for (var i = 0; i < annotations.length; i++) { | ||||||
|  |       var ann = annotations[i]; | ||||||
|  |       tooltip.appendChild(annotationTooltip(ann)); | ||||||
|  |     } | ||||||
|  |     showTooltipFor(e, tooltip, target); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function onMouseOver(cm, e) { | ||||||
|  |     var target = e.target || e.srcElement; | ||||||
|  |     if (!/\bCodeMirror-lint-mark-/.test(target.className)) return; | ||||||
|  |     var box = target.getBoundingClientRect(), x = (box.left + box.right) / 2, y = (box.top + box.bottom) / 2; | ||||||
|  |     var spans = cm.findMarksAt(cm.coordsChar({left: x, top: y}, "client")); | ||||||
|  |  | ||||||
|  |     var annotations = []; | ||||||
|  |     for (var i = 0; i < spans.length; ++i) { | ||||||
|  |       var ann = spans[i].__annotation; | ||||||
|  |       if (ann) annotations.push(ann); | ||||||
|  |     } | ||||||
|  |     if (annotations.length) popupTooltips(annotations, e); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   CodeMirror.defineOption("lint", false, function(cm, val, old) { | ||||||
|  |     if (old && old != CodeMirror.Init) { | ||||||
|  |       clearMarks(cm); | ||||||
|  |       if (cm.state.lint.options.lintOnChange !== false) | ||||||
|  |         cm.off("change", onChange); | ||||||
|  |       CodeMirror.off(cm.getWrapperElement(), "mouseover", cm.state.lint.onMouseOver); | ||||||
|  |       clearTimeout(cm.state.lint.timeout); | ||||||
|  |       delete cm.state.lint; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (val) { | ||||||
|  |       var gutters = cm.getOption("gutters"), hasLintGutter = false; | ||||||
|  |       for (var i = 0; i < gutters.length; ++i) if (gutters[i] == GUTTER_ID) hasLintGutter = true; | ||||||
|  |       var state = cm.state.lint = new LintState(cm, parseOptions(cm, val), hasLintGutter); | ||||||
|  |       if (state.options.lintOnChange !== false) | ||||||
|  |         cm.on("change", onChange); | ||||||
|  |       if (state.options.tooltips != false && state.options.tooltips != "gutter") | ||||||
|  |         CodeMirror.on(cm.getWrapperElement(), "mouseover", state.onMouseOver); | ||||||
|  |  | ||||||
|  |       startLinting(cm); | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   CodeMirror.defineExtension("performLint", function() { | ||||||
|  |     if (this.state.lint) startLinting(this); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
| @@ -90,7 +90,7 @@ | |||||||
|     var state = cm.state.matchHighlighter; |     var state = cm.state.matchHighlighter; | ||||||
|     cm.addOverlay(state.overlay = makeOverlay(query, hasBoundary, style)); |     cm.addOverlay(state.overlay = makeOverlay(query, hasBoundary, style)); | ||||||
|     if (state.options.annotateScrollbar && cm.showMatchesOnScrollbar) { |     if (state.options.annotateScrollbar && cm.showMatchesOnScrollbar) { | ||||||
|       var searchFor = hasBoundary ? new RegExp("\\b" + query + "\\b") : query; |       var searchFor = hasBoundary ? new RegExp("\\b" + query.replace(/[\\\[+*?(){|^$]/g, "\\$&") + "\\b") : query; | ||||||
|       state.matchesonscroll = cm.showMatchesOnScrollbar(searchFor, false, |       state.matchesonscroll = cm.showMatchesOnScrollbar(searchFor, false, | ||||||
|         {className: "CodeMirror-selection-highlight-scrollbar"}); |         {className: "CodeMirror-selection-highlight-scrollbar"}); | ||||||
|     } |     } | ||||||
|   | |||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -846,6 +846,8 @@ CodeMirror.registerHelper("wordChars", "javascript", /[\w$]/); | |||||||
| CodeMirror.defineMIME("text/javascript", "javascript"); | CodeMirror.defineMIME("text/javascript", "javascript"); | ||||||
| CodeMirror.defineMIME("text/ecmascript", "javascript"); | CodeMirror.defineMIME("text/ecmascript", "javascript"); | ||||||
| CodeMirror.defineMIME("application/javascript", "javascript"); | CodeMirror.defineMIME("application/javascript", "javascript"); | ||||||
|  | CodeMirror.defineMIME("application/javascript;env=frontend", "javascript"); | ||||||
|  | CodeMirror.defineMIME("application/javascript;env=backend", "javascript"); | ||||||
| CodeMirror.defineMIME("application/x-javascript", "javascript"); | CodeMirror.defineMIME("application/x-javascript", "javascript"); | ||||||
| CodeMirror.defineMIME("application/ecmascript", "javascript"); | CodeMirror.defineMIME("application/ecmascript", "javascript"); | ||||||
| CodeMirror.defineMIME("application/json", {name: "javascript", json: true}); | CodeMirror.defineMIME("application/json", {name: "javascript", json: true}); | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								src/public/libraries/codemirror/mode/meta.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								src/public/libraries/codemirror/mode/meta.js
									
									
									
									
										vendored
									
									
								
							| @@ -70,7 +70,7 @@ | |||||||
|     {name: "Pug", mime: "text/x-pug", mode: "pug", ext: ["jade", "pug"], alias: ["jade"]}, |     {name: "Pug", mime: "text/x-pug", mode: "pug", ext: ["jade", "pug"], alias: ["jade"]}, | ||||||
|     {name: "Java", mime: "text/x-java", mode: "clike", ext: ["java"]}, |     {name: "Java", mime: "text/x-java", mode: "clike", ext: ["java"]}, | ||||||
|     {name: "Java Server Pages", mime: "application/x-jsp", mode: "htmlembedded", ext: ["jsp"], alias: ["jsp"]}, |     {name: "Java Server Pages", mime: "application/x-jsp", mode: "htmlembedded", ext: ["jsp"], alias: ["jsp"]}, | ||||||
|     {name: "JavaScript", mimes: ["text/javascript", "text/ecmascript", "application/javascript", "application/x-javascript", "application/ecmascript"], |     {name: "JavaScript", mimes: ["text/javascript", "text/ecmascript", "application/javascript", "application/javascript;env=frontend", "application/javascript;env=backend", "application/x-javascript", "application/ecmascript"], | ||||||
|      mode: "javascript", ext: ["js"], alias: ["ecmascript", "js", "node"]}, |      mode: "javascript", ext: ["js"], alias: ["ecmascript", "js", "node"]}, | ||||||
|     {name: "JSON", mimes: ["application/json", "application/x-json"], mode: "javascript", ext: ["json", "map"], alias: ["json5"]}, |     {name: "JSON", mimes: ["application/json", "application/x-json"], mode: "javascript", ext: ["json", "map"], alias: ["json5"]}, | ||||||
|     {name: "JSON-LD", mime: "application/ld+json", mode: "javascript", ext: ["jsonld"], alias: ["jsonld"]}, |     {name: "JSON-LD", mime: "application/ld+json", mode: "javascript", ext: ["jsonld"], alias: ["jsonld"]}, | ||||||
|   | |||||||
							
								
								
									
										101349
									
								
								src/public/libraries/eslint.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										101349
									
								
								src/public/libraries/eslint.js
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @@ -5,12 +5,18 @@ | |||||||
|     display: grid; |     display: grid; | ||||||
|     grid-template-areas: "header header" |     grid-template-areas: "header header" | ||||||
|                          "tree-actions title" |                          "tree-actions title" | ||||||
|  |                          "search note-content" | ||||||
|                          "tree note-content" |                          "tree note-content" | ||||||
|                          "parent-list note-content"; |                          "parent-list note-content" | ||||||
|  |                          "parent-list attribute-list"; | ||||||
|     grid-template-columns: 2fr 5fr; |     grid-template-columns: 2fr 5fr; | ||||||
|     grid-template-rows: auto |     grid-template-rows: auto | ||||||
|                         auto |                         auto | ||||||
|                         1fr; |                         auto | ||||||
|  |                         1fr | ||||||
|  |                         auto | ||||||
|  |                         auto; | ||||||
|  |  | ||||||
|     justify-content: center; |     justify-content: center; | ||||||
|     grid-gap: 10px; |     grid-gap: 10px; | ||||||
| } | } | ||||||
| @@ -66,6 +72,16 @@ span.fancytree-node.fancytree-folder.code > span.fancytree-icon { | |||||||
|     background-image: url("../images/icons/code-folder.png"); |     background-image: url("../images/icons/code-folder.png"); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | span.fancytree-node.attachment > span.fancytree-icon { | ||||||
|  |     background-position: 0 0; | ||||||
|  |     background-image: url("../images/icons/paperclip.png"); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | span.fancytree-node.render > span.fancytree-icon { | ||||||
|  |     background-position: 0 0; | ||||||
|  |     background-image: url("../images/icons/play.png"); | ||||||
|  | } | ||||||
|  |  | ||||||
| span.fancytree-node.protected > span.fancytree-icon { | span.fancytree-node.protected > span.fancytree-icon { | ||||||
|     filter: drop-shadow(2px 2px 2px black); |     filter: drop-shadow(2px 2px 2px black); | ||||||
| } | } | ||||||
| @@ -97,6 +113,9 @@ span.fancytree-active:not(.fancytree-focused) .fancytree-title { | |||||||
|  |  | ||||||
| .icon-action { | .icon-action { | ||||||
|     cursor: pointer; |     cursor: pointer; | ||||||
|  |     display: block; | ||||||
|  |     height: 24px; | ||||||
|  |     width: 24px; | ||||||
| } | } | ||||||
|  |  | ||||||
| #protect-button, #unprotect-button { | #protect-button, #unprotect-button { | ||||||
| @@ -108,7 +127,7 @@ span.fancytree-active:not(.fancytree-focused) .fancytree-title { | |||||||
| } | } | ||||||
|  |  | ||||||
| #header-title { | #header-title { | ||||||
|     padding: 5px 50px 5px 10px; |     padding: 5px 20px 5px 10px; | ||||||
|     font-size: large; |     font-size: large; | ||||||
|     font-weight: bold; |     font-weight: bold; | ||||||
| } | } | ||||||
| @@ -134,6 +153,7 @@ div.ui-tooltip { | |||||||
|     margin-left: 20px; |     margin-left: 20px; | ||||||
|     border-top: 2px solid #eee; |     border-top: 2px solid #eee; | ||||||
|     padding-top: 10px; |     padding-top: 10px; | ||||||
|  |     grid-area: parent-list; | ||||||
| } | } | ||||||
|  |  | ||||||
| #parent-list ul { | #parent-list ul { | ||||||
| @@ -190,11 +210,6 @@ div.ui-tooltip { | |||||||
|     float: right; |     float: right; | ||||||
| } | } | ||||||
|  |  | ||||||
| #note-id-display { |  | ||||||
|     color: lightgrey; |  | ||||||
|     margin-left: 10px; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #note-source { | #note-source { | ||||||
|     height: 98%; |     height: 98%; | ||||||
|     width: 100%; |     width: 100%; | ||||||
| @@ -243,8 +258,9 @@ div.ui-tooltip { | |||||||
| #note-id-display { | #note-id-display { | ||||||
|     position: absolute; |     position: absolute; | ||||||
|     right: 10px; |     right: 10px; | ||||||
|     bottom: 5px; |     bottom: 8px; | ||||||
|     z-index: 1000; |     z-index: 1000; | ||||||
|  |     color: lightgrey; | ||||||
| } | } | ||||||
|  |  | ||||||
| #note-type-dropdown { | #note-type-dropdown { | ||||||
| @@ -253,4 +269,21 @@ div.ui-tooltip { | |||||||
|     overflow-x: hidden; |     overflow-x: hidden; | ||||||
| } | } | ||||||
|  |  | ||||||
| .cm-matchhighlight {background-color: #eeeeee} | .cm-matchhighlight {background-color: #eeeeee} | ||||||
|  |  | ||||||
|  | #attribute-list { | ||||||
|  |     grid-area: attribute-list; | ||||||
|  |     color: #777777; | ||||||
|  |     border-top: 1px solid #eee; | ||||||
|  |     padding: 5px; display: none; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #attribute-list button { | ||||||
|  |     padding: 2px; | ||||||
|  |     margin-right: 5px; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #attachment-table th, #attachment-table td { | ||||||
|  |     padding: 10px; | ||||||
|  |     font-size: large; | ||||||
|  | } | ||||||
							
								
								
									
										74
									
								
								src/routes/api/attachments.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								src/routes/api/attachments.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,74 @@ | |||||||
|  | "use strict"; | ||||||
|  |  | ||||||
|  | const express = require('express'); | ||||||
|  | const router = express.Router(); | ||||||
|  | const sql = require('../../services/sql'); | ||||||
|  | const auth = require('../../services/auth'); | ||||||
|  | const notes = require('../../services/notes'); | ||||||
|  | const attributes = require('../../services/attributes'); | ||||||
|  | const protected_session = require('../../services/protected_session'); | ||||||
|  | const multer = require('multer')(); | ||||||
|  | const wrap = require('express-promise-wrap').wrap; | ||||||
|  |  | ||||||
|  | router.post('/upload/:parentNoteId', auth.checkApiAuthOrElectron, multer.single('upload'), wrap(async (req, res, next) => { | ||||||
|  |     const sourceId = req.headers.source_id; | ||||||
|  |     const parentNoteId = req.params.parentNoteId; | ||||||
|  |     const file = req.file; | ||||||
|  |     const originalName = file.originalname; | ||||||
|  |     const size = file.size; | ||||||
|  |  | ||||||
|  |     const note = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [parentNoteId]); | ||||||
|  |  | ||||||
|  |     if (!note) { | ||||||
|  |         return res.status(404).send(`Note ${parentNoteId} doesn't exist.`); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     await sql.doInTransaction(async () => { | ||||||
|  |         const noteId = (await notes.createNewNote(parentNoteId, { | ||||||
|  |             title: originalName, | ||||||
|  |             content: file.buffer, | ||||||
|  |             target: 'into', | ||||||
|  |             isProtected: false, | ||||||
|  |             type: 'file', | ||||||
|  |             mime: file.mimetype | ||||||
|  |         }, req, sourceId)).noteId; | ||||||
|  |  | ||||||
|  |         await attributes.createAttribute(noteId, "original_file_name", originalName, sourceId); | ||||||
|  |         await attributes.createAttribute(noteId, "file_size", size, sourceId); | ||||||
|  |  | ||||||
|  |         res.send({ | ||||||
|  |             noteId: noteId | ||||||
|  |         }); | ||||||
|  |     }); | ||||||
|  | })); | ||||||
|  |  | ||||||
|  | router.get('/download/:noteId', auth.checkApiAuthOrElectron, wrap(async (req, res, next) => { | ||||||
|  |     const noteId = req.params.noteId; | ||||||
|  |     const note = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId]); | ||||||
|  |     const protectedSessionId = req.query.protectedSessionId; | ||||||
|  |  | ||||||
|  |     if (!note) { | ||||||
|  |         return res.status(404).send(`Note ${noteId} doesn't exist.`); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (note.isProtected) { | ||||||
|  |         const dataKey = protected_session.getDataKeyForProtectedSessionId(protectedSessionId); | ||||||
|  |  | ||||||
|  |         if (!dataKey) { | ||||||
|  |             res.status(401).send("Protected session not available"); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         protected_session.decryptNote(dataKey, note); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const attributeMap = await attributes.getNoteAttributeMap(noteId); | ||||||
|  |     const fileName = attributeMap.original_file_name ? attributeMap.original_file_name : note.title; | ||||||
|  |  | ||||||
|  |     res.setHeader('Content-Disposition', 'attachment; filename=' + fileName); | ||||||
|  |     res.setHeader('Content-Type', note.mime); | ||||||
|  |  | ||||||
|  |     res.send(note.content); | ||||||
|  | })); | ||||||
|  |  | ||||||
|  | module.exports = router; | ||||||
| @@ -7,14 +7,15 @@ const auth = require('../../services/auth'); | |||||||
| const sync_table = require('../../services/sync_table'); | const sync_table = require('../../services/sync_table'); | ||||||
| const utils = require('../../services/utils'); | const utils = require('../../services/utils'); | ||||||
| const wrap = require('express-promise-wrap').wrap; | const wrap = require('express-promise-wrap').wrap; | ||||||
|  | const attributes = require('../../services/attributes'); | ||||||
|  |  | ||||||
| router.get('/:noteId/attributes', auth.checkApiAuth, wrap(async (req, res, next) => { | router.get('/notes/:noteId/attributes', auth.checkApiAuth, wrap(async (req, res, next) => { | ||||||
|     const noteId = req.params.noteId; |     const noteId = req.params.noteId; | ||||||
|  |  | ||||||
|     res.send(await sql.getRows("SELECT * FROM attributes WHERE noteId = ? ORDER BY dateCreated", [noteId])); |     res.send(await sql.getRows("SELECT * FROM attributes WHERE isDeleted = 0 AND noteId = ? ORDER BY position, dateCreated", [noteId])); | ||||||
| })); | })); | ||||||
|  |  | ||||||
| router.put('/:noteId/attributes', auth.checkApiAuth, wrap(async (req, res, next) => { | router.put('/notes/:noteId/attributes', auth.checkApiAuth, wrap(async (req, res, next) => { | ||||||
|     const noteId = req.params.noteId; |     const noteId = req.params.noteId; | ||||||
|     const attributes = req.body; |     const attributes = req.body; | ||||||
|     const now = utils.nowDate(); |     const now = utils.nowDate(); | ||||||
| @@ -22,19 +23,26 @@ router.put('/:noteId/attributes', auth.checkApiAuth, wrap(async (req, res, next) | |||||||
|     await sql.doInTransaction(async () => { |     await sql.doInTransaction(async () => { | ||||||
|         for (const attr of attributes) { |         for (const attr of attributes) { | ||||||
|             if (attr.attributeId) { |             if (attr.attributeId) { | ||||||
|                 await sql.execute("UPDATE attributes SET name = ?, value = ?, dateModified = ? WHERE attributeId = ?", |                 await sql.execute("UPDATE attributes SET name = ?, value = ?, dateModified = ?, isDeleted = ?, position = ? WHERE attributeId = ?", | ||||||
|                     [attr.name, attr.value, now, attr.attributeId]); |                     [attr.name, attr.value, now, attr.isDeleted, attr.position, attr.attributeId]); | ||||||
|             } |             } | ||||||
|             else { |             else { | ||||||
|  |                 // if it was "created" and then immediatelly deleted, we just don't create it at all | ||||||
|  |                 if (attr.isDeleted) { | ||||||
|  |                     continue; | ||||||
|  |                 } | ||||||
|  |  | ||||||
|                 attr.attributeId = utils.newAttributeId(); |                 attr.attributeId = utils.newAttributeId(); | ||||||
|  |  | ||||||
|                 await sql.insert("attributes", { |                 await sql.insert("attributes", { | ||||||
|                    attributeId: attr.attributeId, |                     attributeId: attr.attributeId, | ||||||
|                    noteId: noteId, |                     noteId: noteId, | ||||||
|                    name: attr.name, |                     name: attr.name, | ||||||
|                    value: attr.value, |                     value: attr.value, | ||||||
|                    dateCreated: now, |                     position: attr.position, | ||||||
|                    dateModified: now |                     dateCreated: now, | ||||||
|  |                     dateModified: now, | ||||||
|  |                     isDeleted: false | ||||||
|                 }); |                 }); | ||||||
|             } |             } | ||||||
|  |  | ||||||
| @@ -42,7 +50,29 @@ router.put('/:noteId/attributes', auth.checkApiAuth, wrap(async (req, res, next) | |||||||
|         } |         } | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     res.send(await sql.getRows("SELECT * FROM attributes WHERE noteId = ? ORDER BY dateCreated", [noteId])); |     res.send(await sql.getRows("SELECT * FROM attributes WHERE isDeleted = 0 AND noteId = ? ORDER BY position, dateCreated", [noteId])); | ||||||
|  | })); | ||||||
|  |  | ||||||
|  | router.get('/attributes/names', auth.checkApiAuth, wrap(async (req, res, next) => { | ||||||
|  |     const names = await sql.getColumn("SELECT DISTINCT name FROM attributes WHERE isDeleted = 0"); | ||||||
|  |  | ||||||
|  |     for (const attr of attributes.BUILTIN_ATTRIBUTES) { | ||||||
|  |         if (!names.includes(attr)) { | ||||||
|  |             names.push(attr); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     names.sort(); | ||||||
|  |  | ||||||
|  |     res.send(names); | ||||||
|  | })); | ||||||
|  |  | ||||||
|  | router.get('/attributes/values/:attributeName', auth.checkApiAuth, wrap(async (req, res, next) => { | ||||||
|  |     const attributeName = req.params.attributeName; | ||||||
|  |  | ||||||
|  |     const values = await sql.getColumn("SELECT DISTINCT value FROM attributes WHERE isDeleted = 0 AND name = ? AND value != '' ORDER BY value", [attributeName]); | ||||||
|  |  | ||||||
|  |     res.send(values); | ||||||
| })); | })); | ||||||
|  |  | ||||||
| module.exports = router; | module.exports = router; | ||||||
| @@ -2,56 +2,79 @@ | |||||||
|  |  | ||||||
| const express = require('express'); | const express = require('express'); | ||||||
| const router = express.Router(); | const router = express.Router(); | ||||||
| const rimraf = require('rimraf'); |  | ||||||
| const fs = require('fs'); |  | ||||||
| const sql = require('../../services/sql'); | const sql = require('../../services/sql'); | ||||||
| const data_dir = require('../../services/data_dir'); |  | ||||||
| const html = require('html'); | const html = require('html'); | ||||||
| const auth = require('../../services/auth'); | const auth = require('../../services/auth'); | ||||||
| const wrap = require('express-promise-wrap').wrap; | const wrap = require('express-promise-wrap').wrap; | ||||||
|  | const tar = require('tar-stream'); | ||||||
|  | const sanitize = require("sanitize-filename"); | ||||||
|  | const Repository = require("../../services/repository"); | ||||||
|  |  | ||||||
| router.get('/:noteId/to/:directory', auth.checkApiAuth, wrap(async (req, res, next) => { | router.get('/:noteId/', auth.checkApiAuthOrElectron, wrap(async (req, res, next) => { | ||||||
|     const noteId = req.params.noteId; |     const noteId = req.params.noteId; | ||||||
|     const directory = req.params.directory.replace(/[^0-9a-zA-Z_-]/gi, ''); |     const repo = new Repository(req); | ||||||
|  |  | ||||||
|     if (!fs.existsSync(data_dir.EXPORT_DIR)) { |  | ||||||
|         fs.mkdirSync(data_dir.EXPORT_DIR); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const completeExportDir = data_dir.EXPORT_DIR + '/' + directory; |  | ||||||
|  |  | ||||||
|     if (fs.existsSync(completeExportDir)) { |  | ||||||
|         rimraf.sync(completeExportDir); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     fs.mkdirSync(completeExportDir); |  | ||||||
|  |  | ||||||
|     const noteTreeId = await sql.getValue('SELECT noteTreeId FROM note_tree WHERE noteId = ?', [noteId]); |     const noteTreeId = await sql.getValue('SELECT noteTreeId FROM note_tree WHERE noteId = ?', [noteId]); | ||||||
|  |  | ||||||
|     await exportNote(noteTreeId, completeExportDir); |     const pack = tar.pack(); | ||||||
|  |  | ||||||
|     res.send({}); |     const name = await exportNote(noteTreeId, '', pack, repo); | ||||||
|  |  | ||||||
|  |     pack.finalize(); | ||||||
|  |  | ||||||
|  |     res.setHeader('Content-Disposition', 'attachment; filename="' + name + '.tar"'); | ||||||
|  |     res.setHeader('Content-Type', 'application/tar'); | ||||||
|  |  | ||||||
|  |     pack.pipe(res); | ||||||
| })); | })); | ||||||
|  |  | ||||||
| async function exportNote(noteTreeId, dir) { | async function exportNote(noteTreeId, directory, pack, repo) { | ||||||
|     const noteTree = await sql.getRow("SELECT * FROM note_tree WHERE noteTreeId = ?", [noteTreeId]); |     const noteTree = await sql.getRow("SELECT * FROM note_tree WHERE noteTreeId = ?", [noteTreeId]); | ||||||
|     const note = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteTree.noteId]); |     const note = await repo.getEntity("SELECT notes.* FROM notes WHERE noteId = ?", [noteTree.noteId]); | ||||||
|  |  | ||||||
|     const pos = (noteTree.notePosition + '').padStart(4, '0'); |     if (note.isProtected) { | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     fs.writeFileSync(dir + '/' + pos + '-' + note.title + '.html', html.prettyPrint(note.content, {indent_size: 2})); |     const metadata = await getMetadata(note); | ||||||
|  |  | ||||||
|  |     if ('exclude_from_export' in metadata.attributes) { | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const metadataJson = JSON.stringify(metadata, null, '\t'); | ||||||
|  |     const childFileName = directory + sanitize(note.title); | ||||||
|  |  | ||||||
|  |     pack.entry({ name: childFileName + ".meta", size: metadataJson.length }, metadataJson); | ||||||
|  |  | ||||||
|  |     const content = note.type === 'text' ? html.prettyPrint(note.content, {indent_size: 2}) : note.content; | ||||||
|  |  | ||||||
|  |     pack.entry({ name: childFileName + ".dat", size: content.length }, content); | ||||||
|  |  | ||||||
|     const children = await sql.getRows("SELECT * FROM note_tree WHERE parentNoteId = ? AND isDeleted = 0", [note.noteId]); |     const children = await sql.getRows("SELECT * FROM note_tree WHERE parentNoteId = ? AND isDeleted = 0", [note.noteId]); | ||||||
|  |  | ||||||
|     if (children.length > 0) { |     if (children.length > 0) { | ||||||
|         const childrenDir = dir + '/' + pos + '-' + note.title; |  | ||||||
|  |  | ||||||
|         fs.mkdirSync(childrenDir); |  | ||||||
|  |  | ||||||
|         for (const child of children) { |         for (const child of children) { | ||||||
|             await exportNote(child.noteTreeId, childrenDir); |             await exportNote(child.noteTreeId, childFileName + "/", pack, repo); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     return childFileName; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function getMetadata(note) { | ||||||
|  |     return { | ||||||
|  |         version: 1, | ||||||
|  |         title: note.title, | ||||||
|  |         type: note.type, | ||||||
|  |         mime: note.mime, | ||||||
|  |         attributes: (await note.getAttributes()).map(attr => { | ||||||
|  |             return { | ||||||
|  |                 name: attr.name, | ||||||
|  |                 value: attr.value | ||||||
|  |             }; | ||||||
|  |         }) | ||||||
|  |     }; | ||||||
| } | } | ||||||
|  |  | ||||||
| module.exports = router; | module.exports = router; | ||||||
| @@ -4,16 +4,8 @@ const express = require('express'); | |||||||
| const router = express.Router(); | const router = express.Router(); | ||||||
| const sql = require('../../services/sql'); | const sql = require('../../services/sql'); | ||||||
| const auth = require('../../services/auth'); | const auth = require('../../services/auth'); | ||||||
| const utils = require('../../services/utils'); | const image = require('../../services/image'); | ||||||
| const sync_table = require('../../services/sync_table'); |  | ||||||
| const multer = require('multer')(); | const multer = require('multer')(); | ||||||
| const imagemin = require('imagemin'); |  | ||||||
| const imageminMozJpeg = require('imagemin-mozjpeg'); |  | ||||||
| const imageminPngQuant = require('imagemin-pngquant'); |  | ||||||
| const imageminGifLossy = require('imagemin-giflossy'); |  | ||||||
| const jimp = require('jimp'); |  | ||||||
| const imageType = require('image-type'); |  | ||||||
| const sanitizeFilename = require('sanitize-filename'); |  | ||||||
| const wrap = require('express-promise-wrap').wrap; | const wrap = require('express-promise-wrap').wrap; | ||||||
| const RESOURCE_DIR = require('../../services/resource_dir').RESOURCE_DIR; | const RESOURCE_DIR = require('../../services/resource_dir').RESOURCE_DIR; | ||||||
| const fs = require('fs'); | const fs = require('fs'); | ||||||
| @@ -49,45 +41,7 @@ router.post('', auth.checkApiAuthOrElectron, multer.single('upload'), wrap(async | |||||||
|         return res.status(400).send("Unknown image type: " + file.mimetype); |         return res.status(400).send("Unknown image type: " + file.mimetype); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const now = utils.nowDate(); |     const {fileName, imageId} = await image.saveImage(file, sourceId, noteId); | ||||||
|  |  | ||||||
|     const resizedImage = await resize(file.buffer); |  | ||||||
|     const optimizedImage = await optimize(resizedImage); |  | ||||||
|  |  | ||||||
|     const imageFormat = imageType(optimizedImage); |  | ||||||
|  |  | ||||||
|     const fileNameWithouExtension = file.originalname.replace(/\.[^/.]+$/, ""); |  | ||||||
|     const fileName = sanitizeFilename(fileNameWithouExtension + "." + imageFormat.ext); |  | ||||||
|  |  | ||||||
|     const imageId = utils.newImageId(); |  | ||||||
|  |  | ||||||
|     await sql.doInTransaction(async () => { |  | ||||||
|         await sql.insert("images", { |  | ||||||
|             imageId: imageId, |  | ||||||
|             format: imageFormat.ext, |  | ||||||
|             name: fileName, |  | ||||||
|             checksum: utils.hash(optimizedImage), |  | ||||||
|             data: optimizedImage, |  | ||||||
|             isDeleted: 0, |  | ||||||
|             dateModified: now, |  | ||||||
|             dateCreated: now |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|         await sync_table.addImageSync(imageId, sourceId); |  | ||||||
|  |  | ||||||
|         const noteImageId = utils.newNoteImageId(); |  | ||||||
|  |  | ||||||
|         await sql.insert("note_images", { |  | ||||||
|             noteImageId: noteImageId, |  | ||||||
|             noteId: noteId, |  | ||||||
|             imageId: imageId, |  | ||||||
|             isDeleted: 0, |  | ||||||
|             dateModified: now, |  | ||||||
|             dateCreated: now |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|         await sync_table.addNoteImageSync(noteImageId, sourceId); |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     res.send({ |     res.send({ | ||||||
|         uploaded: true, |         uploaded: true, | ||||||
| @@ -95,54 +49,4 @@ router.post('', auth.checkApiAuthOrElectron, multer.single('upload'), wrap(async | |||||||
|     }); |     }); | ||||||
| })); | })); | ||||||
|  |  | ||||||
| const MAX_SIZE = 1000; |  | ||||||
| const MAX_BYTE_SIZE = 200000; // images should have under 100 KBs |  | ||||||
|  |  | ||||||
| async function resize(buffer) { |  | ||||||
|     const image = await jimp.read(buffer); |  | ||||||
|  |  | ||||||
|     if (image.bitmap.width > image.bitmap.height && image.bitmap.width > MAX_SIZE) { |  | ||||||
|         image.resize(MAX_SIZE, jimp.AUTO); |  | ||||||
|     } |  | ||||||
|     else if (image.bitmap.height > MAX_SIZE) { |  | ||||||
|         image.resize(jimp.AUTO, MAX_SIZE); |  | ||||||
|     } |  | ||||||
|     else if (buffer.byteLength <= MAX_BYTE_SIZE) { |  | ||||||
|         return buffer; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     // we do resizing with max quality which will be trimmed during optimization step next |  | ||||||
|     image.quality(100); |  | ||||||
|  |  | ||||||
|     // when converting PNG to JPG we lose alpha channel, this is replaced by white to match Trilium white background |  | ||||||
|     image.background(0xFFFFFFFF); |  | ||||||
|  |  | ||||||
|     // getBuffer doesn't support promises so this workaround |  | ||||||
|     return await new Promise((resolve, reject) => image.getBuffer(jimp.MIME_JPEG, (err, data) => { |  | ||||||
|         if (err) { |  | ||||||
|             reject(err); |  | ||||||
|         } |  | ||||||
|         else { |  | ||||||
|             resolve(data); |  | ||||||
|         } |  | ||||||
|     })); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| async function optimize(buffer) { |  | ||||||
|     return await imagemin.buffer(buffer, { |  | ||||||
|         plugins: [ |  | ||||||
|             imageminMozJpeg({ |  | ||||||
|                 quality: 50 |  | ||||||
|             }), |  | ||||||
|             imageminPngQuant({ |  | ||||||
|                 quality: "0-70" |  | ||||||
|             }), |  | ||||||
|             imageminGifLossy({ |  | ||||||
|                 lossy: 80, |  | ||||||
|                 optimize: '3' // needs to be string |  | ||||||
|             }) |  | ||||||
|         ] |  | ||||||
|     }); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| module.exports = router; | module.exports = router; | ||||||
| @@ -2,104 +2,136 @@ | |||||||
|  |  | ||||||
| const express = require('express'); | const express = require('express'); | ||||||
| const router = express.Router(); | const router = express.Router(); | ||||||
| const fs = require('fs'); |  | ||||||
| const sql = require('../../services/sql'); | const sql = require('../../services/sql'); | ||||||
| const data_dir = require('../../services/data_dir'); |  | ||||||
| const utils = require('../../services/utils'); |  | ||||||
| const sync_table = require('../../services/sync_table'); |  | ||||||
| const auth = require('../../services/auth'); | const auth = require('../../services/auth'); | ||||||
|  | const attributes = require('../../services/attributes'); | ||||||
|  | const notes = require('../../services/notes'); | ||||||
| const wrap = require('express-promise-wrap').wrap; | const wrap = require('express-promise-wrap').wrap; | ||||||
|  | const tar = require('tar-stream'); | ||||||
|  | const multer = require('multer')(); | ||||||
|  | const stream = require('stream'); | ||||||
|  | const path = require('path'); | ||||||
|  |  | ||||||
| router.get('/:directory/to/:parentNoteId', auth.checkApiAuth, wrap(async (req, res, next) => { | function getFileName(name) { | ||||||
|     const directory = req.params.directory.replace(/[^0-9a-zA-Z_-]/gi, ''); |     let key; | ||||||
|  |  | ||||||
|  |     if (name.endsWith(".dat")) { | ||||||
|  |         key = "data"; | ||||||
|  |         name = name.substr(0, name.length - 4); | ||||||
|  |     } | ||||||
|  |     else if (name.endsWith((".meta"))) { | ||||||
|  |         key = "meta"; | ||||||
|  |         name = name.substr(0, name.length - 5); | ||||||
|  |     } | ||||||
|  |     else { | ||||||
|  |         throw new Error("Unknown file type in import archive: " + name); | ||||||
|  |     } | ||||||
|  |     return {name, key}; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function parseImportFile(file) { | ||||||
|  |     const fileMap = {}; | ||||||
|  |     const files = []; | ||||||
|  |  | ||||||
|  |     const extract = tar.extract(); | ||||||
|  |  | ||||||
|  |     extract.on('entry', function(header, stream, next) { | ||||||
|  |         let {name, key} = getFileName(header.name); | ||||||
|  |  | ||||||
|  |         let file = fileMap[name]; | ||||||
|  |  | ||||||
|  |         if (!file) { | ||||||
|  |             file = fileMap[name] = { | ||||||
|  |                 children: [] | ||||||
|  |             }; | ||||||
|  |  | ||||||
|  |             let parentFileName = path.dirname(header.name); | ||||||
|  |  | ||||||
|  |             if (parentFileName && parentFileName !== '.') { | ||||||
|  |                 fileMap[parentFileName].children.push(file); | ||||||
|  |             } | ||||||
|  |             else { | ||||||
|  |                 files.push(file); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         const chunks = []; | ||||||
|  |  | ||||||
|  |         stream.on("data", function (chunk) { | ||||||
|  |             chunks.push(chunk); | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         // header is the tar header | ||||||
|  |         // stream is the content body (might be an empty stream) | ||||||
|  |         // call next when you are done with this entry | ||||||
|  |  | ||||||
|  |         stream.on('end', function() { | ||||||
|  |             file[key] = Buffer.concat(chunks); | ||||||
|  |  | ||||||
|  |             if (key === "meta") { | ||||||
|  |                 file[key] = JSON.parse(file[key].toString("UTF-8")); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             next(); // ready for next entry | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         stream.resume(); // just auto drain the stream | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     return new Promise(resolve => { | ||||||
|  |         extract.on('finish', function() { | ||||||
|  |             resolve(files); | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         const bufferStream = new stream.PassThrough(); | ||||||
|  |         bufferStream.end(file.buffer); | ||||||
|  |  | ||||||
|  |         bufferStream.pipe(extract); | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | router.post('/:parentNoteId', auth.checkApiAuthOrElectron, multer.single('upload'), wrap(async (req, res, next) => { | ||||||
|  |     const sourceId = req.headers.source_id; | ||||||
|     const parentNoteId = req.params.parentNoteId; |     const parentNoteId = req.params.parentNoteId; | ||||||
|  |     const file = req.file; | ||||||
|  |  | ||||||
|     const dir = data_dir.EXPORT_DIR + '/' + directory; |     const note = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [parentNoteId]); | ||||||
|  |  | ||||||
|     await sql.doInTransaction(async () => await importNotes(dir, parentNoteId)); |     if (!note) { | ||||||
|  |         return res.status(404).send(`Note ${parentNoteId} doesn't exist.`); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const files = await parseImportFile(file); | ||||||
|  |  | ||||||
|  |     await sql.doInTransaction(async () => { | ||||||
|  |         await importNotes(files, parentNoteId, sourceId); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|     res.send({}); |     res.send({}); | ||||||
| })); | })); | ||||||
|  |  | ||||||
| async function importNotes(dir, parentNoteId) { | async function importNotes(files, parentNoteId, sourceId) { | ||||||
|     const parent = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [parentNoteId]); |     for (const file of files) { | ||||||
|  |         if (file.meta.version !== 1) { | ||||||
|     if (!parent) { |             throw new Error("Can't read meta data version " + file.meta.version); | ||||||
|         return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const fileList = fs.readdirSync(dir); |  | ||||||
|  |  | ||||||
|     for (const file of fileList) { |  | ||||||
|         const path = dir + '/' + file; |  | ||||||
|  |  | ||||||
|         if (fs.lstatSync(path).isDirectory()) { |  | ||||||
|             continue; |  | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         if (!file.endsWith('.html')) { |         if (file.meta.type !== 'file') { | ||||||
|             continue; |             file.data = file.data.toString("UTF-8"); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         const fileNameWithoutExt = file.substr(0, file.length - 5); |         const noteId = await notes.createNote(parentNoteId, file.meta.title, file.data, { | ||||||
|  |             type: file.meta.type, | ||||||
|         let noteTitle; |             mime: file.meta.mime, | ||||||
|         let notePos; |             sourceId: sourceId | ||||||
|  |  | ||||||
|         const match = fileNameWithoutExt.match(/^([0-9]{4})-(.*)$/); |  | ||||||
|         if (match) { |  | ||||||
|             notePos = parseInt(match[1]); |  | ||||||
|             noteTitle = match[2]; |  | ||||||
|         } |  | ||||||
|         else { |  | ||||||
|             let maxPos = await sql.getValue("SELECT MAX(notePosition) FROM note_tree WHERE parentNoteId = ? AND isDeleted = 0", [parentNoteId]); |  | ||||||
|             if (maxPos) { |  | ||||||
|                 notePos = maxPos + 1; |  | ||||||
|             } |  | ||||||
|             else { |  | ||||||
|                 notePos = 0; |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             noteTitle = fileNameWithoutExt; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         const noteText = fs.readFileSync(path, "utf8"); |  | ||||||
|  |  | ||||||
|         const noteId = utils.newNoteId(); |  | ||||||
|         const noteTreeId = utils.newnoteRevisionId(); |  | ||||||
|  |  | ||||||
|         const now = utils.nowDate(); |  | ||||||
|  |  | ||||||
|         await sql.insert('note_tree', { |  | ||||||
|             noteTreeId: noteTreeId, |  | ||||||
|             noteId: noteId, |  | ||||||
|             parentNoteId: parentNoteId, |  | ||||||
|             notePosition: notePos, |  | ||||||
|             isExpanded: 0, |  | ||||||
|             isDeleted: 0, |  | ||||||
|             dateModified: now |  | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|         await sync_table.addNoteTreeSync(noteTreeId); |         for (const attr of file.meta.attributes) { | ||||||
|  |             await attributes.createAttribute(noteId, attr.name, attr.value); | ||||||
|  |         } | ||||||
|  |  | ||||||
|         await sql.insert('notes', { |         if (file.children.length > 0) { | ||||||
|             noteId: noteId, |             await importNotes(file.children, noteId, sourceId); | ||||||
|             title: noteTitle, |  | ||||||
|             content: noteText, |  | ||||||
|             isDeleted: 0, |  | ||||||
|             isProtected: 0, |  | ||||||
|             type: 'text', |  | ||||||
|             mime: 'text/html', |  | ||||||
|             dateCreated: now, |  | ||||||
|             dateModified: now |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|         await sync_table.addNoteSync(noteId); |  | ||||||
|  |  | ||||||
|         const noteDir = dir + '/' + fileNameWithoutExt; |  | ||||||
|  |  | ||||||
|         if (fs.existsSync(noteDir) && fs.lstatSync(noteDir).isDirectory()) { |  | ||||||
|             await importNotes(noteDir, noteId); |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -14,7 +14,7 @@ const wrap = require('express-promise-wrap').wrap; | |||||||
| router.post('/sync', wrap(async (req, res, next) => { | router.post('/sync', wrap(async (req, res, next) => { | ||||||
|     const timestampStr = req.body.timestamp; |     const timestampStr = req.body.timestamp; | ||||||
|  |  | ||||||
|     const timestamp = utils.parseDate(timestampStr); |     const timestamp = utils.parseDateTime(timestampStr); | ||||||
|  |  | ||||||
|     const now = new Date(); |     const now = new Date(); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -5,6 +5,7 @@ const router = express.Router(); | |||||||
| const auth = require('../../services/auth'); | const auth = require('../../services/auth'); | ||||||
| const sql = require('../../services/sql'); | const sql = require('../../services/sql'); | ||||||
| const notes = require('../../services/notes'); | const notes = require('../../services/notes'); | ||||||
|  | const attributes = require('../../services/attributes'); | ||||||
| const log = require('../../services/log'); | const log = require('../../services/log'); | ||||||
| const utils = require('../../services/utils'); | const utils = require('../../services/utils'); | ||||||
| const protected_session = require('../../services/protected_session'); | const protected_session = require('../../services/protected_session'); | ||||||
| @@ -25,8 +26,19 @@ router.get('/:noteId', auth.checkApiAuth, wrap(async (req, res, next) => { | |||||||
|  |  | ||||||
|     protected_session.decryptNote(req, detail); |     protected_session.decryptNote(req, detail); | ||||||
|  |  | ||||||
|  |     let attributeMap = null; | ||||||
|  |  | ||||||
|  |     if (detail.type === 'file') { | ||||||
|  |         // no need to transfer attachment payload for this request | ||||||
|  |         detail.content = null; | ||||||
|  |  | ||||||
|  |         // attributes contain important attachment metadata - filename and size | ||||||
|  |         attributeMap = await attributes.getNoteAttributeMap(noteId); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     res.send({ |     res.send({ | ||||||
|         detail: detail |         detail: detail, | ||||||
|  |         attributes: attributeMap | ||||||
|     }); |     }); | ||||||
| })); | })); | ||||||
|  |  | ||||||
| @@ -58,15 +70,114 @@ router.put('/:noteId', auth.checkApiAuth, wrap(async (req, res, next) => { | |||||||
| })); | })); | ||||||
|  |  | ||||||
| router.get('/', auth.checkApiAuth, wrap(async (req, res, next) => { | router.get('/', auth.checkApiAuth, wrap(async (req, res, next) => { | ||||||
|     const search = '%' + utils.sanitizeSql(req.query.search) + '%'; |     let {attrFilters, searchText} = parseFilters(req.query.search); | ||||||
|  |  | ||||||
|     // searching in protected notes is pointless because of encryption |     const {query, params} = getSearchQuery(attrFilters, searchText); | ||||||
|     const noteIds = await sql.getColumn(`SELECT noteId FROM notes  |  | ||||||
|               WHERE isDeleted = 0 AND isProtected = 0 AND (title LIKE ? OR content LIKE ?)`, [search, search]); |     console.log(query, params); | ||||||
|  |  | ||||||
|  |     const noteIds = await sql.getColumn(query, params); | ||||||
|  |  | ||||||
|     res.send(noteIds); |     res.send(noteIds); | ||||||
| })); | })); | ||||||
|  |  | ||||||
|  | function parseFilters(searchText) { | ||||||
|  |     const attrFilters = []; | ||||||
|  |  | ||||||
|  |     const attrRegex = /(\b(and|or)\s+)?@(!?)([\w_-]+|"[^"]+")((=|!=|<|<=|>|>=)([\w_-]+|"[^"]+"))?/i; | ||||||
|  |  | ||||||
|  |     let match = attrRegex.exec(searchText); | ||||||
|  |  | ||||||
|  |     function trimQuotes(str) { return str.startsWith('"') ? str.substr(1, str.length - 2) : str; } | ||||||
|  |  | ||||||
|  |     while (match != null) { | ||||||
|  |         const relation = match[2] !== undefined ? match[2].toLowerCase() : 'and'; | ||||||
|  |         const operator = match[3] === '!' ? 'not-exists' : 'exists'; | ||||||
|  |  | ||||||
|  |         attrFilters.push({ | ||||||
|  |             relation: relation, | ||||||
|  |             name: trimQuotes(match[4]), | ||||||
|  |             operator: match[6] !== undefined ? match[6] : operator, | ||||||
|  |             value: match[7] !== undefined ? trimQuotes(match[7]) : null | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         // remove attributes from further fulltext search | ||||||
|  |         searchText = searchText.split(match[0]).join(''); | ||||||
|  |  | ||||||
|  |         match = attrRegex.exec(searchText); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return {attrFilters, searchText}; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getSearchQuery(attrFilters, searchText) { | ||||||
|  |     const joins = []; | ||||||
|  |     const joinParams = []; | ||||||
|  |     let where = '1'; | ||||||
|  |     const whereParams = []; | ||||||
|  |  | ||||||
|  |     let i = 1; | ||||||
|  |  | ||||||
|  |     for (const filter of attrFilters) { | ||||||
|  |         joins.push(`LEFT JOIN attributes AS attr${i} ON attr${i}.noteId = notes.noteId AND attr${i}.name = ?`); | ||||||
|  |         joinParams.push(filter.name); | ||||||
|  |  | ||||||
|  |         where += " " + filter.relation + " "; | ||||||
|  |  | ||||||
|  |         if (filter.operator === 'exists') { | ||||||
|  |             where += `attr${i}.attributeId IS NOT NULL`; | ||||||
|  |         } | ||||||
|  |         else if (filter.operator === 'not-exists') { | ||||||
|  |             where += `attr${i}.attributeId IS NULL`; | ||||||
|  |         } | ||||||
|  |         else if (filter.operator === '=' || filter.operator === '!=') { | ||||||
|  |             where += `attr${i}.value ${filter.operator} ?`; | ||||||
|  |             whereParams.push(filter.value); | ||||||
|  |         } | ||||||
|  |         else if ([">", ">=", "<", "<="].includes(filter.operator)) { | ||||||
|  |             const floatParam = parseFloat(filter.value); | ||||||
|  |  | ||||||
|  |             if (isNaN(floatParam)) { | ||||||
|  |                 where += `attr${i}.value ${filter.operator} ?`; | ||||||
|  |                 whereParams.push(filter.value); | ||||||
|  |             } | ||||||
|  |             else { | ||||||
|  |                 where += `CAST(attr${i}.value AS DECIMAL) ${filter.operator} ?`; | ||||||
|  |                 whereParams.push(floatParam); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         else { | ||||||
|  |             throw new Error("Unknown operator " + filter.operator); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         i++; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     let searchCondition = ''; | ||||||
|  |     const searchParams = []; | ||||||
|  |  | ||||||
|  |     if (searchText.trim() !== '') { | ||||||
|  |         // searching in protected notes is pointless because of encryption | ||||||
|  |         searchCondition = ' AND (notes.isProtected = 0 AND (notes.title LIKE ? OR notes.content LIKE ?))'; | ||||||
|  |  | ||||||
|  |         searchText = '%' + searchText.trim() + '%'; | ||||||
|  |  | ||||||
|  |         searchParams.push(searchText); | ||||||
|  |         searchParams.push(searchText); // two occurences in searchCondition | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const query = `SELECT DISTINCT notes.noteId FROM notes | ||||||
|  |             ${joins.join('\r\n')} | ||||||
|  |               WHERE  | ||||||
|  |                 notes.isDeleted = 0 | ||||||
|  |                 AND (${where})  | ||||||
|  |                 ${searchCondition}`; | ||||||
|  |  | ||||||
|  |     const params = joinParams.concat(whereParams).concat(searchParams); | ||||||
|  |  | ||||||
|  |     return { query, params }; | ||||||
|  | } | ||||||
|  |  | ||||||
| router.put('/:noteId/sort', auth.checkApiAuth, wrap(async (req, res, next) => { | router.put('/:noteId/sort', auth.checkApiAuth, wrap(async (req, res, next) => { | ||||||
|     const noteId = req.params.noteId; |     const noteId = req.params.noteId; | ||||||
|     const sourceId = req.headers.source_id; |     const sourceId = req.headers.source_id; | ||||||
|   | |||||||
| @@ -45,7 +45,8 @@ async function getRecentNotes() { | |||||||
|         recent_notes.isDeleted = 0 |         recent_notes.isDeleted = 0 | ||||||
|         AND note_tree.isDeleted = 0 |         AND note_tree.isDeleted = 0 | ||||||
|       ORDER BY  |       ORDER BY  | ||||||
|         dateAccessed DESC`); |         dateAccessed DESC | ||||||
|  |       LIMIT 200`); | ||||||
| } | } | ||||||
|  |  | ||||||
| module.exports = router; | module.exports = router; | ||||||
| @@ -4,13 +4,23 @@ const express = require('express'); | |||||||
| const router = express.Router(); | const router = express.Router(); | ||||||
| const auth = require('../../services/auth'); | const auth = require('../../services/auth'); | ||||||
| const wrap = require('express-promise-wrap').wrap; | const wrap = require('express-promise-wrap').wrap; | ||||||
| const notes = require('../../services/notes'); |  | ||||||
| const attributes = require('../../services/attributes'); | const attributes = require('../../services/attributes'); | ||||||
| const script = require('../../services/script'); | const script = require('../../services/script'); | ||||||
| const Repository = require('../../services/repository'); | const Repository = require('../../services/repository'); | ||||||
|  |  | ||||||
| router.post('/exec', auth.checkApiAuth, wrap(async (req, res, next) => { | router.post('/exec', auth.checkApiAuth, wrap(async (req, res, next) => { | ||||||
|     const ret = await script.executeScript(req, req.body.script, req.body.params); |     const ret = await script.executeScript(req, req.body.script, req.body.params, req.body.startNoteId, req.body.currentNoteId); | ||||||
|  |  | ||||||
|  |     res.send({ | ||||||
|  |         executionResult: ret | ||||||
|  |     }); | ||||||
|  | })); | ||||||
|  |  | ||||||
|  | router.post('/run/:noteId', auth.checkApiAuth, wrap(async (req, res, next) => { | ||||||
|  |     const repository = new Repository(req); | ||||||
|  |     const note = await repository.getNote(req.params.noteId); | ||||||
|  |  | ||||||
|  |     const ret = await script.executeNote(req, note); | ||||||
|  |  | ||||||
|     res.send({ |     res.send({ | ||||||
|         executionResult: ret |         executionResult: ret | ||||||
| @@ -18,64 +28,26 @@ router.post('/exec', auth.checkApiAuth, wrap(async (req, res, next) => { | |||||||
| })); | })); | ||||||
|  |  | ||||||
| router.get('/startup', auth.checkApiAuth, wrap(async (req, res, next) => { | router.get('/startup', auth.checkApiAuth, wrap(async (req, res, next) => { | ||||||
|     const noteIds = await attributes.getNoteIdsWithAttribute("run_on_startup"); |     const repository = new Repository(req); | ||||||
|  |     const notes = await attributes.getNotesWithAttribute(repository, "run", "frontend_startup"); | ||||||
|  |  | ||||||
|     const scripts = []; |     const scripts = []; | ||||||
|  |  | ||||||
|     for (const noteId of noteIds) { |     for (const note of notes) { | ||||||
|         scripts.push(await getNoteWithSubtreeScript(noteId, req)); |         const bundle = await script.getScriptBundle(note); | ||||||
|  |  | ||||||
|  |         scripts.push(bundle); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     res.send(scripts); |     res.send(scripts); | ||||||
| })); | })); | ||||||
|  |  | ||||||
| router.get('/subtree/:noteId', auth.checkApiAuth, wrap(async (req, res, next) => { | router.get('/bundle/:noteId', auth.checkApiAuth, wrap(async (req, res, next) => { | ||||||
|     const noteId = req.params.noteId; |  | ||||||
|  |  | ||||||
|     const repository = new Repository(req); |     const repository = new Repository(req); | ||||||
|  |     const note = await repository.getNote(req.params.noteId); | ||||||
|  |     const bundle = await script.getScriptBundle(note); | ||||||
|  |  | ||||||
|     const noteScript = (await repository.getNote(noteId)).content; |     res.send(bundle); | ||||||
|  |  | ||||||
|     const subTreeScripts = await getSubTreeScripts(noteId, [noteId], repository); |  | ||||||
|  |  | ||||||
|     res.send(subTreeScripts + noteScript); |  | ||||||
| })); | })); | ||||||
|  |  | ||||||
| async function getNoteWithSubtreeScript(noteId, req) { |  | ||||||
|     const noteScript = (await notes.getNoteById(noteId, req)).content; |  | ||||||
|  |  | ||||||
|     const subTreeScripts = await getSubTreeScripts(noteId, [noteId], req); |  | ||||||
|  |  | ||||||
|     return subTreeScripts + noteScript; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| async function getSubTreeScripts(parentId, includedNoteIds, repository) { |  | ||||||
|     const children = await repository.getEntities(` |  | ||||||
|                                       SELECT notes.*  |  | ||||||
|                                       FROM notes JOIN note_tree USING(noteId) |  | ||||||
|                                       WHERE note_tree.isDeleted = 0 AND notes.isDeleted = 0 |  | ||||||
|                                            AND note_tree.parentNoteId = ? AND notes.type = 'code' |  | ||||||
|                                            AND (notes.mime = 'application/javascript' OR notes.mime = 'text/html')`, [parentId]); |  | ||||||
|  |  | ||||||
|     let script = "\r\n"; |  | ||||||
|  |  | ||||||
|     for (const child of children) { |  | ||||||
|         if (includedNoteIds.includes(child.noteId)) { |  | ||||||
|             return; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         includedNoteIds.push(child.noteId); |  | ||||||
|  |  | ||||||
|         script += await getSubTreeScripts(child.noteId, includedNoteIds, repository); |  | ||||||
|  |  | ||||||
|         if (child.mime === 'application/javascript') { |  | ||||||
|             child.content = '<script>' + child.content + '</script>'; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         script += child.content + "\r\n"; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return script; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| module.exports = router; | module.exports = router; | ||||||
							
								
								
									
										106
									
								
								src/routes/api/sender.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								src/routes/api/sender.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,106 @@ | |||||||
|  | "use strict"; | ||||||
|  |  | ||||||
|  | const express = require('express'); | ||||||
|  | const router = express.Router(); | ||||||
|  | const image = require('../../services/image'); | ||||||
|  | const utils = require('../../services/utils'); | ||||||
|  | const date_notes = require('../../services/date_notes'); | ||||||
|  | const sql = require('../../services/sql'); | ||||||
|  | const wrap = require('express-promise-wrap').wrap; | ||||||
|  | const notes = require('../../services/notes'); | ||||||
|  | const multer = require('multer')(); | ||||||
|  | const password_encryption = require('../../services/password_encryption'); | ||||||
|  | const options = require('../../services/options'); | ||||||
|  | const sync_table = require('../../services/sync_table'); | ||||||
|  |  | ||||||
|  | router.post('/login', wrap(async (req, res, next) => { | ||||||
|  |     const username = req.body.username; | ||||||
|  |     const password = req.body.password; | ||||||
|  |  | ||||||
|  |     const isUsernameValid = username === await options.getOption('username'); | ||||||
|  |     const isPasswordValid = await password_encryption.verifyPassword(password); | ||||||
|  |  | ||||||
|  |     if (!isUsernameValid || !isPasswordValid) { | ||||||
|  |         res.status(401).send("Incorrect username/password"); | ||||||
|  |     } | ||||||
|  |     else { | ||||||
|  |         const token = utils.randomSecureToken(); | ||||||
|  |  | ||||||
|  |         await sql.doInTransaction(async () => { | ||||||
|  |             const apiTokenId = utils.newApiTokenId(); | ||||||
|  |  | ||||||
|  |             await sql.insert("api_tokens", { | ||||||
|  |                 apiTokenId: apiTokenId, | ||||||
|  |                 token: token, | ||||||
|  |                 dateCreated: utils.nowDate(), | ||||||
|  |                 isDeleted: false | ||||||
|  |             }); | ||||||
|  |  | ||||||
|  |             await sync_table.addApiTokenSync(apiTokenId); | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         res.send({ | ||||||
|  |             token: token | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | })); | ||||||
|  |  | ||||||
|  | async function checkSenderToken(req, res, next) { | ||||||
|  |     const token = req.headers.authorization; | ||||||
|  |  | ||||||
|  |     if (await sql.getValue("SELECT COUNT(*) FROM api_tokens WHERE isDeleted = 0 AND token = ?", [token]) === 0) { | ||||||
|  |         res.status(401).send("Not authorized"); | ||||||
|  |     } | ||||||
|  |     else if (await sql.isDbUpToDate()) { | ||||||
|  |         next(); | ||||||
|  |     } | ||||||
|  |     else { | ||||||
|  |         res.status(409).send("Mismatched app versions"); // need better response than that | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | router.post('/image', checkSenderToken, multer.single('upload'), wrap(async (req, res, next) => { | ||||||
|  |     const file = req.file; | ||||||
|  |  | ||||||
|  |     if (!["image/png", "image/jpeg", "image/gif"].includes(file.mimetype)) { | ||||||
|  |         return res.status(400).send("Unknown image type: " + file.mimetype); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const parentNoteId = await date_notes.getDateNoteId(req.headers['x-local-date']); | ||||||
|  |  | ||||||
|  |     const noteId = (await notes.createNewNote(parentNoteId, { | ||||||
|  |         title: "Sender image", | ||||||
|  |         content: "", | ||||||
|  |         target: 'into', | ||||||
|  |         isProtected: false, | ||||||
|  |         type: 'text', | ||||||
|  |         mime: 'text/html' | ||||||
|  |     })).noteId; | ||||||
|  |  | ||||||
|  |     const {fileName, imageId} = await image.saveImage(file, null, noteId); | ||||||
|  |  | ||||||
|  |     const url = `/api/images/${imageId}/${fileName}`; | ||||||
|  |  | ||||||
|  |     const content = `<img src="${url}"/>`; | ||||||
|  |  | ||||||
|  |     await sql.execute("UPDATE notes SET content = ? WHERE noteId = ?", [content, noteId]); | ||||||
|  |  | ||||||
|  |     res.send({}); | ||||||
|  | })); | ||||||
|  |  | ||||||
|  | router.post('/note', checkSenderToken, wrap(async (req, res, next) => { | ||||||
|  |     const parentNoteId = await date_notes.getDateNoteId(req.headers['x-local-date']); | ||||||
|  |  | ||||||
|  |     await notes.createNewNote(parentNoteId, { | ||||||
|  |         title: req.body.title, | ||||||
|  |         content: req.body.content, | ||||||
|  |         target: 'into', | ||||||
|  |         isProtected: false, | ||||||
|  |         type: 'text', | ||||||
|  |         mime: 'text/html' | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     res.send({}); | ||||||
|  | })); | ||||||
|  |  | ||||||
|  | module.exports = router; | ||||||
| @@ -79,9 +79,12 @@ router.get('/changed', auth.checkApiAuth, wrap(async (req, res, next) => { | |||||||
|  |  | ||||||
| router.get('/notes/:noteId', auth.checkApiAuth, wrap(async (req, res, next) => { | router.get('/notes/:noteId', auth.checkApiAuth, wrap(async (req, res, next) => { | ||||||
|     const noteId = req.params.noteId; |     const noteId = req.params.noteId; | ||||||
|  |     const entity = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId]); | ||||||
|  |  | ||||||
|  |     sync.serializeNoteContentBuffer(entity); | ||||||
|  |  | ||||||
|     res.send({ |     res.send({ | ||||||
|         entity: await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId]) |         entity: entity | ||||||
|     }); |     }); | ||||||
| })); | })); | ||||||
|  |  | ||||||
| @@ -147,6 +150,12 @@ router.get('/attributes/:attributeId', auth.checkApiAuth, wrap(async (req, res, | |||||||
|     res.send(await sql.getRow("SELECT * FROM attributes WHERE attributeId = ?", [attributeId])); |     res.send(await sql.getRow("SELECT * FROM attributes WHERE attributeId = ?", [attributeId])); | ||||||
| })); | })); | ||||||
|  |  | ||||||
|  | router.get('/api_tokens/:apiTokenId', auth.checkApiAuth, wrap(async (req, res, next) => { | ||||||
|  |     const apiTokenId = req.params.apiTokenId; | ||||||
|  |  | ||||||
|  |     res.send(await sql.getRow("SELECT * FROM api_tokens WHERE apiTokenId = ?", [apiTokenId])); | ||||||
|  | })); | ||||||
|  |  | ||||||
| router.put('/notes', auth.checkApiAuth, wrap(async (req, res, next) => { | router.put('/notes', auth.checkApiAuth, wrap(async (req, res, next) => { | ||||||
|     await syncUpdate.updateNote(req.body.entity, req.body.sourceId); |     await syncUpdate.updateNote(req.body.entity, req.body.sourceId); | ||||||
|  |  | ||||||
| @@ -201,4 +210,10 @@ router.put('/attributes', auth.checkApiAuth, wrap(async (req, res, next) => { | |||||||
|     res.send({}); |     res.send({}); | ||||||
| })); | })); | ||||||
|  |  | ||||||
|  | router.put('/api_tokens', auth.checkApiAuth, wrap(async (req, res, next) => { | ||||||
|  |     await syncUpdate.updateApiToken(req.body.entity, req.body.sourceId); | ||||||
|  |  | ||||||
|  |     res.send({}); | ||||||
|  | })); | ||||||
|  |  | ||||||
| module.exports = router; | module.exports = router; | ||||||
| @@ -6,6 +6,7 @@ const sql = require('../../services/sql'); | |||||||
| const options = require('../../services/options'); | const options = require('../../services/options'); | ||||||
| const utils = require('../../services/utils'); | const utils = require('../../services/utils'); | ||||||
| const auth = require('../../services/auth'); | const auth = require('../../services/auth'); | ||||||
|  | const config = require('../../services/config'); | ||||||
| const protected_session = require('../../services/protected_session'); | const protected_session = require('../../services/protected_session'); | ||||||
| const sync_table = require('../../services/sync_table'); | const sync_table = require('../../services/sync_table'); | ||||||
| const wrap = require('express-promise-wrap').wrap; | const wrap = require('express-promise-wrap').wrap; | ||||||
| @@ -29,8 +30,21 @@ router.get('/', auth.checkApiAuth, wrap(async (req, res, next) => { | |||||||
|  |  | ||||||
|     protected_session.decryptNotes(req, notes); |     protected_session.decryptNotes(req, notes); | ||||||
|  |  | ||||||
|  |     const hiddenInAutocomplete = await sql.getColumn(` | ||||||
|  |       SELECT  | ||||||
|  |         DISTINCT noteId  | ||||||
|  |       FROM  | ||||||
|  |         attributes | ||||||
|  |         JOIN notes USING(noteId) | ||||||
|  |       WHERE | ||||||
|  |         attributes.name = 'hide_in_autocomplete'  | ||||||
|  |         AND attributes.isDeleted = 0 | ||||||
|  |         AND notes.isDeleted = 0`); | ||||||
|  |  | ||||||
|     res.send({ |     res.send({ | ||||||
|  |         instanceName: config.General ? config.General.instanceName : null, | ||||||
|         notes: notes, |         notes: notes, | ||||||
|  |         hiddenInAutocomplete: hiddenInAutocomplete, | ||||||
|         start_note_path: await options.getOption('start_note_path') |         start_note_path: await options.getOption('start_note_path') | ||||||
|     }); |     }); | ||||||
| })); | })); | ||||||
|   | |||||||
| @@ -61,10 +61,8 @@ router.put('/:noteTreeId/move-before/:beforeNoteTreeId', auth.checkApiAuth, wrap | |||||||
|  |  | ||||||
|         await sync_table.addNoteReorderingSync(beforeNote.parentNoteId, sourceId); |         await sync_table.addNoteReorderingSync(beforeNote.parentNoteId, sourceId); | ||||||
|  |  | ||||||
|         const now = utils.nowDate(); |  | ||||||
|  |  | ||||||
|         await sql.execute("UPDATE note_tree SET parentNoteId = ?, notePosition = ?, dateModified = ? WHERE noteTreeId = ?", |         await sql.execute("UPDATE note_tree SET parentNoteId = ?, notePosition = ?, dateModified = ? WHERE noteTreeId = ?", | ||||||
|             [beforeNote.parentNoteId, beforeNote.notePosition, now, noteTreeId]); |             [beforeNote.parentNoteId, beforeNote.notePosition, utils.nowDate(), noteTreeId]); | ||||||
|  |  | ||||||
|         await sync_table.addNoteTreeSync(noteTreeId, sourceId); |         await sync_table.addNoteTreeSync(noteTreeId, sourceId); | ||||||
|     }); |     }); | ||||||
|   | |||||||
| @@ -5,13 +5,32 @@ const router = express.Router(); | |||||||
| const auth = require('../services/auth'); | const auth = require('../services/auth'); | ||||||
| const source_id = require('../services/source_id'); | const source_id = require('../services/source_id'); | ||||||
| const sql = require('../services/sql'); | const sql = require('../services/sql'); | ||||||
|  | const Repository = require('../services/repository'); | ||||||
|  | const attributes = require('../services/attributes'); | ||||||
| const wrap = require('express-promise-wrap').wrap; | const wrap = require('express-promise-wrap').wrap; | ||||||
|  |  | ||||||
| router.get('', auth.checkAuth, wrap(async (req, res, next) => { | router.get('', auth.checkAuth, wrap(async (req, res, next) => { | ||||||
|  |     const repository = new Repository(req); | ||||||
|  |  | ||||||
|     res.render('index', { |     res.render('index', { | ||||||
|         sourceId: await source_id.generateSourceId(), |         sourceId: await source_id.generateSourceId(), | ||||||
|         maxSyncIdAtLoad: await sql.getValue("SELECT MAX(id) FROM sync") |         maxSyncIdAtLoad: await sql.getValue("SELECT MAX(id) FROM sync"), | ||||||
|  |         appCss: await getAppCss(repository) | ||||||
|     }); |     }); | ||||||
| })); | })); | ||||||
|  |  | ||||||
|  | async function getAppCss(repository) { | ||||||
|  |     let css = ''; | ||||||
|  |     const notes = attributes.getNotesWithAttribute(repository, 'app_css'); | ||||||
|  |  | ||||||
|  |     for (const note of await notes) { | ||||||
|  |         css += `/* ${note.noteId} */ | ||||||
|  | ${note.content} | ||||||
|  |  | ||||||
|  | `; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return css; | ||||||
|  | } | ||||||
|  |  | ||||||
| module.exports = router; | module.exports = router; | ||||||
|   | |||||||
| @@ -28,6 +28,8 @@ const cleanupRoute = require('./api/cleanup'); | |||||||
| const imageRoute = require('./api/image'); | const imageRoute = require('./api/image'); | ||||||
| const attributesRoute = require('./api/attributes'); | const attributesRoute = require('./api/attributes'); | ||||||
| const scriptRoute = require('./api/script'); | const scriptRoute = require('./api/script'); | ||||||
|  | const senderRoute = require('./api/sender'); | ||||||
|  | const attachmentsRoute = require('./api/attachments'); | ||||||
|  |  | ||||||
| function register(app) { | function register(app) { | ||||||
|     app.use('/', indexRoute); |     app.use('/', indexRoute); | ||||||
| @@ -40,7 +42,7 @@ function register(app) { | |||||||
|     app.use('/api/notes', notesApiRoute); |     app.use('/api/notes', notesApiRoute); | ||||||
|     app.use('/api/tree', treeChangesApiRoute); |     app.use('/api/tree', treeChangesApiRoute); | ||||||
|     app.use('/api/notes', cloningApiRoute); |     app.use('/api/notes', cloningApiRoute); | ||||||
|     app.use('/api/notes', attributesRoute); |     app.use('/api', attributesRoute); | ||||||
|     app.use('/api/notes-history', noteHistoryApiRoute); |     app.use('/api/notes-history', noteHistoryApiRoute); | ||||||
|     app.use('/api/recent-changes', recentChangesApiRoute); |     app.use('/api/recent-changes', recentChangesApiRoute); | ||||||
|     app.use('/api/settings', settingsApiRoute); |     app.use('/api/settings', settingsApiRoute); | ||||||
| @@ -59,6 +61,8 @@ function register(app) { | |||||||
|     app.use('/api/cleanup', cleanupRoute); |     app.use('/api/cleanup', cleanupRoute); | ||||||
|     app.use('/api/images', imageRoute); |     app.use('/api/images', imageRoute); | ||||||
|     app.use('/api/script', scriptRoute); |     app.use('/api/script', scriptRoute); | ||||||
|  |     app.use('/api/sender', senderRoute); | ||||||
|  |     app.use('/api/attachments', attachmentsRoute); | ||||||
| } | } | ||||||
|  |  | ||||||
| module.exports = { | module.exports = { | ||||||
|   | |||||||
							
								
								
									
										
											BIN
										
									
								
								src/scripts/Reddit Importer.tar
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/scripts/Reddit Importer.tar
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										13
									
								
								src/scripts/today.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/scripts/today.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | |||||||
|  | api.addButtonToToolbar('go-today', $('<button class="btn btn-xs" onclick="goToday();"><span class="ui-icon ui-icon-calendar"></span> Today</button>')); | ||||||
|  |  | ||||||
|  | window.goToday = async function() { | ||||||
|  |     const todayDateStr = formatDateISO(new Date()); | ||||||
|  |  | ||||||
|  |     const todayNoteId = await server.exec([todayDateStr], async todayDateStr => { | ||||||
|  |         return await this.getDateNoteId(todayDateStr); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     api.activateNote(todayNoteId); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | $(document).bind('keydown', "alt+t", window.goToday); | ||||||
| @@ -1,4 +1,4 @@ | |||||||
| <form id="weight-form" style="display: flex; width: 500px; justify-content: space-around; align-items: flex-end;"> | <form id="weight-form" style="display: flex; width: 700px; justify-content: space-around; align-items: flex-end;"> | ||||||
|     <div> |     <div> | ||||||
|         <label for="weight-date">Date</label> |         <label for="weight-date">Date</label> | ||||||
|         <input type="text" id="weight-date" class="form-control" style="width: 150px; text-align: center;" /> |         <input type="text" id="weight-date" class="form-control" style="width: 150px; text-align: center;" /> | ||||||
| @@ -7,6 +7,10 @@ | |||||||
|         <label for="weight">Weight</label> |         <label for="weight">Weight</label> | ||||||
|         <input type="number" id="weight" value="80.0" step="0.1" class="form-control" style="text-align: center; width: 100px;" /> |         <input type="number" id="weight" value="80.0" step="0.1" class="form-control" style="text-align: center; width: 100px;" /> | ||||||
|     </div> |     </div> | ||||||
|  |     <div> | ||||||
|  |         <label for="comment">Comment</label> | ||||||
|  |         <input type="text" id="comment" class="form-control" style="width: 200px;" /> | ||||||
|  |     </div> | ||||||
|  |  | ||||||
|     <button type="submit" class="btn btn-primary">Add</button> |     <button type="submit" class="btn btn-primary">Add</button> | ||||||
| </form> | </form> | ||||||
| @@ -16,82 +20,127 @@ | |||||||
| <canvas id="canvas"></canvas> | <canvas id="canvas"></canvas> | ||||||
|  |  | ||||||
| <script> | <script> | ||||||
| (async function() { |     (async function() { | ||||||
|     const dateEl = $("#weight-date"); |         const $form = $("#weight-form"); | ||||||
|     const weightEl = $("#weight"); |         const $date = $("#weight-date"); | ||||||
|  |         const $weight = $("#weight"); | ||||||
|  |         const $comment = $("#comment"); | ||||||
|  |         let chart; | ||||||
|  |  | ||||||
|     dateEl.datepicker(); |         $date.datepicker(); | ||||||
|     dateEl.datepicker('option', 'dateFormat', 'yy-mm-dd'); |         $date.datepicker('option', 'dateFormat', 'yy-mm-dd'); | ||||||
|     dateEl.datepicker('setDate', new Date()); |         $date.datepicker('setDate', new Date()); | ||||||
|  |  | ||||||
|     async function saveWeight() { |         async function saveWeight() { | ||||||
|         await server.exec([dateEl.val(), weightEl.val()], async (date, weight) => { |             await server.exec([$date.val(), parseFloat($weight.val()), $comment.val()], async (date, weight, comment) => { | ||||||
|             const dataNote = await this.getNoteWithAttribute('date_data', date); |                 const dataNote = await this.getNoteWithAttribute('date_data', date); | ||||||
|  |  | ||||||
|             if (dataNote) { |                 if (dataNote) { | ||||||
|                 dataNote.jsonContent.weight = weight; |                     dataNote.jsonContent.weight = weight; | ||||||
|  |  | ||||||
|                 await this.updateEntity(dataNote); |                     if (comment) { | ||||||
|             } |                         dataNote.jsonContent.weight_comment = comment; | ||||||
|             else { |  | ||||||
|                 const parentNoteId = await this.getDateNoteId(date); |  | ||||||
|                 const jsonContent = { weight: weight }; |  | ||||||
|  |  | ||||||
|                 await this.createNote(parentNoteId, 'data', jsonContent, { |  | ||||||
|                     json: true, |  | ||||||
|                     attributes: { |  | ||||||
|                         date_data: date |  | ||||||
|                     } |                     } | ||||||
|                 }); |  | ||||||
|             } |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|         showMessage("Weight has been saved"); |                     await this.updateEntity(dataNote); | ||||||
|  |                 } | ||||||
|  |                 else { | ||||||
|  |                     const parentNoteId = await this.getDateNoteId(date); | ||||||
|  |                     const jsonContent = { weight: weight }; | ||||||
|  |  | ||||||
|  |                     if (comment) { | ||||||
|  |                         jsonContent.weight_comment = comment; | ||||||
|  |                     } | ||||||
|  |  | ||||||
|  |                     await this.createNote(parentNoteId, 'data', jsonContent, { | ||||||
|  |                         json: true, | ||||||
|  |                         attributes: { | ||||||
|  |                             date_data: date, | ||||||
|  |                             hide_in_autocomplete: null | ||||||
|  |                         } | ||||||
|  |                     }); | ||||||
|  |                 } | ||||||
|  |             }); | ||||||
|  |  | ||||||
|  |             showMessage("Weight has been saved"); | ||||||
|  |  | ||||||
|  |             chart.data = await getData(); | ||||||
|  |             chart.update(); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         async function drawChart() { | ||||||
|  |             const data = await getData(); | ||||||
|  |  | ||||||
|  |             const ctx = $("#canvas")[0].getContext("2d"); | ||||||
|  |  | ||||||
|  |             chart = new Chart(ctx, { | ||||||
|  |                 type: 'line', | ||||||
|  |                 data: data, | ||||||
|  |                 options: { | ||||||
|  |                     tooltips: { | ||||||
|  |                         enabled: true, | ||||||
|  |                         mode: 'single', | ||||||
|  |                         callbacks: { | ||||||
|  |                             label: function (tooltipItem, data) { | ||||||
|  |                                 const multistringText = [tooltipItem.yLabel]; | ||||||
|  |                                 const comment = data.comments[tooltipItem['index']]; | ||||||
|  |  | ||||||
|  |                                 if (comment) { | ||||||
|  |                                     multistringText.push(comment); | ||||||
|  |                                 } | ||||||
|  |  | ||||||
|  |                                 return multistringText; | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                     }, | ||||||
|  |                 } | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         async function getData() { | ||||||
|  |             const data = await server.exec([], async () => { | ||||||
|  |                 const notes = await this.getNotesWithAttribute('date_data'); | ||||||
|  |                 const data = []; | ||||||
|  |  | ||||||
|  |                 for (const note of notes) { | ||||||
|  |                     const dateAttr = await note.getAttribute('date_data'); | ||||||
|  |  | ||||||
|  |                     data.push({ | ||||||
|  |                         date: dateAttr.value, | ||||||
|  |                         weight: note.jsonContent.weight, | ||||||
|  |                         comment: note.jsonContent.weight_comment | ||||||
|  |                     }); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 data.sort((a, b) => a.date < b.date ? -1 : +1); | ||||||
|  |  | ||||||
|  |                 return data; | ||||||
|  |             }); | ||||||
|  |  | ||||||
|  |             const datasets = [{ | ||||||
|  |                 label: "Weight", | ||||||
|  |                 backgroundColor: 'red', | ||||||
|  |                 borderColor: 'red', | ||||||
|  |                 data: data.map(row => row.weight), | ||||||
|  |                 fill: false | ||||||
|  |             }]; | ||||||
|  |  | ||||||
|  |             const labels = data.map(row => row.date); | ||||||
|  |             const comments = data.map(row => row.comment); | ||||||
|  |  | ||||||
|  |             return { | ||||||
|  |                 labels: labels, | ||||||
|  |                 datasets: datasets, | ||||||
|  |                 comments: comments | ||||||
|  |             }; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         $form.submit(event => { | ||||||
|  |             saveWeight(); | ||||||
|  |  | ||||||
|  |             event.preventDefault(); | ||||||
|  |         }); | ||||||
|  |  | ||||||
|         drawChart(); |         drawChart(); | ||||||
|     } |     })(); | ||||||
|  |  | ||||||
|     async function drawChart() { |  | ||||||
|         const data = await server.exec([], async () => { |  | ||||||
|             const notes = await this.getNotesWithAttribute('date_data'); |  | ||||||
|             const data = []; |  | ||||||
|  |  | ||||||
|             for (const note of notes) { |  | ||||||
|                 const dateAttr = await note.getAttribute('date_data'); |  | ||||||
|  |  | ||||||
|                 data.push({ |  | ||||||
|                     date: dateAttr.value, |  | ||||||
|                     weight: note.jsonContent.weight |  | ||||||
|                 }); |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             return data; |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|         var config = { |  | ||||||
|             type: 'line', |  | ||||||
|             data: { |  | ||||||
|                 labels: data.map(row => row.date), |  | ||||||
|                 datasets: [{ |  | ||||||
|                     label: "Weight", |  | ||||||
|                     backgroundColor: 'red', |  | ||||||
|                     borderColor: 'red', |  | ||||||
|                     data: data.map(row => row.weight), |  | ||||||
|                     fill: false |  | ||||||
|                 }] |  | ||||||
|             } |  | ||||||
|         }; |  | ||||||
|  |  | ||||||
|         var ctx = $("#canvas")[0].getContext("2d"); |  | ||||||
|         new Chart(ctx, config); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     $("#weight-form").submit(event => { |  | ||||||
|         saveWeight(); |  | ||||||
|  |  | ||||||
|         event.preventDefault(); |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     drawChart(); |  | ||||||
| })(); |  | ||||||
| </script> | </script> | ||||||
| @@ -3,7 +3,7 @@ | |||||||
| const build = require('./build'); | const build = require('./build'); | ||||||
| const packageJson = require('../../package'); | const packageJson = require('../../package'); | ||||||
|  |  | ||||||
| const APP_DB_VERSION = 71; | const APP_DB_VERSION = 78; | ||||||
|  |  | ||||||
| module.exports = { | module.exports = { | ||||||
|     app_version: packageJson.version, |     app_version: packageJson.version, | ||||||
|   | |||||||
| @@ -3,56 +3,76 @@ | |||||||
| const sql = require('./sql'); | const sql = require('./sql'); | ||||||
| const utils = require('./utils'); | const utils = require('./utils'); | ||||||
| const sync_table = require('./sync_table'); | const sync_table = require('./sync_table'); | ||||||
| const Repository = require('./repository'); |  | ||||||
|  | const BUILTIN_ATTRIBUTES = [ | ||||||
|  |     'frontend_startup', | ||||||
|  |     'backend_startup', | ||||||
|  |     'disable_versioning', | ||||||
|  |     'calendar_root', | ||||||
|  |     'hide_in_autocomplete', | ||||||
|  |     'exclude_from_export', | ||||||
|  |     'run', | ||||||
|  |     'manual_transaction_handling', | ||||||
|  |     'disable_inclusion', | ||||||
|  |     'app_css' | ||||||
|  | ]; | ||||||
|  |  | ||||||
| async function getNoteAttributeMap(noteId) { | async function getNoteAttributeMap(noteId) { | ||||||
|     return await sql.getMap(`SELECT name, value FROM attributes WHERE noteId = ?`, [noteId]); |     return await sql.getMap(`SELECT name, value FROM attributes WHERE noteId = ? AND isDeleted = 0`, [noteId]); | ||||||
| } | } | ||||||
|  |  | ||||||
| async function getNoteIdWithAttribute(name, value) { | async function getNoteIdWithAttribute(name, value) { | ||||||
|     return await sql.getValue(`SELECT notes.noteId FROM notes JOIN attributes USING(noteId)  |     return await sql.getValue(`SELECT notes.noteId FROM notes JOIN attributes USING(noteId)  | ||||||
|           WHERE notes.isDeleted = 0 AND attributes.name = ? AND attributes.value = ?`, [name, value]); |           WHERE notes.isDeleted = 0 | ||||||
|  |                 AND attributes.isDeleted = 0 | ||||||
|  |                 AND attributes.name = ?  | ||||||
|  |                 AND attributes.value = ?`, [name, value]); | ||||||
| } | } | ||||||
|  |  | ||||||
| async function getNotesWithAttribute(dataKey, name, value) { | async function getNotesWithAttribute(repository, name, value) { | ||||||
|     const repository = new Repository(dataKey); |  | ||||||
|  |  | ||||||
|     let notes; |     let notes; | ||||||
|  |  | ||||||
|     if (value !== undefined) { |     if (value !== undefined) { | ||||||
|         notes = await repository.getEntities(`SELECT notes.* FROM notes JOIN attributes USING(noteId)  |         notes = await repository.getEntities(`SELECT notes.* FROM notes JOIN attributes USING(noteId)  | ||||||
|           WHERE notes.isDeleted = 0 AND attributes.name = ? AND attributes.value = ?`, [name, value]); |           WHERE notes.isDeleted = 0 AND attributes.isDeleted = 0 AND attributes.name = ? AND attributes.value = ?`, [name, value]); | ||||||
|     } |     } | ||||||
|     else { |     else { | ||||||
|         notes = await repository.getEntities(`SELECT notes.* FROM notes JOIN attributes USING(noteId)  |         notes = await repository.getEntities(`SELECT notes.* FROM notes JOIN attributes USING(noteId)  | ||||||
|           WHERE notes.isDeleted = 0 AND attributes.name = ?`, [name]); |           WHERE notes.isDeleted = 0 AND attributes.isDeleted = 0 AND attributes.name = ?`, [name]); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     return notes; |     return notes; | ||||||
| } | } | ||||||
|  |  | ||||||
| async function getNoteWithAttribute(dataKey, name, value) { | async function getNoteWithAttribute(repository, name, value) { | ||||||
|     const notes = getNotesWithAttribute(dataKey, name, value); |     const notes = getNotesWithAttribute(repository, name, value); | ||||||
|  |  | ||||||
|     return notes.length > 0 ? notes[0] : null; |     return notes.length > 0 ? notes[0] : null; | ||||||
| } | } | ||||||
|  |  | ||||||
| async function getNoteIdsWithAttribute(name) { | async function getNoteIdsWithAttribute(name) { | ||||||
|     return await sql.getColumn(`SELECT DISTINCT notes.noteId FROM notes JOIN attributes USING(noteId)  |     return await sql.getColumn(`SELECT DISTINCT notes.noteId FROM notes JOIN attributes USING(noteId)  | ||||||
|           WHERE notes.isDeleted = 0 AND attributes.name = ?`, [name]); |           WHERE notes.isDeleted = 0 AND attributes.isDeleted = 0 AND attributes.name = ? AND attributes.isDeleted = 0`, [name]); | ||||||
| } | } | ||||||
|  |  | ||||||
| async function createAttribute(noteId, name, value = null, sourceId = null) { | async function createAttribute(noteId, name, value = "", sourceId = null) { | ||||||
|  |     if (value === null || value === undefined) { | ||||||
|  |         value = ""; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     const now = utils.nowDate(); |     const now = utils.nowDate(); | ||||||
|     const attributeId = utils.newAttributeId(); |     const attributeId = utils.newAttributeId(); | ||||||
|  |     const position = 1 + await sql.getValue(`SELECT COALESCE(MAX(position), 0) FROM attributes WHERE noteId = ?`, [noteId]); | ||||||
|  |  | ||||||
|     await sql.insert("attributes", { |     await sql.insert("attributes", { | ||||||
|         attributeId: attributeId, |         attributeId: attributeId, | ||||||
|         noteId: noteId, |         noteId: noteId, | ||||||
|         name: name, |         name: name, | ||||||
|         value: value, |         value: value, | ||||||
|  |         position: position, | ||||||
|         dateModified: now, |         dateModified: now, | ||||||
|         dateCreated: now |         dateCreated: now, | ||||||
|  |         isDeleted: false | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     await sync_table.addAttributeSync(attributeId, sourceId); |     await sync_table.addAttributeSync(attributeId, sourceId); | ||||||
| @@ -64,5 +84,6 @@ module.exports = { | |||||||
|     getNotesWithAttribute, |     getNotesWithAttribute, | ||||||
|     getNoteWithAttribute, |     getNoteWithAttribute, | ||||||
|     getNoteIdsWithAttribute, |     getNoteIdsWithAttribute, | ||||||
|     createAttribute |     createAttribute, | ||||||
|  |     BUILTIN_ATTRIBUTES | ||||||
| }; | }; | ||||||
| @@ -10,7 +10,7 @@ const sync_mutex = require('./sync_mutex'); | |||||||
|  |  | ||||||
| async function regularBackup() { | async function regularBackup() { | ||||||
|     const now = new Date(); |     const now = new Date(); | ||||||
|     const lastBackupDate = utils.parseDate(await options.getOption('last_backup_date')); |     const lastBackupDate = utils.parseDateTime(await options.getOption('last_backup_date')); | ||||||
|  |  | ||||||
|     console.log(lastBackupDate); |     console.log(lastBackupDate); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -214,7 +214,7 @@ async function runAllChecks() { | |||||||
|           FROM  |           FROM  | ||||||
|             notes |             notes | ||||||
|           WHERE  |           WHERE  | ||||||
|             type != 'text' AND type != 'code' AND type != 'render'`, |             type != 'text' AND type != 'code' AND type != 'render' AND type != 'file'`, | ||||||
|         "Note has invalid type", errorList); |         "Note has invalid type", errorList); | ||||||
|  |  | ||||||
|     await runSyncRowChecks("notes", "noteId", errorList); |     await runSyncRowChecks("notes", "noteId", errorList); | ||||||
| @@ -223,6 +223,8 @@ async function runAllChecks() { | |||||||
|     await runSyncRowChecks("recent_notes", "noteTreeId", errorList); |     await runSyncRowChecks("recent_notes", "noteTreeId", errorList); | ||||||
|     await runSyncRowChecks("images", "imageId", errorList); |     await runSyncRowChecks("images", "imageId", errorList); | ||||||
|     await runSyncRowChecks("note_images", "noteImageId", errorList); |     await runSyncRowChecks("note_images", "noteImageId", errorList); | ||||||
|  |     await runSyncRowChecks("attributes", "attributeId", errorList); | ||||||
|  |     await runSyncRowChecks("api_tokens", "apiTokenId", errorList); | ||||||
|  |  | ||||||
|     if (errorList.length === 0) { |     if (errorList.length === 0) { | ||||||
|         // we run this only if basic checks passed since this assumes basic data consistency |         // we run this only if basic checks passed since this assumes basic data consistency | ||||||
|   | |||||||
| @@ -20,6 +20,5 @@ module.exports = { | |||||||
|     DOCUMENT_PATH, |     DOCUMENT_PATH, | ||||||
|     BACKUP_DIR, |     BACKUP_DIR, | ||||||
|     LOG_DIR, |     LOG_DIR, | ||||||
|     EXPORT_DIR, |  | ||||||
|     ANONYMIZED_DB_DIR |     ANONYMIZED_DB_DIR | ||||||
| }; | }; | ||||||
| @@ -88,7 +88,7 @@ function noteTitleIv(iv) { | |||||||
|     return "0" + iv; |     return "0" + iv; | ||||||
| } | } | ||||||
|  |  | ||||||
| function noteTextIv(iv) { | function noteContentIv(iv) { | ||||||
|     return "1" + iv; |     return "1" + iv; | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -97,5 +97,5 @@ module.exports = { | |||||||
|     decrypt, |     decrypt, | ||||||
|     decryptString, |     decryptString, | ||||||
|     noteTitleIv, |     noteTitleIv, | ||||||
|     noteTextIv |     noteContentIv | ||||||
| }; | }; | ||||||
| @@ -3,12 +3,16 @@ | |||||||
| const sql = require('./sql'); | const sql = require('./sql'); | ||||||
| const notes = require('./notes'); | const notes = require('./notes'); | ||||||
| const attributes = require('./attributes'); | const attributes = require('./attributes'); | ||||||
|  | const utils = require('./utils'); | ||||||
|  |  | ||||||
| const CALENDAR_ROOT_ATTRIBUTE = 'calendar_root'; | const CALENDAR_ROOT_ATTRIBUTE = 'calendar_root'; | ||||||
| const YEAR_ATTRIBUTE = 'year_note'; | const YEAR_ATTRIBUTE = 'year_note'; | ||||||
| const MONTH_ATTRIBUTE = 'month_note'; | const MONTH_ATTRIBUTE = 'month_note'; | ||||||
| const DATE_ATTRIBUTE = 'date_note'; | const DATE_ATTRIBUTE = 'date_note'; | ||||||
|  |  | ||||||
|  | const DAYS = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday']; | ||||||
|  | const MONTHS = ['January','February','March','April','May','June','July','August','September','October','November','December']; | ||||||
|  |  | ||||||
| async function createNote(parentNoteId, noteTitle, noteText) { | async function createNote(parentNoteId, noteTitle, noteText) { | ||||||
|     return (await notes.createNewNote(parentNoteId, { |     return (await notes.createNewNote(parentNoteId, { | ||||||
|         title: noteTitle, |         title: noteTitle, | ||||||
| @@ -25,7 +29,7 @@ async function getNoteStartingWith(parentNoteId, startsWith) { | |||||||
|                                     AND note_tree.isDeleted = 0`, [parentNoteId]); |                                     AND note_tree.isDeleted = 0`, [parentNoteId]); | ||||||
| } | } | ||||||
|  |  | ||||||
| async function getRootNoteId() { | async function getRootCalendarNoteId() { | ||||||
|     let rootNoteId = await sql.getValue(`SELECT notes.noteId FROM notes JOIN attributes USING(noteId)  |     let rootNoteId = await sql.getValue(`SELECT notes.noteId FROM notes JOIN attributes USING(noteId)  | ||||||
|               WHERE attributes.name = '${CALENDAR_ROOT_ATTRIBUTE}' AND notes.isDeleted = 0`); |               WHERE attributes.name = '${CALENDAR_ROOT_ATTRIBUTE}' AND notes.isDeleted = 0`); | ||||||
|  |  | ||||||
| @@ -72,7 +76,11 @@ async function getMonthNoteId(dateTimeStr, rootNoteId) { | |||||||
|         monthNoteId = await getNoteStartingWith(yearNoteId, monthNumber); |         monthNoteId = await getNoteStartingWith(yearNoteId, monthNumber); | ||||||
|  |  | ||||||
|         if (!monthNoteId) { |         if (!monthNoteId) { | ||||||
|             monthNoteId = await createNote(yearNoteId, monthNumber); |             const dateObj = utils.parseDate(dateTimeStr); | ||||||
|  |  | ||||||
|  |             const noteTitle = monthNumber + " - " + MONTHS[dateObj.getMonth()]; | ||||||
|  |  | ||||||
|  |             monthNoteId = await createNote(yearNoteId, noteTitle); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         await attributes.createAttribute(monthNoteId, MONTH_ATTRIBUTE, monthStr); |         await attributes.createAttribute(monthNoteId, MONTH_ATTRIBUTE, monthStr); | ||||||
| @@ -83,7 +91,7 @@ async function getMonthNoteId(dateTimeStr, rootNoteId) { | |||||||
|  |  | ||||||
| async function getDateNoteId(dateTimeStr, rootNoteId = null) { | async function getDateNoteId(dateTimeStr, rootNoteId = null) { | ||||||
|     if (!rootNoteId) { |     if (!rootNoteId) { | ||||||
|         rootNoteId = await getRootNoteId(); |         rootNoteId = await getRootCalendarNoteId(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const dateStr = dateTimeStr.substr(0, 10); |     const dateStr = dateTimeStr.substr(0, 10); | ||||||
| @@ -97,7 +105,11 @@ async function getDateNoteId(dateTimeStr, rootNoteId = null) { | |||||||
|         dateNoteId = await getNoteStartingWith(monthNoteId, dayNumber); |         dateNoteId = await getNoteStartingWith(monthNoteId, dayNumber); | ||||||
|  |  | ||||||
|         if (!dateNoteId) { |         if (!dateNoteId) { | ||||||
|             dateNoteId = await createNote(monthNoteId, dayNumber); |             const dateObj = utils.parseDate(dateTimeStr); | ||||||
|  |  | ||||||
|  |             const noteTitle = dayNumber + " - " + DAYS[dateObj.getDay()]; | ||||||
|  |  | ||||||
|  |             dateNoteId = await createNote(monthNoteId, noteTitle); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         await attributes.createAttribute(dateNoteId, DATE_ATTRIBUTE, dateStr); |         await attributes.createAttribute(dateNoteId, DATE_ATTRIBUTE, dateStr); | ||||||
| @@ -107,7 +119,7 @@ async function getDateNoteId(dateTimeStr, rootNoteId = null) { | |||||||
| } | } | ||||||
|  |  | ||||||
| module.exports = { | module.exports = { | ||||||
|     getRootNoteId, |     getRootCalendarNoteId, | ||||||
|     getYearNoteId, |     getYearNoteId, | ||||||
|     getMonthNoteId, |     getMonthNoteId, | ||||||
|     getDateNoteId |     getDateNoteId | ||||||
|   | |||||||
							
								
								
									
										108
									
								
								src/services/image.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								src/services/image.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,108 @@ | |||||||
|  | "use strict"; | ||||||
|  |  | ||||||
|  | const utils = require('./utils'); | ||||||
|  | const sql = require('./sql'); | ||||||
|  | const sync_table = require('./sync_table'); | ||||||
|  | const imagemin = require('imagemin'); | ||||||
|  | const imageminMozJpeg = require('imagemin-mozjpeg'); | ||||||
|  | const imageminPngQuant = require('imagemin-pngquant'); | ||||||
|  | const imageminGifLossy = require('imagemin-giflossy'); | ||||||
|  | const jimp = require('jimp'); | ||||||
|  | const imageType = require('image-type'); | ||||||
|  | const sanitizeFilename = require('sanitize-filename'); | ||||||
|  |  | ||||||
|  | async function saveImage(file, sourceId, noteId) { | ||||||
|  |     const resizedImage = await resize(file.buffer); | ||||||
|  |     const optimizedImage = await optimize(resizedImage); | ||||||
|  |  | ||||||
|  |     const imageFormat = imageType(optimizedImage); | ||||||
|  |  | ||||||
|  |     const fileNameWithouExtension = file.originalname.replace(/\.[^/.]+$/, ""); | ||||||
|  |     const fileName = sanitizeFilename(fileNameWithouExtension + "." + imageFormat.ext); | ||||||
|  |  | ||||||
|  |     const imageId = utils.newImageId(); | ||||||
|  |     const now = utils.nowDate(); | ||||||
|  |  | ||||||
|  |     await sql.doInTransaction(async () => { | ||||||
|  |         await sql.insert("images", { | ||||||
|  |             imageId: imageId, | ||||||
|  |             format: imageFormat.ext, | ||||||
|  |             name: fileName, | ||||||
|  |             checksum: utils.hash(optimizedImage), | ||||||
|  |             data: optimizedImage, | ||||||
|  |             isDeleted: 0, | ||||||
|  |             dateModified: now, | ||||||
|  |             dateCreated: now | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         await sync_table.addImageSync(imageId, sourceId); | ||||||
|  |  | ||||||
|  |         const noteImageId = utils.newNoteImageId(); | ||||||
|  |  | ||||||
|  |         await sql.insert("note_images", { | ||||||
|  |             noteImageId: noteImageId, | ||||||
|  |             noteId: noteId, | ||||||
|  |             imageId: imageId, | ||||||
|  |             isDeleted: 0, | ||||||
|  |             dateModified: now, | ||||||
|  |             dateCreated: now | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         await sync_table.addNoteImageSync(noteImageId, sourceId); | ||||||
|  |     }); | ||||||
|  |     return {fileName, imageId}; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const MAX_SIZE = 1000; | ||||||
|  | const MAX_BYTE_SIZE = 200000; // images should have under 100 KBs | ||||||
|  |  | ||||||
|  | async function resize(buffer) { | ||||||
|  |     const image = await jimp.read(buffer); | ||||||
|  |  | ||||||
|  |     if (image.bitmap.width > image.bitmap.height && image.bitmap.width > MAX_SIZE) { | ||||||
|  |         image.resize(MAX_SIZE, jimp.AUTO); | ||||||
|  |     } | ||||||
|  |     else if (image.bitmap.height > MAX_SIZE) { | ||||||
|  |         image.resize(jimp.AUTO, MAX_SIZE); | ||||||
|  |     } | ||||||
|  |     else if (buffer.byteLength <= MAX_BYTE_SIZE) { | ||||||
|  |         return buffer; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // we do resizing with max quality which will be trimmed during optimization step next | ||||||
|  |     image.quality(100); | ||||||
|  |  | ||||||
|  |     // when converting PNG to JPG we lose alpha channel, this is replaced by white to match Trilium white background | ||||||
|  |     image.background(0xFFFFFFFF); | ||||||
|  |  | ||||||
|  |     // getBuffer doesn't support promises so this workaround | ||||||
|  |     return await new Promise((resolve, reject) => image.getBuffer(jimp.MIME_JPEG, (err, data) => { | ||||||
|  |         if (err) { | ||||||
|  |             reject(err); | ||||||
|  |         } | ||||||
|  |         else { | ||||||
|  |             resolve(data); | ||||||
|  |         } | ||||||
|  |     })); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function optimize(buffer) { | ||||||
|  |     return await imagemin.buffer(buffer, { | ||||||
|  |         plugins: [ | ||||||
|  |             imageminMozJpeg({ | ||||||
|  |                 quality: 50 | ||||||
|  |             }), | ||||||
|  |             imageminPngQuant({ | ||||||
|  |                 quality: "0-70" | ||||||
|  |             }), | ||||||
|  |             imageminGifLossy({ | ||||||
|  |                 lossy: 80, | ||||||
|  |                 optimize: '3' // needs to be string | ||||||
|  |             }) | ||||||
|  |         ] | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | module.exports = { | ||||||
|  |     saveImage | ||||||
|  | }; | ||||||
| @@ -22,7 +22,7 @@ function info(message) { | |||||||
|  |  | ||||||
| function error(message) { | function error(message) { | ||||||
|     // we're using .info() instead of .error() because simple-node-logger emits weird error for showError() |     // we're using .info() instead of .error() because simple-node-logger emits weird error for showError() | ||||||
|     info(message); |     info("ERROR: " + message); | ||||||
| } | } | ||||||
|  |  | ||||||
| const requestBlacklist = [ "/libraries", "/javascripts", "/images", "/stylesheets" ]; | const requestBlacklist = [ "/libraries", "/javascripts", "/images", "/stylesheets" ]; | ||||||
|   | |||||||
| @@ -5,32 +5,6 @@ const sync_table = require('./sync_table'); | |||||||
| const attributes = require('./attributes'); | const attributes = require('./attributes'); | ||||||
| const protected_session = require('./protected_session'); | const protected_session = require('./protected_session'); | ||||||
|  |  | ||||||
| async function updateJsonNote(noteId, data) { |  | ||||||
|     const ret = await createNewNote(noteId, { |  | ||||||
|         title: name, |  | ||||||
|         content: JSON.stringify(data), |  | ||||||
|         target: 'into', |  | ||||||
|         isProtected: false, |  | ||||||
|         type: 'code', |  | ||||||
|         mime: 'application/json' |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     return ret.noteId; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| async function createNewJsonNote(parentNoteId, name, payload) { |  | ||||||
|     const ret = await createNewNote(parentNoteId, { |  | ||||||
|         title: name, |  | ||||||
|         content: JSON.stringify(payload), |  | ||||||
|         target: 'into', |  | ||||||
|         isProtected: false, |  | ||||||
|         type: 'code', |  | ||||||
|         mime: 'application/json' |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     return ret.noteId; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| async function createNewNote(parentNoteId, noteOpts, dataKey, sourceId) { | async function createNewNote(parentNoteId, noteOpts, dataKey, sourceId) { | ||||||
|     const noteId = utils.newNoteId(); |     const noteId = utils.newNoteId(); | ||||||
|     const noteTreeId = utils.newNoteTreeId(); |     const noteTreeId = utils.newNoteTreeId(); | ||||||
| @@ -109,6 +83,40 @@ async function createNewNote(parentNoteId, noteOpts, dataKey, sourceId) { | |||||||
|     }; |     }; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | async function createNote(parentNoteId, title, content = "", extraOptions = {}) { | ||||||
|  |     if (!parentNoteId) throw new Error("Empty parentNoteId"); | ||||||
|  |     if (!title) throw new Error("Empty title"); | ||||||
|  |  | ||||||
|  |     const note = { | ||||||
|  |         title: title, | ||||||
|  |         content: extraOptions.json ? JSON.stringify(content, null, '\t') : content, | ||||||
|  |         target: 'into', | ||||||
|  |         isProtected: extraOptions.isProtected !== undefined ? extraOptions.isProtected : false, | ||||||
|  |         type: extraOptions.type, | ||||||
|  |         mime: extraOptions.mime | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     if (extraOptions.json) { | ||||||
|  |         note.type = "code"; | ||||||
|  |         note.mime = "application/json"; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (!note.type) { | ||||||
|  |         note.type = "text"; | ||||||
|  |         note.mime = "text/html"; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const {noteId} = await createNewNote(parentNoteId, note, extraOptions.dataKey, extraOptions.sourceId); | ||||||
|  |  | ||||||
|  |     if (extraOptions.attributes) { | ||||||
|  |         for (const attrName in extraOptions.attributes) { | ||||||
|  |             await attributes.createAttribute(noteId, attrName, extraOptions.attributes[attrName]); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return noteId; | ||||||
|  | } | ||||||
|  |  | ||||||
| async function protectNoteRecursively(noteId, dataKey, protect, sourceId) { | async function protectNoteRecursively(noteId, dataKey, protect, sourceId) { | ||||||
|     const note = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId]); |     const note = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId]); | ||||||
|  |  | ||||||
| @@ -174,16 +182,20 @@ async function protectNoteHistory(noteId, dataKey, protect, sourceId) { | |||||||
| async function saveNoteHistory(noteId, dataKey, sourceId, nowStr) { | async function saveNoteHistory(noteId, dataKey, sourceId, nowStr) { | ||||||
|     const oldNote = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId]); |     const oldNote = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId]); | ||||||
|  |  | ||||||
|  |     if (oldNote.type === 'file') { | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     if (oldNote.isProtected) { |     if (oldNote.isProtected) { | ||||||
|         protected_session.decryptNote(dataKey, oldNote); |         protected_session.decryptNote(dataKey, oldNote); | ||||||
|  |  | ||||||
|         note.isProtected = false; |         oldNote.isProtected = false; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const newnoteRevisionId = utils.newnoteRevisionId(); |     const newNoteRevisionId = utils.newNoteRevisionId(); | ||||||
|  |  | ||||||
|     await sql.insert('note_revisions', { |     await sql.insert('note_revisions', { | ||||||
|         noteRevisionId: newnoteRevisionId, |         noteRevisionId: newNoteRevisionId, | ||||||
|         noteId: noteId, |         noteId: noteId, | ||||||
|         // title and text should be decrypted now |         // title and text should be decrypted now | ||||||
|         title: oldNote.title, |         title: oldNote.title, | ||||||
| @@ -193,7 +205,7 @@ async function saveNoteHistory(noteId, dataKey, sourceId, nowStr) { | |||||||
|         dateModifiedTo: nowStr |         dateModifiedTo: nowStr | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     await sync_table.addNoteHistorySync(newnoteRevisionId, sourceId); |     await sync_table.addNoteHistorySync(newNoteRevisionId, sourceId); | ||||||
| } | } | ||||||
|  |  | ||||||
| async function saveNoteImages(noteId, noteText, sourceId) { | async function saveNoteImages(noteId, noteText, sourceId) { | ||||||
| @@ -243,7 +255,21 @@ async function saveNoteImages(noteId, noteText, sourceId) { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | async function loadFile(noteId, newNote, dataKey) { | ||||||
|  |     const oldNote = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId]); | ||||||
|  |  | ||||||
|  |     if (oldNote.isProtected) { | ||||||
|  |         await protected_session.decryptNote(dataKey, oldNote); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     newNote.detail.content = oldNote.content; | ||||||
|  | } | ||||||
|  |  | ||||||
| async function updateNote(noteId, newNote, dataKey, sourceId) { | async function updateNote(noteId, newNote, dataKey, sourceId) { | ||||||
|  |     if (newNote.detail.type === 'file') { | ||||||
|  |         await loadFile(noteId, newNote, dataKey); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     if (newNote.detail.isProtected) { |     if (newNote.detail.isProtected) { | ||||||
|         await protected_session.encryptNote(dataKey, newNote.detail); |         await protected_session.encryptNote(dataKey, newNote.detail); | ||||||
|     } |     } | ||||||
| @@ -261,7 +287,7 @@ async function updateNote(noteId, newNote, dataKey, sourceId) { | |||||||
|         "SELECT noteRevisionId FROM note_revisions WHERE noteId = ? AND dateModifiedTo >= ?", [noteId, historyCutoff]); |         "SELECT noteRevisionId FROM note_revisions WHERE noteId = ? AND dateModifiedTo >= ?", [noteId, historyCutoff]); | ||||||
|  |  | ||||||
|     await sql.doInTransaction(async () => { |     await sql.doInTransaction(async () => { | ||||||
|         const msSinceDateCreated = now.getTime() - utils.parseDate(newNote.detail.dateCreated).getTime(); |         const msSinceDateCreated = now.getTime() - utils.parseDateTime(newNote.detail.dateCreated).getTime(); | ||||||
|  |  | ||||||
|         if (attributesMap.disable_versioning !== 'true' |         if (attributesMap.disable_versioning !== 'true' | ||||||
|             && !existingnoteRevisionId |             && !existingnoteRevisionId | ||||||
| @@ -315,6 +341,7 @@ async function deleteNote(noteTreeId, sourceId) { | |||||||
|  |  | ||||||
| module.exports = { | module.exports = { | ||||||
|     createNewNote, |     createNewNote, | ||||||
|  |     createNote, | ||||||
|     updateNote, |     updateNote, | ||||||
|     deleteNote, |     deleteNote, | ||||||
|     protectNoteRecursively |     protectNoteRecursively | ||||||
|   | |||||||
| @@ -26,6 +26,10 @@ function getDataKey(obj) { | |||||||
|  |  | ||||||
|     const protectedSessionId = getProtectedSessionId(obj); |     const protectedSessionId = getProtectedSessionId(obj); | ||||||
|  |  | ||||||
|  |     return getDataKeyForProtectedSessionId(protectedSessionId); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function getDataKeyForProtectedSessionId(protectedSessionId) { | ||||||
|     if (protectedSessionId && session.protectedSessionId === protectedSessionId) { |     if (protectedSessionId && session.protectedSessionId === protectedSessionId) { | ||||||
|         return session.decryptedDataKey; |         return session.decryptedDataKey; | ||||||
|     } |     } | ||||||
| @@ -52,7 +56,14 @@ function decryptNote(dataKey, note) { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     if (note.content) { |     if (note.content) { | ||||||
|         note.content = data_encryption.decryptString(dataKey, data_encryption.noteTextIv(note.noteId), note.content); |         const contentIv = data_encryption.noteContentIv(note.noteId); | ||||||
|  |  | ||||||
|  |         if (note.type === 'file') { | ||||||
|  |             note.content = data_encryption.decrypt(dataKey, contentIv, note.content); | ||||||
|  |         } | ||||||
|  |         else { | ||||||
|  |             note.content = data_encryption.decryptString(dataKey, contentIv, note.content); | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -76,7 +87,7 @@ function decryptNoteHistoryRow(dataKey, hist) { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     if (hist.content) { |     if (hist.content) { | ||||||
|         hist.content = data_encryption.decryptString(dataKey, data_encryption.noteTextIv(hist.noteRevisionId), hist.content); |         hist.content = data_encryption.decryptString(dataKey, data_encryption.noteContentIv(hist.noteRevisionId), hist.content); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -92,19 +103,20 @@ function encryptNote(dataKey, note) { | |||||||
|     dataKey = getDataKey(dataKey); |     dataKey = getDataKey(dataKey); | ||||||
|  |  | ||||||
|     note.title = data_encryption.encrypt(dataKey, data_encryption.noteTitleIv(note.noteId), note.title); |     note.title = data_encryption.encrypt(dataKey, data_encryption.noteTitleIv(note.noteId), note.title); | ||||||
|     note.content = data_encryption.encrypt(dataKey, data_encryption.noteTextIv(note.noteId), note.content); |     note.content = data_encryption.encrypt(dataKey, data_encryption.noteContentIv(note.noteId), note.content); | ||||||
| } | } | ||||||
|  |  | ||||||
| function encryptNoteHistoryRow(dataKey, history) { | function encryptNoteHistoryRow(dataKey, history) { | ||||||
|     dataKey = getDataKey(dataKey); |     dataKey = getDataKey(dataKey); | ||||||
|  |  | ||||||
|     history.title = data_encryption.encrypt(dataKey, data_encryption.noteTitleIv(history.noteRevisionId), history.title); |     history.title = data_encryption.encrypt(dataKey, data_encryption.noteTitleIv(history.noteRevisionId), history.title); | ||||||
|     history.content = data_encryption.encrypt(dataKey, data_encryption.noteTextIv(history.noteRevisionId), history.content); |     history.content = data_encryption.encrypt(dataKey, data_encryption.noteContentIv(history.noteRevisionId), history.content); | ||||||
| } | } | ||||||
|  |  | ||||||
| module.exports = { | module.exports = { | ||||||
|     setDataKey, |     setDataKey, | ||||||
|     getDataKey, |     getDataKey, | ||||||
|  |     getDataKeyForProtectedSessionId, | ||||||
|     isProtectedSessionAvailable, |     isProtectedSessionAvailable, | ||||||
|     decryptNote, |     decryptNote, | ||||||
|     decryptNotes, |     decryptNotes, | ||||||
|   | |||||||
							
								
								
									
										27
									
								
								src/services/scheduler.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								src/services/scheduler.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | |||||||
|  | const script = require('./script'); | ||||||
|  | const Repository = require('./repository'); | ||||||
|  |  | ||||||
|  | const repo = new Repository(); | ||||||
|  |  | ||||||
|  | async function runNotesWithAttribute(runAttrValue) { | ||||||
|  |     const notes = await repo.getEntities(` | ||||||
|  |         SELECT notes.*  | ||||||
|  |         FROM notes  | ||||||
|  |           JOIN attributes ON attributes.noteId = notes.noteId | ||||||
|  |                            AND attributes.isDeleted = 0 | ||||||
|  |                            AND attributes.name = 'run'  | ||||||
|  |                            AND attributes.value = ?  | ||||||
|  |         WHERE | ||||||
|  |           notes.type = 'code' | ||||||
|  |           AND notes.isDeleted = 0`, [runAttrValue]); | ||||||
|  |  | ||||||
|  |     for (const note of notes) { | ||||||
|  |         script.executeNote(null, note); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | setTimeout(() => runNotesWithAttribute('backend_startup'), 10 * 1000); | ||||||
|  |  | ||||||
|  | setInterval(() => runNotesWithAttribute('hourly'), 3600 * 1000); | ||||||
|  |  | ||||||
|  | setInterval(() => runNotesWithAttribute('daily'), 24 * 3600 * 1000); | ||||||
| @@ -1,27 +1,139 @@ | |||||||
| const log = require('./log'); |  | ||||||
| const sql = require('./sql'); | const sql = require('./sql'); | ||||||
| const ScriptContext = require('./script_context'); | const ScriptContext = require('./script_context'); | ||||||
|  | const Repository = require('./repository'); | ||||||
|  |  | ||||||
| async function executeScript(dataKey, script, params) { | async function executeNote(dataKey, note) { | ||||||
|     log.info('Executing script: ' + script); |     if (!note.isJavaScript()) { | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     const ctx = new ScriptContext(dataKey); |     const bundle = await getScriptBundle(note); | ||||||
|  |  | ||||||
|     const paramsStr = getParams(params); |     await executeBundle(dataKey, bundle); | ||||||
|  | } | ||||||
|  |  | ||||||
|     let ret; | async function executeBundle(dataKey, bundle, startNote) { | ||||||
|  |     if (!startNote) { | ||||||
|  |         // this is the default case, the only exception is when we want to preserve frontend startNote | ||||||
|  |         startNote = bundle.note; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     await sql.doInTransaction(async () => { |     // last \r\n is necessary if script contains line comment on its last line | ||||||
|         ret = await (function() { return eval(`(${script})(${paramsStr})`); }.call(ctx)); |     const script = "async function() {\r\n" + bundle.script + "\r\n}"; | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     return ret; |     const ctx = new ScriptContext(dataKey, startNote, bundle.allNotes); | ||||||
|  |  | ||||||
|  |     if (await bundle.note.hasAttribute('manual_transaction_handling')) { | ||||||
|  |         return await execute(ctx, script, ''); | ||||||
|  |     } | ||||||
|  |     else { | ||||||
|  |         return await sql.doInTransaction(async () => execute(ctx, script, '')); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * This method preserves frontend startNode - that's why we start execution from currentNote and override | ||||||
|  |  * bundle's startNote. | ||||||
|  |  */ | ||||||
|  | async function executeScript(dataKey, script, params, startNoteId, currentNoteId) { | ||||||
|  |     const repository = new Repository(dataKey); | ||||||
|  |     const startNote = await repository.getNote(startNoteId); | ||||||
|  |     const currentNote = await repository.getNote(currentNoteId); | ||||||
|  |  | ||||||
|  |     currentNote.content = `return await (${script}\r\n)(${getParams(params)})`; | ||||||
|  |     currentNote.type = 'code'; | ||||||
|  |     currentNote.mime = 'application/javascript;env=backend'; | ||||||
|  |  | ||||||
|  |     const bundle = await getScriptBundle(currentNote); | ||||||
|  |  | ||||||
|  |     return await executeBundle(dataKey, bundle, startNote); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function execute(ctx, script, paramsStr) { | ||||||
|  |     return await (function() { return eval(`const apiContext = this;\r\n(${script}\r\n)(${paramsStr})`); }.call(ctx)); | ||||||
| } | } | ||||||
|  |  | ||||||
| function getParams(params) { | function getParams(params) { | ||||||
|     return params.map(p => JSON.stringify(p)).join(","); |     if (!params) { | ||||||
|  |         return params; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return params.map(p => { | ||||||
|  |         if (typeof p === "string" && p.startsWith("!@#Function: ")) { | ||||||
|  |             return p.substr(13); | ||||||
|  |         } | ||||||
|  |         else { | ||||||
|  |             return JSON.stringify(p); | ||||||
|  |         } | ||||||
|  |     }).join(","); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | async function getScriptBundle(note, root = true, scriptEnv = null, includedNoteIds = []) { | ||||||
|  |     if (!note.isJavaScript() && !note.isHtml() && note.type !== 'render') { | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (await note.hasAttribute('disable_inclusion')) { | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (root) { | ||||||
|  |         scriptEnv = note.getScriptEnv(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (note.type !== 'file' && scriptEnv !== note.getScriptEnv()) { | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const bundle = { | ||||||
|  |         note: note, | ||||||
|  |         script: '', | ||||||
|  |         html: '', | ||||||
|  |         allNotes: [note] | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     if (includedNoteIds.includes(note.noteId)) { | ||||||
|  |         return bundle; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     includedNoteIds.push(note.noteId); | ||||||
|  |  | ||||||
|  |     const modules = []; | ||||||
|  |  | ||||||
|  |     for (const child of await note.getChildren()) { | ||||||
|  |         const childBundle = await getScriptBundle(child, false, scriptEnv, includedNoteIds); | ||||||
|  |  | ||||||
|  |         if (childBundle) { | ||||||
|  |             modules.push(childBundle.note); | ||||||
|  |             bundle.script += childBundle.script; | ||||||
|  |             bundle.html += childBundle.html; | ||||||
|  |             bundle.allNotes = bundle.allNotes.concat(childBundle.allNotes); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (note.isJavaScript()) { | ||||||
|  |         bundle.script += ` | ||||||
|  | apiContext.modules['${note.noteId}'] = {}; | ||||||
|  | ${root ? 'return ' : ''}await (async function(exports, module, api` + (modules.length > 0 ? ', ' : '') + | ||||||
|  |             modules.map(child => sanitizeVariableName(child.title)).join(', ') + `) { | ||||||
|  | ${note.content} | ||||||
|  | })({}, apiContext.modules['${note.noteId}'], apiContext.apis['${note.noteId}']` + (modules.length > 0 ? ', ' : '') + | ||||||
|  |             modules.map(mod => `apiContext.modules['${mod.noteId}'].exports`).join(', ') + `); | ||||||
|  | `; | ||||||
|  |     } | ||||||
|  |     else if (note.isHtml()) { | ||||||
|  |         bundle.html += note.content; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return bundle; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function sanitizeVariableName(str) { | ||||||
|  |     return str.replace(/[^a-z0-9_]/gim, ""); | ||||||
| } | } | ||||||
|  |  | ||||||
| module.exports = { | module.exports = { | ||||||
|     executeScript |     executeNote, | ||||||
|  |     executeScript, | ||||||
|  |     getScriptBundle | ||||||
| }; | }; | ||||||
| @@ -1,20 +1,42 @@ | |||||||
| const log = require('./log'); | const log = require('./log'); | ||||||
| const protected_session = require('./protected_session'); | const protected_session = require('./protected_session'); | ||||||
| const notes = require('./notes'); | const notes = require('./notes'); | ||||||
|  | const sql = require('./sql'); | ||||||
|  | const utils = require('./utils'); | ||||||
| const attributes = require('./attributes'); | const attributes = require('./attributes'); | ||||||
| const date_notes = require('./date_notes'); | const date_notes = require('./date_notes'); | ||||||
|  | const config = require('./config'); | ||||||
| const Repository = require('./repository'); | const Repository = require('./repository'); | ||||||
|  | const axios = require('axios'); | ||||||
|  |  | ||||||
| function ScriptContext(noteId, dataKey) { | function ScriptContext(dataKey, startNote, allNotes) { | ||||||
|     this.dataKey = protected_session.getDataKey(dataKey); |     dataKey = protected_session.getDataKey(dataKey); | ||||||
|     this.repository = new Repository(dataKey); |  | ||||||
|  |     this.modules = {}; | ||||||
|  |     this.notes = utils.toObject(allNotes, note => [note.noteId, note]); | ||||||
|  |     this.apis = utils.toObject(allNotes, note => [note.noteId, new ScriptApi(dataKey, startNote, note)]); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function ScriptApi(dataKey, startNote, currentNote) { | ||||||
|  |     const repository = new Repository(dataKey); | ||||||
|  |     this.startNote = startNote; | ||||||
|  |     this.currentNote = currentNote; | ||||||
|  |  | ||||||
|  |     this.axios = axios; | ||||||
|  |  | ||||||
|  |     this.utils = { | ||||||
|  |         unescapeHtml: utils.unescapeHtml, | ||||||
|  |         isoDateTimeStr: utils.dateStr | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     this.getInstanceName = () => config.General ? config.General.instanceName : null; | ||||||
|  |  | ||||||
|     this.getNoteById = async function(noteId) { |     this.getNoteById = async function(noteId) { | ||||||
|         return this.repository.getNote(noteId); |         return repository.getNote(noteId); | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     this.getNotesWithAttribute = async function (attrName, attrValue) { |     this.getNotesWithAttribute = async function (attrName, attrValue) { | ||||||
|         return await attributes.getNotesWithAttribute(this.dataKey, attrName, attrValue); |         return await attributes.getNotesWithAttribute(repository, attrName, attrValue); | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     this.getNoteWithAttribute = async function (attrName, attrValue) { |     this.getNoteWithAttribute = async function (attrName, attrValue) { | ||||||
| @@ -23,44 +45,22 @@ function ScriptContext(noteId, dataKey) { | |||||||
|         return notes.length > 0 ? notes[0] : null; |         return notes.length > 0 ? notes[0] : null; | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     this.createNote = async function (parentNoteId, name, jsonContent, extraOptions = {}) { |     this.createNote = async function(parentNoteId, title, content = "", extraOptions = {}) { | ||||||
|         const note = { |         extraOptions.dataKey = dataKey; | ||||||
|             title: name, |  | ||||||
|             content: extraOptions.json ? JSON.stringify(jsonContent, null, '\t') : jsonContent, |  | ||||||
|             target: 'into', |  | ||||||
|             isProtected: extraOptions.isProtected !== undefined ? extraOptions.isProtected : false, |  | ||||||
|             type: extraOptions.type, |  | ||||||
|             mime: extraOptions.mime |  | ||||||
|         }; |  | ||||||
|  |  | ||||||
|         if (extraOptions.json) { |         return await notes.createNote(parentNoteId, title, content, extraOptions); | ||||||
|             note.type = "code"; |  | ||||||
|             note.mime = "application/json"; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (!note.type) { |  | ||||||
|             note.type = "text"; |  | ||||||
|             note.mime = "text/html"; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         const noteId = (await notes.createNewNote(parentNoteId, note, this.dataKey)).noteId; |  | ||||||
|  |  | ||||||
|         if (extraOptions.attributes) { |  | ||||||
|             for (const attrName in extraOptions.attributes) { |  | ||||||
|                 await attributes.createAttribute(noteId, attrName, extraOptions.attributes[attrName]); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         return noteId; |  | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     this.updateEntity = this.repository.updateEntity; |     this.createAttribute = attributes.createAttribute; | ||||||
|  |  | ||||||
|     this.log = function(message) { |     this.updateEntity = repository.updateEntity; | ||||||
|         log.info(`Script: ${message}`); |  | ||||||
|     }; |  | ||||||
|  |  | ||||||
|  |     this.log = message => log.info(`Script ${currentNote.noteId}: ${message}`); | ||||||
|  |  | ||||||
|  |     this.getRootCalendarNoteId = date_notes.getRootCalendarNoteId; | ||||||
|     this.getDateNoteId = date_notes.getDateNoteId; |     this.getDateNoteId = date_notes.getDateNoteId; | ||||||
|  |  | ||||||
|  |     this.transaction = sql.doInTransaction; | ||||||
| } | } | ||||||
|  |  | ||||||
| module.exports = ScriptContext; | module.exports = ScriptContext; | ||||||
| @@ -195,6 +195,7 @@ async function doInTransaction(func) { | |||||||
|         await transactionPromise; |         await transactionPromise; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     let ret = null; | ||||||
|     const error = new Error(); // to capture correct stack trace in case of exception |     const error = new Error(); // to capture correct stack trace in case of exception | ||||||
|  |  | ||||||
|     transactionActive = true; |     transactionActive = true; | ||||||
| @@ -202,7 +203,7 @@ async function doInTransaction(func) { | |||||||
|         try { |         try { | ||||||
|             await beginTransaction(); |             await beginTransaction(); | ||||||
|  |  | ||||||
|             await func(); |             ret = await func(); | ||||||
|  |  | ||||||
|             await commit(); |             await commit(); | ||||||
|  |  | ||||||
| @@ -223,6 +224,8 @@ async function doInTransaction(func) { | |||||||
|     if (transactionActive) { |     if (transactionActive) { | ||||||
|         await transactionPromise; |         await transactionPromise; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     return ret; | ||||||
| } | } | ||||||
|  |  | ||||||
| async function isDbUpToDate() { | async function isDbUpToDate() { | ||||||
|   | |||||||
| @@ -149,6 +149,9 @@ async function pullSync(syncContext) { | |||||||
|         else if (sync.entityName === 'attributes') { |         else if (sync.entityName === 'attributes') { | ||||||
|             await syncUpdate.updateAttribute(resp, syncContext.sourceId); |             await syncUpdate.updateAttribute(resp, syncContext.sourceId); | ||||||
|         } |         } | ||||||
|  |         else if (sync.entityName === 'api_tokens') { | ||||||
|  |             await syncUpdate.updateApiToken(resp, syncContext.sourceId); | ||||||
|  |         } | ||||||
|         else { |         else { | ||||||
|             throw new Error(`Unrecognized entity type ${sync.entityName} in sync #${sync.id}`); |             throw new Error(`Unrecognized entity type ${sync.entityName} in sync #${sync.id}`); | ||||||
|         } |         } | ||||||
| @@ -201,6 +204,8 @@ async function pushEntity(sync, syncContext) { | |||||||
|  |  | ||||||
|     if (sync.entityName === 'notes') { |     if (sync.entityName === 'notes') { | ||||||
|         entity = await sql.getRow('SELECT * FROM notes WHERE noteId = ?', [sync.entityId]); |         entity = await sql.getRow('SELECT * FROM notes WHERE noteId = ?', [sync.entityId]); | ||||||
|  |  | ||||||
|  |         serializeNoteContentBuffer(entity); | ||||||
|     } |     } | ||||||
|     else if (sync.entityName === 'note_tree') { |     else if (sync.entityName === 'note_tree') { | ||||||
|         entity = await sql.getRow('SELECT * FROM note_tree WHERE noteTreeId = ?', [sync.entityId]); |         entity = await sql.getRow('SELECT * FROM note_tree WHERE noteTreeId = ?', [sync.entityId]); | ||||||
| @@ -233,6 +238,9 @@ async function pushEntity(sync, syncContext) { | |||||||
|     else if (sync.entityName === 'attributes') { |     else if (sync.entityName === 'attributes') { | ||||||
|         entity = await sql.getRow('SELECT * FROM attributes WHERE attributeId = ?', [sync.entityId]); |         entity = await sql.getRow('SELECT * FROM attributes WHERE attributeId = ?', [sync.entityId]); | ||||||
|     } |     } | ||||||
|  |     else if (sync.entityName === 'api_tokens') { | ||||||
|  |         entity = await sql.getRow('SELECT * FROM api_tokens WHERE apiTokenId = ?', [sync.entityId]); | ||||||
|  |     } | ||||||
|     else { |     else { | ||||||
|         throw new Error(`Unrecognized entity type ${sync.entityName} in sync #${sync.id}`); |         throw new Error(`Unrecognized entity type ${sync.entityName} in sync #${sync.id}`); | ||||||
|     } |     } | ||||||
| @@ -252,6 +260,12 @@ async function pushEntity(sync, syncContext) { | |||||||
|     await syncRequest(syncContext, 'PUT', '/api/sync/' + sync.entityName, payload); |     await syncRequest(syncContext, 'PUT', '/api/sync/' + sync.entityName, payload); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | function serializeNoteContentBuffer(note) { | ||||||
|  |     if (note.type === 'file') { | ||||||
|  |         note.content = note.content.toString("binary"); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
| async function checkContentHash(syncContext) { | async function checkContentHash(syncContext) { | ||||||
|     const resp = await syncRequest(syncContext, 'GET', '/api/sync/check'); |     const resp = await syncRequest(syncContext, 'GET', '/api/sync/check'); | ||||||
|  |  | ||||||
| @@ -344,5 +358,6 @@ sql.dbReady.then(() => { | |||||||
| }); | }); | ||||||
|  |  | ||||||
| module.exports = { | module.exports = { | ||||||
|     sync |     sync, | ||||||
|  |     serializeNoteContentBuffer | ||||||
| }; | }; | ||||||
| @@ -40,6 +40,10 @@ async function addAttributeSync(attributeId, sourceId) { | |||||||
|     await addEntitySync("attributes", attributeId, sourceId); |     await addEntitySync("attributes", attributeId, sourceId); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | async function addApiTokenSync(apiTokenId, sourceId) { | ||||||
|  |     await addEntitySync("api_tokens", apiTokenId, sourceId); | ||||||
|  | } | ||||||
|  |  | ||||||
| async function addEntitySync(entityName, entityId, sourceId) { | async function addEntitySync(entityName, entityId, sourceId) { | ||||||
|     await sql.replace("sync", { |     await sql.replace("sync", { | ||||||
|         entityName: entityName, |         entityName: entityName, | ||||||
| @@ -93,6 +97,7 @@ async function fillAllSyncRows() { | |||||||
|     await fillSyncRows("images", "imageId"); |     await fillSyncRows("images", "imageId"); | ||||||
|     await fillSyncRows("note_images", "noteImageId"); |     await fillSyncRows("note_images", "noteImageId"); | ||||||
|     await fillSyncRows("attributes", "attributeId"); |     await fillSyncRows("attributes", "attributeId"); | ||||||
|  |     await fillSyncRows("api_tokens", "apiTokenId"); | ||||||
| } | } | ||||||
|  |  | ||||||
| module.exports = { | module.exports = { | ||||||
| @@ -105,6 +110,7 @@ module.exports = { | |||||||
|     addImageSync, |     addImageSync, | ||||||
|     addNoteImageSync, |     addNoteImageSync, | ||||||
|     addAttributeSync, |     addAttributeSync, | ||||||
|  |     addApiTokenSync, | ||||||
|     addEntitySync, |     addEntitySync, | ||||||
|     cleanupSyncRowsForMissingEntities, |     cleanupSyncRowsForMissingEntities, | ||||||
|     fillAllSyncRows |     fillAllSyncRows | ||||||
|   | |||||||
| @@ -1,10 +1,17 @@ | |||||||
| const sql = require('./sql'); | const sql = require('./sql'); | ||||||
| const log = require('./log'); | const log = require('./log'); | ||||||
| const eventLog = require('./event_log'); | const eventLog = require('./event_log'); | ||||||
| const notes = require('./notes'); |  | ||||||
| const sync_table = require('./sync_table'); | const sync_table = require('./sync_table'); | ||||||
|  |  | ||||||
|  | function deserializeNoteContentBuffer(note) { | ||||||
|  |     if (note.type === 'file') { | ||||||
|  |         note.content = new Buffer(note.content, 'binary'); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
| async function updateNote(entity, sourceId) { | async function updateNote(entity, sourceId) { | ||||||
|  |     deserializeNoteContentBuffer(entity); | ||||||
|  |  | ||||||
|     const origNote = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [entity.noteId]); |     const origNote = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [entity.noteId]); | ||||||
|  |  | ||||||
|     if (!origNote || origNote.dateModified <= entity.dateModified) { |     if (!origNote || origNote.dateModified <= entity.dateModified) { | ||||||
| @@ -137,6 +144,20 @@ async function updateAttribute(entity, sourceId) { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | async function updateApiToken(entity, sourceId) { | ||||||
|  |     const apiTokenId = await sql.getRow("SELECT * FROM api_tokens WHERE apiTokenId = ?", [entity.apiTokenId]); | ||||||
|  |  | ||||||
|  |     if (!apiTokenId) { | ||||||
|  |         await sql.doInTransaction(async () => { | ||||||
|  |             await sql.replace("api_tokens", entity); | ||||||
|  |  | ||||||
|  |             await sync_table.addApiTokenSync(entity.apiTokenId, sourceId); | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         log.info("Update/sync API token " + entity.apiTokenId); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
| module.exports = { | module.exports = { | ||||||
|     updateNote, |     updateNote, | ||||||
|     updateNoteTree, |     updateNoteTree, | ||||||
| @@ -146,5 +167,6 @@ module.exports = { | |||||||
|     updateRecentNotes, |     updateRecentNotes, | ||||||
|     updateImage, |     updateImage, | ||||||
|     updateNoteImage, |     updateNoteImage, | ||||||
|     updateAttribute |     updateAttribute, | ||||||
|  |     updateApiToken | ||||||
| }; | }; | ||||||
| @@ -2,6 +2,7 @@ | |||||||
|  |  | ||||||
| const sql = require('./sql'); | const sql = require('./sql'); | ||||||
| const sync_table = require('./sync_table'); | const sync_table = require('./sync_table'); | ||||||
|  | const protected_session = require('./protected_session'); | ||||||
|  |  | ||||||
| async function validateParentChild(res, parentNoteId, childNoteId, noteTreeId = null) { | async function validateParentChild(res, parentNoteId, childNoteId, noteTreeId = null) { | ||||||
|     const existing = await getExistingNoteTree(parentNoteId, childNoteId); |     const existing = await getExistingNoteTree(parentNoteId, childNoteId); | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ | |||||||
|  |  | ||||||
| const crypto = require('crypto'); | const crypto = require('crypto'); | ||||||
| const randtoken = require('rand-token').generator({source: 'crypto'}); | const randtoken = require('rand-token').generator({source: 'crypto'}); | ||||||
|  | const unescape = require('unescape'); | ||||||
|  |  | ||||||
| function newNoteId() { | function newNoteId() { | ||||||
|     return randomString(12); |     return randomString(12); | ||||||
| @@ -11,7 +12,7 @@ function newNoteTreeId() { | |||||||
|     return randomString(12); |     return randomString(12); | ||||||
| } | } | ||||||
|  |  | ||||||
| function newnoteRevisionId() { | function newNoteRevisionId() { | ||||||
|     return randomString(12); |     return randomString(12); | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -27,6 +28,10 @@ function newAttributeId() { | |||||||
|     return randomString(12); |     return randomString(12); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | function newApiTokenId() { | ||||||
|  |     return randomString(12); | ||||||
|  | } | ||||||
|  |  | ||||||
| function randomString(length) { | function randomString(length) { | ||||||
|     return randtoken.generate(length); |     return randtoken.generate(length); | ||||||
| } | } | ||||||
| @@ -39,6 +44,14 @@ function nowDate() { | |||||||
|     return dateStr(new Date()); |     return dateStr(new Date()); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | function localDate() { | ||||||
|  |     const date = new Date(); | ||||||
|  |  | ||||||
|  |     return date.getFullYear() + "-" | ||||||
|  |         + (date.getMonth() < 9 ? "0" : "") + (date.getMonth() + 1) + "-" | ||||||
|  |         + (date.getDate() < 10 ? "0" : "") + date.getDate(); | ||||||
|  | } | ||||||
|  |  | ||||||
| function dateStr(date) { | function dateStr(date) { | ||||||
|     return date.toISOString(); |     return date.toISOString(); | ||||||
| } | } | ||||||
| @@ -47,7 +60,7 @@ function dateStr(date) { | |||||||
|  * @param str - needs to be in the ISO 8601 format "YYYY-MM-DDTHH:MM:SS.sssZ" format as outputted by dateStr(). |  * @param str - needs to be in the ISO 8601 format "YYYY-MM-DDTHH:MM:SS.sssZ" format as outputted by dateStr(). | ||||||
|  *              also is assumed to be GMT time (as indicated by the "Z" at the end), *not* local time |  *              also is assumed to be GMT time (as indicated by the "Z" at the end), *not* local time | ||||||
|  */ |  */ | ||||||
| function parseDate(str) { | function parseDateTime(str) { | ||||||
|     try { |     try { | ||||||
|         return new Date(Date.parse(str)); |         return new Date(Date.parse(str)); | ||||||
|     } |     } | ||||||
| @@ -56,6 +69,12 @@ function parseDate(str) { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | function parseDate(str) { | ||||||
|  |     const datePart = str.substr(0, 10); | ||||||
|  |  | ||||||
|  |     return parseDateTime(datePart + "T12:00:00.000Z"); | ||||||
|  | } | ||||||
|  |  | ||||||
| function toBase64(plainText) { | function toBase64(plainText) { | ||||||
|     return Buffer.from(plainText).toString('base64'); |     return Buffer.from(plainText).toString('base64'); | ||||||
| } | } | ||||||
| @@ -111,18 +130,37 @@ async function stopWatch(what, func) { | |||||||
|     return ret; |     return ret; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | function unescapeHtml(str) { | ||||||
|  |     return unescape(str); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function toObject(array, fn) { | ||||||
|  |     const obj = {}; | ||||||
|  |  | ||||||
|  |     for (const item of array) { | ||||||
|  |         const ret = fn(item); | ||||||
|  |  | ||||||
|  |         obj[ret[0]] = ret[1]; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return obj; | ||||||
|  | } | ||||||
|  |  | ||||||
| module.exports = { | module.exports = { | ||||||
|     randomSecureToken, |     randomSecureToken, | ||||||
|     randomString, |     randomString, | ||||||
|     nowDate, |     nowDate, | ||||||
|  |     localDate, | ||||||
|     dateStr, |     dateStr, | ||||||
|     parseDate, |     parseDate, | ||||||
|  |     parseDateTime, | ||||||
|     newNoteId, |     newNoteId, | ||||||
|     newNoteTreeId, |     newNoteTreeId, | ||||||
|     newnoteRevisionId, |     newNoteRevisionId, | ||||||
|     newImageId, |     newImageId, | ||||||
|     newNoteImageId, |     newNoteImageId, | ||||||
|     newAttributeId, |     newAttributeId, | ||||||
|  |     newApiTokenId, | ||||||
|     toBase64, |     toBase64, | ||||||
|     fromBase64, |     fromBase64, | ||||||
|     hmac, |     hmac, | ||||||
| @@ -132,5 +170,7 @@ module.exports = { | |||||||
|     getDateTimeForFile, |     getDateTimeForFile, | ||||||
|     sanitizeSql, |     sanitizeSql, | ||||||
|     assertArguments, |     assertArguments, | ||||||
|     stopWatch |     stopWatch, | ||||||
|  |     unescapeHtml, | ||||||
|  |     toObject | ||||||
| }; | }; | ||||||
| @@ -17,15 +17,20 @@ | |||||||
|           <button class="btn btn-xs" onclick="jumpToNote.showDialog();" title="CTRL+J">Jump to note</button> |           <button class="btn btn-xs" onclick="jumpToNote.showDialog();" title="CTRL+J">Jump to note</button> | ||||||
|           <button class="btn btn-xs" onclick="recentNotes.showDialog();" title="CTRL+E">Recent notes</button> |           <button class="btn btn-xs" onclick="recentNotes.showDialog();" title="CTRL+E">Recent notes</button> | ||||||
|           <button class="btn btn-xs" onclick="recentChanges.showDialog();">Recent changes</button> |           <button class="btn btn-xs" onclick="recentChanges.showDialog();">Recent changes</button> | ||||||
|           <button class="btn btn-xs" onclick="eventLog.showDialog();">Event log</button> |         </div> | ||||||
|  |  | ||||||
|  |         <div id="plugin-buttons"> | ||||||
|         </div> |         </div> | ||||||
|  |  | ||||||
|         <div> |         <div> | ||||||
|           <button class="btn btn-xs" onclick="syncNow();" title="Number of outstanding changes to be pushed to server"> |           <button class="btn btn-xs" onclick="syncNow();" title="Number of outstanding changes to be pushed to server"> | ||||||
|  |             <span class="ui-icon ui-icon-refresh"></span> | ||||||
|  |  | ||||||
|             Sync now (<span id="changes-to-push-count">0</span>) |             Sync now (<span id="changes-to-push-count">0</span>) | ||||||
|           </button> |           </button> | ||||||
|  |  | ||||||
|           <button class="btn btn-xs" onclick="settings.showDialog();">Settings</button> |           <button class="btn btn-xs" onclick="settings.showDialog();"> | ||||||
|  |             <span class="ui-icon ui-icon-gear"></span> Settings</button> | ||||||
|  |  | ||||||
|           <form action="logout" id="logout-button" method="POST" style="display: inline;"> |           <form action="logout" id="logout-button" method="POST" style="display: inline;"> | ||||||
|             <input type="submit" class="btn btn-xs" value="Logout"> |             <input type="submit" class="btn btn-xs" value="Logout"> | ||||||
| @@ -35,30 +40,27 @@ | |||||||
|  |  | ||||||
|       <div class="hide-toggle" style="grid-area: tree-actions;"> |       <div class="hide-toggle" style="grid-area: tree-actions;"> | ||||||
|         <div style="display: flex; justify-content: space-around; padding: 10px 0 10px 0; margin: 0 20px 0 20px; border: 1px solid #ccc;"> |         <div style="display: flex; justify-content: space-around; padding: 10px 0 10px 0; margin: 0 20px 0 20px; border: 1px solid #ccc;"> | ||||||
|           <a onclick="noteTree.createNewTopLevelNote()" title="Create new top level note" class="icon-action"> |           <a onclick="noteTree.createNewTopLevelNote()" title="Create new top level note" class="icon-action" | ||||||
|             <img src="images/icons/file-plus.png" alt="Create new top level note"/> |              style="background: url('images/icons/file-plus.png')"></a> | ||||||
|           </a> |  | ||||||
|  |  | ||||||
|           <a onclick="noteTree.collapseTree()" title="Collapse note tree" class="icon-action"> |           <a onclick="noteTree.collapseTree()" title="Collapse note tree" class="icon-action" | ||||||
|             <img src="images/icons/list.png" alt="Collapse note tree"/> |              style="background: url('images/icons/list.png')"></a> | ||||||
|           </a> |  | ||||||
|  |  | ||||||
|           <a onclick="noteTree.scrollToCurrentNote()" title="Scroll to current note. Shortcut CTRL+." class="icon-action"> |           <a onclick="noteTree.scrollToCurrentNote()" title="Scroll to current note. Shortcut CTRL+." class="icon-action" | ||||||
|             <img src="images/icons/crosshair.png" alt="Scroll to current note"/> |              style="background: url('images/icons/crosshair.png')"></a> | ||||||
|           </a> |  | ||||||
|  |  | ||||||
|           <a onclick="searchTree.toggleSearch()" title="Search in notes" class="icon-action"> |           <a onclick="searchTree.toggleSearch()" title="Search in notes" class="icon-action" | ||||||
|             <img src="images/icons/search.png" alt="Search in notes"/> |              style="background: url('images/icons/search.png')"></a> | ||||||
|           </a> |  | ||||||
|         </div> |         </div> | ||||||
|  |  | ||||||
|         <div id="search-box" style="display: none; padding: 10px; margin-top: 10px;"> |         <input type="file" id="import-upload" style="display: none" /> | ||||||
|           <p> |       </div> | ||||||
|             <label>Search:</label> |  | ||||||
|             <input name="search-text" autocomplete="off"> |       <div id="search-box" class="hide-toggle" style="grid-area: search; display: none; padding: 10px; margin-top: 10px;"> | ||||||
|             <button id="reset-search-button">×</button> |         <div style="display: flex; align-items: center;"> | ||||||
|             <span id="matches"></span> |           <label>Search:</label> | ||||||
|           </p> |           <input name="search-text" style="flex-grow: 100; margin-left: 5px; margin-right: 5px;" autocomplete="off"> | ||||||
|  |           <button id="reset-search-button" class="btn btn-sm" title="Reset search">×</button> | ||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
|  |  | ||||||
| @@ -68,7 +70,7 @@ | |||||||
|       <div id="parent-list" class="hide-toggle"> |       <div id="parent-list" class="hide-toggle"> | ||||||
|         <p><strong>Note locations:</strong></p> |         <p><strong>Note locations:</strong></p> | ||||||
|  |  | ||||||
|         <ul id="parent-list-list"></ul> |         <ul id="parent-list-inner"></ul> | ||||||
|       </div> |       </div> | ||||||
|  |  | ||||||
|       <div class="hide-toggle" style="grid-area: title;"> |       <div class="hide-toggle" style="grid-area: title;"> | ||||||
| @@ -77,17 +79,13 @@ | |||||||
|              title="Protect the note so that password will be required to view the note" |              title="Protect the note so that password will be required to view the note" | ||||||
|              class="icon-action" |              class="icon-action" | ||||||
|              id="protect-button" |              id="protect-button" | ||||||
|              style="display: none;"> |              style="display: none; background: url('images/icons/lock.png')"></a> | ||||||
|             <img src="images/icons/lock.png" alt="Protect note"/> |  | ||||||
|           </a> |  | ||||||
|  |  | ||||||
|           <a onclick="protected_session.unprotectNoteAndSendToServer()" |           <a onclick="protected_session.unprotectNoteAndSendToServer()" | ||||||
|              title="Unprotect note so that password will not be required to access this note in the future" |              title="Unprotect note so that password will not be required to access this note in the future" | ||||||
|              class="icon-action" |              class="icon-action" | ||||||
|              id="unprotect-button" |              id="unprotect-button" | ||||||
|              style="display: none;"> |              style="display: none; background: url('images/icons/unlock.png')"></a> | ||||||
|             <img src="images/icons/unlock.png" alt="Unprotect note"/> |  | ||||||
|           </a> |  | ||||||
|  |  | ||||||
|             |             | ||||||
|  |  | ||||||
| @@ -101,7 +99,7 @@ | |||||||
|                   onclick="noteEditor.executeCurrentNote()">Execute <kbd>Ctrl+Enter</kbd></button> |                   onclick="noteEditor.executeCurrentNote()">Execute <kbd>Ctrl+Enter</kbd></button> | ||||||
|  |  | ||||||
|           <div class="dropdown" id="note-type"> |           <div class="dropdown" id="note-type"> | ||||||
|             <button id="dLabel" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" class="btn btn-sm"> |             <button data-bind="disable: isDisabled()" id="dLabel" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" class="btn btn-sm"> | ||||||
|               Type: <span data-bind="text: typeString()"></span> |               Type: <span data-bind="text: typeString()"></span> | ||||||
|               <span class="caret"></span> |               <span class="caret"></span> | ||||||
|             </button> |             </button> | ||||||
| @@ -126,6 +124,7 @@ | |||||||
|               <li><a onclick="noteHistory.showCurrentNoteHistory();"><kbd>Alt+H</kbd> History</a></li> |               <li><a onclick="noteHistory.showCurrentNoteHistory();"><kbd>Alt+H</kbd> History</a></li> | ||||||
|               <li><a onclick="attributesDialog.showDialog();"><kbd>Alt+A</kbd> Attributes</a></li> |               <li><a onclick="attributesDialog.showDialog();"><kbd>Alt+A</kbd> Attributes</a></li> | ||||||
|               <li><a onclick="noteSource.showDialog();"><kbd>Ctrl+U</kbd> HTML source</a></li> |               <li><a onclick="noteSource.showDialog();"><kbd>Ctrl+U</kbd> HTML source</a></li> | ||||||
|  |               <li><a onclick="uploadAttachment();">Upload attachment</a></li> | ||||||
|             </ul> |             </ul> | ||||||
|           </div> |           </div> | ||||||
|         </div> |         </div> | ||||||
| @@ -137,24 +136,43 @@ | |||||||
|         <div id="note-detail-code"></div> |         <div id="note-detail-code"></div> | ||||||
|  |  | ||||||
|         <div id="note-detail-render"></div> |         <div id="note-detail-render"></div> | ||||||
|  |  | ||||||
|  |         <div id="note-detail-attachment"> | ||||||
|  |           <table id="attachment-table"> | ||||||
|  |             <tr> | ||||||
|  |               <th>File name:</th> | ||||||
|  |               <td id="attachment-filename"></td> | ||||||
|  |             </tr> | ||||||
|  |             <tr> | ||||||
|  |               <th>File type:</th> | ||||||
|  |               <td id="attachment-filetype"></td> | ||||||
|  |             </tr> | ||||||
|  |             <tr> | ||||||
|  |               <th>File size:</th> | ||||||
|  |               <td id="attachment-filesize"></td> | ||||||
|  |             </tr> | ||||||
|  |             <tr> | ||||||
|  |               <td> | ||||||
|  |                 <button id="attachment-download" class="btn btn-primary" type="button">Download</button> | ||||||
|  |                   | ||||||
|  |                 <button id="attachment-open" class="btn btn-primary" type="button">Open</button> | ||||||
|  |               </td> | ||||||
|  |             </tr> | ||||||
|  |           </table> | ||||||
|  |         </div> | ||||||
|  |  | ||||||
|  |         <input type="file" id="attachment-upload" style="display: none" /> | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|  |       <div id="attribute-list"> | ||||||
|  |         <button class="btn btn-sm" onclick="attributesDialog.showDialog();">Attributes:</button> | ||||||
|  |  | ||||||
|  |         <span id="attribute-list-inner"></span> | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
|  |  | ||||||
|     <div id="recent-notes-dialog" title="Recent notes" style="display: none;"> |     <div id="recent-notes-dialog" title="Recent notes" style="display: none;"> | ||||||
|       <select id="recent-notes-select-box" size="20" style="width: 100%"> |       <input id="recent-notes-search-input" class="form-control"/> | ||||||
|       </select> |  | ||||||
|  |  | ||||||
|       <br/><br/> |  | ||||||
|  |  | ||||||
|       <p> |  | ||||||
|         <button class="btn btn-sm" id="recent-notes-jump-to">Jump to <kbd>enter</kbd></button> |  | ||||||
|           |  | ||||||
|         <button class="btn btn-sm" id="recent-notes-add-link">Add link <kbd>l</kbd></button> |  | ||||||
|  |  | ||||||
|         <button class="btn btn-sm" id="recent-notes-add-current-as-child">Add current as child <kbd>c</kbd></button> |  | ||||||
|  |  | ||||||
|         <button class="btn btn-sm" id="recent-notes-add-recent-as-child">Add recent as child <kbd>r</kbd></button> |  | ||||||
|       </p> |  | ||||||
|     </div> |     </div> | ||||||
|  |  | ||||||
|     <div id="add-link-dialog" title="Add link" style="display: none;"> |     <div id="add-link-dialog" title="Add link" style="display: none;"> | ||||||
| @@ -363,8 +381,11 @@ | |||||||
|     </div> |     </div> | ||||||
|  |  | ||||||
|     <div id="sql-console-dialog" title="SQL console" style="display: none; padding: 20px;"> |     <div id="sql-console-dialog" title="SQL console" style="display: none; padding: 20px;"> | ||||||
|       <textarea style="width: 100%; height: 100px" id="sql-console-query"></textarea> |       <div style="height: 150px; width: 100%; border: 1px solid #ccc; margin-bottom: 10px;" id="sql-console-query"></div> | ||||||
|       <button class="btn btn-danger" id="sql-console-execute">Execute <kbd>CTRL+ENTER</kbd></button> |  | ||||||
|  |       <div style="text-align: center"> | ||||||
|  |         <button class="btn btn-danger" id="sql-console-execute">Execute <kbd>CTRL+ENTER</kbd></button> | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|       <table id="sql-console-results" class="table table-striped" style="overflow: scroll; width: 100%;"> |       <table id="sql-console-results" class="table table-striped" style="overflow: scroll; width: 100%;"> | ||||||
|         <thead></thead> |         <thead></thead> | ||||||
| @@ -378,29 +399,40 @@ | |||||||
|  |  | ||||||
|     <div id="attributes-dialog" title="Note attributes" style="display: none; padding: 20px;"> |     <div id="attributes-dialog" title="Note attributes" style="display: none; padding: 20px;"> | ||||||
|       <form data-bind="submit: save"> |       <form data-bind="submit: save"> | ||||||
|       <div style="display: flex; justify-content: space-between; padding: 15px; padding-top: 0;"> |       <div style="text-align: center"> | ||||||
|         <button class="btn-default" type="button" data-bind="click: addNewRow">Add new attribute</button> |         <button class="btn btn-large" style="width: 200px;" id="save-attributes-button" type="submit">Save changes <kbd>enter</kbd></button> | ||||||
|  |  | ||||||
|         <button class="btn-primary" type="submit">Save</button> |  | ||||||
|       </div> |       </div> | ||||||
|  |  | ||||||
|       <div style="height: 97%; overflow: auto"> |       <div style="height: 97%; overflow: auto"> | ||||||
|         <table id="attributes-table" class="table"> |         <table id="attributes-table" class="table"> | ||||||
|           <thead> |           <thead> | ||||||
|             <tr> |             <tr> | ||||||
|  |               <th></th> | ||||||
|               <th>ID</th> |               <th>ID</th> | ||||||
|               <th>Name</th> |               <th>Name</th> | ||||||
|               <th>Value</th> |               <th>Value</th> | ||||||
|  |               <th></th> | ||||||
|             </tr> |             </tr> | ||||||
|           </thead> |           </thead> | ||||||
|           <tbody data-bind="foreach: attributes"> |           <tbody data-bind="foreach: attributes"> | ||||||
|             <tr> |             <tr data-bind="if: isDeleted == 0"> | ||||||
|               <td data-bind="text: attributeId"></td> |               <td class="handle"> | ||||||
|  |                 <span class="glyphicon glyphicon-resize-vertical"></span> | ||||||
|  |                 <input type="hidden" name="position" data-bind="value: position"/> | ||||||
|  |               </td> | ||||||
|  |               <!-- ID column has specific width because if it's empty its size can be deformed when dragging --> | ||||||
|  |               <td data-bind="text: attributeId" style="width: 150px;"></td> | ||||||
|               <td> |               <td> | ||||||
|                 <input type="text" data-bind="value: name"/> |                 <!-- Change to valueUpdate: blur is necessary because jQuery UI autocomplete hijacks change event --> | ||||||
|  |                 <input type="text" class="attribute-name" data-bind="value: name, valueUpdate: 'blur',  event: { blur: $parent.attributeChanged }"/> | ||||||
|  |                 <div style="color: yellowgreen" data-bind="if: $parent.isNotUnique($index())"><span class="glyphicon glyphicon-info-sign"></span> Duplicate attribute.</div> | ||||||
|  |                 <div style="color: red" data-bind="if: $parent.isEmptyName($index())">Attribute name can't be empty.</div> | ||||||
|               </td> |               </td> | ||||||
|               <td> |               <td> | ||||||
|                 <input type="text" data-bind="value: value" style="width: 300px"/> |                 <input type="text" class="attribute-value" data-bind="value: value, valueUpdate: 'blur', event: { blur: $parent.attributeChanged }" style="width: 300px"/> | ||||||
|  |               </td> | ||||||
|  |               <td title="Delete" style="padding: 13px;"> | ||||||
|  |                 <span class="glyphicon glyphicon-trash" data-bind="click: $parent.deleteAttribute"></span> | ||||||
|               </td> |               </td> | ||||||
|             </tr> |             </tr> | ||||||
|           </tbody> |           </tbody> | ||||||
| @@ -438,8 +470,6 @@ | |||||||
|     <link href="libraries/fancytree/skin-win8/ui.fancytree.css" rel="stylesheet"> |     <link href="libraries/fancytree/skin-win8/ui.fancytree.css" rel="stylesheet"> | ||||||
|     <script src="libraries/fancytree/jquery.fancytree-all.min.js"></script> |     <script src="libraries/fancytree/jquery.fancytree-all.min.js"></script> | ||||||
|  |  | ||||||
|     <script src="libraries/ckeditor/ckeditor.js"></script> |  | ||||||
|  |  | ||||||
|     <script src="libraries/jquery.hotkeys.js"></script> |     <script src="libraries/jquery.hotkeys.js"></script> | ||||||
|     <script src="libraries/jquery.fancytree.hotkeys.js"></script> |     <script src="libraries/jquery.fancytree.hotkeys.js"></script> | ||||||
|  |  | ||||||
| @@ -447,15 +477,6 @@ | |||||||
|  |  | ||||||
|     <script src="libraries/knockout.min.js"></script> |     <script src="libraries/knockout.min.js"></script> | ||||||
|  |  | ||||||
|     <script src="libraries/codemirror/codemirror.js"></script> |  | ||||||
|     <link rel="stylesheet" href="libraries/codemirror/codemirror.css"> |  | ||||||
|     <script src="libraries/codemirror/addon/mode/loadmode.js"></script> |  | ||||||
|     <script src="libraries/codemirror/addon/fold/xml-fold.js"></script> |  | ||||||
|     <script src="libraries/codemirror/addon/edit/matchbrackets.js"></script> |  | ||||||
|     <script src="libraries/codemirror/addon/edit/matchtags.js"></script> |  | ||||||
|     <script src="libraries/codemirror/addon/search/match-highlighter.js"></script> |  | ||||||
|     <script src="libraries/codemirror/mode/meta.js"></script> |  | ||||||
|  |  | ||||||
|     <link href="stylesheets/style.css" rel="stylesheet"> |     <link href="stylesheets/style.css" rel="stylesheet"> | ||||||
|  |  | ||||||
|     <script src="javascripts/utils.js"></script> |     <script src="javascripts/utils.js"></script> | ||||||
| @@ -470,6 +491,7 @@ | |||||||
|     <script src="javascripts/drag_and_drop.js"></script> |     <script src="javascripts/drag_and_drop.js"></script> | ||||||
|     <script src="javascripts/context_menu.js"></script> |     <script src="javascripts/context_menu.js"></script> | ||||||
|     <script src="javascripts/search_tree.js"></script> |     <script src="javascripts/search_tree.js"></script> | ||||||
|  |     <script src="javascripts/export.js"></script> | ||||||
|  |  | ||||||
|     <!-- Note detail --> |     <!-- Note detail --> | ||||||
|     <script src="javascripts/note_editor.js"></script> |     <script src="javascripts/note_editor.js"></script> | ||||||
| @@ -492,12 +514,16 @@ | |||||||
|     <script src="javascripts/link.js"></script> |     <script src="javascripts/link.js"></script> | ||||||
|     <script src="javascripts/sync.js"></script> |     <script src="javascripts/sync.js"></script> | ||||||
|     <script src="javascripts/messaging.js"></script> |     <script src="javascripts/messaging.js"></script> | ||||||
|  |     <script src="javascripts/api.js"></script> | ||||||
|  |  | ||||||
|     <script type="text/javascript"> |     <script type="text/javascript"> | ||||||
|       // we hide container initally because otherwise it is rendered first without CSS and then flickers into |       // we hide container initally because otherwise it is rendered first without CSS and then flickers into | ||||||
|       // final form which is pretty ugly. |       // final form which is pretty ugly. | ||||||
|       $("#container").show(); |       $("#container").show(); | ||||||
|     </script> |     </script> | ||||||
|  |  | ||||||
|  |     <style type="text/css"> | ||||||
|  |       <%= appCss %> | ||||||
|  |     </style> | ||||||
|   </body> |   </body> | ||||||
| </html> | </html> | ||||||
		Reference in New Issue
	
	Block a user