mirror of
				https://github.com/zadam/trilium.git
				synced 2025-11-03 20:06:08 +01:00 
			
		
		
		
	Compare commits
	
		
			25 Commits
		
	
	
		
			v0.12.0
			...
			v0.13.0-be
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					cb69914f09 | ||
| 
						 | 
					a372cbb2df | ||
| 
						 | 
					0ce5caefe8 | ||
| 
						 | 
					94dabb81f6 | ||
| 
						 | 
					cd45bcfd03 | ||
| 
						 | 
					49a53f7a45 | ||
| 
						 | 
					9fa6c0918c | ||
| 
						 | 
					e8d089e37e | ||
| 
						 | 
					a931ce25fa | ||
| 
						 | 
					b507abb4f7 | ||
| 
						 | 
					66e7c6de62 | ||
| 
						 | 
					4ce5ea9886 | ||
| 
						 | 
					8c54b62f07 | ||
| 
						 | 
					85eb50ed0f | ||
| 
						 | 
					5ffd621e9d | ||
| 
						 | 
					df93cb09da | ||
| 
						 | 
					bbf04209f0 | ||
| 
						 | 
					834bfa39c7 | ||
| 
						 | 
					52b445f70b | ||
| 
						 | 
					7b9b4fbb0c | ||
| 
						 | 
					5af0ba1fcb | ||
| 
						 | 
					85a9748291 | ||
| 
						 | 
					b4005a7ffe | ||
| 
						 | 
					82de1c88d4 | ||
| 
						 | 
					1687ed7e0b | 
@@ -388,10 +388,14 @@ imageId</ColNames>
 | 
			
		||||
    <column id="91" parent="13" name="title">
 | 
			
		||||
      <Position>2</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
      <DefaultExpression>"unnamed"</DefaultExpression>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="92" parent="13" name="content">
 | 
			
		||||
      <Position>3</Position>
 | 
			
		||||
      <DataType>TEXT|0s</DataType>
 | 
			
		||||
      <NotNull>1</NotNull>
 | 
			
		||||
      <DefaultExpression>""</DefaultExpression>
 | 
			
		||||
    </column>
 | 
			
		||||
    <column id="93" parent="13" name="isProtected">
 | 
			
		||||
      <Position>4</Position>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,4 @@
 | 
			
		||||
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('root', 'root', 'none', 0, null, 1, 0, '2018-01-01T00:00:00.000Z');
 | 
			
		||||
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('dLgtLUFn3GoN', '1Heh2acXfPNt', 'root', 21, null, 1, 0, '2017-12-23T00:46:39.304Z');
 | 
			
		||||
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('QLfS835GSfIh', '3RkyK9LI18dO', '1Heh2acXfPNt', 1, null, 1, 0, '2017-12-23T01:20:04.181Z');
 | 
			
		||||
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, isDeleted, dateModified) VALUES ('QJAcYJ1gGUh9', 'L1Ox40M1aEyy', '3RkyK9LI18dO', 0, null, 0, 0, '2017-12-23T01:20:45.365Z');
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										5
									
								
								db/migrations/0089__add_root_branch.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								db/migrations/0089__add_root_branch.sql
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
			
		||||
INSERT INTO branches (branchId, noteId, parentNoteId, notePosition, prefix, isExpanded, dateModified)
 | 
			
		||||
    VALUES ('root', 'root', 'none', 0, null, 1, '2018-01-01T00:00:00.000Z');
 | 
			
		||||
 | 
			
		||||
INSERT INTO sync (entityName, entityId, sourceId, syncDate)
 | 
			
		||||
    VALUES ('branches' ,'root', 'SYNC_FILL', '2018-01-01T00:00:00.000Z');
 | 
			
		||||
							
								
								
									
										1
									
								
								db/migrations/0090__branch_index.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								db/migrations/0090__branch_index.sql
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
CREATE INDEX IDX_branches_parentNoteId ON branches (parentNoteId);
 | 
			
		||||
							
								
								
									
										2
									
								
								db/migrations/0091__drop_isDeleted_index.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								db/migrations/0091__drop_isDeleted_index.sql
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,2 @@
 | 
			
		||||
-- index confuses planner and is mostly useless anyway since we're mostly used in non-deleted notes (which are presumably majority)
 | 
			
		||||
DROP INDEX IDX_notes_isDeleted;
 | 
			
		||||
							
								
								
									
										2
									
								
								db/migrations/0092__add_type_index.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								db/migrations/0092__add_type_index.sql
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,2 @@
 | 
			
		||||
create index IDX_notes_type
 | 
			
		||||
  on notes (type);
 | 
			
		||||
							
								
								
									
										9
									
								
								db/migrations/0093__add_hash_field.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								db/migrations/0093__add_hash_field.sql
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
			
		||||
ALTER TABLE notes ADD hash TEXT DEFAULT "" NOT NULL;
 | 
			
		||||
ALTER TABLE branches ADD hash TEXT DEFAULT "" NOT NULL;
 | 
			
		||||
ALTER TABLE note_revisions ADD hash TEXT DEFAULT "" NOT NULL;
 | 
			
		||||
ALTER TABLE recent_notes ADD hash TEXT DEFAULT "" NOT NULL;
 | 
			
		||||
ALTER TABLE options ADD hash TEXT DEFAULT "" NOT NULL;
 | 
			
		||||
ALTER TABLE note_images ADD hash TEXT DEFAULT "" NOT NULL;
 | 
			
		||||
ALTER TABLE images ADD hash TEXT DEFAULT "" NOT NULL;
 | 
			
		||||
ALTER TABLE labels ADD hash TEXT DEFAULT "" NOT NULL;
 | 
			
		||||
ALTER TABLE api_tokens ADD hash TEXT DEFAULT "" NOT NULL;
 | 
			
		||||
							
								
								
									
										8501
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										8501
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										43
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										43
									
								
								package.json
									
									
									
									
									
								
							@@ -1,7 +1,7 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "trilium",
 | 
			
		||||
  "description": "Trilium Notes",
 | 
			
		||||
  "version": "0.12.0",
 | 
			
		||||
  "version": "0.13.0-beta",
 | 
			
		||||
  "license": "AGPL-3.0-only",
 | 
			
		||||
  "main": "electron.js",
 | 
			
		||||
  "repository": {
 | 
			
		||||
@@ -9,11 +9,11 @@
 | 
			
		||||
    "url": "https://github.com/zadam/trilium.git"
 | 
			
		||||
  },
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "start": "node ./bin/www",
 | 
			
		||||
    "start": "node ./src/www",
 | 
			
		||||
    "test-electron": "xo",
 | 
			
		||||
    "rebuild-electron": "electron-rebuild",
 | 
			
		||||
    "start-electron": "electron . --disable-gpu",
 | 
			
		||||
    "build-electron": "electron-packager . --out=dist --asar --overwrite --platform=win32,linux --arch=ia32,x64",
 | 
			
		||||
    "build-electron": "electron-packager . --out=dist --asar --overwrite --platform=win32,linux --arch=ia32,x64 --app-version=",
 | 
			
		||||
    "start-forge": "electron-forge start",
 | 
			
		||||
    "package-forge": "electron-forge package",
 | 
			
		||||
    "make-forge": "electron-forge make",
 | 
			
		||||
@@ -21,22 +21,20 @@
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "async-mutex": "^0.1.3",
 | 
			
		||||
    "axios": "^0.17.1",
 | 
			
		||||
    "body-parser": "~1.18.2",
 | 
			
		||||
    "axios": "^0.18",
 | 
			
		||||
    "body-parser": "^1.18.3",
 | 
			
		||||
    "cls-hooked": "^4.2.2",
 | 
			
		||||
    "cookie-parser": "~1.4.3",
 | 
			
		||||
    "debug": "~3.1.0",
 | 
			
		||||
    "devtron": "^1.4.0",
 | 
			
		||||
    "ejs": "~2.5.7",
 | 
			
		||||
    "electron": "^2.0.0-beta.5",
 | 
			
		||||
    "ejs": "~2.6.1",
 | 
			
		||||
    "electron-debug": "^1.5.0",
 | 
			
		||||
    "electron-dl": "^1.11.0",
 | 
			
		||||
    "electron-in-page-search": "^1.2.4",
 | 
			
		||||
    "electron-rebuild": "^1.7.3",
 | 
			
		||||
    "electron-dl": "^1.12.0",
 | 
			
		||||
    "electron-in-page-search": "^1.3.2",
 | 
			
		||||
    "express": "~4.16.3",
 | 
			
		||||
    "express-session": "^1.15.6",
 | 
			
		||||
    "fs-extra": "^4.0.3",
 | 
			
		||||
    "helmet": "^3.12.0",
 | 
			
		||||
    "fs-extra": "^6.0.1",
 | 
			
		||||
    "helmet": "^3.12.1",
 | 
			
		||||
    "html": "^1.0.0",
 | 
			
		||||
    "image-type": "^3.0.0",
 | 
			
		||||
    "imagemin": "^5.3.1",
 | 
			
		||||
