mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 02:16:05 +01:00 
			
		
		
		
	Compare commits
	
		
			42 Commits
		
	
	
		
			v0.5.5-bet
			...
			v0.6.2
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | fd02c6102d | ||
|  | 30c712a6be | ||
|  | 3928c96640 | ||
|  | d86f655658 | ||
|  | abdad1c3ae | ||
|  | 9e5f1a0a87 | ||
|  | 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 | 
| @@ -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 | ||||
| * Fast and easy [navigation between notes](https://github.com/zadam/trilium/wiki/Note-navigation) | ||||
| * 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) | ||||
| * [Synchronization with](https://github.com/zadam/trilium/wiki/Synchronization) self-hosted sync server | ||||
| * 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) | ||||
| * [Note navigation](https://github.com/zadam/trilium/wiki/Note-navigation) | ||||
| * [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) | ||||
| * [Cloning notes](https://github.com/zadam/trilium/wiki/Cloning-notes) | ||||
| * [Protected notes](https://github.com/zadam/trilium/wiki/Protected-notes) | ||||
|   | ||||
| @@ -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); | ||||
| @@ -85,8 +85,10 @@ CREATE TABLE IF NOT EXISTS "attributes" | ||||
|   noteId TEXT NOT NULL, | ||||
|   name TEXT NOT NULL, | ||||
|   value TEXT, | ||||
|   position INT NOT NULL DEFAULT 0, | ||||
|   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` ( | ||||
|   `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_noteId_imageId ON note_images (noteId, imageId); | ||||
| 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 | ||||
| ); | ||||
							
								
								
									
										22
									
								
								electron.js
									
									
									
									
									
								
							
							
						
						
									
										22
									
								
								electron.js
									
									
									
									
									
								
							| @@ -3,9 +3,11 @@ | ||||
| const electron = require('electron'); | ||||
| const path = require('path'); | ||||
| const config = require('./src/services/config'); | ||||
| const log = require('./src/services/log'); | ||||
| const url = require("url"); | ||||
|  | ||||
| const app = electron.app; | ||||
| const globalShortcut = electron.globalShortcut; | ||||
|  | ||||
| // Adds debug features like hotkeys for triggering dev tools and reload | ||||
| require('electron-debug')(); | ||||
| @@ -67,6 +69,26 @@ app.on('activate', () => { | ||||
|  | ||||
| app.on('ready', () => { | ||||
|     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'); | ||||
|   | ||||
							
								
								
									
										78
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										78
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @@ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "trilium", | ||||
|   "version": "0.4.1", | ||||
|   "version": "0.6.1", | ||||
|   "lockfileVersion": 1, | ||||
|   "requires": true, | ||||
|   "dependencies": { | ||||
| @@ -3061,19 +3061,19 @@ | ||||
|       "integrity": "sha1-zIcsFoiArjxxiXYv1f/ACJbJUYo=" | ||||
|     }, | ||||
|     "electron": { | ||||
|       "version": "1.8.2-beta.4", | ||||
|       "resolved": "https://registry.npmjs.org/electron/-/electron-1.8.2-beta.4.tgz", | ||||
|       "integrity": "sha1-GDayBO6s6dx3Bi7Ugg/bxsvZoZU=", | ||||
|       "version": "1.8.2", | ||||
|       "resolved": "https://registry.npmjs.org/electron/-/electron-1.8.2.tgz", | ||||
|       "integrity": "sha512-0TV5Hy92g8ACnPn+PVol6a/2uk+khzmRtWxhah/FcKs6StCytm5hD14QqOdZxEdJN8HljXIVCayN/wJX+0wDiQ==", | ||||
|       "requires": { | ||||
|         "@types/node": "8.5.9", | ||||
|         "@types/node": "8.9.4", | ||||
|         "electron-download": "3.3.0", | ||||
|         "extract-zip": "1.6.5" | ||||
|       }, | ||||
|       "dependencies": { | ||||
|         "@types/node": { | ||||
|           "version": "8.5.9", | ||||
|           "resolved": "https://registry.npmjs.org/@types/node/-/node-8.5.9.tgz", | ||||
|           "integrity": "sha512-s+c3AjymyAccTI4hcgNFK4mToH8l+hyPDhu4LIkn71lRy56FLijGu00fyLgldjM/846Pmk9N4KFUs2P8GDs0pA==" | ||||
|           "version": "8.9.4", | ||||
|           "resolved": "https://registry.npmjs.org/@types/node/-/node-8.9.4.tgz", | ||||
|           "integrity": "sha512-dSvD36qnQs78G1BPsrZFdPpvLgMW/dnvr5+nTW2csMs5TiP9MOXrjUbnMZOEwnIuBklXtn7b6TPA2Cuq07bDHA==" | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
| @@ -3325,9 +3325,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "electron-packager": { | ||||
|       "version": "10.1.1", | ||||
|       "resolved": "https://registry.npmjs.org/electron-packager/-/electron-packager-10.1.1.tgz", | ||||
|       "integrity": "sha1-MWp/ossf/CYz9YBcn8IJE8vAnZQ=", | ||||
|       "version": "11.0.1", | ||||
|       "resolved": "https://registry.npmjs.org/electron-packager/-/electron-packager-11.0.1.tgz", | ||||
|       "integrity": "sha1-wtH/nsqBEL6evIGCbiqSHATRIA4=", | ||||
|       "dev": true, | ||||
|       "requires": { | ||||
|         "asar": "0.14.0", | ||||
| @@ -3343,13 +3343,19 @@ | ||||
|         "pify": "3.0.0", | ||||
|         "plist": "2.1.0", | ||||
|         "pruner": "0.0.7", | ||||
|         "rcedit": "0.9.0", | ||||
|         "rcedit": "1.0.0", | ||||
|         "resolve": "1.4.0", | ||||
|         "sanitize-filename": "1.6.1", | ||||
|         "semver": "5.4.1", | ||||
|         "yargs-parser": "8.1.0" | ||||
|         "yargs-parser": "9.0.2" | ||||
|       }, | ||||
|       "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": { | ||||
|           "version": "4.1.0", | ||||
|           "resolved": "https://registry.npmjs.org/electron-download/-/electron-download-4.1.0.tgz", | ||||
| @@ -3437,6 +3443,12 @@ | ||||
|           "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", | ||||
|           "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": { | ||||
|           "version": "2.0.2", | ||||
|           "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-2.0.2.tgz", | ||||
| @@ -3456,20 +3468,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": { | ||||
|       "version": "1.8.2-beta.4", | ||||
|       "resolved": "https://registry.npmjs.org/electron-prebuilt-compile/-/electron-prebuilt-compile-1.8.2-beta.4.tgz", | ||||
|       "integrity": "sha512-whVdRgFEDovWSFrAsbMXIiush6RQ8IV3XhYdL59zShck4U1eXGmdkaBCy+2tlkGmUGr0fRu+S4FpUx2ebBkRhQ==", | ||||
|       "version": "1.8.2", | ||||
|       "resolved": "https://registry.npmjs.org/electron-prebuilt-compile/-/electron-prebuilt-compile-1.8.2.tgz", | ||||
|       "integrity": "sha512-wiDVjy8S0PA/K/TUM0lw5gzZ+SmyVVGQ0qt9iFYXHJc6t8TzDXFY3DsoK37H3A7nWnkvXvoPdpJ5/h9KbTMoAw==", | ||||
|       "dev": true, | ||||
|       "requires": { | ||||
|         "babel-plugin-array-includes": "2.0.3", | ||||
|         "babel-plugin-transform-async-to-generator": "6.24.1", | ||||
|         "babel-preset-es2016-node5": "1.1.2", | ||||
|         "babel-preset-react": "6.24.1", | ||||
|         "electron": "1.8.2-beta.4", | ||||
|         "electron": "1.8.2", | ||||
|         "electron-compile": "6.4.2", | ||||
|         "electron-compilers": "5.9.0", | ||||
|         "yargs": "6.6.0" | ||||
| @@ -8472,12 +8493,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": { | ||||
|       "version": "3.1.0", | ||||
|       "resolved": "https://registry.npmjs.org/read-all-stream/-/read-all-stream-3.1.0.tgz", | ||||
| @@ -11694,23 +11709,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": { | ||||
|       "version": "2.4.1", | ||||
|       "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.4.1.tgz", | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| { | ||||
|   "name": "trilium", | ||||
|   "description": "Trilium Notes", | ||||
|   "version": "0.5.5-beta", | ||||
|   "version": "0.6.2", | ||||
|   "license": "AGPL-3.0-only", | ||||
|   "main": "electron.js", | ||||
|   "repository": { | ||||
| @@ -27,7 +27,7 @@ | ||||
|     "debug": "~3.1.0", | ||||
|     "devtron": "^1.4.0", | ||||
|     "ejs": "~2.5.7", | ||||
|     "electron": "^1.8.2-beta.4", | ||||
|     "electron": "^1.8.2", | ||||
|     "electron-debug": "^1.5.0", | ||||
|     "electron-in-page-search": "^1.2.4", | ||||
|     "express": "~4.16.2", | ||||
| @@ -60,8 +60,8 @@ | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "electron-compile": "^6.4.2", | ||||
|     "electron-packager": "^10.1.1", | ||||
|     "electron-prebuilt-compile": "1.8.2-beta.4", | ||||
|     "electron-packager": "^11.0.1", | ||||
|     "electron-prebuilt-compile": "1.8.2", | ||||
|     "electron-rebuild": "^1.7.3", | ||||
|     "tape": "^4.8.0", | ||||
|     "xo": "^0.18.0" | ||||
|   | ||||
| @@ -24,7 +24,7 @@ class Note extends Entity { | ||||
|     } | ||||
|  | ||||
|     async getAttributes() { | ||||
|         return this.repository.getEntities("SELECT * FROM attributes WHERE noteId = ?", [this.noteId]); | ||||
|         return this.repository.getEntities("SELECT * FROM attributes WHERE noteId = ? AND isDeleted = 0", [this.noteId]); | ||||
|     } | ||||
|  | ||||
|     async getAttribute(name) { | ||||
|   | ||||
| @@ -38,6 +38,7 @@ async function getDateNoteIdForReddit(dateTimeStr, rootNoteId) { | ||||
|         redditDateNoteId = await createNote(dateNoteId, "Reddit"); | ||||
|  | ||||
|         await attributes.createAttribute(redditDateNoteId, REDDIT_DATE_ATTRIBUTE, dateStr); | ||||
|         await attributes.createAttribute(redditDateNoteId, "hide_in_autocomplete"); | ||||
|     } | ||||
|  | ||||
|     return redditDateNoteId; | ||||
|   | ||||
| @@ -1,18 +1,18 @@ | ||||
| "use strict"; | ||||
|  | ||||
| const addLink = (function() { | ||||
|     const dialogEl = $("#add-link-dialog"); | ||||
|     const formEl = $("#add-link-form"); | ||||
|     const autoCompleteEl = $("#note-autocomplete"); | ||||
|     const linkTitleEl = $("#link-title"); | ||||
|     const clonePrefixEl = $("#clone-prefix"); | ||||
|     const linkTitleFormGroup = $("#add-link-title-form-group"); | ||||
|     const prefixFormGroup = $("#add-link-prefix-form-group"); | ||||
|     const linkTypeEls = $("input[name='add-link-type']"); | ||||
|     const linkTypeHtmlEl = linkTypeEls.filter('input[value="html"]'); | ||||
|     const $dialog = $("#add-link-dialog"); | ||||
|     const $form = $("#add-link-form"); | ||||
|     const $autoComplete = $("#note-autocomplete"); | ||||
|     const $linkTitle = $("#link-title"); | ||||
|     const $clonePrefix = $("#clone-prefix"); | ||||
|     const $linkTitleFormGroup = $("#add-link-title-form-group"); | ||||
|     const $prefixFormGroup = $("#add-link-prefix-form-group"); | ||||
|     const $linkTypes = $("input[name='add-link-type']"); | ||||
|     const $linkTypeHtml = $linkTypes.filter('input[value="html"]'); | ||||
|  | ||||
|     function setLinkType(linkType) { | ||||
|         linkTypeEls.each(function () { | ||||
|         $linkTypes.each(function () { | ||||
|             $(this).prop('checked', $(this).val() === linkType); | ||||
|         }); | ||||
|  | ||||
| @@ -20,39 +20,39 @@ const addLink = (function() { | ||||
|     } | ||||
|  | ||||
|     function showDialog() { | ||||
|         glob.activeDialog = dialogEl; | ||||
|         glob.activeDialog = $dialog; | ||||
|  | ||||
|         if (noteEditor.getCurrentNoteType() === 'text') { | ||||
|             linkTypeHtmlEl.prop('disabled', false); | ||||
|             $linkTypeHtml.prop('disabled', false); | ||||
|  | ||||
|             setLinkType('html'); | ||||
|         } | ||||
|         else { | ||||
|             linkTypeHtmlEl.prop('disabled', true); | ||||
|             $linkTypeHtml.prop('disabled', true); | ||||
|  | ||||
|             setLinkType('selected-to-current'); | ||||
|         } | ||||
|  | ||||
|         dialogEl.dialog({ | ||||
|         $dialog.dialog({ | ||||
|             modal: true, | ||||
|             width: 700 | ||||
|         }); | ||||
|  | ||||
|         autoCompleteEl.val('').focus(); | ||||
|         clonePrefixEl.val(''); | ||||
|         linkTitleEl.val(''); | ||||
|         $autoComplete.val('').focus(); | ||||
|         $clonePrefix.val(''); | ||||
|         $linkTitle.val(''); | ||||
|  | ||||
|         function setDefaultLinkTitle(noteId) { | ||||
|             const noteTitle = noteTree.getNoteTitle(noteId); | ||||
|  | ||||
|             linkTitleEl.val(noteTitle); | ||||
|             $linkTitle.val(noteTitle); | ||||
|         } | ||||
|  | ||||
|         autoCompleteEl.autocomplete({ | ||||
|         $autoComplete.autocomplete({ | ||||
|             source: noteTree.getAutocompleteItems(), | ||||
|             minLength: 0, | ||||
|             change: () => { | ||||
|                 const val = autoCompleteEl.val(); | ||||
|                 const val = $autoComplete.val(); | ||||
|                 const notePath = link.getNodePathFromLabel(val); | ||||
|                 if (!notePath) { | ||||
|                     return; | ||||
| @@ -75,8 +75,8 @@ const addLink = (function() { | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     formEl.submit(() => { | ||||
|         const value = autoCompleteEl.val(); | ||||
|     $form.submit(() => { | ||||
|         const value = $autoComplete.val(); | ||||
|  | ||||
|         const notePath = link.getNodePathFromLabel(value); | ||||
|         const noteId = treeUtils.getNoteIdFromNotePath(notePath); | ||||
| @@ -85,25 +85,25 @@ const addLink = (function() { | ||||
|             const linkType = $("input[name='add-link-type']:checked").val(); | ||||
|  | ||||
|             if (linkType === 'html') { | ||||
|                 const linkTitle = linkTitleEl.val(); | ||||
|                 const linkTitle = $linkTitle.val(); | ||||
|  | ||||
|                 dialogEl.dialog("close"); | ||||
|                 $dialog.dialog("close"); | ||||
|  | ||||
|                 link.addLinkToEditor(linkTitle, '#' + notePath); | ||||
|             } | ||||
|             else if (linkType === 'selected-to-current') { | ||||
|                 const prefix = clonePrefixEl.val(); | ||||
|                 const prefix = $clonePrefix.val(); | ||||
|  | ||||
|                 cloning.cloneNoteTo(noteId, noteEditor.getCurrentNoteId(), prefix); | ||||
|  | ||||
|                 dialogEl.dialog("close"); | ||||
|                 $dialog.dialog("close"); | ||||
|             } | ||||
|             else if (linkType === 'current-to-selected') { | ||||
|                 const prefix = clonePrefixEl.val(); | ||||
|                 const prefix = $clonePrefix.val(); | ||||
|  | ||||
|                 cloning.cloneNoteTo(noteEditor.getCurrentNoteId(), noteId, prefix); | ||||
|  | ||||
|                 dialogEl.dialog("close"); | ||||
|                 $dialog.dialog("close"); | ||||
|             } | ||||
|         } | ||||
|  | ||||
| @@ -111,19 +111,19 @@ const addLink = (function() { | ||||
|     }); | ||||
|  | ||||
|     function linkTypeChanged() { | ||||
|         const value = linkTypeEls.filter(":checked").val(); | ||||
|         const value = $linkTypes.filter(":checked").val(); | ||||
|  | ||||
|         if (value === 'html') { | ||||
|             linkTitleFormGroup.show(); | ||||
|             prefixFormGroup.hide(); | ||||
|             $linkTitleFormGroup.show(); | ||||
|             $prefixFormGroup.hide(); | ||||
|         } | ||||
|         else { | ||||
|             linkTitleFormGroup.hide(); | ||||
|             prefixFormGroup.show(); | ||||
|             $linkTitleFormGroup.hide(); | ||||
|             $prefixFormGroup.show(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     linkTypeEls.change(linkTypeChanged); | ||||
|     $linkTypes.change(linkTypeChanged); | ||||
|  | ||||
|     $(document).bind('keydown', 'ctrl+l', e => { | ||||
|         showDialog(); | ||||
|   | ||||
| @@ -1,8 +1,10 @@ | ||||
| "use strict"; | ||||
|  | ||||
| const attributesDialog = (function() { | ||||
|     const dialogEl = $("#attributes-dialog"); | ||||
|     const saveAttributesButton = $("#save-attributes-button"); | ||||
|     const $dialog = $("#attributes-dialog"); | ||||
|     const $saveAttributesButton = $("#save-attributes-button"); | ||||
|     const $attributesBody = $('#attributes-table tbody'); | ||||
|  | ||||
|     const attributesModel = new AttributesModel(); | ||||
|     let attributeNames = []; | ||||
|  | ||||
| @@ -22,12 +24,42 @@ const attributesDialog = (function() { | ||||
|  | ||||
|             attributeNames = await server.get('attributes/names'); | ||||
|  | ||||
|             $(".attribute-name:last").focus(); | ||||
|             // 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) || self.isNotUnique(i)) { | ||||
|                 if (self.isEmptyName(i)) { | ||||
|                     return false; | ||||
|                 } | ||||
|             } | ||||
| @@ -39,7 +71,7 @@ const attributesDialog = (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(); | ||||
|             $saveAttributesButton.focus(); | ||||
|  | ||||
|             if (!isValid()) { | ||||
|                 alert("Please fix all validation errors and try saving again."); | ||||
| @@ -64,26 +96,26 @@ const attributesDialog = (function() { | ||||
|         }; | ||||
|  | ||||
|         function addLastEmptyRow() { | ||||
|             const attrs = self.attributes(); | ||||
|             const last = attrs[attrs.length - 1](); | ||||
|             const attrs = self.attributes().filter(attr => attr().isDeleted === 0); | ||||
|             const last = attrs.length === 0 ? null : attrs[attrs.length - 1](); | ||||
|  | ||||
|             if (last.name.trim() !== "" || last.value !== "") { | ||||
|             if (!last || last.name.trim() !== "" || last.value !== "") { | ||||
|                 self.attributes.push(ko.observable({ | ||||
|                     attributeId: '', | ||||
|                     name: '', | ||||
|                     value: '' | ||||
|                     value: '', | ||||
|                     isDeleted: 0, | ||||
|                     position: 0 | ||||
|                 })); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         this.attributeChanged = function (row) { | ||||
|         this.attributeChanged = function (data, event) { | ||||
|             addLastEmptyRow(); | ||||
|  | ||||
|             for (const attr of self.attributes()) { | ||||
|                 if (row.attributeId === attr().attributeId) { | ||||
|             const attr = self.getTargetAttribute(event.target); | ||||
|  | ||||
|             attr.valueHasMutated(); | ||||
|                 } | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         this.isNotUnique = function(index) { | ||||
| @@ -108,15 +140,22 @@ const attributesDialog = (function() { | ||||
|             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() { | ||||
|         glob.activeDialog = dialogEl; | ||||
|         glob.activeDialog = $dialog; | ||||
|  | ||||
|         await attributesModel.loadAttributes(); | ||||
|  | ||||
|         dialogEl.dialog({ | ||||
|         $dialog.dialog({ | ||||
|             modal: true, | ||||
|             width: 800, | ||||
|             height: 500 | ||||
| @@ -149,8 +188,6 @@ const attributesDialog = (function() { | ||||
|         $(this).autocomplete("search", $(this).val()); | ||||
|     }); | ||||
|  | ||||
|     $(document).on('blur', '.attribute-name', function (e) { console.log("blur!"); }); | ||||
|  | ||||
|     $(document).on('focus', '.attribute-value', async function (e) { | ||||
|         if (!$(this).hasClass("ui-autocomplete-input")) { | ||||
|             const attributeName = $(this).parent().parent().find('.attribute-name').val(); | ||||
|   | ||||
| @@ -1,17 +1,17 @@ | ||||
| "use strict"; | ||||
|  | ||||
| const editTreePrefix = (function() { | ||||
|     const dialogEl = $("#edit-tree-prefix-dialog"); | ||||
|     const formEl = $("#edit-tree-prefix-form"); | ||||
|     const treePrefixInputEl = $("#tree-prefix-input"); | ||||
|     const noteTitleEl = $('#tree-prefix-note-title'); | ||||
|     const $dialog = $("#edit-tree-prefix-dialog"); | ||||
|     const $form = $("#edit-tree-prefix-form"); | ||||
|     const $treePrefixInput = $("#tree-prefix-input"); | ||||
|     const $noteTitle = $('#tree-prefix-note-title'); | ||||
|  | ||||
|     let noteTreeId; | ||||
|  | ||||
|     async function showDialog() { | ||||
|         glob.activeDialog = dialogEl; | ||||
|         glob.activeDialog = $dialog; | ||||
|  | ||||
|         await dialogEl.dialog({ | ||||
|         await $dialog.dialog({ | ||||
|             modal: true, | ||||
|             width: 500 | ||||
|         }); | ||||
| @@ -20,21 +20,21 @@ const editTreePrefix = (function() { | ||||
|  | ||||
|         noteTreeId = currentNode.data.noteTreeId; | ||||
|  | ||||
|         treePrefixInputEl.val(currentNode.data.prefix).focus(); | ||||
|         $treePrefixInput.val(currentNode.data.prefix).focus(); | ||||
|  | ||||
|         const noteTitle = noteTree.getNoteTitle(currentNode.data.noteId); | ||||
|  | ||||
|         noteTitleEl.html(noteTitle); | ||||
|         $noteTitle.html(noteTitle); | ||||
|     } | ||||
|  | ||||
|     formEl.submit(() => { | ||||
|         const prefix = treePrefixInputEl.val(); | ||||
|     $form.submit(() => { | ||||
|         const prefix = $treePrefixInput.val(); | ||||
|  | ||||
|         server.put('tree/' + noteTreeId + '/set-prefix', { | ||||
|             prefix: prefix | ||||
|         }).then(() => noteTree.setPrefix(noteTreeId, prefix)); | ||||
|  | ||||
|         dialogEl.dialog("close"); | ||||
|         $dialog.dialog("close"); | ||||
|  | ||||
|         return false; | ||||
|     }); | ||||
|   | ||||
| @@ -1,13 +1,13 @@ | ||||
| "use strict"; | ||||
|  | ||||
| const eventLog = (function() { | ||||
|     const dialogEl = $("#event-log-dialog"); | ||||
|     const listEl = $("#event-log-list"); | ||||
|     const $dialog = $("#event-log-dialog"); | ||||
|     const $list = $("#event-log-list"); | ||||
|  | ||||
|     async function showDialog() { | ||||
|         glob.activeDialog = dialogEl; | ||||
|         glob.activeDialog = $dialog; | ||||
|  | ||||
|         dialogEl.dialog({ | ||||
|         $dialog.dialog({ | ||||
|             modal: true, | ||||
|             width: 800, | ||||
|             height: 700 | ||||
| @@ -15,7 +15,7 @@ const eventLog = (function() { | ||||
|  | ||||
|         const result = await server.get('event-log'); | ||||
|  | ||||
|         listEl.html(''); | ||||
|         $list.html(''); | ||||
|  | ||||
|         for (const event of result) { | ||||
|             const dateTime = formatDateTime(parseDate(event.dateAdded)); | ||||
| @@ -28,7 +28,7 @@ const eventLog = (function() { | ||||
|  | ||||
|             const eventEl = $('<li>').html(dateTime + " - " + event.comment); | ||||
|  | ||||
|             listEl.append(eventEl); | ||||
|             $list.append(eventEl); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -1,28 +1,28 @@ | ||||
| "use strict"; | ||||
|  | ||||
| const jumpToNote = (function() { | ||||
|     const dialogEl = $("#jump-to-note-dialog"); | ||||
|     const autoCompleteEl = $("#jump-to-note-autocomplete"); | ||||
|     const formEl = $("#jump-to-note-form"); | ||||
|     const $dialog = $("#jump-to-note-dialog"); | ||||
|     const $autoComplete = $("#jump-to-note-autocomplete"); | ||||
|     const $form = $("#jump-to-note-form"); | ||||
|  | ||||
|     async function showDialog() { | ||||
|         glob.activeDialog = dialogEl; | ||||
|         glob.activeDialog = $dialog; | ||||
|  | ||||
|         autoCompleteEl.val(''); | ||||
|         $autoComplete.val(''); | ||||
|  | ||||
|         dialogEl.dialog({ | ||||
|         $dialog.dialog({ | ||||
|             modal: true, | ||||
|             width: 800 | ||||
|         }); | ||||
|  | ||||
|         await autoCompleteEl.autocomplete({ | ||||
|         await $autoComplete.autocomplete({ | ||||
|             source: await stopWatch("building autocomplete", noteTree.getAutocompleteItems), | ||||
|             minLength: 0 | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     function getSelectedNotePath() { | ||||
|         const val = autoCompleteEl.val(); | ||||
|         const val = $autoComplete.val(); | ||||
|         return link.getNodePathFromLabel(val); | ||||
|     } | ||||
|  | ||||
| @@ -32,7 +32,7 @@ const jumpToNote = (function() { | ||||
|         if (notePath) { | ||||
|             noteTree.activateNode(notePath); | ||||
|  | ||||
|             dialogEl.dialog('close'); | ||||
|             $dialog.dialog('close'); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -42,8 +42,8 @@ const jumpToNote = (function() { | ||||
|         e.preventDefault(); | ||||
|     }); | ||||
|  | ||||
|     formEl.submit(() => { | ||||
|         const action = dialogEl.find("button:focus").val(); | ||||
|     $form.submit(() => { | ||||
|         const action = $dialog.find("button:focus").val(); | ||||
|  | ||||
|         goToNote(); | ||||
|  | ||||
|   | ||||
| @@ -1,10 +1,10 @@ | ||||
| "use strict"; | ||||
|  | ||||
| const noteHistory = (function() { | ||||
|     const dialogEl = $("#note-history-dialog"); | ||||
|     const listEl = $("#note-history-list"); | ||||
|     const contentEl = $("#note-history-content"); | ||||
|     const titleEl = $("#note-history-title"); | ||||
|     const $dialog = $("#note-history-dialog"); | ||||
|     const $list = $("#note-history-list"); | ||||
|     const $content = $("#note-history-content"); | ||||
|     const $title = $("#note-history-title"); | ||||
|  | ||||
|     let historyItems = []; | ||||
|  | ||||
| @@ -13,23 +13,23 @@ const noteHistory = (function() { | ||||
|     } | ||||
|  | ||||
|     async function showNoteHistoryDialog(noteId, noteRevisionId) { | ||||
|         glob.activeDialog = dialogEl; | ||||
|         glob.activeDialog = $dialog; | ||||
|  | ||||
|         dialogEl.dialog({ | ||||
|         $dialog.dialog({ | ||||
|             modal: true, | ||||
|             width: 800, | ||||
|             height: 700 | ||||
|         }); | ||||
|  | ||||
|         listEl.empty(); | ||||
|         contentEl.empty(); | ||||
|         $list.empty(); | ||||
|         $content.empty(); | ||||
|  | ||||
|         historyItems = await server.get('notes-history/' + noteId); | ||||
|  | ||||
|         for (const item of historyItems) { | ||||
|             const dateModified = parseDate(item.dateModifiedFrom); | ||||
|  | ||||
|             listEl.append($('<option>', { | ||||
|             $list.append($('<option>', { | ||||
|                 value: item.noteRevisionId, | ||||
|                 text: formatDateTime(dateModified) | ||||
|             })); | ||||
| @@ -37,13 +37,13 @@ const noteHistory = (function() { | ||||
|  | ||||
|         if (historyItems.length > 0) { | ||||
|             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 { | ||||
|             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(); | ||||
|     }); | ||||
|  | ||||
|     listEl.on('change', () => { | ||||
|         const optVal = listEl.find(":selected").val(); | ||||
|     $list.on('change', () => { | ||||
|         const optVal = $list.find(":selected").val(); | ||||
|  | ||||
|         const historyItem = historyItems.find(r => r.noteRevisionId === optVal); | ||||
|  | ||||
|         titleEl.html(historyItem.title); | ||||
|         contentEl.html(historyItem.content); | ||||
|         $title.html(historyItem.title); | ||||
|         $content.html(historyItem.content); | ||||
|     }); | ||||
|  | ||||
|     $(document).on('click', "a[action='note-history']", event => { | ||||
|   | ||||
| @@ -1,13 +1,13 @@ | ||||
| "use strict"; | ||||
|  | ||||
| const noteSource = (function() { | ||||
|     const dialogEl = $("#note-source-dialog"); | ||||
|     const noteSourceEl = $("#note-source"); | ||||
|     const $dialog = $("#note-source-dialog"); | ||||
|     const $noteSource = $("#note-source"); | ||||
|  | ||||
|     function showDialog() { | ||||
|         glob.activeDialog = dialogEl; | ||||
|         glob.activeDialog = $dialog; | ||||
|  | ||||
|         dialogEl.dialog({ | ||||
|         $dialog.dialog({ | ||||
|             modal: true, | ||||
|             width: 800, | ||||
|             height: 500 | ||||
| @@ -15,7 +15,7 @@ const noteSource = (function() { | ||||
|  | ||||
|         const noteText = noteEditor.getCurrentNote().detail.content; | ||||
|  | ||||
|         noteSourceEl.text(formatHtml(noteText)); | ||||
|         $noteSource.text(formatHtml(noteText)); | ||||
|     } | ||||
|  | ||||
|     function formatHtml(str) { | ||||
|   | ||||
| @@ -1,12 +1,12 @@ | ||||
| "use strict"; | ||||
|  | ||||
| const recentChanges = (function() { | ||||
|     const dialogEl = $("#recent-changes-dialog"); | ||||
|     const $dialog = $("#recent-changes-dialog"); | ||||
|  | ||||
|     async function showDialog() { | ||||
|         glob.activeDialog = dialogEl; | ||||
|         glob.activeDialog = $dialog; | ||||
|  | ||||
|         dialogEl.dialog({ | ||||
|         $dialog.dialog({ | ||||
|             modal: true, | ||||
|             width: 800, | ||||
|             height: 700 | ||||
| @@ -14,7 +14,7 @@ const recentChanges = (function() { | ||||
|  | ||||
|         const result = await server.get('recent-changes/'); | ||||
|  | ||||
|         dialogEl.html(''); | ||||
|         $dialog.html(''); | ||||
|  | ||||
|         const groupedByDate = groupByDate(result); | ||||
|  | ||||
| @@ -48,7 +48,7 @@ const recentChanges = (function() { | ||||
|                     .append(' (').append(revLink).append(')')); | ||||
|             } | ||||
|  | ||||
|             dialogEl.append(dayEl); | ||||
|             $dialog.append(dayEl); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -1,13 +1,9 @@ | ||||
| "use strict"; | ||||
|  | ||||
| const recentNotes = (function() { | ||||
|     const dialogEl = $("#recent-notes-dialog"); | ||||
|     const selectBoxEl = $('#recent-notes-select-box'); | ||||
|     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'); | ||||
|     const $dialog = $("#recent-notes-dialog"); | ||||
|     const $searchInput = $('#recent-notes-search-input'); | ||||
|  | ||||
|     // list of recent note paths | ||||
|     let list = []; | ||||
|  | ||||
| @@ -29,97 +25,66 @@ const recentNotes = (function() { | ||||
|     } | ||||
|  | ||||
|     function showDialog() { | ||||
|         glob.activeDialog = dialogEl; | ||||
|         glob.activeDialog = $dialog; | ||||
|  | ||||
|         dialogEl.dialog({ | ||||
|         $dialog.dialog({ | ||||
|             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 | ||||
|         const recNotes = list.filter(note => note !== noteTree.getCurrentNotePath()); | ||||
|  | ||||
|         $.each(recNotes, (key, valueNotePath) => { | ||||
|             const noteTitle = noteTree.getNotePathTitle(valueNotePath); | ||||
|         $searchInput.autocomplete({ | ||||
|             source: recNotes.map(notePath => { | ||||
|                 let noteTitle; | ||||
|  | ||||
|             const option = $("<option></option>") | ||||
|                 .attr("value", valueNotePath) | ||||
|                 .text(noteTitle); | ||||
|                 try { | ||||
|                     noteTitle = noteTree.getNotePathTitle(notePath); | ||||
|                 } | ||||
|                 catch (e) { | ||||
|                     noteTitle = "[error - can't find note title]"; | ||||
|  | ||||
|             // select the first one (most recent one) by default | ||||
|             if (key === 0) { | ||||
|                 option.attr("selected", "selected"); | ||||
|                     messaging.logError("Could not find title for notePath=" + notePath + ", stack=" + e.stack); | ||||
|                 } | ||||
|  | ||||
|             selectBoxEl.append(option); | ||||
|         }); | ||||
|                 return { | ||||
|                     label: noteTitle, | ||||
|                     value: notePath | ||||
|                 } | ||||
|             }), | ||||
|             minLength: 0, | ||||
|             autoFocus: true, | ||||
|             select: function (event, ui) { | ||||
|                 noteTree.activateNode(ui.item.value); | ||||
|  | ||||
|     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() | ||||
|                 $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 { | ||||
|             return; // avoid prevent default | ||||
|                     // 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" | ||||
|             } | ||||
|  | ||||
|         e.preventDefault(); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     reload(); | ||||
|  | ||||
| @@ -129,15 +94,6 @@ const recentNotes = (function() { | ||||
|         e.preventDefault(); | ||||
|     }); | ||||
|  | ||||
|     selectBoxEl.dblclick(e => { | ||||
|         setActiveNoteBasedOnRecentNotes(); | ||||
|     }); | ||||
|  | ||||
|     jumpToButtonEl.click(setActiveNoteBasedOnRecentNotes); | ||||
|     addLinkButtonEl.click(addLinkBasedOnRecentNotes); | ||||
|     addCurrentAsChildEl.click(addCurrentAsChild); | ||||
|     addRecentAsChildEl.click(addRecentAsChild); | ||||
|  | ||||
|     return { | ||||
|         showDialog, | ||||
|         addRecentNote, | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| "use strict"; | ||||
|  | ||||
| const settings = (function() { | ||||
|     const dialogEl = $("#settings-dialog"); | ||||
|     const tabsEl = $("#settings-tabs"); | ||||
|     const $dialog = $("#settings-dialog"); | ||||
|     const $tabs = $("#settings-tabs"); | ||||
|  | ||||
|     const settingModules = []; | ||||
|  | ||||
| @@ -11,16 +11,16 @@ const settings = (function() { | ||||
|     } | ||||
|  | ||||
|     async function showDialog() { | ||||
|         glob.activeDialog = dialogEl; | ||||
|         glob.activeDialog = $dialog; | ||||
|  | ||||
|         const settings = await server.get('settings'); | ||||
|  | ||||
|         dialogEl.dialog({ | ||||
|         $dialog.dialog({ | ||||
|             modal: true, | ||||
|             width: 900 | ||||
|         }); | ||||
|  | ||||
|         tabsEl.tabs(); | ||||
|         $tabs.tabs(); | ||||
|  | ||||
|         for (const module of settingModules) { | ||||
|             if (module.settingsLoaded) { | ||||
| @@ -46,22 +46,22 @@ const settings = (function() { | ||||
| })(); | ||||
|  | ||||
| settings.addModule((function() { | ||||
|     const formEl = $("#change-password-form"); | ||||
|     const oldPasswordEl = $("#old-password"); | ||||
|     const newPassword1El = $("#new-password1"); | ||||
|     const newPassword2El = $("#new-password2"); | ||||
|     const $form = $("#change-password-form"); | ||||
|     const $oldPassword = $("#old-password"); | ||||
|     const $newPassword1 = $("#new-password1"); | ||||
|     const $newPassword2 = $("#new-password2"); | ||||
|  | ||||
|     function settingsLoaded(settings) { | ||||
|     } | ||||
|  | ||||
|     formEl.submit(() => { | ||||
|         const oldPassword = oldPasswordEl.val(); | ||||
|         const newPassword1 = newPassword1El.val(); | ||||
|         const newPassword2 = newPassword2El.val(); | ||||
|     $form.submit(() => { | ||||
|         const oldPassword = $oldPassword.val(); | ||||
|         const newPassword1 = $newPassword1.val(); | ||||
|         const newPassword2 = $newPassword2.val(); | ||||
|  | ||||
|         oldPasswordEl.val(''); | ||||
|         newPassword1El.val(''); | ||||
|         newPassword2El.val(''); | ||||
|         $oldPassword.val(''); | ||||
|         $newPassword1.val(''); | ||||
|         $newPassword2.val(''); | ||||
|  | ||||
|         if (newPassword1 !== newPassword2) { | ||||
|             alert("New passwords are not the same."); | ||||
| @@ -92,16 +92,16 @@ settings.addModule((function() { | ||||
| })()); | ||||
|  | ||||
| settings.addModule((function() { | ||||
|     const formEl = $("#protected-session-timeout-form"); | ||||
|     const protectedSessionTimeoutEl = $("#protected-session-timeout-in-seconds"); | ||||
|     const $form = $("#protected-session-timeout-form"); | ||||
|     const $protectedSessionTimeout = $("#protected-session-timeout-in-seconds"); | ||||
|     const settingName = 'protected_session_timeout'; | ||||
|  | ||||
|     function settingsLoaded(settings) { | ||||
|         protectedSessionTimeoutEl.val(settings[settingName]); | ||||
|         $protectedSessionTimeout.val(settings[settingName]); | ||||
|     } | ||||
|  | ||||
|     formEl.submit(() => { | ||||
|         const protectedSessionTimeout = protectedSessionTimeoutEl.val(); | ||||
|     $form.submit(() => { | ||||
|         const protectedSessionTimeout = $protectedSessionTimeout.val(); | ||||
|  | ||||
|         settings.saveSettings(settingName, protectedSessionTimeout).then(() => { | ||||
|             protected_session.setProtectedSessionTimeout(protectedSessionTimeout); | ||||
| @@ -116,16 +116,16 @@ settings.addModule((function() { | ||||
| })()); | ||||
|  | ||||
| settings.addModule((function () { | ||||
|     const formEl = $("#history-snapshot-time-interval-form"); | ||||
|     const timeIntervalEl = $("#history-snapshot-time-interval-in-seconds"); | ||||
|     const $form = $("#history-snapshot-time-interval-form"); | ||||
|     const $timeInterval = $("#history-snapshot-time-interval-in-seconds"); | ||||
|     const settingName = 'history_snapshot_time_interval'; | ||||
|  | ||||
|     function settingsLoaded(settings) { | ||||
|         timeIntervalEl.val(settings[settingName]); | ||||
|         $timeInterval.val(settings[settingName]); | ||||
|     } | ||||
|  | ||||
|     formEl.submit(() => { | ||||
|         settings.saveSettings(settingName, timeIntervalEl.val()); | ||||
|     $form.submit(() => { | ||||
|         settings.saveSettings(settingName, $timeInterval.val()); | ||||
|  | ||||
|         return false; | ||||
|     }); | ||||
| @@ -136,50 +136,50 @@ settings.addModule((function () { | ||||
| })()); | ||||
|  | ||||
| settings.addModule((async function () { | ||||
|     const appVersionEl = $("#app-version"); | ||||
|     const dbVersionEl = $("#db-version"); | ||||
|     const buildDateEl = $("#build-date"); | ||||
|     const buildRevisionEl = $("#build-revision"); | ||||
|     const $appVersion = $("#app-version"); | ||||
|     const $dbVersion = $("#db-version"); | ||||
|     const $buildDate = $("#build-date"); | ||||
|     const $buildRevision = $("#build-revision"); | ||||
|  | ||||
|     const appInfo = await server.get('app-info'); | ||||
|  | ||||
|     appVersionEl.html(appInfo.app_version); | ||||
|     dbVersionEl.html(appInfo.db_version); | ||||
|     buildDateEl.html(appInfo.build_date); | ||||
|     buildRevisionEl.html(appInfo.build_revision); | ||||
|     buildRevisionEl.attr('href', 'https://github.com/zadam/trilium/commit/' + appInfo.build_revision); | ||||
|     $appVersion.html(appInfo.app_version); | ||||
|     $dbVersion.html(appInfo.db_version); | ||||
|     $buildDate.html(appInfo.build_date); | ||||
|     $buildRevision.html(appInfo.build_revision); | ||||
|     $buildRevision.attr('href', 'https://github.com/zadam/trilium/commit/' + appInfo.build_revision); | ||||
|  | ||||
|     return {}; | ||||
| })()); | ||||
|  | ||||
| settings.addModule((async function () { | ||||
|     const forceFullSyncButton = $("#force-full-sync-button"); | ||||
|     const fillSyncRowsButton = $("#fill-sync-rows-button"); | ||||
|     const anonymizeButton = $("#anonymize-button"); | ||||
|     const cleanupSoftDeletedButton = $("#cleanup-soft-deleted-items-button"); | ||||
|     const cleanupUnusedImagesButton = $("#cleanup-unused-images-button"); | ||||
|     const vacuumDatabaseButton = $("#vacuum-database-button"); | ||||
|     const $forceFullSyncButton = $("#force-full-sync-button"); | ||||
|     const $fillSyncRowsButton = $("#fill-sync-rows-button"); | ||||
|     const $anonymizeButton = $("#anonymize-button"); | ||||
|     const $cleanupSoftDeletedButton = $("#cleanup-soft-deleted-items-button"); | ||||
|     const $cleanupUnusedImagesButton = $("#cleanup-unused-images-button"); | ||||
|     const $vacuumDatabaseButton = $("#vacuum-database-button"); | ||||
|  | ||||
|     forceFullSyncButton.click(async () => { | ||||
|     $forceFullSyncButton.click(async () => { | ||||
|         await server.post('sync/force-full-sync'); | ||||
|  | ||||
|         showMessage("Full sync triggered"); | ||||
|     }); | ||||
|  | ||||
|     fillSyncRowsButton.click(async () => { | ||||
|     $fillSyncRowsButton.click(async () => { | ||||
|         await server.post('sync/fill-sync-rows'); | ||||
|  | ||||
|         showMessage("Sync rows filled successfully"); | ||||
|     }); | ||||
|  | ||||
|  | ||||
|     anonymizeButton.click(async () => { | ||||
|     $anonymizeButton.click(async () => { | ||||
|         await server.post('anonymization/anonymize'); | ||||
|  | ||||
|         showMessage("Created anonymized database"); | ||||
|     }); | ||||
|  | ||||
|     cleanupSoftDeletedButton.click(async () => { | ||||
|     $cleanupSoftDeletedButton.click(async () => { | ||||
|         if (confirm("Do you really want to clean up 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?")) { | ||||
|             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'); | ||||
|  | ||||
|         showMessage("Database has been vacuumed"); | ||||
|   | ||||
| @@ -1,24 +1,44 @@ | ||||
| "use strict"; | ||||
|  | ||||
| const sqlConsole = (function() { | ||||
|     const dialogEl = $("#sql-console-dialog"); | ||||
|     const queryEl = $('#sql-console-query'); | ||||
|     const executeButton = $('#sql-console-execute'); | ||||
|     const resultHeadEl = $('#sql-console-results thead'); | ||||
|     const resultBodyEl = $('#sql-console-results tbody'); | ||||
|     const $dialog = $("#sql-console-dialog"); | ||||
|     const $query = $('#sql-console-query'); | ||||
|     const $executeButton = $('#sql-console-execute'); | ||||
|     const $resultHead = $('#sql-console-results thead'); | ||||
|     const $resultBody = $('#sql-console-results tbody'); | ||||
|  | ||||
|     let codeEditor; | ||||
|  | ||||
|     function showDialog() { | ||||
|         glob.activeDialog = dialogEl; | ||||
|         glob.activeDialog = $dialog; | ||||
|  | ||||
|         dialogEl.dialog({ | ||||
|         $dialog.dialog({ | ||||
|             modal: true, | ||||
|             width: $(window).width(), | ||||
|             height: $(window).height() | ||||
|             height: $(window).height(), | ||||
|             open: function() { | ||||
|                 CodeMirror.keyMap.default["Shift-Tab"] = "indentLess"; | ||||
|                 CodeMirror.keyMap.default["Tab"] = "indentMore"; | ||||
|  | ||||
|                 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() { | ||||
|         const sqlQuery = queryEl.val(); | ||||
|         const sqlQuery = codeEditor.getValue(); | ||||
|  | ||||
|         const result = await server.post("sql/execute", { | ||||
|             query: sqlQuery | ||||
| @@ -34,8 +54,8 @@ const sqlConsole = (function() { | ||||
|  | ||||
|         const rows = result.rows; | ||||
|  | ||||
|         resultHeadEl.empty(); | ||||
|         resultBodyEl.empty(); | ||||
|         $resultHead.empty(); | ||||
|         $resultBody.empty(); | ||||
|  | ||||
|         if (rows.length > 0) { | ||||
|             const result = rows[0]; | ||||
| @@ -45,7 +65,7 @@ const sqlConsole = (function() { | ||||
|                 rowEl.append($("<th>").html(key)); | ||||
|             } | ||||
|  | ||||
|             resultHeadEl.append(rowEl); | ||||
|             $resultHead.append(rowEl); | ||||
|         } | ||||
|  | ||||
|         for (const result of rows) { | ||||
| @@ -55,15 +75,15 @@ const sqlConsole = (function() { | ||||
|                 rowEl.append($("<td>").html(result[key])); | ||||
|             } | ||||
|  | ||||
|             resultBodyEl.append(rowEl); | ||||
|             $resultBody.append(rowEl); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     $(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 { | ||||
|         showDialog | ||||
|   | ||||
| @@ -126,6 +126,10 @@ $.ui.autocomplete.filter = (array, terms) => { | ||||
|  | ||||
|         if (found) { | ||||
|             results.push(item); | ||||
|  | ||||
|             if (results.length > 100) { | ||||
|                 break; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -193,3 +197,20 @@ $(document).ready(() => { | ||||
|         } | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| 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); | ||||
|     }); | ||||
| } | ||||
| @@ -116,6 +116,32 @@ const noteEditor = (function() { | ||||
|         isNewNoteCreated = true; | ||||
|     } | ||||
|  | ||||
|     function setContent(content) { | ||||
|         if (currentNote.detail.type === 'text') { | ||||
|             // temporary workaround for https://github.com/ckeditor/ckeditor5-enter/issues/49 | ||||
|             editor.setData(content ? content : "<p></p>"); | ||||
|  | ||||
|             noteDetailEl.show(); | ||||
|             noteDetailCodeEl.hide(); | ||||
|             noteDetailRenderEl.html('').hide(); | ||||
|         } | ||||
|         else if (currentNote.detail.type === 'code') { | ||||
|             noteDetailEl.hide(); | ||||
|             noteDetailCodeEl.show(); | ||||
|             noteDetailRenderEl.html('').hide(); | ||||
|  | ||||
|             // 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); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     async function loadNoteToEditor(noteId) { | ||||
|         currentNote = await loadNote(noteId); | ||||
|  | ||||
| @@ -146,30 +172,7 @@ const noteEditor = (function() { | ||||
|         noteType.setNoteType(currentNote.detail.type); | ||||
|         noteType.setNoteMime(currentNote.detail.mime); | ||||
|  | ||||
|         if (currentNote.detail.type === 'text') { | ||||
|             // temporary workaround for https://github.com/ckeditor/ckeditor5-enter/issues/49 | ||||
|             editor.setData(currentNote.detail.content ? currentNote.detail.content : "<p></p>"); | ||||
|  | ||||
|             noteDetailEl.show(); | ||||
|             noteDetailCodeEl.hide(); | ||||
|             noteDetailRenderEl.html('').hide(); | ||||
|         } | ||||
|         else if (currentNote.detail.type === 'code') { | ||||
|             noteDetailEl.hide(); | ||||
|             noteDetailCodeEl.show(); | ||||
|             noteDetailRenderEl.html('').hide(); | ||||
|  | ||||
|             // this needs to happen after the element is shown, otherwise the editor won't be refresheds | ||||
|             codeEditor.setValue(currentNote.detail.content); | ||||
|  | ||||
|             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') { | ||||
|         if (currentNote.detail.type === 'render') { | ||||
|             noteDetailEl.hide(); | ||||
|             noteDetailCodeEl.hide(); | ||||
|             noteDetailRenderEl.html('').show(); | ||||
| @@ -179,7 +182,7 @@ const noteEditor = (function() { | ||||
|             noteDetailRenderEl.html(subTree); | ||||
|         } | ||||
|         else { | ||||
|             throwError("Unrecognized type " + currentNote.detail.type); | ||||
|             setContent(currentNote.detail.content); | ||||
|         } | ||||
|  | ||||
|         noteChangeDisabled = false; | ||||
| @@ -314,6 +317,7 @@ const noteEditor = (function() { | ||||
|         getEditor, | ||||
|         focus, | ||||
|         executeCurrentNote, | ||||
|         loadAttributeList | ||||
|         loadAttributeList, | ||||
|         setContent | ||||
|     }; | ||||
| })(); | ||||
| @@ -3,7 +3,7 @@ | ||||
| const noteTree = (function() { | ||||
|     const treeEl = $("#tree"); | ||||
|     const parentListEl = $("#parent-list"); | ||||
|     const parentListListEl = $("#parent-list-list"); | ||||
|     const parentListListEl = $("#parent-list-inner"); | ||||
|  | ||||
|     let startNotePath = null; | ||||
|     let notesTreeMap = {}; | ||||
| @@ -14,6 +14,8 @@ const noteTree = (function() { | ||||
|     let parentChildToNoteTreeId = {}; | ||||
|     let noteIdToTitle = {}; | ||||
|  | ||||
|     let hiddenInAutocomplete = {}; | ||||
|  | ||||
|     function getNoteTreeId(parentNoteId, childNoteId) { | ||||
|         assertArguments(parentNoteId, childNoteId); | ||||
|  | ||||
| @@ -640,16 +642,21 @@ const noteTree = (function() { | ||||
|         return document.location.hash.substr(1); // strip initial # | ||||
|     } | ||||
|  | ||||
|     function loadTree() { | ||||
|         return server.get('tree').then(resp => { | ||||
|     async function loadTree() { | ||||
|         const resp = await server.get('tree'); | ||||
|         startNotePath = resp.start_note_path; | ||||
|  | ||||
|         if (document.location.hash) { | ||||
|             startNotePath = getNotePathFromAddress(); | ||||
|         } | ||||
|  | ||||
|         hiddenInAutocomplete = {}; | ||||
|  | ||||
|         for (const noteId of resp.hiddenInAutocomplete) { | ||||
|             hiddenInAutocomplete[noteId] = true; | ||||
|         } | ||||
|  | ||||
|         return prepareNoteTree(resp.notes); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     $(() => loadTree().then(noteTree => initFancyTree(noteTree))); | ||||
| @@ -706,6 +713,10 @@ const noteTree = (function() { | ||||
|         const autocompleteItems = []; | ||||
|  | ||||
|         for (const childNoteId of parentToChildren[parentNoteId]) { | ||||
|             if (hiddenInAutocomplete[childNoteId]) { | ||||
|                 continue; | ||||
|             } | ||||
|  | ||||
|             const childNotePath = (notePath ? (notePath + '/') : '') + childNoteId; | ||||
|             const childTitlePath = (titlePath ? (titlePath + ' / ') : '') + getNoteTitle(childNoteId, parentNoteId); | ||||
|  | ||||
| @@ -775,7 +786,7 @@ const noteTree = (function() { | ||||
|         }; | ||||
|  | ||||
|         if (target === 'after') { | ||||
|             node.appendSibling(newNode).setActive(true); | ||||
|             await node.appendSibling(newNode).setActive(true); | ||||
|         } | ||||
|         else if (target === 'into') { | ||||
|             if (!node.getChildren() && node.isFolder()) { | ||||
| @@ -785,7 +796,7 @@ const noteTree = (function() { | ||||
|                 node.addChildren(newNode); | ||||
|             } | ||||
|  | ||||
|             node.getLastChild().setActive(true); | ||||
|             await node.getLastChild().setActive(true); | ||||
|  | ||||
|             node.folder = true; | ||||
|             node.renderTitle(); | ||||
| @@ -794,6 +805,8 @@ const noteTree = (function() { | ||||
|             throwError("Unrecognized target: " + target); | ||||
|         } | ||||
|  | ||||
|         clearSelectedNodes(); // to unmark previously active node | ||||
|  | ||||
|         showMessage("Created!"); | ||||
|     } | ||||
|  | ||||
| @@ -803,6 +816,10 @@ const noteTree = (function() { | ||||
|         await reload(); | ||||
|     } | ||||
|  | ||||
|     function noteExists(noteId) { | ||||
|         return !!childToParents[noteId]; | ||||
|     } | ||||
|  | ||||
|     $(document).bind('keydown', 'ctrl+o', e => { | ||||
|         const node = getCurrentNode(); | ||||
|         const parentNoteId = node.data.parentNoteId; | ||||
| @@ -876,6 +893,7 @@ const noteTree = (function() { | ||||
|         removeParentChildRelation, | ||||
|         setParentChildRelation, | ||||
|         getSelectedNodes, | ||||
|         sortAlphabetically | ||||
|         sortAlphabetically, | ||||
|         noteExists | ||||
|     }; | ||||
| })(); | ||||
| @@ -104,6 +104,8 @@ const server = (function() { | ||||
|         post, | ||||
|         put, | ||||
|         remove, | ||||
|         exec | ||||
|         exec, | ||||
|         // don't remove, used from CKEditor image upload! | ||||
|         getHeaders | ||||
|     } | ||||
| })(); | ||||
| @@ -121,7 +121,7 @@ function executeScript(script) { | ||||
| } | ||||
|  | ||||
| function formatValueWithWhitespace(val) { | ||||
|     return /\s/.test(val) ? '"' + val + '"' : val; | ||||
|     return /[^\w_-]/.test(val) ? '"' + val + '"' : val; | ||||
| } | ||||
|  | ||||
| function formatAttribute(attr) { | ||||
|   | ||||
							
								
								
									
										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
											
										
									
								
							| @@ -5,13 +5,18 @@ | ||||
|     display: grid; | ||||
|     grid-template-areas: "header header" | ||||
|                          "tree-actions title" | ||||
|                          "search note-content" | ||||
|                          "tree note-content" | ||||
|                          "parent-list note-content" | ||||
|                          "parent-list attribute-list"; | ||||
|     grid-template-columns: 2fr 5fr; | ||||
|     grid-template-rows: auto | ||||
|                         auto | ||||
|                         1fr; | ||||
|                         auto | ||||
|                         1fr | ||||
|                         auto | ||||
|                         auto; | ||||
|  | ||||
|     justify-content: center; | ||||
|     grid-gap: 10px; | ||||
| } | ||||
| @@ -135,6 +140,7 @@ div.ui-tooltip { | ||||
|     margin-left: 20px; | ||||
|     border-top: 2px solid #eee; | ||||
|     padding-top: 10px; | ||||
|     grid-area: parent-list; | ||||
| } | ||||
|  | ||||
| #parent-list ul { | ||||
|   | ||||
| @@ -12,7 +12,7 @@ const attributes = require('../../services/attributes'); | ||||
| router.get('/notes/:noteId/attributes', auth.checkApiAuth, wrap(async (req, res, next) => { | ||||
|     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('/notes/:noteId/attributes', auth.checkApiAuth, wrap(async (req, res, next) => { | ||||
| @@ -23,10 +23,15 @@ router.put('/notes/:noteId/attributes', auth.checkApiAuth, wrap(async (req, res, | ||||
|     await sql.doInTransaction(async () => { | ||||
|         for (const attr of attributes) { | ||||
|             if (attr.attributeId) { | ||||
|                 await sql.execute("UPDATE attributes SET name = ?, value = ?, dateModified = ? WHERE attributeId = ?", | ||||
|                     [attr.name, attr.value, now, attr.attributeId]); | ||||
|                 await sql.execute("UPDATE attributes SET name = ?, value = ?, dateModified = ?, isDeleted = ?, position = ? WHERE attributeId = ?", | ||||
|                     [attr.name, attr.value, now, attr.isDeleted, attr.position, attr.attributeId]); | ||||
|             } | ||||
|             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(); | ||||
|  | ||||
|                 await sql.insert("attributes", { | ||||
| @@ -34,8 +39,10 @@ router.put('/notes/:noteId/attributes', auth.checkApiAuth, wrap(async (req, res, | ||||
|                     noteId: noteId, | ||||
|                     name: attr.name, | ||||
|                     value: attr.value, | ||||
|                     position: attr.position, | ||||
|                     dateCreated: now, | ||||
|                    dateModified: now | ||||
|                     dateModified: now, | ||||
|                     isDeleted: false | ||||
|                 }); | ||||
|             } | ||||
|  | ||||
| @@ -43,11 +50,11 @@ router.put('/notes/:noteId/attributes', auth.checkApiAuth, wrap(async (req, res, | ||||
|         } | ||||
|     }); | ||||
|  | ||||
|     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"); | ||||
|     const names = await sql.getColumn("SELECT DISTINCT name FROM attributes WHERE isDeleted = 0"); | ||||
|  | ||||
|     for (const attr of attributes.BUILTIN_ATTRIBUTES) { | ||||
|         if (!names.includes(attr)) { | ||||
| @@ -63,7 +70,7 @@ router.get('/attributes/names', auth.checkApiAuth, wrap(async (req, res, next) = | ||||
| 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 name = ? AND value != '' ORDER BY value", [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); | ||||
| })); | ||||
|   | ||||
| @@ -4,16 +4,8 @@ const express = require('express'); | ||||
| const router = express.Router(); | ||||
| const sql = require('../../services/sql'); | ||||
| const auth = require('../../services/auth'); | ||||
| const utils = require('../../services/utils'); | ||||
| const sync_table = require('../../services/sync_table'); | ||||
| const image = require('../../services/image'); | ||||
| 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 RESOURCE_DIR = require('../../services/resource_dir').RESOURCE_DIR; | ||||
| 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); | ||||
|     } | ||||
|  | ||||
|     const now = utils.nowDate(); | ||||
|  | ||||
|     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); | ||||
|     }); | ||||
|     const {fileName, imageId} = await image.saveImage(file, sourceId, noteId); | ||||
|  | ||||
|     res.send({ | ||||
|         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; | ||||
| @@ -66,7 +66,7 @@ async function importNotes(dir, parentNoteId) { | ||||
|         const noteText = fs.readFileSync(path, "utf8"); | ||||
|  | ||||
|         const noteId = utils.newNoteId(); | ||||
|         const noteTreeId = utils.newnoteRevisionId(); | ||||
|         const noteTreeId = utils.newNoteRevisionId(); | ||||
|  | ||||
|         const now = utils.nowDate(); | ||||
|  | ||||
|   | ||||
| @@ -14,7 +14,7 @@ const wrap = require('express-promise-wrap').wrap; | ||||
| router.post('/sync', wrap(async (req, res, next) => { | ||||
|     const timestampStr = req.body.timestamp; | ||||
|  | ||||
|     const timestamp = utils.parseDate(timestampStr); | ||||
|     const timestamp = utils.parseDateTime(timestampStr); | ||||
|  | ||||
|     const now = new Date(); | ||||
|  | ||||
|   | ||||
| @@ -62,6 +62,8 @@ router.get('/', auth.checkApiAuth, wrap(async (req, res, next) => { | ||||
|  | ||||
|     const {query, params} = getSearchQuery(attrFilters, searchText); | ||||
|  | ||||
|     console.log(query, params); | ||||
|  | ||||
|     const noteIds = await sql.getColumn(query, params); | ||||
|  | ||||
|     res.send(noteIds); | ||||
| @@ -88,7 +90,7 @@ function parseFilters(searchText) { | ||||
|         }); | ||||
|  | ||||
|         // remove attributes from further fulltext search | ||||
|         searchText = searchText.replace(new RegExp(match[0], 'g'), ''); | ||||
|         searchText = searchText.split(match[0]).join(''); | ||||
|  | ||||
|         match = attrRegex.exec(searchText); | ||||
|     } | ||||
| @@ -152,7 +154,7 @@ function getSearchQuery(attrFilters, searchText) { | ||||
|         searchParams.push(searchText); // two occurences in searchCondition | ||||
|     } | ||||
|  | ||||
|     const query = `SELECT notes.noteId FROM notes | ||||
|     const query = `SELECT DISTINCT notes.noteId FROM notes | ||||
|             ${joins.join('\r\n')} | ||||
|               WHERE  | ||||
|                 notes.isDeleted = 0 | ||||
|   | ||||
| @@ -45,7 +45,8 @@ async function getRecentNotes() { | ||||
|         recent_notes.isDeleted = 0 | ||||
|         AND note_tree.isDeleted = 0 | ||||
|       ORDER BY  | ||||
|         dateAccessed DESC`); | ||||
|         dateAccessed DESC | ||||
|       LIMIT 200`); | ||||
| } | ||||
|  | ||||
| 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; | ||||
| @@ -147,6 +147,12 @@ router.get('/attributes/:attributeId', auth.checkApiAuth, wrap(async (req, res, | ||||
|     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) => { | ||||
|     await syncUpdate.updateNote(req.body.entity, req.body.sourceId); | ||||
|  | ||||
| @@ -201,4 +207,10 @@ router.put('/attributes', auth.checkApiAuth, wrap(async (req, res, next) => { | ||||
|     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; | ||||
| @@ -29,8 +29,20 @@ router.get('/', auth.checkApiAuth, wrap(async (req, res, next) => { | ||||
|  | ||||
|     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({ | ||||
|         notes: notes, | ||||
|         hiddenInAutocomplete: hiddenInAutocomplete, | ||||
|         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); | ||||
|  | ||||
|         const now = utils.nowDate(); | ||||
|  | ||||
|         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); | ||||
|     }); | ||||
|   | ||||
| @@ -28,6 +28,7 @@ const cleanupRoute = require('./api/cleanup'); | ||||
| const imageRoute = require('./api/image'); | ||||
| const attributesRoute = require('./api/attributes'); | ||||
| const scriptRoute = require('./api/script'); | ||||
| const senderRoute = require('./api/sender'); | ||||
|  | ||||
| function register(app) { | ||||
|     app.use('/', indexRoute); | ||||
| @@ -59,6 +60,7 @@ function register(app) { | ||||
|     app.use('/api/cleanup', cleanupRoute); | ||||
|     app.use('/api/images', imageRoute); | ||||
|     app.use('/api/script', scriptRoute); | ||||
|     app.use('/api/sender', senderRoute); | ||||
| } | ||||
|  | ||||
| module.exports = { | ||||
|   | ||||
| @@ -9,3 +9,5 @@ window.goToday = async function() { | ||||
|  | ||||
|     api.activateNote(todayNoteId); | ||||
| }; | ||||
|  | ||||
| $(document).bind('keydown', "alt+t", window.goToday); | ||||
| @@ -40,7 +40,8 @@ | ||||
|                 await this.createNote(parentNoteId, 'data', jsonContent, { | ||||
|                     json: true, | ||||
|                     attributes: { | ||||
|                         date_data: date | ||||
|                         date_data: date, | ||||
|                         hide_in_autocomplete: null | ||||
|                     } | ||||
|                 }); | ||||
|             } | ||||
| @@ -70,7 +71,9 @@ | ||||
|             return data; | ||||
|         }); | ||||
|  | ||||
|         var config = { | ||||
|         const ctx = $("#canvas")[0].getContext("2d"); | ||||
|  | ||||
|         new Chart(ctx, { | ||||
|             type: 'line', | ||||
|             data: { | ||||
|                 labels: data.map(row => row.date), | ||||
| @@ -82,10 +85,7 @@ | ||||
|                     fill: false | ||||
|                 }] | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         var ctx = $("#canvas")[0].getContext("2d"); | ||||
|         new Chart(ctx, config); | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     $("#weight-form").submit(event => { | ||||
|   | ||||
| @@ -3,7 +3,7 @@ | ||||
| const build = require('./build'); | ||||
| const packageJson = require('../../package'); | ||||
|  | ||||
| const APP_DB_VERSION = 71; | ||||
| const APP_DB_VERSION = 76; | ||||
|  | ||||
| module.exports = { | ||||
|     app_version: packageJson.version, | ||||
|   | ||||
| @@ -5,7 +5,12 @@ const utils = require('./utils'); | ||||
| const sync_table = require('./sync_table'); | ||||
| const Repository = require('./repository'); | ||||
|  | ||||
| const BUILTIN_ATTRIBUTES = [ 'run_on_startup', 'disable_versioning' ]; | ||||
| const BUILTIN_ATTRIBUTES = [ | ||||
|     'run_on_startup', | ||||
|     'disable_versioning', | ||||
|     'calendar_root', | ||||
|     'hide_in_autocomplete' | ||||
| ]; | ||||
|  | ||||
| async function getNoteAttributeMap(noteId) { | ||||
|     return await sql.getMap(`SELECT name, value FROM attributes WHERE noteId = ?`, [noteId]); | ||||
| @@ -13,7 +18,10 @@ async function getNoteAttributeMap(noteId) { | ||||
|  | ||||
| async function getNoteIdWithAttribute(name, value) { | ||||
|     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) { | ||||
| @@ -23,11 +31,11 @@ async function getNotesWithAttribute(dataKey, name, value) { | ||||
|  | ||||
|     if (value !== undefined) { | ||||
|         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 { | ||||
|         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; | ||||
| @@ -41,7 +49,7 @@ async function getNoteWithAttribute(dataKey, name, value) { | ||||
|  | ||||
| async function getNoteIdsWithAttribute(name) { | ||||
|     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) { | ||||
| @@ -54,7 +62,8 @@ async function createAttribute(noteId, name, value = null, sourceId = null) { | ||||
|         name: name, | ||||
|         value: value, | ||||
|         dateModified: now, | ||||
|         dateCreated: now | ||||
|         dateCreated: now, | ||||
|         isDeleted: false | ||||
|     }); | ||||
|  | ||||
|     await sync_table.addAttributeSync(attributeId, sourceId); | ||||
|   | ||||
| @@ -10,7 +10,7 @@ const sync_mutex = require('./sync_mutex'); | ||||
|  | ||||
| async function regularBackup() { | ||||
|     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); | ||||
|  | ||||
|   | ||||
| @@ -223,6 +223,8 @@ async function runAllChecks() { | ||||
|     await runSyncRowChecks("recent_notes", "noteTreeId", errorList); | ||||
|     await runSyncRowChecks("images", "imageId", errorList); | ||||
|     await runSyncRowChecks("note_images", "noteImageId", errorList); | ||||
|     await runSyncRowChecks("attributes", "attributeId", errorList); | ||||
|     await runSyncRowChecks("api_tokens", "apiTokenId", errorList); | ||||
|  | ||||
|     if (errorList.length === 0) { | ||||
|         // we run this only if basic checks passed since this assumes basic data consistency | ||||
|   | ||||
| @@ -3,12 +3,16 @@ | ||||
| const sql = require('./sql'); | ||||
| const notes = require('./notes'); | ||||
| const attributes = require('./attributes'); | ||||
| const utils = require('./utils'); | ||||
|  | ||||
| const CALENDAR_ROOT_ATTRIBUTE = 'calendar_root'; | ||||
| const YEAR_ATTRIBUTE = 'year_note'; | ||||
| const MONTH_ATTRIBUTE = 'month_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) { | ||||
|     return (await notes.createNewNote(parentNoteId, { | ||||
|         title: noteTitle, | ||||
| @@ -72,7 +76,11 @@ async function getMonthNoteId(dateTimeStr, rootNoteId) { | ||||
|         monthNoteId = await getNoteStartingWith(yearNoteId, monthNumber); | ||||
|  | ||||
|         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); | ||||
| @@ -97,7 +105,11 @@ async function getDateNoteId(dateTimeStr, rootNoteId = null) { | ||||
|         dateNoteId = await getNoteStartingWith(monthNoteId, dayNumber); | ||||
|  | ||||
|         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); | ||||
|   | ||||
							
								
								
									
										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) { | ||||
|     // 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" ]; | ||||
|   | ||||
| @@ -154,10 +154,10 @@ async function saveNoteHistory(noteId, dataKey, sourceId, nowStr) { | ||||
|         note.isProtected = false; | ||||
|     } | ||||
|  | ||||
|     const newnoteRevisionId = utils.newnoteRevisionId(); | ||||
|     const newNoteRevisionId = utils.newNoteRevisionId(); | ||||
|  | ||||
|     await sql.insert('note_revisions', { | ||||
|         noteRevisionId: newnoteRevisionId, | ||||
|         noteRevisionId: newNoteRevisionId, | ||||
|         noteId: noteId, | ||||
|         // title and text should be decrypted now | ||||
|         title: oldNote.title, | ||||
| @@ -167,7 +167,7 @@ async function saveNoteHistory(noteId, dataKey, sourceId, nowStr) { | ||||
|         dateModifiedTo: nowStr | ||||
|     }); | ||||
|  | ||||
|     await sync_table.addNoteHistorySync(newnoteRevisionId, sourceId); | ||||
|     await sync_table.addNoteHistorySync(newNoteRevisionId, sourceId); | ||||
| } | ||||
|  | ||||
| async function saveNoteImages(noteId, noteText, sourceId) { | ||||
| @@ -235,7 +235,7 @@ async function updateNote(noteId, newNote, dataKey, sourceId) { | ||||
|         "SELECT noteRevisionId FROM note_revisions WHERE noteId = ? AND dateModifiedTo >= ?", [noteId, historyCutoff]); | ||||
|  | ||||
|     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' | ||||
|             && !existingnoteRevisionId | ||||
|   | ||||
| @@ -54,6 +54,8 @@ function ScriptContext(noteId, dataKey) { | ||||
|         return noteId; | ||||
|     }; | ||||
|  | ||||
|     this.createAttribute = attributes.createAttribute; | ||||
|  | ||||
|     this.updateEntity = this.repository.updateEntity; | ||||
|  | ||||
|     this.log = function(message) { | ||||
|   | ||||
| @@ -149,6 +149,9 @@ async function pullSync(syncContext) { | ||||
|         else if (sync.entityName === 'attributes') { | ||||
|             await syncUpdate.updateAttribute(resp, syncContext.sourceId); | ||||
|         } | ||||
|         else if (sync.entityName === 'api_tokens') { | ||||
|             await syncUpdate.updateApiToken(resp, syncContext.sourceId); | ||||
|         } | ||||
|         else { | ||||
|             throw new Error(`Unrecognized entity type ${sync.entityName} in sync #${sync.id}`); | ||||
|         } | ||||
| @@ -233,6 +236,9 @@ async function pushEntity(sync, syncContext) { | ||||
|     else if (sync.entityName === 'attributes') { | ||||
|         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 { | ||||
|         throw new Error(`Unrecognized entity type ${sync.entityName} in sync #${sync.id}`); | ||||
|     } | ||||
|   | ||||
| @@ -40,6 +40,10 @@ async function addAttributeSync(attributeId, sourceId) { | ||||
|     await addEntitySync("attributes", attributeId, sourceId); | ||||
| } | ||||
|  | ||||
| async function addApiTokenSync(apiTokenId, sourceId) { | ||||
|     await addEntitySync("api_tokens", apiTokenId, sourceId); | ||||
| } | ||||
|  | ||||
| async function addEntitySync(entityName, entityId, sourceId) { | ||||
|     await sql.replace("sync", { | ||||
|         entityName: entityName, | ||||
| @@ -93,6 +97,7 @@ async function fillAllSyncRows() { | ||||
|     await fillSyncRows("images", "imageId"); | ||||
|     await fillSyncRows("note_images", "noteImageId"); | ||||
|     await fillSyncRows("attributes", "attributeId"); | ||||
|     await fillSyncRows("api_tokens", "apiTokenId"); | ||||
| } | ||||
|  | ||||
| module.exports = { | ||||
| @@ -105,6 +110,7 @@ module.exports = { | ||||
|     addImageSync, | ||||
|     addNoteImageSync, | ||||
|     addAttributeSync, | ||||
|     addApiTokenSync, | ||||
|     addEntitySync, | ||||
|     cleanupSyncRowsForMissingEntities, | ||||
|     fillAllSyncRows | ||||
|   | ||||
| @@ -137,6 +137,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 = { | ||||
|     updateNote, | ||||
|     updateNoteTree, | ||||
| @@ -146,5 +160,6 @@ module.exports = { | ||||
|     updateRecentNotes, | ||||
|     updateImage, | ||||
|     updateNoteImage, | ||||
|     updateAttribute | ||||
|     updateAttribute, | ||||
|     updateApiToken | ||||
| }; | ||||
| @@ -2,6 +2,7 @@ | ||||
|  | ||||
| const sql = require('./sql'); | ||||
| const sync_table = require('./sync_table'); | ||||
| const protected_session = require('./protected_session'); | ||||
|  | ||||
| async function validateParentChild(res, parentNoteId, childNoteId, noteTreeId = null) { | ||||
|     const existing = await getExistingNoteTree(parentNoteId, childNoteId); | ||||
|   | ||||
| @@ -11,7 +11,7 @@ function newNoteTreeId() { | ||||
|     return randomString(12); | ||||
| } | ||||
|  | ||||
| function newnoteRevisionId() { | ||||
| function newNoteRevisionId() { | ||||
|     return randomString(12); | ||||
| } | ||||
|  | ||||
| @@ -27,6 +27,10 @@ function newAttributeId() { | ||||
|     return randomString(12); | ||||
| } | ||||
|  | ||||
| function newApiTokenId() { | ||||
|     return randomString(12); | ||||
| } | ||||
|  | ||||
| function randomString(length) { | ||||
|     return randtoken.generate(length); | ||||
| } | ||||
| @@ -39,6 +43,14 @@ function nowDate() { | ||||
|     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) { | ||||
|     return date.toISOString(); | ||||
| } | ||||
| @@ -47,7 +59,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(). | ||||
|  *              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 { | ||||
|         return new Date(Date.parse(str)); | ||||
|     } | ||||
| @@ -56,6 +68,12 @@ function parseDate(str) { | ||||
|     } | ||||
| } | ||||
|  | ||||
| function parseDate(str) { | ||||
|     const datePart = str.substr(0, 10); | ||||
|  | ||||
|     return parseDateTime(datePart + "T12:00:00.000Z"); | ||||
| } | ||||
|  | ||||
| function toBase64(plainText) { | ||||
|     return Buffer.from(plainText).toString('base64'); | ||||
| } | ||||
| @@ -115,14 +133,17 @@ module.exports = { | ||||
|     randomSecureToken, | ||||
|     randomString, | ||||
|     nowDate, | ||||
|     localDate, | ||||
|     dateStr, | ||||
|     parseDate, | ||||
|     parseDateTime, | ||||
|     newNoteId, | ||||
|     newNoteTreeId, | ||||
|     newnoteRevisionId, | ||||
|     newNoteRevisionId, | ||||
|     newImageId, | ||||
|     newNoteImageId, | ||||
|     newAttributeId, | ||||
|     newApiTokenId, | ||||
|     toBase64, | ||||
|     fromBase64, | ||||
|     hmac, | ||||
|   | ||||
| @@ -56,15 +56,15 @@ | ||||
|             <img src="images/icons/search.png" alt="Search in notes"/> | ||||
|           </a> | ||||
|         </div> | ||||
|       </div> | ||||
|  | ||||
|         <div id="search-box" style="display: none; padding: 10px; margin-top: 10px;"> | ||||
|       <div id="search-box" class="hide-toggle" style="grid-area: search; display: none; padding: 10px; margin-top: 10px;"> | ||||
|         <div style="display: flex; align-items: center;"> | ||||
|           <label>Search:</label> | ||||
|           <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 id="tree" class="hide-toggle" style="grid-area: tree; overflow: auto;"> | ||||
|       </div> | ||||
| @@ -72,7 +72,7 @@ | ||||
|       <div id="parent-list" class="hide-toggle"> | ||||
|         <p><strong>Note locations:</strong></p> | ||||
|  | ||||
|         <ul id="parent-list-list"></ul> | ||||
|         <ul id="parent-list-inner"></ul> | ||||
|       </div> | ||||
|  | ||||
|       <div class="hide-toggle" style="grid-area: title;"> | ||||
| @@ -151,20 +151,7 @@ | ||||
|     </div> | ||||
|  | ||||
|     <div id="recent-notes-dialog" title="Recent notes" style="display: none;"> | ||||
|       <select id="recent-notes-select-box" size="20" style="width: 100%"> | ||||
|       </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> | ||||
|       <input id="recent-notes-search-input" class="form-control"/> | ||||
|     </div> | ||||
|  | ||||
|     <div id="add-link-dialog" title="Add link" style="display: none;"> | ||||
| @@ -373,8 +360,11 @@ | ||||
|     </div> | ||||
|  | ||||
|     <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> | ||||
|  | ||||
|       <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%;"> | ||||
|         <thead></thead> | ||||
| @@ -389,31 +379,40 @@ | ||||
|     <div id="attributes-dialog" title="Note attributes" style="display: none; padding: 20px;"> | ||||
|       <form data-bind="submit: save"> | ||||
|       <div style="text-align: center"> | ||||
|         <button class="btn-primary btn-large" id="save-attributes-button" type="submit">Save</button> | ||||
|         <button class="btn btn-large" style="width: 200px;" id="save-attributes-button" type="submit">Save changes <kbd>enter</kbd></button> | ||||
|       </div> | ||||
|  | ||||
|       <div style="height: 97%; overflow: auto"> | ||||
|         <table id="attributes-table" class="table"> | ||||
|           <thead> | ||||
|             <tr> | ||||
|               <th></th> | ||||
|               <th>ID</th> | ||||
|               <th>Name</th> | ||||
|               <th>Value</th> | ||||
|               <th></th> | ||||
|             </tr> | ||||
|           </thead> | ||||
|           <tbody data-bind="foreach: attributes"> | ||||
|             <tr> | ||||
|               <td data-bind="text: attributeId"></td> | ||||
|             <tr data-bind="if: isDeleted == 0"> | ||||
|               <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> | ||||
|                 <!-- 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: red" data-bind="if: $parent.isNotUnique($index())">Attribute name must be unique per note.</div> | ||||
|                 <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> | ||||
|                 <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> | ||||
|             </tr> | ||||
|           </tbody> | ||||
|         </table> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user