@@ -45,30 +43,33 @@
 | 
			
		||||
    "imagemin-pngquant": "^5.1.0",
 | 
			
		||||
    "ini": "^1.3.5",
 | 
			
		||||
    "jimp": "^0.2.28",
 | 
			
		||||
    "moment": "^2.21.0",
 | 
			
		||||
    "moment": "^2.22.1",
 | 
			
		||||
    "multer": "^1.3.0",
 | 
			
		||||
    "open": "0.0.5",
 | 
			
		||||
    "rand-token": "^0.4.0",
 | 
			
		||||
    "request": "^2.85.0",
 | 
			
		||||
    "rcedit": "^1.1.0",
 | 
			
		||||
    "request": "^2.87.0",
 | 
			
		||||
    "request-promise": "^4.2.2",
 | 
			
		||||
    "rimraf": "^2.6.2",
 | 
			
		||||
    "sanitize-filename": "^1.6.1",
 | 
			
		||||
    "scrypt": "^6.0.3",
 | 
			
		||||
    "serve-favicon": "~2.4.5",
 | 
			
		||||
    "serve-favicon": "~2.5.0",
 | 
			
		||||
    "session-file-store": "^1.2.0",
 | 
			
		||||
    "simple-node-logger": "^0.93.37",
 | 
			
		||||
    "sqlite": "^2.9.1",
 | 
			
		||||
    "tar-stream": "^1.5.5",
 | 
			
		||||
    "sqlite": "^2.9.2",
 | 
			
		||||
    "tar-stream": "^1.6.1",
 | 
			
		||||
    "unescape": "^1.0.1",
 | 
			
		||||
    "ws": "^3.3.3"
 | 
			
		||||
    "ws": "^5.2.0"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "electron": "^2.0.1",
 | 
			
		||||
    "electron-compile": "^6.4.2",
 | 
			
		||||
    "electron-packager": "^11.1.0",
 | 
			
		||||
    "electron-prebuilt-compile": "2.0.0-beta.5",
 | 
			
		||||
    "electron-packager": "^12.1.0",
 | 
			
		||||
    "electron-prebuilt-compile": "2.0.0",
 | 
			
		||||
    "electron-rebuild": "^1.7.3",
 | 
			
		||||
    "lorem-ipsum": "^1.0.4",
 | 
			
		||||
    "tape": "^4.9.0",
 | 
			
		||||
    "xo": "^0.18.0"
 | 
			
		||||
    "xo": "^0.21.1"
 | 
			
		||||
  },
 | 
			
		||||
  "config": {
 | 
			
		||||
    "forge": {
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,7 @@ const dateUtils = require('../services/date_utils');
 | 
			
		||||
class ApiToken extends Entity {
 | 
			
		||||
    static get tableName() { return "api_tokens"; }
 | 
			
		||||
    static get primaryKeyName() { return "apiTokenId"; }
 | 
			
		||||
    static get hashedProperties() { return ["apiTokenId", "token", "dateCreated", "isDeleted"]; }
 | 
			
		||||
 | 
			
		||||
    beforeSaving() {
 | 
			
		||||
        super.beforeSaving();
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,8 @@ const sql = require('../services/sql');
 | 
			
		||||
class Branch extends Entity {
 | 
			
		||||
    static get tableName() { return "branches"; }
 | 
			
		||||
    static get primaryKeyName() { return "branchId"; }
 | 
			
		||||
    // notePosition is not part of hash because it would produce a lot of updates in case of reordering
 | 
			
		||||
    static get hashedProperties() { return ["branchId", "noteId", "parentNoteId", "dateModified", "isDeleted", "prefix"]; }
 | 
			
		||||
 | 
			
		||||
    async getNote() {
 | 
			
		||||
        return await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.noteId]);
 | 
			
		||||
 
 | 
			
		||||
@@ -14,6 +14,17 @@ class Entity {
 | 
			
		||||
        if (!this[this.constructor.primaryKeyName]) {
 | 
			
		||||
            this[this.constructor.primaryKeyName] = utils.newEntityId();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let contentToHash = "";
 | 
			
		||||
 | 
			
		||||
        for (const propertyName of this.constructor.hashedProperties) {
 | 
			
		||||
            contentToHash += "|" + this[propertyName];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // this IF is to ease the migration from before hashed options, can be later removed
 | 
			
		||||
        if (this.constructor.tableName !== 'options' || this.isSynced) {
 | 
			
		||||
            this["hash"] = utils.hash(contentToHash).substr(0, 10);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async save() {
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,7 @@ const Branch = require('../entities/branch');
 | 
			
		||||
const Label = require('../entities/label');
 | 
			
		||||
const RecentNote = require('../entities/recent_note');
 | 
			
		||||
const ApiToken = require('../entities/api_token');
 | 
			
		||||
const Option = require('../entities/option');
 | 
			
		||||
const repository = require('../services/repository');
 | 
			
		||||
 | 
			
		||||
function createEntityFromRow(row) {
 | 
			
		||||
@@ -35,6 +36,9 @@ function createEntityFromRow(row) {
 | 
			
		||||
    else if (row.noteId) {
 | 
			
		||||
        entity = new Note(row);
 | 
			
		||||
    }
 | 
			
		||||
    else if (row.name) {
 | 
			
		||||
        entity = new Option(row);
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
        throw new Error('Unknown entity type for row: ' + JSON.stringify(row));
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,7 @@ const dateUtils = require('../services/date_utils');
 | 
			
		||||
class Image extends Entity {
 | 
			
		||||
    static get tableName() { return "images"; }
 | 
			
		||||
    static get primaryKeyName() { return "imageId"; }
 | 
			
		||||
    static get hashedProperties() { return ["imageId", "format", "checksum", "name", "isDeleted", "dateModified", "dateCreated"]; }
 | 
			
		||||
 | 
			
		||||
    beforeSaving() {
 | 
			
		||||
        super.beforeSaving();
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,7 @@ const sql = require('../services/sql');
 | 
			
		||||
class Label extends Entity {
 | 
			
		||||
    static get tableName() { return "labels"; }
 | 
			
		||||
    static get primaryKeyName() { return "labelId"; }
 | 
			
		||||
    static get hashedProperties() { return ["labelId", "noteId", "name", "value", "dateModified", "dateCreated"]; }
 | 
			
		||||
 | 
			
		||||
    async getNote() {
 | 
			
		||||
        return await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.noteId]);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,20 +1,21 @@
 | 
			
		||||
"use strict";
 | 
			
		||||
 | 
			
		||||
const Entity = require('./entity');
 | 
			
		||||
const protected_session = require('../services/protected_session');
 | 
			
		||||
const protectedSessionService = require('../services/protected_session');
 | 
			
		||||
const repository = require('../services/repository');
 | 
			
		||||
const dateUtils = require('../services/date_utils');
 | 
			
		||||
 | 
			
		||||
class Note extends Entity {
 | 
			
		||||
    static get tableName() { return "notes"; }
 | 
			
		||||
    static get primaryKeyName() { return "noteId"; }
 | 
			
		||||
    static get hashedProperties() { return ["noteId", "title", "content", "type", "dateModified", "isProtected", "isDeleted"]; }
 | 
			
		||||
 | 
			
		||||
    constructor(row) {
 | 
			
		||||
        super(row);
 | 
			
		||||
 | 
			
		||||
        // check if there's noteId, otherwise this is a new entity which wasn't encrypted yet
 | 
			
		||||
        if (this.isProtected && this.noteId) {
 | 
			
		||||
            protected_session.decryptNote(this);
 | 
			
		||||
            protectedSessionService.decryptNote(this);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.setContent(this.content);
 | 
			
		||||
@@ -146,7 +147,7 @@ class Note extends Entity {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (this.isProtected) {
 | 
			
		||||
            protected_session.encryptNote(this);
 | 
			
		||||
            protectedSessionService.encryptNote(this);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!this.isDeleted) {
 | 
			
		||||
 
 | 
			
		||||
@@ -7,6 +7,7 @@ const dateUtils = require('../services/date_utils');
 | 
			
		||||
class NoteImage extends Entity {
 | 
			
		||||
    static get tableName() { return "note_images"; }
 | 
			
		||||
    static get primaryKeyName() { return "noteImageId"; }
 | 
			
		||||
    static get hashedProperties() { return ["noteImageId", "noteId", "imageId", "isDeleted", "dateModified", "dateCreated"]; }
 | 
			
		||||
 | 
			
		||||
    async getNote() {
 | 
			
		||||
        return await repository.getEntity("SELECT * FROM notes WHERE noteId = ?", [this.noteId]);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,19 +1,19 @@
 | 
			
		||||
"use strict";
 | 
			
		||||
 | 
			
		||||
const Entity = require('./entity');
 | 
			
		||||
const protected_session = require('../services/protected_session');
 | 
			
		||||
const utils = require('../services/utils');
 | 
			
		||||
const protectedSessionService = require('../services/protected_session');
 | 
			
		||||
const repository = require('../services/repository');
 | 
			
		||||
 | 
			
		||||
class NoteRevision extends Entity {
 | 
			
		||||
    static get tableName() { return "note_revisions"; }
 | 
			
		||||
    static get primaryKeyName() { return "noteRevisionId"; }
 | 
			
		||||
    static get hashedProperties() { return ["noteRevisionId", "noteId", "title", "content", "dateModifiedFrom", "dateModifiedTo"]; }
 | 
			
		||||
 | 
			
		||||
    constructor(row) {
 | 
			
		||||
        super(row);
 | 
			
		||||
 | 
			
		||||
        if (this.isProtected) {
 | 
			
		||||
            protected_session.decryptNoteRevision(this);
 | 
			
		||||
            protectedSessionService.decryptNoteRevision(this);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -25,7 +25,7 @@ class NoteRevision extends Entity {
 | 
			
		||||
        super.beforeSaving();
 | 
			
		||||
 | 
			
		||||
        if (this.isProtected) {
 | 
			
		||||
            protected_session.encryptNoteRevision(this);
 | 
			
		||||
            protectedSessionService.encryptNoteRevision(this);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										18
									
								
								src/entities/option.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								src/entities/option.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
			
		||||
"use strict";
 | 
			
		||||
 | 
			
		||||
const Entity = require('./entity');
 | 
			
		||||
const dateUtils = require('../services/date_utils');
 | 
			
		||||
 | 
			
		||||
class Option extends Entity {
 | 
			
		||||
    static get tableName() { return "options"; }
 | 
			
		||||
    static get primaryKeyName() { return "name"; }
 | 
			
		||||
    static get hashedProperties() { return ["name", "value"]; }
 | 
			
		||||
 | 
			
		||||
    beforeSaving() {
 | 
			
		||||
        super.beforeSaving();
 | 
			
		||||
 | 
			
		||||
        this.dateModified = dateUtils.nowDate();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = Option;
 | 
			
		||||
@@ -5,6 +5,7 @@ const Entity = require('./entity');
 | 
			
		||||
class RecentNote extends Entity {
 | 
			
		||||
    static get tableName() { return "recent_notes"; }
 | 
			
		||||
    static get primaryKeyName() { return "branchId"; }
 | 
			
		||||
    static get hashedProperties() { return ["branchId", "notePath", "dateAccessed", "isDeleted"]; }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = RecentNote;
 | 
			
		||||
@@ -25,7 +25,7 @@ async function showDialog() {
 | 
			
		||||
 | 
			
		||||
    $treePrefixInput.val(branch.prefix).focus();
 | 
			
		||||
 | 
			
		||||
    const noteTitle = treeUtils.getNoteTitle(currentNode.data.noteId);
 | 
			
		||||
    const noteTitle = await treeUtils.getNoteTitle(currentNode.data.noteId);
 | 
			
		||||
 | 
			
		||||
    $noteTitle.html(noteTitle);
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -22,7 +22,7 @@ async function showDialog() {
 | 
			
		||||
        const dateTime = utils.formatDateTime(utils.parseDate(event.dateAdded));
 | 
			
		||||
 | 
			
		||||
        if (event.noteId) {
 | 
			
		||||
            const noteLink = linkService.createNoteLink(event.noteId).prop('outerHTML');
 | 
			
		||||
            const noteLink = await linkService.createNoteLink(event.noteId).prop('outerHTML');
 | 
			
		||||
 | 
			
		||||
            event.comment = event.comment.replace('<note>', noteLink);
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,6 @@
 | 
			
		||||
import treeService from '../services/tree.js';
 | 
			
		||||
import linkService from '../services/link.js';
 | 
			
		||||
import utils from '../services/utils.js';
 | 
			
		||||
import autocompleteService from '../services/autocomplete.js';
 | 
			
		||||
import server from '../services/server.js';
 | 
			
		||||
 | 
			
		||||
const $dialog = $("#jump-to-note-dialog");
 | 
			
		||||
const $autoComplete = $("#jump-to-note-autocomplete");
 | 
			
		||||
@@ -18,8 +17,12 @@ async function showDialog() {
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await $autoComplete.autocomplete({
 | 
			
		||||
        source: await utils.stopWatch("building autocomplete", autocompleteService.getAutocompleteItems),
 | 
			
		||||
        minLength: 1
 | 
			
		||||
        source: async function(request, response) {
 | 
			
		||||
            const result = await server.get('autocomplete?query=' + encodeURIComponent(request.term));
 | 
			
		||||
 | 
			
		||||
            response(result);
 | 
			
		||||
        },
 | 
			
		||||
        minLength: 2
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -40,7 +40,7 @@ async function showDialog() {
 | 
			
		||||
                noteLink = change.current_title;
 | 
			
		||||
            }
 | 
			
		||||
            else {
 | 
			
		||||
                noteLink = linkService.createNoteLink(change.noteId, change.title);
 | 
			
		||||
                noteLink = await linkService.createNoteLink(change.noteId, change.title);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            changesListEl.append($('<li>')
 | 
			
		||||
 
 | 
			
		||||
@@ -14,13 +14,15 @@ class NoteShort {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async getBranches() {
 | 
			
		||||
        const branches = [];
 | 
			
		||||
        const branchIds = this.treeCache.parents[this.noteId].map(
 | 
			
		||||
            parentNoteId => this.treeCache.getBranchIdByChildParent(this.noteId, parentNoteId));
 | 
			
		||||
 | 
			
		||||
        for (const parent of this.treeCache.parents[this.noteId]) {
 | 
			
		||||
            branches.push(await this.treeCache.getBranchByChildParent(this.noteId, parent.noteId));
 | 
			
		||||
        return this.treeCache.getBranches(branchIds);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
        return branches;
 | 
			
		||||
    hasChildren() {
 | 
			
		||||
        return this.treeCache.children[this.noteId]
 | 
			
		||||
            && this.treeCache.children[this.noteId].length > 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async getChildBranches() {
 | 
			
		||||
@@ -28,23 +30,28 @@ class NoteShort {
 | 
			
		||||
            return [];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const branches = [];
 | 
			
		||||
        const branchIds = this.treeCache.children[this.noteId].map(
 | 
			
		||||
            childNoteId => this.treeCache.getBranchIdByChildParent(childNoteId, this.noteId));
 | 
			
		||||
 | 
			
		||||
        for (const child of this.treeCache.children[this.noteId]) {
 | 
			
		||||
            branches.push(await this.treeCache.getBranchByChildParent(child.noteId, this.noteId));
 | 
			
		||||
        return await this.treeCache.getBranches(branchIds);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
        return branches;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async getParentNotes() {
 | 
			
		||||
    getParentNoteIds() {
 | 
			
		||||
        return this.treeCache.parents[this.noteId] || [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async getChildNotes() {
 | 
			
		||||
    async getParentNotes() {
 | 
			
		||||
        return await this.treeCache.getNotes(this.getParentNoteIds());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getChildNoteIds() {
 | 
			
		||||
        return this.treeCache.children[this.noteId] || [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async getChildNotes() {
 | 
			
		||||
        return await this.treeCache.getNotes(this.getChildNoteIds());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    get toString() {
 | 
			
		||||
        return `Note(noteId=${this.noteId}, title=${this.title})`;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -23,11 +23,11 @@ function getNodePathFromLabel(label) {
 | 
			
		||||
    return null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function createNoteLink(notePath, noteTitle) {
 | 
			
		||||
async function createNoteLink(notePath, noteTitle) {
 | 
			
		||||
    if (!noteTitle) {
 | 
			
		||||
        const noteId = treeUtils.getNoteIdFromNotePath(notePath);
 | 
			
		||||
 | 
			
		||||
        noteTitle = treeUtils.getNoteTitle(noteId);
 | 
			
		||||
        noteTitle = await treeUtils.getNoteTitle(noteId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const noteLink = $("<a>", {
 | 
			
		||||
 
 | 
			
		||||
@@ -203,7 +203,7 @@ async function showParentList(noteId, node) {
 | 
			
		||||
                item = $("<span/>").attr("title", "Current note").append(title);
 | 
			
		||||
            }
 | 
			
		||||
            else {
 | 
			
		||||
                item = linkService.createNoteLink(notePath, title);
 | 
			
		||||
                item = await linkService.createNoteLink(notePath, title);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            $parentListList.append($("<li/>").append(item));
 | 
			
		||||
@@ -285,14 +285,14 @@ async function treeInitialized() {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function initFancyTree(branch) {
 | 
			
		||||
    utils.assertArguments(branch);
 | 
			
		||||
function initFancyTree(tree) {
 | 
			
		||||
    utils.assertArguments(tree);
 | 
			
		||||
 | 
			
		||||
    $tree.fancytree({
 | 
			
		||||
        autoScroll: true,
 | 
			
		||||
        keyboard: false, // we takover keyboard handling in the hotkeys plugin
 | 
			
		||||
        extensions: ["hotkeys", "filter", "dnd", "clones"],
 | 
			
		||||
        source: branch,
 | 
			
		||||
        source: tree,
 | 
			
		||||
        scrollParent: $tree,
 | 
			
		||||
        click: (event, data) => {
 | 
			
		||||
            const targetType = data.targetType;
 | 
			
		||||
@@ -375,7 +375,7 @@ async function loadTree() {
 | 
			
		||||
        startNotePath = getNotePathFromAddress();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return await treeBuilder.prepareTree(resp.notes, resp.branches);
 | 
			
		||||
    return await treeBuilder.prepareTree(resp.notes, resp.branches, resp.relations);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function collapseTree(node = null) {
 | 
			
		||||
 
 | 
			
		||||
@@ -5,10 +5,10 @@ import server from "./server.js";
 | 
			
		||||
import treeCache from "./tree_cache.js";
 | 
			
		||||
import messagingService from "./messaging.js";
 | 
			
		||||
 | 
			
		||||
async function prepareTree(noteRows, branchRows) {
 | 
			
		||||
    utils.assertArguments(noteRows);
 | 
			
		||||
async function prepareTree(noteRows, branchRows, relations) {
 | 
			
		||||
    utils.assertArguments(noteRows, branchRows, relations);
 | 
			
		||||
 | 
			
		||||
    treeCache.load(noteRows, branchRows);
 | 
			
		||||
    treeCache.load(noteRows, branchRows, relations);
 | 
			
		||||
 | 
			
		||||
    return await prepareRealBranch(await treeCache.getNote('root'));
 | 
			
		||||
}
 | 
			
		||||
@@ -49,9 +49,7 @@ async function prepareRealBranch(parentNote) {
 | 
			
		||||
            expanded: note.type !== 'search' && branch.isExpanded
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        const hasChildren = (await note.getChildNotes()).length > 0;
 | 
			
		||||
 | 
			
		||||
        if (hasChildren || note.type === 'search') {
 | 
			
		||||
        if (note.hasChildren() || note.type === 'search') {
 | 
			
		||||
            node.folder = true;
 | 
			
		||||
 | 
			
		||||
            if (node.expanded && note.type !== 'search') {
 | 
			
		||||
@@ -96,7 +94,7 @@ async function getExtraClasses(note) {
 | 
			
		||||
        extraClasses.push("protected");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if ((await note.getParentNotes()).length > 1) {
 | 
			
		||||
    if (note.getParentNoteIds().length > 1) {
 | 
			
		||||
        extraClasses.push("multiple-parents");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -2,45 +2,85 @@ import utils from "./utils.js";
 | 
			
		||||
import Branch from "../entities/branch.js";
 | 
			
		||||
import NoteShort from "../entities/note_short.js";
 | 
			
		||||
import infoService from "./info.js";
 | 
			
		||||
import server from "./server.js";
 | 
			
		||||
 | 
			
		||||
class TreeCache {
 | 
			
		||||
    load(noteRows, branchRows) {
 | 
			
		||||
        this.parents = [];
 | 
			
		||||
        this.children = [];
 | 
			
		||||
    load(noteRows, branchRows, relations) {
 | 
			
		||||
        this.parents = {};
 | 
			
		||||
        this.children = {};
 | 
			
		||||
        this.childParentToBranch = {};
 | 
			
		||||
 | 
			
		||||
        /** @type {Object.<string, NoteShort>} */
 | 
			
		||||
        this.notes = {};
 | 
			
		||||
 | 
			
		||||
        /** @type {Object.<string, Branch>} */
 | 
			
		||||
        this.branches = {};
 | 
			
		||||
 | 
			
		||||
        this.addResp(noteRows, branchRows, relations);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    addResp(noteRows, branchRows, relations) {
 | 
			
		||||
        for (const noteRow of noteRows) {
 | 
			
		||||
            const note = new NoteShort(this, noteRow);
 | 
			
		||||
 | 
			
		||||
            this.notes[note.noteId] = note;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /** @type {Object.<string, Branch>} */
 | 
			
		||||
        this.branches = {};
 | 
			
		||||
        for (const branchRow of branchRows) {
 | 
			
		||||
            const branch = new Branch(this, branchRow);
 | 
			
		||||
 | 
			
		||||
            this.addBranch(branch);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        for (const relation of relations) {
 | 
			
		||||
            this.addBranchRelationship(relation.branchId, relation.childNoteId, relation.parentNoteId);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async getNotes(noteIds) {
 | 
			
		||||
        const missingNoteIds = noteIds.filter(noteId => this.notes[noteId] === undefined);
 | 
			
		||||
 | 
			
		||||
        if (missingNoteIds.length > 0) {
 | 
			
		||||
            const resp = await server.post('tree/load', { noteIds: missingNoteIds });
 | 
			
		||||
 | 
			
		||||
            this.addResp(resp.notes, resp.branches, resp.relations);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return noteIds.map(noteId => {
 | 
			
		||||
            if (!this.notes[noteId]) {
 | 
			
		||||
                throw new Error(`Can't find note ${noteId}`);
 | 
			
		||||
            }
 | 
			
		||||
            else {
 | 
			
		||||
                return this.notes[noteId];
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @return NoteShort */
 | 
			
		||||
    async getNote(noteId) {
 | 
			
		||||
        return this.notes[noteId];
 | 
			
		||||
        return (await this.getNotes([noteId]))[0];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    addBranch(branch) {
 | 
			
		||||
        this.branches[branch.branchId] = branch;
 | 
			
		||||
 | 
			
		||||
        this.parents[branch.noteId] = this.parents[branch.noteId] || [];
 | 
			
		||||
        this.parents[branch.noteId].push(this.notes[branch.parentNoteId]);
 | 
			
		||||
        this.addBranchRelationship(branch.branchId, branch.noteId, branch.parentNoteId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
        this.children[branch.parentNoteId] = this.children[branch.parentNoteId] || [];
 | 
			
		||||
        this.children[branch.parentNoteId].push(this.notes[branch.noteId]);
 | 
			
		||||
    addBranchRelationship(branchId, childNoteId, parentNoteId) {
 | 
			
		||||
        this.childParentToBranch[childNoteId + '-' + parentNoteId] = branchId;
 | 
			
		||||
 | 
			
		||||
        this.childParentToBranch[branch.noteId + '-' + branch.parentNoteId] = branch;
 | 
			
		||||
        this.parents[childNoteId] = this.parents[childNoteId] || [];
 | 
			
		||||
 | 
			
		||||
        if (!this.parents[childNoteId].includes(parentNoteId)) {
 | 
			
		||||
            this.parents[childNoteId].push(parentNoteId);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.children[parentNoteId] = this.children[parentNoteId] || [];
 | 
			
		||||
 | 
			
		||||
        if (!this.children[parentNoteId].includes(childNoteId)) {
 | 
			
		||||
            this.children[parentNoteId].push(childNoteId);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    add(note, branch) {
 | 
			
		||||
@@ -49,21 +89,46 @@ class TreeCache {
 | 
			
		||||
        this.addBranch(branch);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async getBranches(branchIds) {
 | 
			
		||||
        const missingBranchIds = branchIds.filter(branchId => this.branches[branchId] === undefined);
 | 
			
		||||
 | 
			
		||||
        if (missingBranchIds.length > 0) {
 | 
			
		||||
            const resp = await server.post('tree/load', { branchIds: branchIds });
 | 
			
		||||
 | 
			
		||||
            this.addResp(resp.notes, resp.branches, resp.relations);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return branchIds.map(branchId => {
 | 
			
		||||
            if (!this.branches[branchId]) {
 | 
			
		||||
                throw new Error(`Can't find branch ${branchId}`);
 | 
			
		||||
            }
 | 
			
		||||
            else {
 | 
			
		||||
                return this.branches[branchId];
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @return Branch */
 | 
			
		||||
    async getBranch(branchId) {
 | 
			
		||||
        return this.branches[branchId];
 | 
			
		||||
        return (await this.getBranches([branchId]))[0];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /** @return Branch */
 | 
			
		||||
    async getBranchByChildParent(childNoteId, parentNoteId) {
 | 
			
		||||
        const key = (childNoteId + '-' + parentNoteId);
 | 
			
		||||
        const branch = this.childParentToBranch[key];
 | 
			
		||||
        const branchId = this.getBranchIdByChildParent(childNoteId, parentNoteId);
 | 
			
		||||
 | 
			
		||||
        if (!branch) {
 | 
			
		||||
        return await this.getBranch(branchId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getBranchIdByChildParent(childNoteId, parentNoteId) {
 | 
			
		||||
        const key = childNoteId + '-' + parentNoteId;
 | 
			
		||||
        const branchId = this.childParentToBranch[key];
 | 
			
		||||
 | 
			
		||||
        if (!branchId) {
 | 
			
		||||
            infoService.throwError("Cannot find branch for child-parent=" + key);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return branch;
 | 
			
		||||
        return branchId;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /* Move note from one parent to another. */
 | 
			
		||||
@@ -78,33 +143,14 @@ class TreeCache {
 | 
			
		||||
        delete treeCache.childParentToBranch[childNoteId + '-' + oldParentNoteId]; // this is correct because we know that oldParentId isn't same as newParentId
 | 
			
		||||
 | 
			
		||||
        // remove old associations
 | 
			
		||||
        treeCache.parents[childNoteId] = treeCache.parents[childNoteId].filter(p => p.noteId !== oldParentNoteId);
 | 
			
		||||
        treeCache.children[oldParentNoteId] = treeCache.children[oldParentNoteId].filter(ch => ch.noteId !== childNoteId);
 | 
			
		||||
        treeCache.parents[childNoteId] = treeCache.parents[childNoteId].filter(p => p !== oldParentNoteId);
 | 
			
		||||
        treeCache.children[oldParentNoteId] = treeCache.children[oldParentNoteId].filter(ch => ch !== childNoteId);
 | 
			
		||||
 | 
			
		||||
        // add new associations
 | 
			
		||||
        treeCache.parents[childNoteId].push(await treeCache.getNote(newParentNoteId));
 | 
			
		||||
        treeCache.parents[childNoteId].push(newParentNoteId);
 | 
			
		||||
 | 
			
		||||
        treeCache.children[newParentNoteId] = treeCache.children[newParentNoteId] || []; // this might be first child
 | 
			
		||||
        treeCache.children[newParentNoteId].push(await treeCache.getNote(childNoteId));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    removeParentChildRelation(parentNoteId, childNoteId) {
 | 
			
		||||
        utils.assertArguments(parentNoteId, childNoteId);
 | 
			
		||||
 | 
			
		||||
        treeCache.parents[childNoteId] = treeCache.parents[childNoteId].filter(p => p.noteId !== parentNoteId);
 | 
			
		||||
        treeCache.children[parentNoteId] = treeCache.children[parentNoteId].filter(ch => ch.noteId !== childNoteId);
 | 
			
		||||
 | 
			
		||||
        delete treeCache.childParentToBranch[childNoteId + '-' + parentNoteId];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async setParentChildRelation(branchId, parentNoteId, childNoteId) {
 | 
			
		||||
        treeCache.parents[childNoteId] = treeCache.parents[childNoteId] || [];
 | 
			
		||||
        treeCache.parents[childNoteId].push(await treeCache.getNote(parentNoteId));
 | 
			
		||||
 | 
			
		||||
        treeCache.children[parentNoteId] = treeCache.children[parentNoteId] || [];
 | 
			
		||||
        treeCache.children[parentNoteId].push(await treeCache.getNote(childNoteId));
 | 
			
		||||
 | 
			
		||||
        treeCache.childParentToBranch[childNoteId + '-' + parentNoteId] = await treeCache.getBranch(branchId);
 | 
			
		||||
        treeCache.children[newParentNoteId].push(childNoteId);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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
											
										
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@@ -6,7 +6,7 @@
 | 
			
		||||
    grid-template-areas: "header header"
 | 
			
		||||
                         "left-pane title"
 | 
			
		||||
                         "left-pane note-detail";
 | 
			
		||||
    grid-template-columns: 2fr 5fr;
 | 
			
		||||
    grid-template-columns: 29% 70%;
 | 
			
		||||
    grid-template-rows: auto
 | 
			
		||||
                        auto
 | 
			
		||||
                        1fr;
 | 
			
		||||
@@ -33,7 +33,7 @@
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#note-detail-component-wrapper {
 | 
			
		||||
    flex-grow: 1;
 | 
			
		||||
    flex-grow: 100;
 | 
			
		||||
    position: relative;
 | 
			
		||||
    overflow: auto;
 | 
			
		||||
    flex-basis: content;
 | 
			
		||||
@@ -61,8 +61,6 @@
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ul.fancytree-container {
 | 
			
		||||
    overflow: auto;
 | 
			
		||||
    position: relative;
 | 
			
		||||
    outline: none !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -161,12 +159,21 @@ div.ui-tooltip {
 | 
			
		||||
    width: auto;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#tree {
 | 
			
		||||
    overflow: auto;
 | 
			
		||||
    flex-grow: 100;
 | 
			
		||||
    flex-shrink: 100;
 | 
			
		||||
    margin-top: 10px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#parent-list {
 | 
			
		||||
    display: none;
 | 
			
		||||
    margin-left: 20px;
 | 
			
		||||
    border-top: 2px solid #eee;
 | 
			
		||||
    padding-top: 10px;
 | 
			
		||||
    grid-area: parent-list;
 | 
			
		||||
    max-height: 300px;
 | 
			
		||||
    overflow: auto;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#parent-list ul {
 | 
			
		||||
@@ -264,7 +271,6 @@ div.ui-tooltip {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.CodeMirror {
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    font-family: "Liberation Mono", "Lucida Console", monospace;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -331,3 +337,14 @@ div.ui-tooltip {
 | 
			
		||||
.child-overview a {
 | 
			
		||||
    color: #444;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#sql-console-query {
 | 
			
		||||
    height: 150px;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    border: 1px solid #ccc;
 | 
			
		||||
    margin-bottom: 10px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#sql-console-query .CodeMirror {
 | 
			
		||||
    height: 150px;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										20
									
								
								src/routes/api/autocomplete.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/routes/api/autocomplete.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
			
		||||
"use strict";
 | 
			
		||||
 | 
			
		||||
const autocompleteService = require('../../services/autocomplete');
 | 
			
		||||
 | 
			
		||||
async function getAutocomplete(req) {
 | 
			
		||||
    const query = req.query.query;
 | 
			
		||||
 | 
			
		||||
    const results = autocompleteService.getResults(query);
 | 
			
		||||
 | 
			
		||||
    return results.map(res => {
 | 
			
		||||
        return {
 | 
			
		||||
            value: res.title + ' (' + res.path + ')',
 | 
			
		||||
            title: res.title
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
    getAutocomplete
 | 
			
		||||
};
 | 
			
		||||
@@ -7,6 +7,8 @@ const sourceIdService = require('../../services/source_id');
 | 
			
		||||
const passwordEncryptionService = require('../../services/password_encryption');
 | 
			
		||||
const protectedSessionService = require('../../services/protected_session');
 | 
			
		||||
const appInfo = require('../../services/app_info');
 | 
			
		||||
const eventService = require('../../services/events');
 | 
			
		||||
const cls = require('../../services/cls');
 | 
			
		||||
 | 
			
		||||
async function loginSync(req) {
 | 
			
		||||
    const timestampStr = req.body.timestamp;
 | 
			
		||||
@@ -53,7 +55,12 @@ async function loginToProtectedSession(req) {
 | 
			
		||||
 | 
			
		||||
    const decryptedDataKey = await passwordEncryptionService.getDataKey(password);
 | 
			
		||||
 | 
			
		||||
    const protectedSessionId = protectedSessionService.setDataKey(req, decryptedDataKey);
 | 
			
		||||
    const protectedSessionId = protectedSessionService.setDataKey(decryptedDataKey);
 | 
			
		||||
 | 
			
		||||
    // this is set here so that event handlers have access to the protected session
 | 
			
		||||
    cls.namespace.set('protectedSessionId', protectedSessionId);
 | 
			
		||||
 | 
			
		||||
    eventService.emit(eventService.ENTER_PROTECTED_SESSION);
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        success: true,
 | 
			
		||||
 
 | 
			
		||||
@@ -47,7 +47,7 @@ async function sortNotes(req) {
 | 
			
		||||
 | 
			
		||||
async function protectBranch(req) {
 | 
			
		||||
    const noteId = req.params.noteId;
 | 
			
		||||
    const note = repository.getNote(noteId);
 | 
			
		||||
    const note = await repository.getNote(noteId);
 | 
			
		||||
    const protect = !!parseInt(req.params.isProtected);
 | 
			
		||||
 | 
			
		||||
    await noteService.protectNoteRecursively(note, protect);
 | 
			
		||||
 
 | 
			
		||||
@@ -4,58 +4,79 @@ const sql = require('../../services/sql');
 | 
			
		||||
const optionService = require('../../services/options');
 | 
			
		||||
const protectedSessionService = require('../../services/protected_session');
 | 
			
		||||
 | 
			
		||||
async function getTree() {
 | 
			
		||||
    const branches = await sql.getRows(`
 | 
			
		||||
      SELECT 
 | 
			
		||||
        branchId,
 | 
			
		||||
        noteId,
 | 
			
		||||
        parentNoteId,
 | 
			
		||||
        notePosition,
 | 
			
		||||
        prefix,
 | 
			
		||||
        isExpanded
 | 
			
		||||
      FROM
 | 
			
		||||
        branches 
 | 
			
		||||
      WHERE 
 | 
			
		||||
        isDeleted = 0
 | 
			
		||||
      ORDER BY 
 | 
			
		||||
        notePosition`);
 | 
			
		||||
async function getNotes(noteIds) {
 | 
			
		||||
    const questionMarks = noteIds.map(() => "?").join(",");
 | 
			
		||||
 | 
			
		||||
    const notes = [{
 | 
			
		||||
        noteId: 'root',
 | 
			
		||||
        title: 'root',
 | 
			
		||||
        isProtected: false,
 | 
			
		||||
        type: 'none',
 | 
			
		||||
        mime: 'none'
 | 
			
		||||
    }].concat(await sql.getRows(`
 | 
			
		||||
      SELECT 
 | 
			
		||||
        notes.noteId,
 | 
			
		||||
        notes.title,
 | 
			
		||||
        notes.isProtected,
 | 
			
		||||
        notes.type,
 | 
			
		||||
        notes.mime,
 | 
			
		||||
        hideInAutocomplete.labelId AS 'hideInAutocomplete'
 | 
			
		||||
      FROM
 | 
			
		||||
        notes
 | 
			
		||||
        LEFT JOIN labels AS hideInAutocomplete ON hideInAutocomplete.noteId = notes.noteId
 | 
			
		||||
                             AND hideInAutocomplete.name = 'hideInAutocomplete'
 | 
			
		||||
                             AND hideInAutocomplete.isDeleted = 0
 | 
			
		||||
      WHERE 
 | 
			
		||||
        notes.isDeleted = 0`));
 | 
			
		||||
    const notes = await sql.getRows(`
 | 
			
		||||
      SELECT noteId, title, isProtected, type, mime
 | 
			
		||||
      FROM notes WHERE isDeleted = 0 AND noteId IN (${questionMarks})`, noteIds);
 | 
			
		||||
 | 
			
		||||
    protectedSessionService.decryptNotes(notes);
 | 
			
		||||
 | 
			
		||||
    notes.forEach(note => {
 | 
			
		||||
        note.hideInAutocomplete = !!note.hideInAutocomplete;
 | 
			
		||||
        note.isProtected = !!note.isProtected;
 | 
			
		||||
    });
 | 
			
		||||
    notes.forEach(note => note.isProtected = !!note.isProtected);
 | 
			
		||||
    return notes;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function getRelations(noteIds) {
 | 
			
		||||
    const questionMarks = noteIds.map(() => "?").join(",");
 | 
			
		||||
    const doubledNoteIds = noteIds.concat(noteIds);
 | 
			
		||||
 | 
			
		||||
    return await sql.getRows(`SELECT branchId, noteId AS 'childNoteId', parentNoteId FROM branches WHERE isDeleted = 0 
 | 
			
		||||
         AND (parentNoteId IN (${questionMarks}) OR noteId IN (${questionMarks}))`, doubledNoteIds);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function getTree() {
 | 
			
		||||
    // we fetch all branches of notes, even if that particular branch isn't visible
 | 
			
		||||
    // this allows us to e.g. detect and properly display clones
 | 
			
		||||
    const branches = await sql.getRows(`
 | 
			
		||||
        WITH RECURSIVE
 | 
			
		||||
            tree(branchId, noteId, isExpanded) AS (
 | 
			
		||||
            SELECT branchId, noteId, isExpanded FROM branches WHERE branchId = 'root' 
 | 
			
		||||
            UNION ALL
 | 
			
		||||
            SELECT branches.branchId, branches.noteId, branches.isExpanded FROM branches
 | 
			
		||||
              JOIN tree ON branches.parentNoteId = tree.noteId
 | 
			
		||||
              WHERE tree.isExpanded = 1 AND branches.isDeleted = 0
 | 
			
		||||
          )
 | 
			
		||||
        SELECT branches.* FROM tree JOIN branches USING(noteId) ORDER BY branches.notePosition`);
 | 
			
		||||
 | 
			
		||||
    const noteIds = branches.map(b => b.noteId);
 | 
			
		||||
 | 
			
		||||
    const notes = await getNotes(noteIds);
 | 
			
		||||
 | 
			
		||||
    const relations = await getRelations(noteIds);
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        startNotePath: await optionService.getOption('startNotePath'),
 | 
			
		||||
        branches: branches,
 | 
			
		||||
        notes: notes
 | 
			
		||||
        branches,
 | 
			
		||||
        notes,
 | 
			
		||||
        relations
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function load(req) {
 | 
			
		||||
    let noteIds = req.body.noteIds;
 | 
			
		||||
    const branchIds = req.body.branchIds;
 | 
			
		||||
 | 
			
		||||
    if (branchIds && branchIds.length > 0) {
 | 
			
		||||
        noteIds = await sql.getColumn(`SELECT noteId FROM branches WHERE isDeleted = 0 AND branchId IN(${branchIds.map(() => "?").join(",")})`, branchIds);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const questionMarks = noteIds.map(() => "?").join(",");
 | 
			
		||||
 | 
			
		||||
    const branches = await sql.getRows(`SELECT * FROM branches WHERE isDeleted = 0 AND noteId IN (${questionMarks})`, noteIds);
 | 
			
		||||
 | 
			
		||||
    const notes = await getNotes(noteIds);
 | 
			
		||||
 | 
			
		||||
    const relations = await getRelations(noteIds);
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        branches,
 | 
			
		||||
        notes,
 | 
			
		||||
        relations
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
    getTree
 | 
			
		||||
    getTree,
 | 
			
		||||
    load
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,7 @@ const multer = require('multer')();
 | 
			
		||||
const treeApiRoute = require('./api/tree');
 | 
			
		||||
const notesApiRoute = require('./api/notes');
 | 
			
		||||
const branchesApiRoute = require('./api/branches');
 | 
			
		||||
const autocompleteApiRoute = require('./api/autocomplete');
 | 
			
		||||
const cloningApiRoute = require('./api/cloning');
 | 
			
		||||
const noteRevisionsApiRoute = require('./api/note_revisions');
 | 
			
		||||
const recentChangesApiRoute = require('./api/recent_changes');
 | 
			
		||||
@@ -99,6 +100,7 @@ function register(app) {
 | 
			
		||||
    route(GET, '/setup', [auth.checkAppNotInitialized], setupRoute.setupPage);
 | 
			
		||||
 | 
			
		||||
    apiRoute(GET, '/api/tree', treeApiRoute.getTree);
 | 
			
		||||
    apiRoute(POST, '/api/tree/load', treeApiRoute.load);
 | 
			
		||||
    apiRoute(PUT, '/api/branches/:branchId/set-prefix', branchesApiRoute.setPrefix);
 | 
			
		||||
 | 
			
		||||
    apiRoute(PUT, '/api/branches/:branchId/move-to/:parentNoteId', branchesApiRoute.moveBranchToParent);
 | 
			
		||||
@@ -107,6 +109,8 @@ function register(app) {
 | 
			
		||||
    apiRoute(PUT, '/api/branches/:branchId/expanded/:expanded', branchesApiRoute.setExpanded);
 | 
			
		||||
    apiRoute(DELETE, '/api/branches/:branchId', branchesApiRoute.deleteBranch);
 | 
			
		||||
 | 
			
		||||
    apiRoute(GET, '/api/autocomplete', autocompleteApiRoute.getAutocomplete);
 | 
			
		||||
 | 
			
		||||
    apiRoute(GET, '/api/notes/:noteId', notesApiRoute.getNote);
 | 
			
		||||
    apiRoute(PUT, '/api/notes/:noteId', notesApiRoute.updateNote);
 | 
			
		||||
    apiRoute(POST, '/api/notes/:parentNoteId/children', notesApiRoute.createNote);
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,7 @@
 | 
			
		||||
const build = require('./build');
 | 
			
		||||
const packageJson = require('../../package');
 | 
			
		||||
 | 
			
		||||
const APP_DB_VERSION = 88;
 | 
			
		||||
const APP_DB_VERSION = 93;
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
    appVersion: packageJson.version,
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										248
									
								
								src/services/autocomplete.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										248
									
								
								src/services/autocomplete.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,248 @@
 | 
			
		||||
const sql = require('./sql');
 | 
			
		||||
const sqlInit = require('./sql_init');
 | 
			
		||||
const eventService = require('./events');
 | 
			
		||||
const repository = require('./repository');
 | 
			
		||||
const protectedSessionService = require('./protected_session');
 | 
			
		||||
 | 
			
		||||
let noteTitles;
 | 
			
		||||
let protectedNoteTitles;
 | 
			
		||||
let noteIds;
 | 
			
		||||
const childToParent = {};
 | 
			
		||||
const hideInAutocomplete = {};
 | 
			
		||||
 | 
			
		||||
// key is 'childNoteId-parentNoteId' as a replacement for branchId which we don't use here
 | 
			
		||||
let prefixes = {};
 | 
			
		||||
 | 
			
		||||
async function load() {
 | 
			
		||||
    noteTitles = await sql.getMap(`SELECT noteId, LOWER(title) FROM notes WHERE isDeleted = 0 AND isProtected = 0`);
 | 
			
		||||
    noteIds = Object.keys(noteTitles);
 | 
			
		||||
 | 
			
		||||
    prefixes = await sql.getMap(`SELECT noteId || '-' || parentNoteId, LOWER(prefix) FROM branches WHERE prefix IS NOT NULL AND prefix != ''`);
 | 
			
		||||
 | 
			
		||||
    const relations = await sql.getRows(`SELECT noteId, parentNoteId FROM branches WHERE isDeleted = 0`);
 | 
			
		||||
 | 
			
		||||
    for (const rel of relations) {
 | 
			
		||||
        childToParent[rel.noteId] = childToParent[rel.noteId] || [];
 | 
			
		||||
        childToParent[rel.noteId].push(rel.parentNoteId);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const hiddenLabels = await sql.getColumn(`SELECT noteId FROM labels WHERE isDeleted = 0 AND name = 'hideInAutocomplete'`);
 | 
			
		||||
 | 
			
		||||
    for (const noteId of hiddenLabels) {
 | 
			
		||||
        hideInAutocomplete[noteId] = true;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getResults(query) {
 | 
			
		||||
    if (!noteTitles || query.length <= 2) {
 | 
			
		||||
        return [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const tokens = query.toLowerCase().split(" ");
 | 
			
		||||
    const results = [];
 | 
			
		||||
 | 
			
		||||
    let noteIds = Object.keys(noteTitles);
 | 
			
		||||
 | 
			
		||||
    if (protectedSessionService.isProtectedSessionAvailable()) {
 | 
			
		||||
        noteIds = noteIds.concat(Object.keys(protectedNoteTitles));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    for (const noteId of noteIds) {
 | 
			
		||||
        if (hideInAutocomplete[noteId]) {
 | 
			
		||||
            continue;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const parents = childToParent[noteId];
 | 
			
		||||
        if (!parents) {
 | 
			
		||||
            continue;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        for (const parentNoteId of parents) {
 | 
			
		||||
            const title = getNoteTitle(noteId, parentNoteId);
 | 
			
		||||
            const foundTokens = [];
 | 
			
		||||
 | 
			
		||||
            for (const token of tokens) {
 | 
			
		||||
                if (title.includes(token)) {
 | 
			
		||||
                    foundTokens.push(token);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (foundTokens.length > 0) {
 | 
			
		||||
                const remainingTokens = tokens.filter(token => !foundTokens.includes(token));
 | 
			
		||||
 | 
			
		||||
                search(parentNoteId, remainingTokens, [noteId], results);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    results.sort((a, b) => a.title < b.title ? -1 : 1);
 | 
			
		||||
 | 
			
		||||
    return results;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function search(noteId, tokens, path, results) {
 | 
			
		||||
    if (tokens.length === 0) {
 | 
			
		||||
        const retPath = getSomePath(noteId, path);
 | 
			
		||||
 | 
			
		||||
        if (retPath) {
 | 
			
		||||
            const noteTitle = getNoteTitleForPath(retPath);
 | 
			
		||||
 | 
			
		||||
            results.push({
 | 
			
		||||
                title: noteTitle,
 | 
			
		||||
                path: retPath.join('/')
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const parents = childToParent[noteId];
 | 
			
		||||
    if (!parents) {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    for (const parentNoteId of parents) {
 | 
			
		||||
        if (results.length >= 200) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (parentNoteId === 'root' || hideInAutocomplete[parentNoteId]) {
 | 
			
		||||
            continue;
 | 
			
		||||
        }
 | 
			
		||||
        const title = getNoteTitle(noteId, parentNoteId);
 | 
			
		||||
        const foundTokens = [];
 | 
			
		||||
 | 
			
		||||
        for (const token of tokens) {
 | 
			
		||||
            if (title.includes(token)) {
 | 
			
		||||
                foundTokens.push(token);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (foundTokens.length > 0) {
 | 
			
		||||
            const remainingTokens = tokens.filter(token => !foundTokens.includes(token));
 | 
			
		||||
 | 
			
		||||
            search(parentNoteId, remainingTokens, path.concat([noteId]), results);
 | 
			
		||||
        }
 | 
			
		||||
        else {
 | 
			
		||||
            search(parentNoteId, tokens, path.concat([noteId]), results);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getNoteTitle(noteId, parentNoteId) {
 | 
			
		||||
    const prefix = prefixes[noteId + '-' + parentNoteId];
 | 
			
		||||
 | 
			
		||||
    let title = noteTitles[noteId];
 | 
			
		||||
 | 
			
		||||
    if (!title) {
 | 
			
		||||
        if (protectedSessionService.isProtectedSessionAvailable()) {
 | 
			
		||||
            title = protectedNoteTitles[noteId];
 | 
			
		||||
        }
 | 
			
		||||
        else {
 | 
			
		||||
            title = '[protected]';
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (prefix ? (prefix + ' - ') : '') + title;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getNoteTitleForPath(path) {
 | 
			
		||||
    const titles = [];
 | 
			
		||||
 | 
			
		||||
    let parentNoteId = 'root';
 | 
			
		||||
 | 
			
		||||
    for (const noteId of path) {
 | 
			
		||||
        const title = getNoteTitle(noteId, parentNoteId);
 | 
			
		||||
 | 
			
		||||
        titles.push(title);
 | 
			
		||||
        parentNoteId = noteId;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return titles.join(' / ');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getSomePath(noteId, path) {
 | 
			
		||||
    if (noteId === 'root') {
 | 
			
		||||
        path.reverse();
 | 
			
		||||
 | 
			
		||||
        return path;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const parents = childToParent[noteId];
 | 
			
		||||
    if (!parents || parents.length === 0) {
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    for (const parentNoteId of parents) {
 | 
			
		||||
        const retPath = getSomePath(parentNoteId, path.concat([noteId]));
 | 
			
		||||
 | 
			
		||||
        if (retPath) {
 | 
			
		||||
            return retPath;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return false;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
eventService.subscribe(eventService.ENTITY_CHANGED, async ({entityName, entityId}) => {
 | 
			
		||||
    if (entityName === 'notes') {
 | 
			
		||||
        const note = await repository.getNote(entityId);
 | 
			
		||||
 | 
			
		||||
        if (note.isDeleted) {
 | 
			
		||||
            delete noteTitles[note.noteId];
 | 
			
		||||
            delete childToParent[note.noteId];
 | 
			
		||||
        }
 | 
			
		||||
        else {
 | 
			
		||||
            noteTitles[note.noteId] = note.title;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    else if (entityName === 'branches') {
 | 
			
		||||
        const branch = await repository.getBranch(entityId);
 | 
			
		||||
 | 
			
		||||
        if (childToParent[branch.noteId]) {
 | 
			
		||||
            childToParent[branch.noteId] = childToParent[branch.noteId].filter(noteId => noteId !== branch.parentNoteId)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (branch.isDeleted) {
 | 
			
		||||
            delete prefixes[branch.noteId + '-' + branch.parentNoteId];
 | 
			
		||||
        }
 | 
			
		||||
        else {
 | 
			
		||||
            if (branch.prefix) {
 | 
			
		||||
                prefixes[branch.noteId + '-' + branch.parentNoteId] = branch.prefix;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            childToParent[branch.noteId] = childToParent[branch.noteId] || [];
 | 
			
		||||
            childToParent[branch.noteId].push(branch.parentNoteId);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    else if (entityName === 'labels') {
 | 
			
		||||
        const label = await repository.getLabel(entityId);
 | 
			
		||||
 | 
			
		||||
        if (label.name === 'hideInAutocomplete') {
 | 
			
		||||
            // we're not using label object directly, since there might be other non-deleted hideInAutocomplete label
 | 
			
		||||
            const hideLabel = await repository.getEntity(`SELECT * FROM labels WHERE isDeleted = 0 
 | 
			
		||||
                                 AND name = 'hideInAutocomplete' AND noteId = ?`, [label.noteId]);
 | 
			
		||||
 | 
			
		||||
            if (hideLabel) {
 | 
			
		||||
                hideInAutocomplete[label.noteId] = true;
 | 
			
		||||
            }
 | 
			
		||||
            else {
 | 
			
		||||
                delete hideInAutocomplete[label.noteId];
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
eventService.subscribe(eventService.ENTER_PROTECTED_SESSION, async () => {
 | 
			
		||||
    protectedNoteTitles = await sql.getMap(`SELECT noteId, title FROM notes WHERE isDeleted = 0 AND isProtected = 1`);
 | 
			
		||||
 | 
			
		||||
    for (const noteId in protectedNoteTitles) {
 | 
			
		||||
        protectedNoteTitles[noteId] = protectedSessionService.decryptNoteTitle(noteId, protectedNoteTitles[noteId]);
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
sqlInit.dbReady.then(load);
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
    getResults
 | 
			
		||||
};
 | 
			
		||||
@@ -1 +1 @@
 | 
			
		||||
module.exports = { buildDate:"2018-04-14T08:28:50-04:00", buildRevision: "d57057ba28d2d93ffaeed15900116836fc791968" };
 | 
			
		||||
module.exports = { buildDate:"2018-05-22T23:51:43-04:00", buildRevision: "a372cbb2dfa918084e2d447a01fca6f076ddf486" };
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,7 @@
 | 
			
		||||
const sql = require('./sql');
 | 
			
		||||
const sqlInit = require('./sql_init');
 | 
			
		||||
const log = require('./log');
 | 
			
		||||
const utils = require('./utils');
 | 
			
		||||
const messagingService = require('./messaging');
 | 
			
		||||
const syncMutexService = require('./sync_mutex');
 | 
			
		||||
const cls = require('./cls');
 | 
			
		||||
@@ -116,7 +117,7 @@ async function runAllChecks() {
 | 
			
		||||
          WHERE 
 | 
			
		||||
            notes.isDeleted = 1 
 | 
			
		||||
            AND branches.isDeleted = 0`,
 | 
			
		||||
        "Note tree is not deleted even though main note is deleted for following branch IDs", errorList);
 | 
			
		||||
        "Branch is not deleted even though main note is deleted for following branch IDs", errorList);
 | 
			
		||||
 | 
			
		||||
    await runCheck(`
 | 
			
		||||
          SELECT 
 | 
			
		||||
@@ -125,12 +126,12 @@ async function runAllChecks() {
 | 
			
		||||
            branches AS child
 | 
			
		||||
          WHERE 
 | 
			
		||||
            child.isDeleted = 0
 | 
			
		||||
            AND child.parentNoteId != 'root'
 | 
			
		||||
            AND child.parentNoteId != 'none'
 | 
			
		||||
            AND (SELECT COUNT(*) FROM branches AS parent WHERE parent.noteId = child.parentNoteId 
 | 
			
		||||
                                                                 AND parent.isDeleted = 0) = 0`,
 | 
			
		||||
        "All parent branches are deleted but child note tree is not for these child note tree IDs", errorList);
 | 
			
		||||
        "All parent branches are deleted but child branch is not for these child branch IDs", errorList);
 | 
			
		||||
 | 
			
		||||
    // we do extra JOIN to eliminate orphan notes without note tree (which are reported separately)
 | 
			
		||||
    // we do extra JOIN to eliminate orphan notes without branches (which are reported separately)
 | 
			
		||||
    await runCheck(`
 | 
			
		||||
          SELECT
 | 
			
		||||
            DISTINCT noteId
 | 
			
		||||
@@ -150,7 +151,7 @@ async function runAllChecks() {
 | 
			
		||||
            LEFT JOIN branches AS parent ON parent.noteId = child.parentNoteId 
 | 
			
		||||
          WHERE 
 | 
			
		||||
            parent.noteId IS NULL 
 | 
			
		||||
            AND child.parentNoteId != 'root'`,
 | 
			
		||||
            AND child.parentNoteId != 'none'`,
 | 
			
		||||
        "Not existing parent in the following parent > child relations", errorList);
 | 
			
		||||
 | 
			
		||||
    await runCheck(`
 | 
			
		||||
 
 | 
			
		||||
@@ -5,117 +5,40 @@ const utils = require('./utils');
 | 
			
		||||
const log = require('./log');
 | 
			
		||||
const eventLogService = require('./event_log');
 | 
			
		||||
const messagingService = require('./messaging');
 | 
			
		||||
const ApiToken = require('../entities/api_token');
 | 
			
		||||
const Branch = require('../entities/branch');
 | 
			
		||||
const Image = require('../entities/image');
 | 
			
		||||
const Note = require('../entities/note');
 | 
			
		||||
const NoteImage = require('../entities/note_image');
 | 
			
		||||
const Label = require('../entities/label');
 | 
			
		||||
const NoteRevision = require('../entities/note_revision');
 | 
			
		||||
const RecentNote = require('../entities/recent_note');
 | 
			
		||||
const Option = require('../entities/option');
 | 
			
		||||
 | 
			
		||||
function getHash(rows) {
 | 
			
		||||
    let hash = '';
 | 
			
		||||
async function getHash(entityConstructor, whereBranch) {
 | 
			
		||||
    let contentToHash = await sql.getValue(`SELECT GROUP_CONCAT(hash) FROM ${entityConstructor.tableName} `
 | 
			
		||||
                + (whereBranch ? `WHERE ${whereBranch} ` : '') + `ORDER BY ${entityConstructor.primaryKeyName}`);
 | 
			
		||||
 | 
			
		||||
    for (const row of rows) {
 | 
			
		||||
        hash = utils.hash(hash + JSON.stringify(row));
 | 
			
		||||
    if (!contentToHash) { // might be null in case of no rows
 | 
			
		||||
        contentToHash = "";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return hash;
 | 
			
		||||
    return utils.hash(contentToHash);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function getHashes() {
 | 
			
		||||
    const startTime = new Date();
 | 
			
		||||
 | 
			
		||||
    const hashes = {
 | 
			
		||||
        notes: getHash(await sql.getRows(`
 | 
			
		||||
            SELECT
 | 
			
		||||
              noteId,
 | 
			
		||||
              title,
 | 
			
		||||
              content,
 | 
			
		||||
              type,
 | 
			
		||||
              dateModified,
 | 
			
		||||
              isProtected,
 | 
			
		||||
              isDeleted
 | 
			
		||||
            FROM notes
 | 
			
		||||
            ORDER BY noteId`)),
 | 
			
		||||
 | 
			
		||||
        branches: getHash(await sql.getRows(`
 | 
			
		||||
            SELECT
 | 
			
		||||
               branchId,
 | 
			
		||||
               noteId,
 | 
			
		||||
               parentNoteId,
 | 
			
		||||
               notePosition,
 | 
			
		||||
               dateModified,
 | 
			
		||||
               isDeleted,
 | 
			
		||||
               prefix
 | 
			
		||||
             FROM branches
 | 
			
		||||
             ORDER BY branchId`)),
 | 
			
		||||
 | 
			
		||||
        note_revisions: getHash(await sql.getRows(`
 | 
			
		||||
            SELECT
 | 
			
		||||
              noteRevisionId,
 | 
			
		||||
              noteId,
 | 
			
		||||
              title,
 | 
			
		||||
              content,
 | 
			
		||||
              dateModifiedFrom,
 | 
			
		||||
              dateModifiedTo
 | 
			
		||||
            FROM note_revisions
 | 
			
		||||
            ORDER BY noteRevisionId`)),
 | 
			
		||||
 | 
			
		||||
        recent_notes: getHash(await sql.getRows(`
 | 
			
		||||
           SELECT
 | 
			
		||||
             branchId,
 | 
			
		||||
             notePath,
 | 
			
		||||
             dateAccessed,
 | 
			
		||||
             isDeleted
 | 
			
		||||
           FROM recent_notes
 | 
			
		||||
           ORDER BY notePath`)),
 | 
			
		||||
 | 
			
		||||
        options: getHash(await sql.getRows(`
 | 
			
		||||
           SELECT 
 | 
			
		||||
             name,
 | 
			
		||||
             value 
 | 
			
		||||
           FROM options 
 | 
			
		||||
           WHERE isSynced = 1
 | 
			
		||||
           ORDER BY name`)),
 | 
			
		||||
 | 
			
		||||
        // we don't include image data on purpose because they are quite large, checksum is good enough
 | 
			
		||||
        // to represent the data anyway
 | 
			
		||||
        images: getHash(await sql.getRows(`
 | 
			
		||||
          SELECT 
 | 
			
		||||
            imageId,
 | 
			
		||||
            format,
 | 
			
		||||
            checksum,
 | 
			
		||||
            name,
 | 
			
		||||
            isDeleted,
 | 
			
		||||
            dateModified,
 | 
			
		||||
            dateCreated
 | 
			
		||||
          FROM images  
 | 
			
		||||
          ORDER BY imageId`)),
 | 
			
		||||
 | 
			
		||||
        note_images: getHash(await sql.getRows(`
 | 
			
		||||
          SELECT
 | 
			
		||||
            noteImageId,
 | 
			
		||||
            noteId,
 | 
			
		||||
            imageId,
 | 
			
		||||
            isDeleted,
 | 
			
		||||
            dateModified,
 | 
			
		||||
            dateCreated
 | 
			
		||||
          FROM note_images
 | 
			
		||||
          ORDER BY noteImageId`)),
 | 
			
		||||
 | 
			
		||||
        labels: getHash(await sql.getRows(`
 | 
			
		||||
          SELECT 
 | 
			
		||||
            labelId,
 | 
			
		||||
            noteId,
 | 
			
		||||
            name,
 | 
			
		||||
            value,
 | 
			
		||||
            dateModified,
 | 
			
		||||
            dateCreated
 | 
			
		||||
          FROM labels  
 | 
			
		||||
          ORDER BY labelId`)),
 | 
			
		||||
 | 
			
		||||
        api_tokens: getHash(await sql.getRows(`
 | 
			
		||||
          SELECT 
 | 
			
		||||
            apiTokenId,
 | 
			
		||||
            token,
 | 
			
		||||
            dateCreated,
 | 
			
		||||
            isDeleted
 | 
			
		||||
          FROM api_tokens  
 | 
			
		||||
          ORDER BY apiTokenId`))
 | 
			
		||||
        notes: await getHash(Note),
 | 
			
		||||
        branches: await getHash(Branch),
 | 
			
		||||
        note_revisions: await getHash(NoteRevision),
 | 
			
		||||
        recent_notes: await getHash(RecentNote),
 | 
			
		||||
        options: await getHash(Option, "isSynced = 1"),
 | 
			
		||||
        images: await getHash(Image),
 | 
			
		||||
        note_images: await getHash(NoteImage),
 | 
			
		||||
        labels: await getHash(Label),
 | 
			
		||||
        api_tokens: await getHash(ApiToken)
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const elapseTimeMs = new Date().getTime() - startTime.getTime();
 | 
			
		||||
 
 | 
			
		||||
@@ -31,6 +31,7 @@ async function getNoteStartingWith(parentNoteId, startsWith) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function getRootCalendarNote() {
 | 
			
		||||
    // some caching here could be useful (e.g. in CLS)
 | 
			
		||||
    let rootNote = await labelService.getNoteWithLabel(CALENDAR_ROOT_LABEL);
 | 
			
		||||
 | 
			
		||||
    if (!rootNote) {
 | 
			
		||||
@@ -89,10 +90,8 @@ async function getMonthNote(dateTimeStr, rootNote) {
 | 
			
		||||
    return monthNote;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function getDateNote(dateTimeStr, rootNote = null) {
 | 
			
		||||
    if (!rootNote) {
 | 
			
		||||
        rootNote = await getRootCalendarNote();
 | 
			
		||||
    }
 | 
			
		||||
async function getDateNote(dateTimeStr) {
 | 
			
		||||
    const rootNote = await getRootCalendarNote();
 | 
			
		||||
 | 
			
		||||
    const dateStr = dateTimeStr.substr(0, 10);
 | 
			
		||||
    const dayNumber = dateTimeStr.substr(8, 2);
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										28
									
								
								src/services/events.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								src/services/events.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,28 @@
 | 
			
		||||
const ENTER_PROTECTED_SESSION = "ENTER_PROTECTED_SESSION";
 | 
			
		||||
const ENTITY_CHANGED = "ENTITY_CHANGED";
 | 
			
		||||
 | 
			
		||||
const eventListeners = {};
 | 
			
		||||
 | 
			
		||||
function subscribe(eventType, listener) {
 | 
			
		||||
    eventListeners[eventType] = eventListeners[eventType] || [];
 | 
			
		||||
    eventListeners[eventType].push(listener);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function emit(eventType, data) {
 | 
			
		||||
    const listeners = eventListeners[eventType];
 | 
			
		||||
 | 
			
		||||
    if (listeners) {
 | 
			
		||||
        for (const listener of listeners) {
 | 
			
		||||
            // not awaiting for async processing
 | 
			
		||||
            listener(data);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = {
 | 
			
		||||
    subscribe,
 | 
			
		||||
    emit,
 | 
			
		||||
    // event types:
 | 
			
		||||
    ENTER_PROTECTED_SESSION,
 | 
			
		||||
    ENTITY_CHANGED
 | 
			
		||||
};
 | 
			
		||||
@@ -1,45 +1,37 @@
 | 
			
		||||
const sql = require('./sql');
 | 
			
		||||
const repository = require('./repository');
 | 
			
		||||
const utils = require('./utils');
 | 
			
		||||
const dateUtils = require('./date_utils');
 | 
			
		||||
const syncTableService = require('./sync_table');
 | 
			
		||||
const appInfo = require('./app_info');
 | 
			
		||||
const Option = require('../entities/option');
 | 
			
		||||
 | 
			
		||||
async function getOption(name) {
 | 
			
		||||
    const row = await await sql.getRowOrNull("SELECT value FROM options WHERE name = ?", [name]);
 | 
			
		||||
    const option = await repository.getOption(name);
 | 
			
		||||
 | 
			
		||||
    if (!row) {
 | 
			
		||||
    if (!option) {
 | 
			
		||||
        throw new Error("Option " + name + " doesn't exist");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return row.value;
 | 
			
		||||
    return option.value;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function setOption(name, value) {
 | 
			
		||||
    const opt = await sql.getRow("SELECT * FROM options WHERE name = ?", [name]);
 | 
			
		||||
    const option = await repository.getOption(name);
 | 
			
		||||
 | 
			
		||||
    if (!opt) {
 | 
			
		||||
    if (!option) {
 | 
			
		||||
        throw new Error(`Option ${name} doesn't exist`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (opt.isSynced) {
 | 
			
		||||
        await syncTableService.addOptionsSync(name);
 | 
			
		||||
    }
 | 
			
		||||
    option.value = value;
 | 
			
		||||
 | 
			
		||||
    await sql.execute("UPDATE options SET value = ?, dateModified = ? WHERE name = ?",
 | 
			
		||||
        [value, dateUtils.nowDate(), name]);
 | 
			
		||||
    await option.save();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function createOption(name, value, isSynced) {
 | 
			
		||||
    await sql.insert("options", {
 | 
			
		||||
    await new Option({
 | 
			
		||||
        name: name,
 | 
			
		||||
        value: value,
 | 
			
		||||
        isSynced: isSynced,
 | 
			
		||||
        dateModified: dateUtils.nowDate()
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    if (isSynced) {
 | 
			
		||||
        await syncTableService.addOptionsSync(name);
 | 
			
		||||
    }
 | 
			
		||||
        isSynced: isSynced
 | 
			
		||||
    }).save();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function initOptions(startNotePath) {
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@ const cls = require('./cls');
 | 
			
		||||
 | 
			
		||||
const dataKeyMap = {};
 | 
			
		||||
 | 
			
		||||
function setDataKey(req, decryptedDataKey) {
 | 
			
		||||
function setDataKey(decryptedDataKey) {
 | 
			
		||||
    const protectedSessionId = utils.randomSecureToken(32);
 | 
			
		||||
 | 
			
		||||
    dataKeyMap[protectedSessionId] = Array.from(decryptedDataKey); // can't store buffer in session
 | 
			
		||||
@@ -28,12 +28,20 @@ function getDataKey() {
 | 
			
		||||
    return dataKeyMap[protectedSessionId];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function isProtectedSessionAvailable(req) {
 | 
			
		||||
    const protectedSessionId = getProtectedSessionId(req);
 | 
			
		||||
function isProtectedSessionAvailable() {
 | 
			
		||||
    const protectedSessionId = getProtectedSessionId();
 | 
			
		||||
 | 
			
		||||
    return !!dataKeyMap[protectedSessionId];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function decryptNoteTitle(noteId, encryptedTitle) {
 | 
			
		||||
    const dataKey = getDataKey();
 | 
			
		||||
 | 
			
		||||
    const iv = dataEncryptionService.noteTitleIv(noteId);
 | 
			
		||||
 | 
			
		||||
    return dataEncryptionService.decryptString(dataKey, iv, encryptedTitle);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function decryptNote(note) {
 | 
			
		||||
    const dataKey = getDataKey();
 | 
			
		||||
 | 
			
		||||
@@ -99,6 +107,7 @@ module.exports = {
 | 
			
		||||
    setDataKey,
 | 
			
		||||
    getDataKey,
 | 
			
		||||
    isProtectedSessionAvailable,
 | 
			
		||||
    decryptNoteTitle,
 | 
			
		||||
    decryptNote,
 | 
			
		||||
    decryptNotes,
 | 
			
		||||
    decryptNoteRevision,
 | 
			
		||||
 
 | 
			
		||||
@@ -41,6 +41,10 @@ async function getLabel(labelId) {
 | 
			
		||||
    return await getEntity("SELECT * FROM labels WHERE labelId = ?", [labelId]);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function getOption(name) {
 | 
			
		||||
    return await getEntity("SELECT * FROM options WHERE name = ?", [name]);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function updateEntity(entity) {
 | 
			
		||||
    if (entity.beforeSaving) {
 | 
			
		||||
        await entity.beforeSaving();
 | 
			
		||||
@@ -55,7 +59,9 @@ async function updateEntity(entity) {
 | 
			
		||||
 | 
			
		||||
        const primaryKey = entity[entity.constructor.primaryKeyName];
 | 
			
		||||
 | 
			
		||||
        if (entity.constructor.tableName !== 'options' || entity.isSynced) {
 | 
			
		||||
            await syncTableService.addEntitySync(entity.constructor.tableName, primaryKey);
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -66,6 +72,7 @@ module.exports = {
 | 
			
		||||
    getBranch,
 | 
			
		||||
    getImage,
 | 
			
		||||
    getLabel,
 | 
			
		||||
    getOption,
 | 
			
		||||
    updateEntity,
 | 
			
		||||
    setEntityConstructor
 | 
			
		||||
};
 | 
			
		||||
@@ -1,6 +1,8 @@
 | 
			
		||||
const sql = require('./sql');
 | 
			
		||||
const ScriptContext = require('./script_context');
 | 
			
		||||
const repository = require('./repository');
 | 
			
		||||
const cls = require('./cls');
 | 
			
		||||
const sourceIdService = require('./source_id');
 | 
			
		||||
 | 
			
		||||
async function executeNote(note) {
 | 
			
		||||
    if (!note.isJavaScript()) {
 | 
			
		||||
@@ -49,6 +51,9 @@ async function executeScript(script, params, startNoteId, currentNoteId) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function execute(ctx, script, paramsStr) {
 | 
			
		||||
    // scripts run as "server" sourceId so clients recognize the changes as "foreign" and update themselves
 | 
			
		||||
    cls.namespace.set('sourceId', sourceIdService.getCurrentSourceId());
 | 
			
		||||
 | 
			
		||||
    return await (function() { return eval(`const apiContext = this;\r\n(${script}\r\n)(${paramsStr})`); }.call(ctx));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -212,7 +212,8 @@ const primaryKeys = {
 | 
			
		||||
    "images": "imageId",
 | 
			
		||||
    "note_images": "noteImageId",
 | 
			
		||||
    "labels": "labelId",
 | 
			
		||||
    "api_tokens": "apiTokenId"
 | 
			
		||||
    "api_tokens": "apiTokenId",
 | 
			
		||||
    "options": "name"
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
async function getEntityRow(entityName, entityId) {
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,7 @@ const dateUtils = require('./date_utils');
 | 
			
		||||
const syncSetup = require('./sync_setup');
 | 
			
		||||
const log = require('./log');
 | 
			
		||||
const cls = require('./cls');
 | 
			
		||||
const eventService = require('./events');
 | 
			
		||||
 | 
			
		||||
async function addNoteSync(noteId, sourceId) {
 | 
			
		||||
    await addEntitySync("notes", noteId, sourceId)
 | 
			
		||||
@@ -58,6 +59,11 @@ async function addEntitySync(entityName, entityId, sourceId) {
 | 
			
		||||
        // useful when you fork the DB for new "client" instance, it won't try to sync the whole DB
 | 
			
		||||
        await sql.execute("UPDATE options SET value = (SELECT MAX(id) FROM sync) WHERE name IN('lastSyncedPush', 'lastSyncedPull')");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    eventService.emit(eventService.ENTITY_CHANGED, {
 | 
			
		||||
        entityName,
 | 
			
		||||
        entityId
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function cleanupSyncRowsForMissingEntities(entityName, entityKey) {
 | 
			
		||||
 
 | 
			
		||||
@@ -58,7 +58,7 @@ async function start() {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // we'll create clones for 20% of notes
 | 
			
		||||
    for (let i = 0; i < (noteCount / 5); i++) {
 | 
			
		||||
    for (let i = 0; i < (noteCount / 50); i++) {
 | 
			
		||||
        const noteIdToClone = getRandomParentNoteId();
 | 
			
		||||
        const parentNoteId = getRandomParentNoteId();
 | 
			
		||||
        const prefix = Math.random() > 0.8 ? "prefix" : null;
 | 
			
		||||
 
 | 
			
		||||
@@ -68,8 +68,7 @@
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div id="tree" class="hide-toggle" style="overflow: auto; flex-grow: 100; flex-shrink: 100; margin-top: 10px;">
 | 
			
		||||
        </div>
 | 
			
		||||
        <div id="tree"></div>
 | 
			
		||||
 | 
			
		||||
        <div id="parent-list">
 | 
			
		||||
          <p><strong>Note locations:</strong></p>
 | 
			
		||||
@@ -78,8 +77,8 @@
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div class="hide-toggle" style="grid-area: title;">
 | 
			
		||||
        <div style="display: flex; align-items: center;">
 | 
			
		||||
      <div style="grid-area: title;">
 | 
			
		||||
        <div class="hide-toggle" style="display: flex; align-items: center;">
 | 
			
		||||
          <a title="Protect the note so that password will be required to view the note"
 | 
			
		||||
             class="icon-action"
 | 
			
		||||
             id="protect-button"
 | 
			
		||||
@@ -425,7 +424,7 @@
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div id="sql-console-dialog" title="SQL console" style="display: none; padding: 20px;">
 | 
			
		||||
      <div style="height: 150px; width: 100%; border: 1px solid #ccc; margin-bottom: 10px;" id="sql-console-query"></div>
 | 
			
		||||
      <div 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>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user