mirror of
				https://github.com/zadam/trilium.git
				synced 2025-11-03 20:06:08 +01:00 
			
		
		
		
	Compare commits
	
		
			52 Commits
		
	
	
		
			v0.5.1-bet
			...
			v0.6.1
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					8028b09351 | ||
| 
						 | 
					ebe66eaed9 | ||
| 
						 | 
					5bce9a5f94 | ||
| 
						 | 
					dfd9927310 | ||
| 
						 | 
					9bf1735bde | ||
| 
						 | 
					2e8eeda5ab | ||
| 
						 | 
					1cef0ce5f9 | ||
| 
						 | 
					1efac99828 | ||
| 
						 | 
					0e9473119e | ||
| 
						 | 
					7bbfef7af3 | ||
| 
						 | 
					5cb93509c1 | ||
| 
						 | 
					89e89e04d8 | ||
| 
						 | 
					72df0d8861 | ||
| 
						 | 
					9910aebf45 | ||
| 
						 | 
					f9f8ecb2b1 | ||
| 
						 | 
					438f7c5b0b | ||
| 
						 | 
					4b1d1aba74 | ||
| 
						 | 
					6dea73cfe2 | ||
| 
						 | 
					58f5d0cf6e | ||
| 
						 | 
					7b77e40514 | ||
| 
						 | 
					660908c54b | ||
| 
						 | 
					e970564036 | ||
| 
						 | 
					b3038487f8 | ||
| 
						 | 
					cac98392a6 | ||
| 
						 | 
					dbd28377e3 | ||
| 
						 | 
					c76e4faf5d | ||
| 
						 | 
					e011b9ae63 | ||
| 
						 | 
					7c74c77a2c | ||
| 
						 | 
					c2a2f195aa | ||
| 
						 | 
					85d32c66f2 | ||
| 
						 | 
					4e70cebf70 | ||
| 
						 | 
					214d2e7659 | ||
| 
						 | 
					f380bb7f65 | ||
| 
						 | 
					0a9a032daa | ||
| 
						 | 
					23a2b58b24 | ||
| 
						 | 
					aee64b2522 | ||
| 
						 | 
					02e07ec03a | ||
| 
						 | 
					3d2dc8e699 | ||
| 
						 | 
					c84e15c9be | ||
| 
						 | 
					e18d0b9fd4 | ||
| 
						 | 
					52817504d1 | ||
| 
						 | 
					a3b31fab54 | ||
| 
						 | 
					bc4aa3e40a | ||
| 
						 | 
					873ea67e9c | ||
| 
						 | 
					2c5115003b | ||
| 
						 | 
					e8ed913374 | ||
| 
						 | 
					5bffba4e2f | ||
| 
						 | 
					05575913db | ||
| 
						 | 
					31c32ff42c | ||
| 
						 | 
					6a671a5c02 | ||
| 
						 | 
					e174aec299 | ||
| 
						 | 
					d1329f60c3 | 
@@ -10,6 +10,7 @@ Trilium Notes is a hierarchical note taking application. Picture tells a thousan
 | 
				
			|||||||
* WYSIWYG (What You See Is What You Get) editing
 | 
					* WYSIWYG (What You See Is What You Get) editing
 | 
				
			||||||
* Fast and easy [navigation between notes](https://github.com/zadam/trilium/wiki/Note-navigation)
 | 
					* Fast and easy [navigation between notes](https://github.com/zadam/trilium/wiki/Note-navigation)
 | 
				
			||||||
* Seamless note versioning
 | 
					* Seamless note versioning
 | 
				
			||||||
 | 
					* Note attributes can be used to tag/label notes as an alternative note organization and querying
 | 
				
			||||||
* Can be deployed as web application and / or desktop application with offline access (electron based)
 | 
					* Can be deployed as web application and / or desktop application with offline access (electron based)
 | 
				
			||||||
* [Synchronization with](https://github.com/zadam/trilium/wiki/Synchronization) self-hosted sync server
 | 
					* [Synchronization with](https://github.com/zadam/trilium/wiki/Synchronization) self-hosted sync server
 | 
				
			||||||
* Strong [note encryption](https://github.com/zadam/trilium/wiki/Protected-notes)
 | 
					* Strong [note encryption](https://github.com/zadam/trilium/wiki/Protected-notes)
 | 
				
			||||||
@@ -34,6 +35,7 @@ List of documentation pages:
 | 
				
			|||||||
* [Installation as webapp](https://github.com/zadam/trilium/wiki/Installation-as-webapp)
 | 
					* [Installation as webapp](https://github.com/zadam/trilium/wiki/Installation-as-webapp)
 | 
				
			||||||
* [Note navigation](https://github.com/zadam/trilium/wiki/Note-navigation)
 | 
					* [Note navigation](https://github.com/zadam/trilium/wiki/Note-navigation)
 | 
				
			||||||
* [Tree manipulation](https://github.com/zadam/trilium/wiki/Tree-manipulation)
 | 
					* [Tree manipulation](https://github.com/zadam/trilium/wiki/Tree-manipulation)
 | 
				
			||||||
 | 
					* [Attributes](https://github.com/zadam/trilium/wiki/Attributes)
 | 
				
			||||||
* [Links](https://github.com/zadam/trilium/wiki/Links)
 | 
					* [Links](https://github.com/zadam/trilium/wiki/Links)
 | 
				
			||||||
* [Cloning notes](https://github.com/zadam/trilium/wiki/Cloning-notes)
 | 
					* [Cloning notes](https://github.com/zadam/trilium/wiki/Cloning-notes)
 | 
				
			||||||
* [Protected notes](https://github.com/zadam/trilium/wiki/Protected-notes)
 | 
					* [Protected notes](https://github.com/zadam/trilium/wiki/Protected-notes)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,15 +7,15 @@ rm -r dist/*
 | 
				
			|||||||
echo "Rebuilding binaries for linux-ia32"
 | 
					echo "Rebuilding binaries for linux-ia32"
 | 
				
			||||||
./node_modules/.bin/electron-rebuild --arch=ia32
 | 
					./node_modules/.bin/electron-rebuild --arch=ia32
 | 
				
			||||||
 | 
					
 | 
				
			||||||
./node_modules/.bin/electron-packager src/electron --out=dist --platform=linux --arch=ia32 --overwrite
 | 
					./node_modules/.bin/electron-packager . --out=dist --platform=linux --arch=ia32 --overwrite
 | 
				
			||||||
 | 
					
 | 
				
			||||||
./node_modules/.bin/electron-packager src/electron --out=dist --platform=win32 --arch=x64 --overwrite
 | 
					./node_modules/.bin/electron-packager . --out=dist --platform=win32 --arch=x64 --overwrite
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# we build x64 as second so that we keep X64 binaries in node_modules for local development
 | 
					# we build x64 as second so that we keep X64 binaries in node_modules for local development
 | 
				
			||||||
echo "Rebuilding binaries for linux-x64"
 | 
					echo "Rebuilding binaries for linux-x64"
 | 
				
			||||||
./node_modules/.bin/electron-rebuild --arch=x64
 | 
					./node_modules/.bin/electron-rebuild --arch=x64
 | 
				
			||||||
 | 
					
 | 
				
			||||||
./node_modules/.bin/electron-packager src/electron --out=dist --platform=linux --arch=x64 --overwrite
 | 
					./node_modules/.bin/electron-packager . --out=dist --platform=linux --arch=x64 --overwrite
 | 
				
			||||||
 | 
					
 | 
				
			||||||
echo "Copying required windows binaries"
 | 
					echo "Copying required windows binaries"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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);
 | 
				
			||||||
							
								
								
									
										200
									
								
								db/schema.sql
									
									
									
									
									
								
							
							
						
						
									
										200
									
								
								db/schema.sql
									
									
									
									
									
								
							@@ -1,119 +1,131 @@
 | 
				
			|||||||
CREATE TABLE IF NOT EXISTS "options" (
 | 
					CREATE TABLE IF NOT EXISTS "options" (
 | 
				
			||||||
	`opt_name`	TEXT NOT NULL PRIMARY KEY,
 | 
					    `name`	TEXT NOT NULL PRIMARY KEY,
 | 
				
			||||||
	`opt_value`	TEXT,
 | 
					    `value`	TEXT,
 | 
				
			||||||
	`date_modified` INT
 | 
					    `dateModified` INT,
 | 
				
			||||||
, is_synced INTEGER NOT NULL DEFAULT 0);
 | 
					    isSynced INTEGER NOT NULL DEFAULT 0);
 | 
				
			||||||
CREATE TABLE IF NOT EXISTS "sync" (
 | 
					CREATE TABLE IF NOT EXISTS "sync" (
 | 
				
			||||||
  `id`	INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
 | 
					  `id`	INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
 | 
				
			||||||
    `entity_name`	TEXT NOT NULL,
 | 
					  `entityName`	TEXT NOT NULL,
 | 
				
			||||||
    `entity_id`	TEXT NOT NULL,
 | 
					  `entityId`	TEXT NOT NULL,
 | 
				
			||||||
    `source_id` TEXT NOT NULL,
 | 
					  `sourceId` TEXT NOT NULL,
 | 
				
			||||||
    `sync_date`	TEXT NOT NULL);
 | 
					  `syncDate`	TEXT NOT NULL);
 | 
				
			||||||
CREATE UNIQUE INDEX `IDX_sync_entity_name_id` ON `sync` (
 | 
					CREATE TABLE IF NOT EXISTS "source_ids" (
 | 
				
			||||||
  `entity_name`,
 | 
					  `sourceId`	TEXT NOT NULL,
 | 
				
			||||||
  `entity_id`
 | 
					  `dateCreated`	TEXT NOT NULL,
 | 
				
			||||||
);
 | 
					  PRIMARY KEY(`sourceId`)
 | 
				
			||||||
CREATE INDEX `IDX_sync_sync_date` ON `sync` (
 | 
					 | 
				
			||||||
  `sync_date`
 | 
					 | 
				
			||||||
);
 | 
					 | 
				
			||||||
CREATE TABLE `source_ids` (
 | 
					 | 
				
			||||||
  `source_id`	TEXT NOT NULL,
 | 
					 | 
				
			||||||
  `date_created`	TEXT NOT NULL,
 | 
					 | 
				
			||||||
  PRIMARY KEY(`source_id`)
 | 
					 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
CREATE TABLE IF NOT EXISTS "notes" (
 | 
					CREATE TABLE IF NOT EXISTS "notes" (
 | 
				
			||||||
    `note_id`	TEXT NOT NULL,
 | 
					  `noteId`	TEXT NOT NULL,
 | 
				
			||||||
    `note_title`	TEXT,
 | 
					  `title`	TEXT,
 | 
				
			||||||
    `note_text`	TEXT,
 | 
					  `content`	TEXT,
 | 
				
			||||||
    `is_protected`	INT NOT NULL DEFAULT 0,
 | 
					  `isProtected`	INT NOT NULL DEFAULT 0,
 | 
				
			||||||
    `is_deleted`	INT NOT NULL DEFAULT 0,
 | 
					  `isDeleted`	INT NOT NULL DEFAULT 0,
 | 
				
			||||||
    `date_created`	TEXT NOT NULL,
 | 
					  `dateCreated`	TEXT NOT NULL,
 | 
				
			||||||
    `date_modified`	TEXT NOT NULL, type TEXT NOT NULL DEFAULT 'text', mime TEXT NOT NULL DEFAULT 'text/html',
 | 
					  `dateModified`	TEXT NOT NULL,
 | 
				
			||||||
    PRIMARY KEY(`note_id`)
 | 
					  type TEXT NOT NULL DEFAULT 'text',
 | 
				
			||||||
 | 
					  mime TEXT NOT NULL DEFAULT 'text/html',
 | 
				
			||||||
 | 
					  PRIMARY KEY(`noteId`)
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
CREATE INDEX `IDX_notes_is_deleted` ON `notes` (
 | 
					CREATE TABLE IF NOT EXISTS "event_log" (
 | 
				
			||||||
    `is_deleted`
 | 
					 | 
				
			||||||
);
 | 
					 | 
				
			||||||
CREATE TABLE `event_log` (
 | 
					 | 
				
			||||||
  `id`	INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
 | 
					  `id`	INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
 | 
				
			||||||
    `note_id`	TEXT,
 | 
					  `noteId`	TEXT,
 | 
				
			||||||
  `comment`	TEXT,
 | 
					  `comment`	TEXT,
 | 
				
			||||||
    `date_added`	TEXT NOT NULL,
 | 
					  `dateAdded`	TEXT NOT NULL,
 | 
				
			||||||
    FOREIGN KEY(note_id) REFERENCES notes(note_id)
 | 
					  FOREIGN KEY(noteId) REFERENCES notes(noteId)
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
CREATE TABLE IF NOT EXISTS "notes_tree" (
 | 
					CREATE TABLE IF NOT EXISTS "note_tree" (
 | 
				
			||||||
  `note_tree_id`	TEXT NOT NULL,
 | 
					  `noteTreeId`	TEXT NOT NULL,
 | 
				
			||||||
  `note_id`	TEXT NOT NULL,
 | 
					  `noteId`	TEXT NOT NULL,
 | 
				
			||||||
  `parent_note_id`	TEXT NOT NULL,
 | 
					  `parentNoteId`	TEXT NOT NULL,
 | 
				
			||||||
  `note_position`	INTEGER NOT NULL,
 | 
					  `notePosition`	INTEGER NOT NULL,
 | 
				
			||||||
  `prefix`	TEXT,
 | 
					  `prefix`	TEXT,
 | 
				
			||||||
  `is_expanded`	BOOLEAN,
 | 
					  `isExpanded`	BOOLEAN,
 | 
				
			||||||
  `is_deleted`	INTEGER NOT NULL DEFAULT 0,
 | 
					  `isDeleted`	INTEGER NOT NULL DEFAULT 0,
 | 
				
			||||||
  `date_modified`	TEXT NOT NULL,
 | 
					  `dateModified`	TEXT NOT NULL,
 | 
				
			||||||
  PRIMARY KEY(`note_tree_id`)
 | 
					  PRIMARY KEY(`noteTreeId`)
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
CREATE INDEX `IDX_notes_tree_note_id` ON `notes_tree` (
 | 
					CREATE TABLE IF NOT EXISTS "note_revisions" (
 | 
				
			||||||
  `note_id`
 | 
					  `noteRevisionId`	TEXT NOT NULL PRIMARY KEY,
 | 
				
			||||||
 | 
					  `noteId`	TEXT NOT NULL,
 | 
				
			||||||
 | 
					  `title`	TEXT,
 | 
				
			||||||
 | 
					  `content`	TEXT,
 | 
				
			||||||
 | 
					  `isProtected`	INT NOT NULL DEFAULT 0,
 | 
				
			||||||
 | 
					  `dateModifiedFrom` TEXT NOT NULL,
 | 
				
			||||||
 | 
					  `dateModifiedTo` TEXT NOT NULL
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
CREATE TABLE IF NOT EXISTS "notes_history" (
 | 
					CREATE TABLE IF NOT EXISTS "recent_notes" (
 | 
				
			||||||
  `note_history_id`	TEXT NOT NULL PRIMARY KEY,
 | 
					  `noteTreeId` TEXT NOT NULL PRIMARY KEY,
 | 
				
			||||||
  `note_id`	TEXT NOT NULL,
 | 
					  `notePath` TEXT NOT NULL,
 | 
				
			||||||
  `note_title`	TEXT,
 | 
					  `dateAccessed` TEXT NOT NULL,
 | 
				
			||||||
  `note_text`	TEXT,
 | 
					  isDeleted INT
 | 
				
			||||||
  `is_protected`	INT NOT NULL DEFAULT 0,
 | 
					 | 
				
			||||||
  `date_modified_from` TEXT NOT NULL,
 | 
					 | 
				
			||||||
  `date_modified_to` TEXT NOT NULL
 | 
					 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
CREATE INDEX `IDX_notes_history_note_id` ON `notes_history` (
 | 
					CREATE TABLE IF NOT EXISTS "images"
 | 
				
			||||||
  `note_id`
 | 
					 | 
				
			||||||
);
 | 
					 | 
				
			||||||
CREATE INDEX `IDX_notes_history_note_date_modified_from` ON `notes_history` (
 | 
					 | 
				
			||||||
  `date_modified_from`
 | 
					 | 
				
			||||||
);
 | 
					 | 
				
			||||||
CREATE INDEX `IDX_notes_history_note_date_modified_to` ON `notes_history` (
 | 
					 | 
				
			||||||
  `date_modified_to`
 | 
					 | 
				
			||||||
);
 | 
					 | 
				
			||||||
CREATE TABLE `recent_notes` (
 | 
					 | 
				
			||||||
  `note_tree_id` TEXT NOT NULL PRIMARY KEY,
 | 
					 | 
				
			||||||
  `note_path` TEXT NOT NULL,
 | 
					 | 
				
			||||||
  `date_accessed` TEXT NOT NULL,
 | 
					 | 
				
			||||||
  is_deleted INT
 | 
					 | 
				
			||||||
);
 | 
					 | 
				
			||||||
CREATE INDEX `IDX_notes_tree_note_id_parent_note_id` ON `notes_tree` (
 | 
					 | 
				
			||||||
  `note_id`,
 | 
					 | 
				
			||||||
  `parent_note_id`
 | 
					 | 
				
			||||||
);
 | 
					 | 
				
			||||||
CREATE TABLE images
 | 
					 | 
				
			||||||
(
 | 
					(
 | 
				
			||||||
  image_id TEXT PRIMARY KEY NOT NULL,
 | 
					  imageId TEXT PRIMARY KEY NOT NULL,
 | 
				
			||||||
  format TEXT NOT NULL,
 | 
					  format TEXT NOT NULL,
 | 
				
			||||||
  checksum TEXT NOT NULL,
 | 
					  checksum TEXT NOT NULL,
 | 
				
			||||||
  name TEXT NOT NULL,
 | 
					  name TEXT NOT NULL,
 | 
				
			||||||
  data BLOB,
 | 
					  data BLOB,
 | 
				
			||||||
  is_deleted INT NOT NULL DEFAULT 0,
 | 
					  isDeleted INT NOT NULL DEFAULT 0,
 | 
				
			||||||
  date_modified TEXT NOT NULL,
 | 
					  dateModified TEXT NOT NULL,
 | 
				
			||||||
  date_created TEXT NOT NULL
 | 
					  dateCreated TEXT NOT NULL
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
CREATE TABLE notes_image
 | 
					CREATE TABLE note_images
 | 
				
			||||||
(
 | 
					(
 | 
				
			||||||
  note_image_id TEXT PRIMARY KEY NOT NULL,
 | 
					  noteImageId TEXT PRIMARY KEY NOT NULL,
 | 
				
			||||||
  note_id TEXT NOT NULL,
 | 
					  noteId TEXT NOT NULL,
 | 
				
			||||||
  image_id TEXT NOT NULL,
 | 
					  imageId TEXT NOT NULL,
 | 
				
			||||||
  is_deleted INT NOT NULL DEFAULT 0,
 | 
					  isDeleted INT NOT NULL DEFAULT 0,
 | 
				
			||||||
  date_modified TEXT NOT NULL,
 | 
					  dateModified TEXT NOT NULL,
 | 
				
			||||||
  date_created TEXT NOT NULL
 | 
					  dateCreated TEXT NOT NULL
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
CREATE INDEX notes_image_note_id_index ON notes_image (note_id);
 | 
					CREATE TABLE IF NOT EXISTS "attributes"
 | 
				
			||||||
CREATE INDEX notes_image_image_id_index ON notes_image (image_id);
 | 
					 | 
				
			||||||
CREATE INDEX notes_image_note_id_image_id_index ON notes_image (note_id, image_id);
 | 
					 | 
				
			||||||
CREATE TABLE attributes
 | 
					 | 
				
			||||||
(
 | 
					(
 | 
				
			||||||
  attribute_id TEXT PRIMARY KEY NOT NULL,
 | 
					  attributeId TEXT PRIMARY KEY NOT NULL,
 | 
				
			||||||
  note_id TEXT NOT NULL,
 | 
					  noteId TEXT NOT NULL,
 | 
				
			||||||
  name TEXT NOT NULL,
 | 
					  name TEXT NOT NULL,
 | 
				
			||||||
  value TEXT,
 | 
					  value TEXT,
 | 
				
			||||||
  date_created TEXT NOT NULL,
 | 
					  position INT NOT NULL DEFAULT 0,
 | 
				
			||||||
  date_modified TEXT NOT NULL
 | 
					  dateCreated TEXT NOT NULL,
 | 
				
			||||||
 | 
					  dateModified TEXT NOT NULL,
 | 
				
			||||||
 | 
					  isDeleted INT NOT NULL
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					CREATE UNIQUE INDEX `IDX_sync_entityName_entityId` ON `sync` (
 | 
				
			||||||
 | 
					  `entityName`,
 | 
				
			||||||
 | 
					  `entityId`
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					CREATE INDEX `IDX_sync_syncDate` ON `sync` (
 | 
				
			||||||
 | 
					  `syncDate`
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					CREATE INDEX `IDX_notes_isDeleted` ON `notes` (
 | 
				
			||||||
 | 
					  `isDeleted`
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					CREATE INDEX `IDX_note_tree_noteId` ON `note_tree` (
 | 
				
			||||||
 | 
					  `noteId`
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					CREATE INDEX `IDX_note_tree_noteId_parentNoteId` ON `note_tree` (
 | 
				
			||||||
 | 
					  `noteId`,
 | 
				
			||||||
 | 
					  `parentNoteId`
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					CREATE INDEX `IDX_note_revisions_noteId` ON `note_revisions` (
 | 
				
			||||||
 | 
					  `noteId`
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					CREATE INDEX `IDX_note_revisions_dateModifiedFrom` ON `note_revisions` (
 | 
				
			||||||
 | 
					  `dateModifiedFrom`
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					CREATE INDEX `IDX_note_revisions_dateModifiedTo` ON `note_revisions` (
 | 
				
			||||||
 | 
					  `dateModifiedTo`
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					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 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
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
CREATE INDEX attributes_note_id_index ON attributes (note_id);
 | 
					 | 
				
			||||||
CREATE UNIQUE INDEX attributes_note_id_name_index ON attributes (note_id, name);
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,10 +2,12 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
const electron = require('electron');
 | 
					const electron = require('electron');
 | 
				
			||||||
const path = require('path');
 | 
					const path = require('path');
 | 
				
			||||||
const config = require('./services/config');
 | 
					const config = require('./src/services/config');
 | 
				
			||||||
const url = require("url");
 | 
					const url = require("url");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const app = electron.app;
 | 
					const app = electron.app;
 | 
				
			||||||
 | 
					const globalShortcut = electron.globalShortcut;
 | 
				
			||||||
 | 
					const clipboard = electron.clipboard;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Adds debug features like hotkeys for triggering dev tools and reload
 | 
					// Adds debug features like hotkeys for triggering dev tools and reload
 | 
				
			||||||
require('electron-debug')();
 | 
					require('electron-debug')();
 | 
				
			||||||
@@ -24,7 +26,7 @@ function createMainWindow() {
 | 
				
			|||||||
        width: 1200,
 | 
					        width: 1200,
 | 
				
			||||||
        height: 900,
 | 
					        height: 900,
 | 
				
			||||||
        title: 'Trilium Notes',
 | 
					        title: 'Trilium Notes',
 | 
				
			||||||
        icon: path.join(__dirname, 'public/images/app-icons/png/256x256.png')
 | 
					        icon: path.join(__dirname, 'src/public/images/app-icons/png/256x256.png')
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const port = config['Network']['port'] || '3000';
 | 
					    const port = config['Network']['port'] || '3000';
 | 
				
			||||||
@@ -67,6 +69,22 @@ app.on('activate', () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
app.on('ready', () => {
 | 
					app.on('ready', () => {
 | 
				
			||||||
    mainWindow = createMainWindow();
 | 
					    mainWindow = createMainWindow();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    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);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
require('./www');
 | 
					app.on('will-quit', () => {
 | 
				
			||||||
 | 
					    globalShortcut.unregisterAll();
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					require('./src/www');
 | 
				
			||||||
@@ -1,8 +1,9 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
  "name": "trilium",
 | 
					  "name": "trilium",
 | 
				
			||||||
  "description": "Trilium Notes",
 | 
					  "description": "Trilium Notes",
 | 
				
			||||||
  "version": "0.5.1-beta",
 | 
					  "version": "0.6.1",
 | 
				
			||||||
  "license": "AGPL-3.0-only",
 | 
					  "license": "AGPL-3.0-only",
 | 
				
			||||||
 | 
					  "main": "electron.js",
 | 
				
			||||||
  "repository": {
 | 
					  "repository": {
 | 
				
			||||||
    "type": "git",
 | 
					    "type": "git",
 | 
				
			||||||
    "url": "https://github.com/zadam/trilium.git"
 | 
					    "url": "https://github.com/zadam/trilium.git"
 | 
				
			||||||
@@ -11,8 +12,8 @@
 | 
				
			|||||||
    "start": "node ./bin/www",
 | 
					    "start": "node ./bin/www",
 | 
				
			||||||
    "test-electron": "xo",
 | 
					    "test-electron": "xo",
 | 
				
			||||||
    "rebuild-electron": "electron-rebuild",
 | 
					    "rebuild-electron": "electron-rebuild",
 | 
				
			||||||
    "start-electron": "electron src/electron",
 | 
					    "start-electron": "electron .",
 | 
				
			||||||
    "build-electron": "electron-packager src/electron --out=dist --asar --overwrite --all",
 | 
					    "build-electron": "electron-packager . --out=dist --asar --overwrite --all",
 | 
				
			||||||
    "start-forge": "electron-forge start",
 | 
					    "start-forge": "electron-forge start",
 | 
				
			||||||
    "package-forge": "electron-forge package",
 | 
					    "package-forge": "electron-forge package",
 | 
				
			||||||
    "make-forge": "electron-forge make",
 | 
					    "make-forge": "electron-forge make",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -24,7 +24,7 @@ class Note extends Entity {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async getAttributes() {
 | 
					    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) {
 | 
					    async getAttribute(name) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -38,6 +38,7 @@ async function getDateNoteIdForReddit(dateTimeStr, rootNoteId) {
 | 
				
			|||||||
        redditDateNoteId = await createNote(dateNoteId, "Reddit");
 | 
					        redditDateNoteId = await createNote(dateNoteId, "Reddit");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        await attributes.createAttribute(redditDateNoteId, REDDIT_DATE_ATTRIBUTE, dateStr);
 | 
					        await attributes.createAttribute(redditDateNoteId, REDDIT_DATE_ATTRIBUTE, dateStr);
 | 
				
			||||||
 | 
					        await attributes.createAttribute(redditDateNoteId, "hide_in_autocomplete");
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return redditDateNoteId;
 | 
					    return redditDateNoteId;
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										21
									
								
								src/public/javascripts/api.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/public/javascripts/api.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
				
			|||||||
 | 
					const api = (function() {
 | 
				
			||||||
 | 
					    const pluginButtonsEl = $("#plugin-buttons");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async function activateNote(notePath) {
 | 
				
			||||||
 | 
					        await noteTree.activateNode(notePath);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    function addButtonToToolbar(buttonId, button) {
 | 
				
			||||||
 | 
					        $("#" + buttonId).remove();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        button.attr('id', buttonId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        pluginButtonsEl.append(button);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					        addButtonToToolbar,
 | 
				
			||||||
 | 
					        activateNote
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					})();
 | 
				
			||||||
@@ -1,18 +1,18 @@
 | 
				
			|||||||
"use strict";
 | 
					"use strict";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const addLink = (function() {
 | 
					const addLink = (function() {
 | 
				
			||||||
    const dialogEl = $("#add-link-dialog");
 | 
					    const $dialog = $("#add-link-dialog");
 | 
				
			||||||
    const formEl = $("#add-link-form");
 | 
					    const $form = $("#add-link-form");
 | 
				
			||||||
    const autoCompleteEl = $("#note-autocomplete");
 | 
					    const $autoComplete = $("#note-autocomplete");
 | 
				
			||||||
    const linkTitleEl = $("#link-title");
 | 
					    const $linkTitle = $("#link-title");
 | 
				
			||||||
    const clonePrefixEl = $("#clone-prefix");
 | 
					    const $clonePrefix = $("#clone-prefix");
 | 
				
			||||||
    const linkTitleFormGroup = $("#add-link-title-form-group");
 | 
					    const $linkTitleFormGroup = $("#add-link-title-form-group");
 | 
				
			||||||
    const prefixFormGroup = $("#add-link-prefix-form-group");
 | 
					    const $prefixFormGroup = $("#add-link-prefix-form-group");
 | 
				
			||||||
    const linkTypeEls = $("input[name='add-link-type']");
 | 
					    const $linkTypes = $("input[name='add-link-type']");
 | 
				
			||||||
    const linkTypeHtmlEl = linkTypeEls.filter('input[value="html"]');
 | 
					    const $linkTypeHtml = $linkTypes.filter('input[value="html"]');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    function setLinkType(linkType) {
 | 
					    function setLinkType(linkType) {
 | 
				
			||||||
        linkTypeEls.each(function () {
 | 
					        $linkTypes.each(function () {
 | 
				
			||||||
            $(this).prop('checked', $(this).val() === linkType);
 | 
					            $(this).prop('checked', $(this).val() === linkType);
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -20,39 +20,39 @@ const addLink = (function() {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    function showDialog() {
 | 
					    function showDialog() {
 | 
				
			||||||
        glob.activeDialog = dialogEl;
 | 
					        glob.activeDialog = $dialog;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (noteEditor.getCurrentNoteType() === 'text') {
 | 
					        if (noteEditor.getCurrentNoteType() === 'text') {
 | 
				
			||||||
            linkTypeHtmlEl.prop('disabled', false);
 | 
					            $linkTypeHtml.prop('disabled', false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            setLinkType('html');
 | 
					            setLinkType('html');
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        else {
 | 
					        else {
 | 
				
			||||||
            linkTypeHtmlEl.prop('disabled', true);
 | 
					            $linkTypeHtml.prop('disabled', true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            setLinkType('selected-to-current');
 | 
					            setLinkType('selected-to-current');
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        dialogEl.dialog({
 | 
					        $dialog.dialog({
 | 
				
			||||||
            modal: true,
 | 
					            modal: true,
 | 
				
			||||||
            width: 700
 | 
					            width: 700
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        autoCompleteEl.val('').focus();
 | 
					        $autoComplete.val('').focus();
 | 
				
			||||||
        clonePrefixEl.val('');
 | 
					        $clonePrefix.val('');
 | 
				
			||||||
        linkTitleEl.val('');
 | 
					        $linkTitle.val('');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        function setDefaultLinkTitle(noteId) {
 | 
					        function setDefaultLinkTitle(noteId) {
 | 
				
			||||||
            const noteTitle = noteTree.getNoteTitle(noteId);
 | 
					            const noteTitle = noteTree.getNoteTitle(noteId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            linkTitleEl.val(noteTitle);
 | 
					            $linkTitle.val(noteTitle);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        autoCompleteEl.autocomplete({
 | 
					        $autoComplete.autocomplete({
 | 
				
			||||||
            source: noteTree.getAutocompleteItems(),
 | 
					            source: noteTree.getAutocompleteItems(),
 | 
				
			||||||
            minLength: 0,
 | 
					            minLength: 0,
 | 
				
			||||||
            change: () => {
 | 
					            change: () => {
 | 
				
			||||||
                const val = autoCompleteEl.val();
 | 
					                const val = $autoComplete.val();
 | 
				
			||||||
                const notePath = link.getNodePathFromLabel(val);
 | 
					                const notePath = link.getNodePathFromLabel(val);
 | 
				
			||||||
                if (!notePath) {
 | 
					                if (!notePath) {
 | 
				
			||||||
                    return;
 | 
					                    return;
 | 
				
			||||||
@@ -75,8 +75,8 @@ const addLink = (function() {
 | 
				
			|||||||
        });
 | 
					        });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    formEl.submit(() => {
 | 
					    $form.submit(() => {
 | 
				
			||||||
        const value = autoCompleteEl.val();
 | 
					        const value = $autoComplete.val();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const notePath = link.getNodePathFromLabel(value);
 | 
					        const notePath = link.getNodePathFromLabel(value);
 | 
				
			||||||
        const noteId = treeUtils.getNoteIdFromNotePath(notePath);
 | 
					        const noteId = treeUtils.getNoteIdFromNotePath(notePath);
 | 
				
			||||||
@@ -85,25 +85,25 @@ const addLink = (function() {
 | 
				
			|||||||
            const linkType = $("input[name='add-link-type']:checked").val();
 | 
					            const linkType = $("input[name='add-link-type']:checked").val();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (linkType === 'html') {
 | 
					            if (linkType === 'html') {
 | 
				
			||||||
                const linkTitle = linkTitleEl.val();
 | 
					                const linkTitle = $linkTitle.val();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                dialogEl.dialog("close");
 | 
					                $dialog.dialog("close");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                link.addLinkToEditor(linkTitle, '#' + notePath);
 | 
					                link.addLinkToEditor(linkTitle, '#' + notePath);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            else if (linkType === 'selected-to-current') {
 | 
					            else if (linkType === 'selected-to-current') {
 | 
				
			||||||
                const prefix = clonePrefixEl.val();
 | 
					                const prefix = $clonePrefix.val();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                cloning.cloneNoteTo(noteId, noteEditor.getCurrentNoteId(), prefix);
 | 
					                cloning.cloneNoteTo(noteId, noteEditor.getCurrentNoteId(), prefix);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                dialogEl.dialog("close");
 | 
					                $dialog.dialog("close");
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            else if (linkType === 'current-to-selected') {
 | 
					            else if (linkType === 'current-to-selected') {
 | 
				
			||||||
                const prefix = clonePrefixEl.val();
 | 
					                const prefix = $clonePrefix.val();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                cloning.cloneNoteTo(noteEditor.getCurrentNoteId(), noteId, prefix);
 | 
					                cloning.cloneNoteTo(noteEditor.getCurrentNoteId(), noteId, prefix);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                dialogEl.dialog("close");
 | 
					                $dialog.dialog("close");
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -111,19 +111,19 @@ const addLink = (function() {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    function linkTypeChanged() {
 | 
					    function linkTypeChanged() {
 | 
				
			||||||
        const value = linkTypeEls.filter(":checked").val();
 | 
					        const value = $linkTypes.filter(":checked").val();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (value === 'html') {
 | 
					        if (value === 'html') {
 | 
				
			||||||
            linkTitleFormGroup.show();
 | 
					            $linkTitleFormGroup.show();
 | 
				
			||||||
            prefixFormGroup.hide();
 | 
					            $prefixFormGroup.hide();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        else {
 | 
					        else {
 | 
				
			||||||
            linkTitleFormGroup.hide();
 | 
					            $linkTitleFormGroup.hide();
 | 
				
			||||||
            prefixFormGroup.show();
 | 
					            $prefixFormGroup.show();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    linkTypeEls.change(linkTypeChanged);
 | 
					    $linkTypes.change(linkTypeChanged);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    $(document).bind('keydown', 'ctrl+l', e => {
 | 
					    $(document).bind('keydown', 'ctrl+l', e => {
 | 
				
			||||||
        showDialog();
 | 
					        showDialog();
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,8 +1,12 @@
 | 
				
			|||||||
"use strict";
 | 
					"use strict";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const attributesDialog = (function() {
 | 
					const attributesDialog = (function() {
 | 
				
			||||||
    const dialogEl = $("#attributes-dialog");
 | 
					    const $dialog = $("#attributes-dialog");
 | 
				
			||||||
 | 
					    const $saveAttributesButton = $("#save-attributes-button");
 | 
				
			||||||
 | 
					    const $attributesBody = $('#attributes-table tbody');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const attributesModel = new AttributesModel();
 | 
					    const attributesModel = new AttributesModel();
 | 
				
			||||||
 | 
					    let attributeNames = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    function AttributesModel() {
 | 
					    function AttributesModel() {
 | 
				
			||||||
        const self = this;
 | 
					        const self = this;
 | 
				
			||||||
@@ -14,38 +18,148 @@ const attributesDialog = (function() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            const attributes = await server.get('notes/' + noteId + '/attributes');
 | 
					            const attributes = await server.get('notes/' + noteId + '/attributes');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            this.attributes(attributes);
 | 
					            self.attributes(attributes.map(ko.observable));
 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        this.addNewRow = function() {
 | 
					            addLastEmptyRow();
 | 
				
			||||||
            self.attributes.push({
 | 
					
 | 
				
			||||||
                attributeId: '',
 | 
					            attributeNames = await server.get('attributes/names');
 | 
				
			||||||
                name: '',
 | 
					
 | 
				
			||||||
                value: ''
 | 
					            // attribute might not be rendered immediatelly so could not focus
 | 
				
			||||||
 | 
					            setTimeout(() => $(".attribute-name:last").focus(), 100);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            $attributesBody.sortable({
 | 
				
			||||||
 | 
					                handle: '.handle',
 | 
				
			||||||
 | 
					                containment: $attributesBody,
 | 
				
			||||||
 | 
					                update: function() {
 | 
				
			||||||
 | 
					                    let position = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    // we need to update positions by searching in the DOM, because order of the
 | 
				
			||||||
 | 
					                    // attributes in the viewmodel (self.attributes()) stays the same
 | 
				
			||||||
 | 
					                    $attributesBody.find('input[name="position"]').each(function() {
 | 
				
			||||||
 | 
					                        const attr = self.getTargetAttribute(this);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        attr().position = position++;
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.deleteAttribute = function(data, event) {
 | 
				
			||||||
 | 
					            const attr = self.getTargetAttribute(event.target);
 | 
				
			||||||
 | 
					            const attrData = attr();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (attrData) {
 | 
				
			||||||
 | 
					                attrData.isDeleted = 1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                attr(attrData);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                addLastEmptyRow();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        function isValid() {
 | 
				
			||||||
 | 
					            for (let attrs = self.attributes(), i = 0; i < attrs.length; i++) {
 | 
				
			||||||
 | 
					                if (self.isEmptyName(i)) {
 | 
				
			||||||
 | 
					                    return false;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return true;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        this.save = async function() {
 | 
					        this.save = async function() {
 | 
				
			||||||
 | 
					            // we need to defocus from input (in case of enter-triggered save) because value is updated
 | 
				
			||||||
 | 
					            // on blur event (because of conflict with jQuery UI Autocomplete). Without this, input would
 | 
				
			||||||
 | 
					            // stay in focus, blur wouldn't be triggered and change wouldn't be updated in the viewmodel.
 | 
				
			||||||
 | 
					            $saveAttributesButton.focus();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (!isValid()) {
 | 
				
			||||||
 | 
					                alert("Please fix all validation errors and try saving again.");
 | 
				
			||||||
 | 
					                return;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            const noteId = noteEditor.getCurrentNoteId();
 | 
					            const noteId = noteEditor.getCurrentNoteId();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            const attributes = await server.put('notes/' + noteId + '/attributes', this.attributes());
 | 
					            const attributesToSave = self.attributes()
 | 
				
			||||||
 | 
					                .map(attr => attr())
 | 
				
			||||||
 | 
					                .filter(attr => attr.attributeId !== "" || attr.name !== "");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            self.attributes(attributes);
 | 
					            const attributes = await server.put('notes/' + noteId + '/attributes', attributesToSave);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            self.attributes(attributes.map(ko.observable));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            addLastEmptyRow();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            showMessage("Attributes have been saved.");
 | 
					            showMessage("Attributes have been saved.");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            noteEditor.loadAttributeList();
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        function addLastEmptyRow() {
 | 
				
			||||||
 | 
					            const attrs = self.attributes().filter(attr => attr().isDeleted === 0);
 | 
				
			||||||
 | 
					            const last = attrs.length === 0 ? null : attrs[attrs.length - 1]();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (!last || last.name.trim() !== "" || last.value !== "") {
 | 
				
			||||||
 | 
					                self.attributes.push(ko.observable({
 | 
				
			||||||
 | 
					                    attributeId: '',
 | 
				
			||||||
 | 
					                    name: '',
 | 
				
			||||||
 | 
					                    value: '',
 | 
				
			||||||
 | 
					                    isDeleted: 0,
 | 
				
			||||||
 | 
					                    position: 0
 | 
				
			||||||
 | 
					                }));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.attributeChanged = function (data, event) {
 | 
				
			||||||
 | 
					            addLastEmptyRow();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            const attr = self.getTargetAttribute(event.target);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            attr.valueHasMutated();
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.isNotUnique = function(index) {
 | 
				
			||||||
 | 
					            const cur = self.attributes()[index]();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (cur.name.trim() === "") {
 | 
				
			||||||
 | 
					                return false;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            for (let attrs = self.attributes(), i = 0; i < attrs.length; i++) {
 | 
				
			||||||
 | 
					                const attr = attrs[i]();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                if (index !== i && cur.name === attr.name) {
 | 
				
			||||||
 | 
					                    return true;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return false;
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.isEmptyName = function(index) {
 | 
				
			||||||
 | 
					            const cur = self.attributes()[index]();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return cur.name.trim() === "" && (cur.attributeId !== "" || cur.value !== "");
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.getTargetAttribute = function(target) {
 | 
				
			||||||
 | 
					            const context = ko.contextFor(target);
 | 
				
			||||||
 | 
					            const index = context.$index();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return self.attributes()[index];
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async function showDialog() {
 | 
					    async function showDialog() {
 | 
				
			||||||
        glob.activeDialog = dialogEl;
 | 
					        glob.activeDialog = $dialog;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        dialogEl.dialog({
 | 
					        await attributesModel.loadAttributes();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $dialog.dialog({
 | 
				
			||||||
            modal: true,
 | 
					            modal: true,
 | 
				
			||||||
            width: 800,
 | 
					            width: 800,
 | 
				
			||||||
            height: 700
 | 
					            height: 500
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					 | 
				
			||||||
        attributesModel.loadAttributes();
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    $(document).bind('keydown', 'alt+a', e => {
 | 
					    $(document).bind('keydown', 'alt+a', e => {
 | 
				
			||||||
@@ -56,6 +170,54 @@ const attributesDialog = (function() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    ko.applyBindings(attributesModel, document.getElementById('attributes-dialog'));
 | 
					    ko.applyBindings(attributesModel, document.getElementById('attributes-dialog'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    $(document).on('focus', '.attribute-name', function (e) {
 | 
				
			||||||
 | 
					        if (!$(this).hasClass("ui-autocomplete-input")) {
 | 
				
			||||||
 | 
					            $(this).autocomplete({
 | 
				
			||||||
 | 
					                // shouldn't be required and autocomplete should just accept array of strings, but that fails
 | 
				
			||||||
 | 
					                // because we have overriden filter() function in init.js
 | 
				
			||||||
 | 
					                source: attributeNames.map(attr => {
 | 
				
			||||||
 | 
					                    return {
 | 
				
			||||||
 | 
					                        label: attr,
 | 
				
			||||||
 | 
					                        value: attr
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }),
 | 
				
			||||||
 | 
					                minLength: 0
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $(this).autocomplete("search", $(this).val());
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    $(document).on('focus', '.attribute-value', async function (e) {
 | 
				
			||||||
 | 
					        if (!$(this).hasClass("ui-autocomplete-input")) {
 | 
				
			||||||
 | 
					            const attributeName = $(this).parent().parent().find('.attribute-name').val();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (attributeName.trim() === "") {
 | 
				
			||||||
 | 
					                return;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            const attributeValues = await server.get('attributes/values/' + encodeURIComponent(attributeName));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (attributeValues.length === 0) {
 | 
				
			||||||
 | 
					                return;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            $(this).autocomplete({
 | 
				
			||||||
 | 
					                // shouldn't be required and autocomplete should just accept array of strings, but that fails
 | 
				
			||||||
 | 
					                // because we have overriden filter() function in init.js
 | 
				
			||||||
 | 
					                source: attributeValues.map(attr => {
 | 
				
			||||||
 | 
					                    return {
 | 
				
			||||||
 | 
					                        label: attr,
 | 
				
			||||||
 | 
					                        value: attr
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }),
 | 
				
			||||||
 | 
					                minLength: 0
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $(this).autocomplete("search", $(this).val());
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
        showDialog
 | 
					        showDialog
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,17 +1,17 @@
 | 
				
			|||||||
"use strict";
 | 
					"use strict";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const editTreePrefix = (function() {
 | 
					const editTreePrefix = (function() {
 | 
				
			||||||
    const dialogEl = $("#edit-tree-prefix-dialog");
 | 
					    const $dialog = $("#edit-tree-prefix-dialog");
 | 
				
			||||||
    const formEl = $("#edit-tree-prefix-form");
 | 
					    const $form = $("#edit-tree-prefix-form");
 | 
				
			||||||
    const treePrefixInputEl = $("#tree-prefix-input");
 | 
					    const $treePrefixInput = $("#tree-prefix-input");
 | 
				
			||||||
    const noteTitleEl = $('#tree-prefix-note-title');
 | 
					    const $noteTitle = $('#tree-prefix-note-title');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let noteTreeId;
 | 
					    let noteTreeId;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async function showDialog() {
 | 
					    async function showDialog() {
 | 
				
			||||||
        glob.activeDialog = dialogEl;
 | 
					        glob.activeDialog = $dialog;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        await dialogEl.dialog({
 | 
					        await $dialog.dialog({
 | 
				
			||||||
            modal: true,
 | 
					            modal: true,
 | 
				
			||||||
            width: 500
 | 
					            width: 500
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
@@ -20,21 +20,21 @@ const editTreePrefix = (function() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        noteTreeId = currentNode.data.noteTreeId;
 | 
					        noteTreeId = currentNode.data.noteTreeId;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        treePrefixInputEl.val(currentNode.data.prefix).focus();
 | 
					        $treePrefixInput.val(currentNode.data.prefix).focus();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const noteTitle = noteTree.getNoteTitle(currentNode.data.noteId);
 | 
					        const noteTitle = noteTree.getNoteTitle(currentNode.data.noteId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        noteTitleEl.html(noteTitle);
 | 
					        $noteTitle.html(noteTitle);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    formEl.submit(() => {
 | 
					    $form.submit(() => {
 | 
				
			||||||
        const prefix = treePrefixInputEl.val();
 | 
					        const prefix = $treePrefixInput.val();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        server.put('tree/' + noteTreeId + '/set-prefix', {
 | 
					        server.put('tree/' + noteTreeId + '/set-prefix', {
 | 
				
			||||||
            prefix: prefix
 | 
					            prefix: prefix
 | 
				
			||||||
        }).then(() => noteTree.setPrefix(noteTreeId, prefix));
 | 
					        }).then(() => noteTree.setPrefix(noteTreeId, prefix));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        dialogEl.dialog("close");
 | 
					        $dialog.dialog("close");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return false;
 | 
					        return false;
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,13 +1,13 @@
 | 
				
			|||||||
"use strict";
 | 
					"use strict";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const eventLog = (function() {
 | 
					const eventLog = (function() {
 | 
				
			||||||
    const dialogEl = $("#event-log-dialog");
 | 
					    const $dialog = $("#event-log-dialog");
 | 
				
			||||||
    const listEl = $("#event-log-list");
 | 
					    const $list = $("#event-log-list");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async function showDialog() {
 | 
					    async function showDialog() {
 | 
				
			||||||
        glob.activeDialog = dialogEl;
 | 
					        glob.activeDialog = $dialog;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        dialogEl.dialog({
 | 
					        $dialog.dialog({
 | 
				
			||||||
            modal: true,
 | 
					            modal: true,
 | 
				
			||||||
            width: 800,
 | 
					            width: 800,
 | 
				
			||||||
            height: 700
 | 
					            height: 700
 | 
				
			||||||
@@ -15,7 +15,7 @@ const eventLog = (function() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        const result = await server.get('event-log');
 | 
					        const result = await server.get('event-log');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        listEl.html('');
 | 
					        $list.html('');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        for (const event of result) {
 | 
					        for (const event of result) {
 | 
				
			||||||
            const dateTime = formatDateTime(parseDate(event.dateAdded));
 | 
					            const dateTime = formatDateTime(parseDate(event.dateAdded));
 | 
				
			||||||
@@ -28,7 +28,7 @@ const eventLog = (function() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            const eventEl = $('<li>').html(dateTime + " - " + event.comment);
 | 
					            const eventEl = $('<li>').html(dateTime + " - " + event.comment);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            listEl.append(eventEl);
 | 
					            $list.append(eventEl);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,28 +1,28 @@
 | 
				
			|||||||
"use strict";
 | 
					"use strict";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const jumpToNote = (function() {
 | 
					const jumpToNote = (function() {
 | 
				
			||||||
    const dialogEl = $("#jump-to-note-dialog");
 | 
					    const $dialog = $("#jump-to-note-dialog");
 | 
				
			||||||
    const autoCompleteEl = $("#jump-to-note-autocomplete");
 | 
					    const $autoComplete = $("#jump-to-note-autocomplete");
 | 
				
			||||||
    const formEl = $("#jump-to-note-form");
 | 
					    const $form = $("#jump-to-note-form");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async function showDialog() {
 | 
					    async function showDialog() {
 | 
				
			||||||
        glob.activeDialog = dialogEl;
 | 
					        glob.activeDialog = $dialog;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        autoCompleteEl.val('');
 | 
					        $autoComplete.val('');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        dialogEl.dialog({
 | 
					        $dialog.dialog({
 | 
				
			||||||
            modal: true,
 | 
					            modal: true,
 | 
				
			||||||
            width: 800
 | 
					            width: 800
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        await autoCompleteEl.autocomplete({
 | 
					        await $autoComplete.autocomplete({
 | 
				
			||||||
            source: await stopWatch("building autocomplete", noteTree.getAutocompleteItems),
 | 
					            source: await stopWatch("building autocomplete", noteTree.getAutocompleteItems),
 | 
				
			||||||
            minLength: 0
 | 
					            minLength: 0
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    function getSelectedNotePath() {
 | 
					    function getSelectedNotePath() {
 | 
				
			||||||
        const val = autoCompleteEl.val();
 | 
					        const val = $autoComplete.val();
 | 
				
			||||||
        return link.getNodePathFromLabel(val);
 | 
					        return link.getNodePathFromLabel(val);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -32,7 +32,7 @@ const jumpToNote = (function() {
 | 
				
			|||||||
        if (notePath) {
 | 
					        if (notePath) {
 | 
				
			||||||
            noteTree.activateNode(notePath);
 | 
					            noteTree.activateNode(notePath);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            dialogEl.dialog('close');
 | 
					            $dialog.dialog('close');
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -42,8 +42,8 @@ const jumpToNote = (function() {
 | 
				
			|||||||
        e.preventDefault();
 | 
					        e.preventDefault();
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    formEl.submit(() => {
 | 
					    $form.submit(() => {
 | 
				
			||||||
        const action = dialogEl.find("button:focus").val();
 | 
					        const action = $dialog.find("button:focus").val();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        goToNote();
 | 
					        goToNote();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,10 +1,10 @@
 | 
				
			|||||||
"use strict";
 | 
					"use strict";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const noteHistory = (function() {
 | 
					const noteHistory = (function() {
 | 
				
			||||||
    const dialogEl = $("#note-history-dialog");
 | 
					    const $dialog = $("#note-history-dialog");
 | 
				
			||||||
    const listEl = $("#note-history-list");
 | 
					    const $list = $("#note-history-list");
 | 
				
			||||||
    const contentEl = $("#note-history-content");
 | 
					    const $content = $("#note-history-content");
 | 
				
			||||||
    const titleEl = $("#note-history-title");
 | 
					    const $title = $("#note-history-title");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let historyItems = [];
 | 
					    let historyItems = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -13,23 +13,23 @@ const noteHistory = (function() {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async function showNoteHistoryDialog(noteId, noteRevisionId) {
 | 
					    async function showNoteHistoryDialog(noteId, noteRevisionId) {
 | 
				
			||||||
        glob.activeDialog = dialogEl;
 | 
					        glob.activeDialog = $dialog;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        dialogEl.dialog({
 | 
					        $dialog.dialog({
 | 
				
			||||||
            modal: true,
 | 
					            modal: true,
 | 
				
			||||||
            width: 800,
 | 
					            width: 800,
 | 
				
			||||||
            height: 700
 | 
					            height: 700
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        listEl.empty();
 | 
					        $list.empty();
 | 
				
			||||||
        contentEl.empty();
 | 
					        $content.empty();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        historyItems = await server.get('notes-history/' + noteId);
 | 
					        historyItems = await server.get('notes-history/' + noteId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        for (const item of historyItems) {
 | 
					        for (const item of historyItems) {
 | 
				
			||||||
            const dateModified = parseDate(item.dateModifiedFrom);
 | 
					            const dateModified = parseDate(item.dateModifiedFrom);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            listEl.append($('<option>', {
 | 
					            $list.append($('<option>', {
 | 
				
			||||||
                value: item.noteRevisionId,
 | 
					                value: item.noteRevisionId,
 | 
				
			||||||
                text: formatDateTime(dateModified)
 | 
					                text: formatDateTime(dateModified)
 | 
				
			||||||
            }));
 | 
					            }));
 | 
				
			||||||
@@ -37,13 +37,13 @@ const noteHistory = (function() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        if (historyItems.length > 0) {
 | 
					        if (historyItems.length > 0) {
 | 
				
			||||||
            if (!noteRevisionId) {
 | 
					            if (!noteRevisionId) {
 | 
				
			||||||
                noteRevisionId = listEl.find("option:first").val();
 | 
					                noteRevisionId = $list.find("option:first").val();
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            listEl.val(noteRevisionId).trigger('change');
 | 
					            $list.val(noteRevisionId).trigger('change');
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        else {
 | 
					        else {
 | 
				
			||||||
            titleEl.text("No history for this note yet...");
 | 
					            $title.text("No history for this note yet...");
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -53,13 +53,13 @@ const noteHistory = (function() {
 | 
				
			|||||||
        e.preventDefault();
 | 
					        e.preventDefault();
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    listEl.on('change', () => {
 | 
					    $list.on('change', () => {
 | 
				
			||||||
        const optVal = listEl.find(":selected").val();
 | 
					        const optVal = $list.find(":selected").val();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const historyItem = historyItems.find(r => r.noteRevisionId === optVal);
 | 
					        const historyItem = historyItems.find(r => r.noteRevisionId === optVal);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        titleEl.html(historyItem.title);
 | 
					        $title.html(historyItem.title);
 | 
				
			||||||
        contentEl.html(historyItem.content);
 | 
					        $content.html(historyItem.content);
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    $(document).on('click', "a[action='note-history']", event => {
 | 
					    $(document).on('click', "a[action='note-history']", event => {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,13 +1,13 @@
 | 
				
			|||||||
"use strict";
 | 
					"use strict";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const noteSource = (function() {
 | 
					const noteSource = (function() {
 | 
				
			||||||
    const dialogEl = $("#note-source-dialog");
 | 
					    const $dialog = $("#note-source-dialog");
 | 
				
			||||||
    const noteSourceEl = $("#note-source");
 | 
					    const $noteSource = $("#note-source");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    function showDialog() {
 | 
					    function showDialog() {
 | 
				
			||||||
        glob.activeDialog = dialogEl;
 | 
					        glob.activeDialog = $dialog;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        dialogEl.dialog({
 | 
					        $dialog.dialog({
 | 
				
			||||||
            modal: true,
 | 
					            modal: true,
 | 
				
			||||||
            width: 800,
 | 
					            width: 800,
 | 
				
			||||||
            height: 500
 | 
					            height: 500
 | 
				
			||||||
@@ -15,7 +15,7 @@ const noteSource = (function() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        const noteText = noteEditor.getCurrentNote().detail.content;
 | 
					        const noteText = noteEditor.getCurrentNote().detail.content;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        noteSourceEl.text(formatHtml(noteText));
 | 
					        $noteSource.text(formatHtml(noteText));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    function formatHtml(str) {
 | 
					    function formatHtml(str) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,12 +1,12 @@
 | 
				
			|||||||
"use strict";
 | 
					"use strict";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const recentChanges = (function() {
 | 
					const recentChanges = (function() {
 | 
				
			||||||
    const dialogEl = $("#recent-changes-dialog");
 | 
					    const $dialog = $("#recent-changes-dialog");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async function showDialog() {
 | 
					    async function showDialog() {
 | 
				
			||||||
        glob.activeDialog = dialogEl;
 | 
					        glob.activeDialog = $dialog;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        dialogEl.dialog({
 | 
					        $dialog.dialog({
 | 
				
			||||||
            modal: true,
 | 
					            modal: true,
 | 
				
			||||||
            width: 800,
 | 
					            width: 800,
 | 
				
			||||||
            height: 700
 | 
					            height: 700
 | 
				
			||||||
@@ -14,7 +14,7 @@ const recentChanges = (function() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        const result = await server.get('recent-changes/');
 | 
					        const result = await server.get('recent-changes/');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        dialogEl.html('');
 | 
					        $dialog.html('');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const groupedByDate = groupByDate(result);
 | 
					        const groupedByDate = groupByDate(result);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -48,7 +48,7 @@ const recentChanges = (function() {
 | 
				
			|||||||
                    .append(' (').append(revLink).append(')'));
 | 
					                    .append(' (').append(revLink).append(')'));
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            dialogEl.append(dayEl);
 | 
					            $dialog.append(dayEl);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,13 +1,9 @@
 | 
				
			|||||||
"use strict";
 | 
					"use strict";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const recentNotes = (function() {
 | 
					const recentNotes = (function() {
 | 
				
			||||||
    const dialogEl = $("#recent-notes-dialog");
 | 
					    const $dialog = $("#recent-notes-dialog");
 | 
				
			||||||
    const selectBoxEl = $('#recent-notes-select-box');
 | 
					    const $searchInput = $('#recent-notes-search-input');
 | 
				
			||||||
    const jumpToButtonEl = $('#recent-notes-jump-to');
 | 
					
 | 
				
			||||||
    const addLinkButtonEl = $('#recent-notes-add-link');
 | 
					 | 
				
			||||||
    const addCurrentAsChildEl = $("#recent-notes-add-current-as-child");
 | 
					 | 
				
			||||||
    const addRecentAsChildEl = $("#recent-notes-add-recent-as-child");
 | 
					 | 
				
			||||||
    const noteDetailEl = $('#note-detail');
 | 
					 | 
				
			||||||
    // list of recent note paths
 | 
					    // list of recent note paths
 | 
				
			||||||
    let list = [];
 | 
					    let list = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -29,97 +25,66 @@ const recentNotes = (function() {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    function showDialog() {
 | 
					    function showDialog() {
 | 
				
			||||||
        glob.activeDialog = dialogEl;
 | 
					        glob.activeDialog = $dialog;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        dialogEl.dialog({
 | 
					        $dialog.dialog({
 | 
				
			||||||
            modal: true,
 | 
					            modal: true,
 | 
				
			||||||
            width: 800
 | 
					            width: 800,
 | 
				
			||||||
 | 
					            height: 100,
 | 
				
			||||||
 | 
					            position: { my: "center top+100", at: "top", of: window }
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        selectBoxEl.find('option').remove();
 | 
					        $searchInput.val('');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // remove the current note
 | 
					        // remove the current note
 | 
				
			||||||
        const recNotes = list.filter(note => note !== noteTree.getCurrentNotePath());
 | 
					        const recNotes = list.filter(note => note !== noteTree.getCurrentNotePath());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $.each(recNotes, (key, valueNotePath) => {
 | 
					        $searchInput.autocomplete({
 | 
				
			||||||
            const noteTitle = noteTree.getNotePathTitle(valueNotePath);
 | 
					            source: recNotes.map(notePath => {
 | 
				
			||||||
 | 
					                let noteTitle;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            const option = $("<option></option>")
 | 
					                try {
 | 
				
			||||||
                .attr("value", valueNotePath)
 | 
					                    noteTitle = noteTree.getNotePathTitle(notePath);
 | 
				
			||||||
                .text(noteTitle);
 | 
					                }
 | 
				
			||||||
 | 
					                catch (e) {
 | 
				
			||||||
 | 
					                    noteTitle = "[error - can't find note title]";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            // select the first one (most recent one) by default
 | 
					                    messaging.logError("Could not find title for notePath=" + notePath + ", stack=" + e.stack);
 | 
				
			||||||
            if (key === 0) {
 | 
					 | 
				
			||||||
                option.attr("selected", "selected");
 | 
					 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            selectBoxEl.append(option);
 | 
					                return {
 | 
				
			||||||
        });
 | 
					                    label: noteTitle,
 | 
				
			||||||
 | 
					                    value: notePath
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					            }),
 | 
				
			||||||
 | 
					            minLength: 0,
 | 
				
			||||||
 | 
					            autoFocus: true,
 | 
				
			||||||
 | 
					            select: function (event, ui) {
 | 
				
			||||||
 | 
					                noteTree.activateNode(ui.item.value);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    function getSelectedNotePath() {
 | 
					                $searchInput.autocomplete('destroy');
 | 
				
			||||||
        return selectBoxEl.find("option:selected").val();
 | 
					                $dialog.dialog('close');
 | 
				
			||||||
    }
 | 
					            },
 | 
				
			||||||
 | 
					            focus: function (event, ui) {
 | 
				
			||||||
    function getSelectedNoteId() {
 | 
					                event.preventDefault();
 | 
				
			||||||
        const notePath = getSelectedNotePath();
 | 
					            },
 | 
				
			||||||
        return treeUtils.getNoteIdFromNotePath(notePath);
 | 
					            close: function (event, ui) {
 | 
				
			||||||
    }
 | 
					                if (event.keyCode === 27) { // escape closes dialog
 | 
				
			||||||
 | 
					                    $searchInput.autocomplete('destroy');
 | 
				
			||||||
    function setActiveNoteBasedOnRecentNotes() {
 | 
					                    $dialog.dialog('close');
 | 
				
			||||||
        const notePath = getSelectedNotePath();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        noteTree.activateNode(notePath);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        dialogEl.dialog('close');
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    function addLinkBasedOnRecentNotes() {
 | 
					 | 
				
			||||||
        const notePath = getSelectedNotePath();
 | 
					 | 
				
			||||||
        const noteId = treeUtils.getNoteIdFromNotePath(notePath);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const linkTitle = noteTree.getNoteTitle(noteId);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        dialogEl.dialog("close");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        link.addLinkToEditor(linkTitle, '#' + notePath);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async function addCurrentAsChild() {
 | 
					 | 
				
			||||||
        await cloning.cloneNoteTo(noteEditor.getCurrentNoteId(), getSelectedNoteId());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        dialogEl.dialog("close");
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    async function addRecentAsChild() {
 | 
					 | 
				
			||||||
        await cloning.cloneNoteTo(getSelectedNoteId(), noteEditor.getCurrentNoteId());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        dialogEl.dialog("close");
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    selectBoxEl.keydown(e => {
 | 
					 | 
				
			||||||
        const key = e.which;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        // to get keycodes use http://keycode.info/
 | 
					 | 
				
			||||||
        if (key === 13)// the enter key code
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            setActiveNoteBasedOnRecentNotes();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        else if (key === 76 /* l */) {
 | 
					 | 
				
			||||||
            addLinkBasedOnRecentNotes();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        else if (key === 67 /* c */) {
 | 
					 | 
				
			||||||
            addCurrentAsChild();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        else if (key === 82 /* r */) {
 | 
					 | 
				
			||||||
            addRecentAsChild()
 | 
					 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                else {
 | 
					                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();
 | 
					    reload();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -129,15 +94,6 @@ const recentNotes = (function() {
 | 
				
			|||||||
        e.preventDefault();
 | 
					        e.preventDefault();
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    selectBoxEl.dblclick(e => {
 | 
					 | 
				
			||||||
        setActiveNoteBasedOnRecentNotes();
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    jumpToButtonEl.click(setActiveNoteBasedOnRecentNotes);
 | 
					 | 
				
			||||||
    addLinkButtonEl.click(addLinkBasedOnRecentNotes);
 | 
					 | 
				
			||||||
    addCurrentAsChildEl.click(addCurrentAsChild);
 | 
					 | 
				
			||||||
    addRecentAsChildEl.click(addRecentAsChild);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
        showDialog,
 | 
					        showDialog,
 | 
				
			||||||
        addRecentNote,
 | 
					        addRecentNote,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,8 +1,8 @@
 | 
				
			|||||||
"use strict";
 | 
					"use strict";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const settings = (function() {
 | 
					const settings = (function() {
 | 
				
			||||||
    const dialogEl = $("#settings-dialog");
 | 
					    const $dialog = $("#settings-dialog");
 | 
				
			||||||
    const tabsEl = $("#settings-tabs");
 | 
					    const $tabs = $("#settings-tabs");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const settingModules = [];
 | 
					    const settingModules = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -11,16 +11,16 @@ const settings = (function() {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async function showDialog() {
 | 
					    async function showDialog() {
 | 
				
			||||||
        glob.activeDialog = dialogEl;
 | 
					        glob.activeDialog = $dialog;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const settings = await server.get('settings');
 | 
					        const settings = await server.get('settings');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        dialogEl.dialog({
 | 
					        $dialog.dialog({
 | 
				
			||||||
            modal: true,
 | 
					            modal: true,
 | 
				
			||||||
            width: 900
 | 
					            width: 900
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        tabsEl.tabs();
 | 
					        $tabs.tabs();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        for (const module of settingModules) {
 | 
					        for (const module of settingModules) {
 | 
				
			||||||
            if (module.settingsLoaded) {
 | 
					            if (module.settingsLoaded) {
 | 
				
			||||||
@@ -46,22 +46,22 @@ const settings = (function() {
 | 
				
			|||||||
})();
 | 
					})();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
settings.addModule((function() {
 | 
					settings.addModule((function() {
 | 
				
			||||||
    const formEl = $("#change-password-form");
 | 
					    const $form = $("#change-password-form");
 | 
				
			||||||
    const oldPasswordEl = $("#old-password");
 | 
					    const $oldPassword = $("#old-password");
 | 
				
			||||||
    const newPassword1El = $("#new-password1");
 | 
					    const $newPassword1 = $("#new-password1");
 | 
				
			||||||
    const newPassword2El = $("#new-password2");
 | 
					    const $newPassword2 = $("#new-password2");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    function settingsLoaded(settings) {
 | 
					    function settingsLoaded(settings) {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    formEl.submit(() => {
 | 
					    $form.submit(() => {
 | 
				
			||||||
        const oldPassword = oldPasswordEl.val();
 | 
					        const oldPassword = $oldPassword.val();
 | 
				
			||||||
        const newPassword1 = newPassword1El.val();
 | 
					        const newPassword1 = $newPassword1.val();
 | 
				
			||||||
        const newPassword2 = newPassword2El.val();
 | 
					        const newPassword2 = $newPassword2.val();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        oldPasswordEl.val('');
 | 
					        $oldPassword.val('');
 | 
				
			||||||
        newPassword1El.val('');
 | 
					        $newPassword1.val('');
 | 
				
			||||||
        newPassword2El.val('');
 | 
					        $newPassword2.val('');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (newPassword1 !== newPassword2) {
 | 
					        if (newPassword1 !== newPassword2) {
 | 
				
			||||||
            alert("New passwords are not the same.");
 | 
					            alert("New passwords are not the same.");
 | 
				
			||||||
@@ -92,16 +92,16 @@ settings.addModule((function() {
 | 
				
			|||||||
})());
 | 
					})());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
settings.addModule((function() {
 | 
					settings.addModule((function() {
 | 
				
			||||||
    const formEl = $("#protected-session-timeout-form");
 | 
					    const $form = $("#protected-session-timeout-form");
 | 
				
			||||||
    const protectedSessionTimeoutEl = $("#protected-session-timeout-in-seconds");
 | 
					    const $protectedSessionTimeout = $("#protected-session-timeout-in-seconds");
 | 
				
			||||||
    const settingName = 'protected_session_timeout';
 | 
					    const settingName = 'protected_session_timeout';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    function settingsLoaded(settings) {
 | 
					    function settingsLoaded(settings) {
 | 
				
			||||||
        protectedSessionTimeoutEl.val(settings[settingName]);
 | 
					        $protectedSessionTimeout.val(settings[settingName]);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    formEl.submit(() => {
 | 
					    $form.submit(() => {
 | 
				
			||||||
        const protectedSessionTimeout = protectedSessionTimeoutEl.val();
 | 
					        const protectedSessionTimeout = $protectedSessionTimeout.val();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        settings.saveSettings(settingName, protectedSessionTimeout).then(() => {
 | 
					        settings.saveSettings(settingName, protectedSessionTimeout).then(() => {
 | 
				
			||||||
            protected_session.setProtectedSessionTimeout(protectedSessionTimeout);
 | 
					            protected_session.setProtectedSessionTimeout(protectedSessionTimeout);
 | 
				
			||||||
@@ -116,16 +116,16 @@ settings.addModule((function() {
 | 
				
			|||||||
})());
 | 
					})());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
settings.addModule((function () {
 | 
					settings.addModule((function () {
 | 
				
			||||||
    const formEl = $("#history-snapshot-time-interval-form");
 | 
					    const $form = $("#history-snapshot-time-interval-form");
 | 
				
			||||||
    const timeIntervalEl = $("#history-snapshot-time-interval-in-seconds");
 | 
					    const $timeInterval = $("#history-snapshot-time-interval-in-seconds");
 | 
				
			||||||
    const settingName = 'history_snapshot_time_interval';
 | 
					    const settingName = 'history_snapshot_time_interval';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    function settingsLoaded(settings) {
 | 
					    function settingsLoaded(settings) {
 | 
				
			||||||
        timeIntervalEl.val(settings[settingName]);
 | 
					        $timeInterval.val(settings[settingName]);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    formEl.submit(() => {
 | 
					    $form.submit(() => {
 | 
				
			||||||
        settings.saveSettings(settingName, timeIntervalEl.val());
 | 
					        settings.saveSettings(settingName, $timeInterval.val());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return false;
 | 
					        return false;
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
@@ -136,50 +136,50 @@ settings.addModule((function () {
 | 
				
			|||||||
})());
 | 
					})());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
settings.addModule((async function () {
 | 
					settings.addModule((async function () {
 | 
				
			||||||
    const appVersionEl = $("#app-version");
 | 
					    const $appVersion = $("#app-version");
 | 
				
			||||||
    const dbVersionEl = $("#db-version");
 | 
					    const $dbVersion = $("#db-version");
 | 
				
			||||||
    const buildDateEl = $("#build-date");
 | 
					    const $buildDate = $("#build-date");
 | 
				
			||||||
    const buildRevisionEl = $("#build-revision");
 | 
					    const $buildRevision = $("#build-revision");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const appInfo = await server.get('app-info');
 | 
					    const appInfo = await server.get('app-info');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    appVersionEl.html(appInfo.app_version);
 | 
					    $appVersion.html(appInfo.app_version);
 | 
				
			||||||
    dbVersionEl.html(appInfo.db_version);
 | 
					    $dbVersion.html(appInfo.db_version);
 | 
				
			||||||
    buildDateEl.html(appInfo.build_date);
 | 
					    $buildDate.html(appInfo.build_date);
 | 
				
			||||||
    buildRevisionEl.html(appInfo.build_revision);
 | 
					    $buildRevision.html(appInfo.build_revision);
 | 
				
			||||||
    buildRevisionEl.attr('href', 'https://github.com/zadam/trilium/commit/' + appInfo.build_revision);
 | 
					    $buildRevision.attr('href', 'https://github.com/zadam/trilium/commit/' + appInfo.build_revision);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return {};
 | 
					    return {};
 | 
				
			||||||
})());
 | 
					})());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
settings.addModule((async function () {
 | 
					settings.addModule((async function () {
 | 
				
			||||||
    const forceFullSyncButton = $("#force-full-sync-button");
 | 
					    const $forceFullSyncButton = $("#force-full-sync-button");
 | 
				
			||||||
    const fillSyncRowsButton = $("#fill-sync-rows-button");
 | 
					    const $fillSyncRowsButton = $("#fill-sync-rows-button");
 | 
				
			||||||
    const anonymizeButton = $("#anonymize-button");
 | 
					    const $anonymizeButton = $("#anonymize-button");
 | 
				
			||||||
    const cleanupSoftDeletedButton = $("#cleanup-soft-deleted-items-button");
 | 
					    const $cleanupSoftDeletedButton = $("#cleanup-soft-deleted-items-button");
 | 
				
			||||||
    const cleanupUnusedImagesButton = $("#cleanup-unused-images-button");
 | 
					    const $cleanupUnusedImagesButton = $("#cleanup-unused-images-button");
 | 
				
			||||||
    const vacuumDatabaseButton = $("#vacuum-database-button");
 | 
					    const $vacuumDatabaseButton = $("#vacuum-database-button");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    forceFullSyncButton.click(async () => {
 | 
					    $forceFullSyncButton.click(async () => {
 | 
				
			||||||
        await server.post('sync/force-full-sync');
 | 
					        await server.post('sync/force-full-sync');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        showMessage("Full sync triggered");
 | 
					        showMessage("Full sync triggered");
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    fillSyncRowsButton.click(async () => {
 | 
					    $fillSyncRowsButton.click(async () => {
 | 
				
			||||||
        await server.post('sync/fill-sync-rows');
 | 
					        await server.post('sync/fill-sync-rows');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        showMessage("Sync rows filled successfully");
 | 
					        showMessage("Sync rows filled successfully");
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    anonymizeButton.click(async () => {
 | 
					    $anonymizeButton.click(async () => {
 | 
				
			||||||
        await server.post('anonymization/anonymize');
 | 
					        await server.post('anonymization/anonymize');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        showMessage("Created anonymized database");
 | 
					        showMessage("Created anonymized database");
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    cleanupSoftDeletedButton.click(async () => {
 | 
					    $cleanupSoftDeletedButton.click(async () => {
 | 
				
			||||||
        if (confirm("Do you really want to clean up soft-deleted items?")) {
 | 
					        if (confirm("Do you really want to clean up soft-deleted items?")) {
 | 
				
			||||||
            await server.post('cleanup/cleanup-soft-deleted-items');
 | 
					            await server.post('cleanup/cleanup-soft-deleted-items');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -187,7 +187,7 @@ settings.addModule((async function () {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    cleanupUnusedImagesButton.click(async () => {
 | 
					    $cleanupUnusedImagesButton.click(async () => {
 | 
				
			||||||
        if (confirm("Do you really want to clean up unused images?")) {
 | 
					        if (confirm("Do you really want to clean up unused images?")) {
 | 
				
			||||||
            await server.post('cleanup/cleanup-unused-images');
 | 
					            await server.post('cleanup/cleanup-unused-images');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -195,7 +195,7 @@ settings.addModule((async function () {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    vacuumDatabaseButton.click(async () => {
 | 
					    $vacuumDatabaseButton.click(async () => {
 | 
				
			||||||
        await server.post('cleanup/vacuum-database');
 | 
					        await server.post('cleanup/vacuum-database');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        showMessage("Database has been vacuumed");
 | 
					        showMessage("Database has been vacuumed");
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,24 +1,44 @@
 | 
				
			|||||||
"use strict";
 | 
					"use strict";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const sqlConsole = (function() {
 | 
					const sqlConsole = (function() {
 | 
				
			||||||
    const dialogEl = $("#sql-console-dialog");
 | 
					    const $dialog = $("#sql-console-dialog");
 | 
				
			||||||
    const queryEl = $('#sql-console-query');
 | 
					    const $query = $('#sql-console-query');
 | 
				
			||||||
    const executeButton = $('#sql-console-execute');
 | 
					    const $executeButton = $('#sql-console-execute');
 | 
				
			||||||
    const resultHeadEl = $('#sql-console-results thead');
 | 
					    const $resultHead = $('#sql-console-results thead');
 | 
				
			||||||
    const resultBodyEl = $('#sql-console-results tbody');
 | 
					    const $resultBody = $('#sql-console-results tbody');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let codeEditor;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    function showDialog() {
 | 
					    function showDialog() {
 | 
				
			||||||
        glob.activeDialog = dialogEl;
 | 
					        glob.activeDialog = $dialog;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        dialogEl.dialog({
 | 
					        $dialog.dialog({
 | 
				
			||||||
            modal: true,
 | 
					            modal: true,
 | 
				
			||||||
            width: $(window).width(),
 | 
					            width: $(window).width(),
 | 
				
			||||||
            height: $(window).height()
 | 
					            height: $(window).height(),
 | 
				
			||||||
 | 
					            open: function() {
 | 
				
			||||||
 | 
					                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() {
 | 
					    async function execute() {
 | 
				
			||||||
        const sqlQuery = queryEl.val();
 | 
					        const sqlQuery = codeEditor.getValue();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const result = await server.post("sql/execute", {
 | 
					        const result = await server.post("sql/execute", {
 | 
				
			||||||
            query: sqlQuery
 | 
					            query: sqlQuery
 | 
				
			||||||
@@ -34,8 +54,8 @@ const sqlConsole = (function() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        const rows = result.rows;
 | 
					        const rows = result.rows;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        resultHeadEl.empty();
 | 
					        $resultHead.empty();
 | 
				
			||||||
        resultBodyEl.empty();
 | 
					        $resultBody.empty();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (rows.length > 0) {
 | 
					        if (rows.length > 0) {
 | 
				
			||||||
            const result = rows[0];
 | 
					            const result = rows[0];
 | 
				
			||||||
@@ -45,7 +65,7 @@ const sqlConsole = (function() {
 | 
				
			|||||||
                rowEl.append($("<th>").html(key));
 | 
					                rowEl.append($("<th>").html(key));
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            resultHeadEl.append(rowEl);
 | 
					            $resultHead.append(rowEl);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        for (const result of rows) {
 | 
					        for (const result of rows) {
 | 
				
			||||||
@@ -55,15 +75,15 @@ const sqlConsole = (function() {
 | 
				
			|||||||
                rowEl.append($("<td>").html(result[key]));
 | 
					                rowEl.append($("<td>").html(result[key]));
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            resultBodyEl.append(rowEl);
 | 
					            $resultBody.append(rowEl);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    $(document).bind('keydown', 'alt+o', showDialog);
 | 
					    $(document).bind('keydown', 'alt+o', showDialog);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    queryEl.bind('keydown', 'ctrl+return', execute);
 | 
					    $query.bind('keydown', 'ctrl+return', execute);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    executeButton.click(execute);
 | 
					    $executeButton.click(execute);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
        showDialog
 | 
					        showDialog
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -54,24 +54,6 @@ $(document).bind('keydown', 'ctrl+f', () => {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
$(document).bind('keydown', "ctrl+shift+left", () => {
 | 
					 | 
				
			||||||
    const node = noteTree.getCurrentNode();
 | 
					 | 
				
			||||||
    node.navigate($.ui.keyCode.LEFT, true);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    $("#note-detail").focus();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return false;
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
$(document).bind('keydown', "ctrl+shift+right", () => {
 | 
					 | 
				
			||||||
    const node = noteTree.getCurrentNode();
 | 
					 | 
				
			||||||
    node.navigate($.ui.keyCode.RIGHT, true);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    $("#note-detail").focus();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return false;
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
$(document).bind('keydown', "ctrl+shift+up", () => {
 | 
					$(document).bind('keydown', "ctrl+shift+up", () => {
 | 
				
			||||||
    const node = noteTree.getCurrentNode();
 | 
					    const node = noteTree.getCurrentNode();
 | 
				
			||||||
    node.navigate($.ui.keyCode.UP, true);
 | 
					    node.navigate($.ui.keyCode.UP, true);
 | 
				
			||||||
@@ -123,7 +105,7 @@ $(window).on('beforeunload', () => {
 | 
				
			|||||||
// Overrides the default autocomplete filter function to search for matched on atleast 1 word in each of the input term's words
 | 
					// Overrides the default autocomplete filter function to search for matched on atleast 1 word in each of the input term's words
 | 
				
			||||||
$.ui.autocomplete.filter = (array, terms) => {
 | 
					$.ui.autocomplete.filter = (array, terms) => {
 | 
				
			||||||
    if (!terms) {
 | 
					    if (!terms) {
 | 
				
			||||||
        return [];
 | 
					        return array;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const startDate = new Date();
 | 
					    const startDate = new Date();
 | 
				
			||||||
@@ -144,6 +126,10 @@ $.ui.autocomplete.filter = (array, terms) => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        if (found) {
 | 
					        if (found) {
 | 
				
			||||||
            results.push(item);
 | 
					            results.push(item);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (results.length > 100) {
 | 
				
			||||||
 | 
					                break;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -211,3 +197,18 @@ $(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);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const node = noteTree.getCurrentNode();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        await noteTree.createNote(node, node.data.noteId, 'into', node.data.isProtected);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -9,6 +9,8 @@ const noteEditor = (function() {
 | 
				
			|||||||
    const unprotectButton = $("#unprotect-button");
 | 
					    const unprotectButton = $("#unprotect-button");
 | 
				
			||||||
    const noteDetailWrapperEl = $("#note-detail-wrapper");
 | 
					    const noteDetailWrapperEl = $("#note-detail-wrapper");
 | 
				
			||||||
    const noteIdDisplayEl = $("#note-id-display");
 | 
					    const noteIdDisplayEl = $("#note-id-display");
 | 
				
			||||||
 | 
					    const attributeListEl = $("#attribute-list");
 | 
				
			||||||
 | 
					    const attributeListInnerEl = $("#attribute-list-inner");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let editor = null;
 | 
					    let editor = null;
 | 
				
			||||||
    let codeEditor = null;
 | 
					    let codeEditor = null;
 | 
				
			||||||
@@ -114,6 +116,32 @@ const noteEditor = (function() {
 | 
				
			|||||||
        isNewNoteCreated = true;
 | 
					        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) {
 | 
					    async function loadNoteToEditor(noteId) {
 | 
				
			||||||
        currentNote = await loadNote(noteId);
 | 
					        currentNote = await loadNote(noteId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -144,30 +172,7 @@ const noteEditor = (function() {
 | 
				
			|||||||
        noteType.setNoteType(currentNote.detail.type);
 | 
					        noteType.setNoteType(currentNote.detail.type);
 | 
				
			||||||
        noteType.setNoteMime(currentNote.detail.mime);
 | 
					        noteType.setNoteMime(currentNote.detail.mime);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (currentNote.detail.type === 'text') {
 | 
					        if (currentNote.detail.type === 'render') {
 | 
				
			||||||
            // 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') {
 | 
					 | 
				
			||||||
            noteDetailEl.hide();
 | 
					            noteDetailEl.hide();
 | 
				
			||||||
            noteDetailCodeEl.hide();
 | 
					            noteDetailCodeEl.hide();
 | 
				
			||||||
            noteDetailRenderEl.html('').show();
 | 
					            noteDetailRenderEl.html('').show();
 | 
				
			||||||
@@ -177,7 +182,7 @@ const noteEditor = (function() {
 | 
				
			|||||||
            noteDetailRenderEl.html(subTree);
 | 
					            noteDetailRenderEl.html(subTree);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        else {
 | 
					        else {
 | 
				
			||||||
            throwError("Unrecognized type " + currentNote.detail.type);
 | 
					            setContent(currentNote.detail.content);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        noteChangeDisabled = false;
 | 
					        noteChangeDisabled = false;
 | 
				
			||||||
@@ -187,6 +192,27 @@ const noteEditor = (function() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        // after loading new note make sure editor is scrolled to the top
 | 
					        // after loading new note make sure editor is scrolled to the top
 | 
				
			||||||
        noteDetailWrapperEl.scrollTop(0);
 | 
					        noteDetailWrapperEl.scrollTop(0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        loadAttributeList();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async function loadAttributeList() {
 | 
				
			||||||
 | 
					        const noteId = getCurrentNoteId();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const attributes = await server.get('notes/' + noteId + '/attributes');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        attributeListInnerEl.html('');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (attributes.length > 0) {
 | 
				
			||||||
 | 
					            for (const attr of attributes) {
 | 
				
			||||||
 | 
					                attributeListInnerEl.append(formatAttribute(attr) + " ");
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            attributeListEl.show();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        else {
 | 
				
			||||||
 | 
					            attributeListEl.hide();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async function loadNote(noteId) {
 | 
					    async function loadNote(noteId) {
 | 
				
			||||||
@@ -290,6 +316,8 @@ const noteEditor = (function() {
 | 
				
			|||||||
        newNoteCreated,
 | 
					        newNoteCreated,
 | 
				
			||||||
        getEditor,
 | 
					        getEditor,
 | 
				
			||||||
        focus,
 | 
					        focus,
 | 
				
			||||||
        executeCurrentNote
 | 
					        executeCurrentNote,
 | 
				
			||||||
 | 
					        loadAttributeList,
 | 
				
			||||||
 | 
					        setContent
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
})();
 | 
					})();
 | 
				
			||||||
@@ -3,7 +3,7 @@
 | 
				
			|||||||
const noteTree = (function() {
 | 
					const noteTree = (function() {
 | 
				
			||||||
    const treeEl = $("#tree");
 | 
					    const treeEl = $("#tree");
 | 
				
			||||||
    const parentListEl = $("#parent-list");
 | 
					    const parentListEl = $("#parent-list");
 | 
				
			||||||
    const parentListListEl = $("#parent-list-list");
 | 
					    const parentListListEl = $("#parent-list-inner");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let startNotePath = null;
 | 
					    let startNotePath = null;
 | 
				
			||||||
    let notesTreeMap = {};
 | 
					    let notesTreeMap = {};
 | 
				
			||||||
@@ -14,6 +14,8 @@ const noteTree = (function() {
 | 
				
			|||||||
    let parentChildToNoteTreeId = {};
 | 
					    let parentChildToNoteTreeId = {};
 | 
				
			||||||
    let noteIdToTitle = {};
 | 
					    let noteIdToTitle = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let hiddenInAutocomplete = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    function getNoteTreeId(parentNoteId, childNoteId) {
 | 
					    function getNoteTreeId(parentNoteId, childNoteId) {
 | 
				
			||||||
        assertArguments(parentNoteId, childNoteId);
 | 
					        assertArguments(parentNoteId, childNoteId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -640,16 +642,21 @@ const noteTree = (function() {
 | 
				
			|||||||
        return document.location.hash.substr(1); // strip initial #
 | 
					        return document.location.hash.substr(1); // strip initial #
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    function loadTree() {
 | 
					    async function loadTree() {
 | 
				
			||||||
        return server.get('tree').then(resp => {
 | 
					        const resp = await server.get('tree');
 | 
				
			||||||
        startNotePath = resp.start_note_path;
 | 
					        startNotePath = resp.start_note_path;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (document.location.hash) {
 | 
					        if (document.location.hash) {
 | 
				
			||||||
            startNotePath = getNotePathFromAddress();
 | 
					            startNotePath = getNotePathFromAddress();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        hiddenInAutocomplete = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for (const noteId of resp.hiddenInAutocomplete) {
 | 
				
			||||||
 | 
					            hiddenInAutocomplete[noteId] = true;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return prepareNoteTree(resp.notes);
 | 
					        return prepareNoteTree(resp.notes);
 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    $(() => loadTree().then(noteTree => initFancyTree(noteTree)));
 | 
					    $(() => loadTree().then(noteTree => initFancyTree(noteTree)));
 | 
				
			||||||
@@ -706,6 +713,10 @@ const noteTree = (function() {
 | 
				
			|||||||
        const autocompleteItems = [];
 | 
					        const autocompleteItems = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        for (const childNoteId of parentToChildren[parentNoteId]) {
 | 
					        for (const childNoteId of parentToChildren[parentNoteId]) {
 | 
				
			||||||
 | 
					            if (hiddenInAutocomplete[childNoteId]) {
 | 
				
			||||||
 | 
					                continue;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            const childNotePath = (notePath ? (notePath + '/') : '') + childNoteId;
 | 
					            const childNotePath = (notePath ? (notePath + '/') : '') + childNoteId;
 | 
				
			||||||
            const childTitlePath = (titlePath ? (titlePath + ' / ') : '') + getNoteTitle(childNoteId, parentNoteId);
 | 
					            const childTitlePath = (titlePath ? (titlePath + ' / ') : '') + getNoteTitle(childNoteId, parentNoteId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -775,7 +786,7 @@ const noteTree = (function() {
 | 
				
			|||||||
        };
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (target === 'after') {
 | 
					        if (target === 'after') {
 | 
				
			||||||
            node.appendSibling(newNode).setActive(true);
 | 
					            await node.appendSibling(newNode).setActive(true);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        else if (target === 'into') {
 | 
					        else if (target === 'into') {
 | 
				
			||||||
            if (!node.getChildren() && node.isFolder()) {
 | 
					            if (!node.getChildren() && node.isFolder()) {
 | 
				
			||||||
@@ -785,7 +796,7 @@ const noteTree = (function() {
 | 
				
			|||||||
                node.addChildren(newNode);
 | 
					                node.addChildren(newNode);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            node.getLastChild().setActive(true);
 | 
					            await node.getLastChild().setActive(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            node.folder = true;
 | 
					            node.folder = true;
 | 
				
			||||||
            node.renderTitle();
 | 
					            node.renderTitle();
 | 
				
			||||||
@@ -794,6 +805,8 @@ const noteTree = (function() {
 | 
				
			|||||||
            throwError("Unrecognized target: " + target);
 | 
					            throwError("Unrecognized target: " + target);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        clearSelectedNodes(); // to unmark previously active node
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        showMessage("Created!");
 | 
					        showMessage("Created!");
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -803,6 +816,10 @@ const noteTree = (function() {
 | 
				
			|||||||
        await reload();
 | 
					        await reload();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    function noteExists(noteId) {
 | 
				
			||||||
 | 
					        return !!childToParents[noteId];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    $(document).bind('keydown', 'ctrl+o', e => {
 | 
					    $(document).bind('keydown', 'ctrl+o', e => {
 | 
				
			||||||
        const node = getCurrentNode();
 | 
					        const node = getCurrentNode();
 | 
				
			||||||
        const parentNoteId = node.data.parentNoteId;
 | 
					        const parentNoteId = node.data.parentNoteId;
 | 
				
			||||||
@@ -876,6 +893,7 @@ const noteTree = (function() {
 | 
				
			|||||||
        removeParentChildRelation,
 | 
					        removeParentChildRelation,
 | 
				
			||||||
        setParentChildRelation,
 | 
					        setParentChildRelation,
 | 
				
			||||||
        getSelectedNodes,
 | 
					        getSelectedNodes,
 | 
				
			||||||
        sortAlphabetically
 | 
					        sortAlphabetically,
 | 
				
			||||||
 | 
					        noteExists
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
})();
 | 
					})();
 | 
				
			||||||
@@ -104,6 +104,8 @@ const server = (function() {
 | 
				
			|||||||
        post,
 | 
					        post,
 | 
				
			||||||
        put,
 | 
					        put,
 | 
				
			||||||
        remove,
 | 
					        remove,
 | 
				
			||||||
        exec
 | 
					        exec,
 | 
				
			||||||
 | 
					        // don't remove, used from CKEditor image upload!
 | 
				
			||||||
 | 
					        getHeaders
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
})();
 | 
					})();
 | 
				
			||||||
@@ -116,5 +116,20 @@ async function stopWatch(what, func) {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function executeScript(script) {
 | 
					function executeScript(script) {
 | 
				
			||||||
    eval("(async function() {" + script + "})()");
 | 
					    // last \r\n is necessary if script contains line comment on its last line
 | 
				
			||||||
 | 
					    eval("(async function() {" + script + "\r\n})()");
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function formatValueWithWhitespace(val) {
 | 
				
			||||||
 | 
					    return /[^\w_-]/.test(val) ? '"' + val + '"' : val;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function formatAttribute(attr) {
 | 
				
			||||||
 | 
					    let str = "@" + formatValueWithWhitespace(attr.name);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (attr.value !== "") {
 | 
				
			||||||
 | 
					        str += "=" + formatValueWithWhitespace(attr.value);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return str;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
							
								
								
									
										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,12 +5,18 @@
 | 
				
			|||||||
    display: grid;
 | 
					    display: grid;
 | 
				
			||||||
    grid-template-areas: "header header"
 | 
					    grid-template-areas: "header header"
 | 
				
			||||||
                         "tree-actions title"
 | 
					                         "tree-actions title"
 | 
				
			||||||
 | 
					                         "search note-content"
 | 
				
			||||||
                         "tree note-content"
 | 
					                         "tree note-content"
 | 
				
			||||||
                         "parent-list note-content";
 | 
					                         "parent-list note-content"
 | 
				
			||||||
 | 
					                         "parent-list attribute-list";
 | 
				
			||||||
    grid-template-columns: 2fr 5fr;
 | 
					    grid-template-columns: 2fr 5fr;
 | 
				
			||||||
    grid-template-rows: auto
 | 
					    grid-template-rows: auto
 | 
				
			||||||
                        auto
 | 
					                        auto
 | 
				
			||||||
                        1fr;
 | 
					                        auto
 | 
				
			||||||
 | 
					                        1fr
 | 
				
			||||||
 | 
					                        auto
 | 
				
			||||||
 | 
					                        auto;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    justify-content: center;
 | 
					    justify-content: center;
 | 
				
			||||||
    grid-gap: 10px;
 | 
					    grid-gap: 10px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -108,7 +114,7 @@ span.fancytree-active:not(.fancytree-focused) .fancytree-title {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#header-title {
 | 
					#header-title {
 | 
				
			||||||
    padding: 5px 50px 5px 10px;
 | 
					    padding: 5px 20px 5px 10px;
 | 
				
			||||||
    font-size: large;
 | 
					    font-size: large;
 | 
				
			||||||
    font-weight: bold;
 | 
					    font-weight: bold;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -134,6 +140,7 @@ div.ui-tooltip {
 | 
				
			|||||||
    margin-left: 20px;
 | 
					    margin-left: 20px;
 | 
				
			||||||
    border-top: 2px solid #eee;
 | 
					    border-top: 2px solid #eee;
 | 
				
			||||||
    padding-top: 10px;
 | 
					    padding-top: 10px;
 | 
				
			||||||
 | 
					    grid-area: parent-list;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#parent-list ul {
 | 
					#parent-list ul {
 | 
				
			||||||
@@ -190,11 +197,6 @@ div.ui-tooltip {
 | 
				
			|||||||
    float: right;
 | 
					    float: right;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#note-id-display {
 | 
					 | 
				
			||||||
    color: lightgrey;
 | 
					 | 
				
			||||||
    margin-left: 10px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#note-source {
 | 
					#note-source {
 | 
				
			||||||
    height: 98%;
 | 
					    height: 98%;
 | 
				
			||||||
    width: 100%;
 | 
					    width: 100%;
 | 
				
			||||||
@@ -243,8 +245,9 @@ div.ui-tooltip {
 | 
				
			|||||||
#note-id-display {
 | 
					#note-id-display {
 | 
				
			||||||
    position: absolute;
 | 
					    position: absolute;
 | 
				
			||||||
    right: 10px;
 | 
					    right: 10px;
 | 
				
			||||||
    bottom: 5px;
 | 
					    bottom: 8px;
 | 
				
			||||||
    z-index: 1000;
 | 
					    z-index: 1000;
 | 
				
			||||||
 | 
					    color: lightgrey;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#note-type-dropdown {
 | 
					#note-type-dropdown {
 | 
				
			||||||
@@ -254,3 +257,15 @@ div.ui-tooltip {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.cm-matchhighlight {background-color: #eeeeee}
 | 
					.cm-matchhighlight {background-color: #eeeeee}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#attribute-list {
 | 
				
			||||||
 | 
					    grid-area: attribute-list;
 | 
				
			||||||
 | 
					    color: #777777;
 | 
				
			||||||
 | 
					    border-top: 1px solid #eee;
 | 
				
			||||||
 | 
					    padding: 5px; display: none;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#attribute-list button {
 | 
				
			||||||
 | 
					    padding: 2px;
 | 
				
			||||||
 | 
					    margin-right: 5px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -7,14 +7,15 @@ const auth = require('../../services/auth');
 | 
				
			|||||||
const sync_table = require('../../services/sync_table');
 | 
					const sync_table = require('../../services/sync_table');
 | 
				
			||||||
const utils = require('../../services/utils');
 | 
					const utils = require('../../services/utils');
 | 
				
			||||||
const wrap = require('express-promise-wrap').wrap;
 | 
					const wrap = require('express-promise-wrap').wrap;
 | 
				
			||||||
 | 
					const attributes = require('../../services/attributes');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
router.get('/:noteId/attributes', auth.checkApiAuth, wrap(async (req, res, next) => {
 | 
					router.get('/notes/:noteId/attributes', auth.checkApiAuth, wrap(async (req, res, next) => {
 | 
				
			||||||
    const noteId = req.params.noteId;
 | 
					    const noteId = req.params.noteId;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    res.send(await sql.getRows("SELECT * FROM attributes WHERE noteId = ? ORDER BY dateCreated", [noteId]));
 | 
					    res.send(await sql.getRows("SELECT * FROM attributes WHERE isDeleted = 0 AND noteId = ? ORDER BY position, dateCreated", [noteId]));
 | 
				
			||||||
}));
 | 
					}));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
router.put('/:noteId/attributes', auth.checkApiAuth, wrap(async (req, res, next) => {
 | 
					router.put('/notes/:noteId/attributes', auth.checkApiAuth, wrap(async (req, res, next) => {
 | 
				
			||||||
    const noteId = req.params.noteId;
 | 
					    const noteId = req.params.noteId;
 | 
				
			||||||
    const attributes = req.body;
 | 
					    const attributes = req.body;
 | 
				
			||||||
    const now = utils.nowDate();
 | 
					    const now = utils.nowDate();
 | 
				
			||||||
@@ -22,10 +23,15 @@ router.put('/:noteId/attributes', auth.checkApiAuth, wrap(async (req, res, next)
 | 
				
			|||||||
    await sql.doInTransaction(async () => {
 | 
					    await sql.doInTransaction(async () => {
 | 
				
			||||||
        for (const attr of attributes) {
 | 
					        for (const attr of attributes) {
 | 
				
			||||||
            if (attr.attributeId) {
 | 
					            if (attr.attributeId) {
 | 
				
			||||||
                await sql.execute("UPDATE attributes SET name = ?, value = ?, dateModified = ? WHERE attributeId = ?",
 | 
					                await sql.execute("UPDATE attributes SET name = ?, value = ?, dateModified = ?, isDeleted = ?, position = ? WHERE attributeId = ?",
 | 
				
			||||||
                    [attr.name, attr.value, now, attr.attributeId]);
 | 
					                    [attr.name, attr.value, now, attr.isDeleted, attr.position, attr.attributeId]);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            else {
 | 
					            else {
 | 
				
			||||||
 | 
					                // if it was "created" and then immediatelly deleted, we just don't create it at all
 | 
				
			||||||
 | 
					                if (attr.isDeleted) {
 | 
				
			||||||
 | 
					                    continue;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                attr.attributeId = utils.newAttributeId();
 | 
					                attr.attributeId = utils.newAttributeId();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                await sql.insert("attributes", {
 | 
					                await sql.insert("attributes", {
 | 
				
			||||||
@@ -33,8 +39,10 @@ router.put('/:noteId/attributes', auth.checkApiAuth, wrap(async (req, res, next)
 | 
				
			|||||||
                    noteId: noteId,
 | 
					                    noteId: noteId,
 | 
				
			||||||
                    name: attr.name,
 | 
					                    name: attr.name,
 | 
				
			||||||
                    value: attr.value,
 | 
					                    value: attr.value,
 | 
				
			||||||
 | 
					                    position: attr.position,
 | 
				
			||||||
                    dateCreated: now,
 | 
					                    dateCreated: now,
 | 
				
			||||||
                   dateModified: now
 | 
					                    dateModified: now,
 | 
				
			||||||
 | 
					                    isDeleted: false
 | 
				
			||||||
                });
 | 
					                });
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -42,7 +50,29 @@ router.put('/:noteId/attributes', auth.checkApiAuth, wrap(async (req, res, next)
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    res.send(await sql.getRows("SELECT * FROM attributes WHERE noteId = ? ORDER BY dateCreated", [noteId]));
 | 
					    res.send(await sql.getRows("SELECT * FROM attributes WHERE isDeleted = 0 AND noteId = ? ORDER BY position, dateCreated", [noteId]));
 | 
				
			||||||
 | 
					}));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					router.get('/attributes/names', auth.checkApiAuth, wrap(async (req, res, next) => {
 | 
				
			||||||
 | 
					    const names = await sql.getColumn("SELECT DISTINCT name FROM attributes WHERE isDeleted = 0");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (const attr of attributes.BUILTIN_ATTRIBUTES) {
 | 
				
			||||||
 | 
					        if (!names.includes(attr)) {
 | 
				
			||||||
 | 
					            names.push(attr);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    names.sort();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    res.send(names);
 | 
				
			||||||
 | 
					}));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					router.get('/attributes/values/:attributeName', auth.checkApiAuth, wrap(async (req, res, next) => {
 | 
				
			||||||
 | 
					    const attributeName = req.params.attributeName;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const values = await sql.getColumn("SELECT DISTINCT value FROM attributes WHERE isDeleted = 0 AND name = ? AND value != '' ORDER BY value", [attributeName]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    res.send(values);
 | 
				
			||||||
}));
 | 
					}));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module.exports = router;
 | 
					module.exports = router;
 | 
				
			||||||
@@ -4,16 +4,8 @@ const express = require('express');
 | 
				
			|||||||
const router = express.Router();
 | 
					const router = express.Router();
 | 
				
			||||||
const sql = require('../../services/sql');
 | 
					const sql = require('../../services/sql');
 | 
				
			||||||
const auth = require('../../services/auth');
 | 
					const auth = require('../../services/auth');
 | 
				
			||||||
const utils = require('../../services/utils');
 | 
					const image = require('../../services/image');
 | 
				
			||||||
const sync_table = require('../../services/sync_table');
 | 
					 | 
				
			||||||
const multer = require('multer')();
 | 
					const multer = require('multer')();
 | 
				
			||||||
const imagemin = require('imagemin');
 | 
					 | 
				
			||||||
const imageminMozJpeg = require('imagemin-mozjpeg');
 | 
					 | 
				
			||||||
const imageminPngQuant = require('imagemin-pngquant');
 | 
					 | 
				
			||||||
const imageminGifLossy = require('imagemin-giflossy');
 | 
					 | 
				
			||||||
const jimp = require('jimp');
 | 
					 | 
				
			||||||
const imageType = require('image-type');
 | 
					 | 
				
			||||||
const sanitizeFilename = require('sanitize-filename');
 | 
					 | 
				
			||||||
const wrap = require('express-promise-wrap').wrap;
 | 
					const wrap = require('express-promise-wrap').wrap;
 | 
				
			||||||
const RESOURCE_DIR = require('../../services/resource_dir').RESOURCE_DIR;
 | 
					const RESOURCE_DIR = require('../../services/resource_dir').RESOURCE_DIR;
 | 
				
			||||||
const fs = require('fs');
 | 
					const fs = require('fs');
 | 
				
			||||||
@@ -49,45 +41,7 @@ router.post('', auth.checkApiAuthOrElectron, multer.single('upload'), wrap(async
 | 
				
			|||||||
        return res.status(400).send("Unknown image type: " + file.mimetype);
 | 
					        return res.status(400).send("Unknown image type: " + file.mimetype);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const now = utils.nowDate();
 | 
					    const {fileName, imageId} = await image.saveImage(file, sourceId, noteId);
 | 
				
			||||||
 | 
					 | 
				
			||||||
    const resizedImage = await resize(file.buffer);
 | 
					 | 
				
			||||||
    const optimizedImage = await optimize(resizedImage);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const imageFormat = imageType(optimizedImage);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const fileNameWithouExtension = file.originalname.replace(/\.[^/.]+$/, "");
 | 
					 | 
				
			||||||
    const fileName = sanitizeFilename(fileNameWithouExtension + "." + imageFormat.ext);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const imageId = utils.newImageId();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    await sql.doInTransaction(async () => {
 | 
					 | 
				
			||||||
        await sql.insert("images", {
 | 
					 | 
				
			||||||
            imageId: imageId,
 | 
					 | 
				
			||||||
            format: imageFormat.ext,
 | 
					 | 
				
			||||||
            name: fileName,
 | 
					 | 
				
			||||||
            checksum: utils.hash(optimizedImage),
 | 
					 | 
				
			||||||
            data: optimizedImage,
 | 
					 | 
				
			||||||
            isDeleted: 0,
 | 
					 | 
				
			||||||
            dateModified: now,
 | 
					 | 
				
			||||||
            dateCreated: now
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        await sync_table.addImageSync(imageId, sourceId);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        const noteImageId = utils.newNoteImageId();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        await sql.insert("note_images", {
 | 
					 | 
				
			||||||
            noteImageId: noteImageId,
 | 
					 | 
				
			||||||
            noteId: noteId,
 | 
					 | 
				
			||||||
            imageId: imageId,
 | 
					 | 
				
			||||||
            isDeleted: 0,
 | 
					 | 
				
			||||||
            dateModified: now,
 | 
					 | 
				
			||||||
            dateCreated: now
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        await sync_table.addNoteImageSync(noteImageId, sourceId);
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    res.send({
 | 
					    res.send({
 | 
				
			||||||
        uploaded: true,
 | 
					        uploaded: true,
 | 
				
			||||||
@@ -95,54 +49,4 @@ router.post('', auth.checkApiAuthOrElectron, multer.single('upload'), wrap(async
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
}));
 | 
					}));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const MAX_SIZE = 1000;
 | 
					 | 
				
			||||||
const MAX_BYTE_SIZE = 200000; // images should have under 100 KBs
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function resize(buffer) {
 | 
					 | 
				
			||||||
    const image = await jimp.read(buffer);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (image.bitmap.width > image.bitmap.height && image.bitmap.width > MAX_SIZE) {
 | 
					 | 
				
			||||||
        image.resize(MAX_SIZE, jimp.AUTO);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    else if (image.bitmap.height > MAX_SIZE) {
 | 
					 | 
				
			||||||
        image.resize(jimp.AUTO, MAX_SIZE);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    else if (buffer.byteLength <= MAX_BYTE_SIZE) {
 | 
					 | 
				
			||||||
        return buffer;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // we do resizing with max quality which will be trimmed during optimization step next
 | 
					 | 
				
			||||||
    image.quality(100);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // when converting PNG to JPG we lose alpha channel, this is replaced by white to match Trilium white background
 | 
					 | 
				
			||||||
    image.background(0xFFFFFFFF);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // getBuffer doesn't support promises so this workaround
 | 
					 | 
				
			||||||
    return await new Promise((resolve, reject) => image.getBuffer(jimp.MIME_JPEG, (err, data) => {
 | 
					 | 
				
			||||||
        if (err) {
 | 
					 | 
				
			||||||
            reject(err);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        else {
 | 
					 | 
				
			||||||
            resolve(data);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }));
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function optimize(buffer) {
 | 
					 | 
				
			||||||
    return await imagemin.buffer(buffer, {
 | 
					 | 
				
			||||||
        plugins: [
 | 
					 | 
				
			||||||
            imageminMozJpeg({
 | 
					 | 
				
			||||||
                quality: 50
 | 
					 | 
				
			||||||
            }),
 | 
					 | 
				
			||||||
            imageminPngQuant({
 | 
					 | 
				
			||||||
                quality: "0-70"
 | 
					 | 
				
			||||||
            }),
 | 
					 | 
				
			||||||
            imageminGifLossy({
 | 
					 | 
				
			||||||
                lossy: 80,
 | 
					 | 
				
			||||||
                optimize: '3' // needs to be string
 | 
					 | 
				
			||||||
            })
 | 
					 | 
				
			||||||
        ]
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
module.exports = router;
 | 
					module.exports = router;
 | 
				
			||||||
@@ -66,7 +66,7 @@ async function importNotes(dir, parentNoteId) {
 | 
				
			|||||||
        const noteText = fs.readFileSync(path, "utf8");
 | 
					        const noteText = fs.readFileSync(path, "utf8");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const noteId = utils.newNoteId();
 | 
					        const noteId = utils.newNoteId();
 | 
				
			||||||
        const noteTreeId = utils.newnoteRevisionId();
 | 
					        const noteTreeId = utils.newNoteRevisionId();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const now = utils.nowDate();
 | 
					        const now = utils.nowDate();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,7 +14,7 @@ const wrap = require('express-promise-wrap').wrap;
 | 
				
			|||||||
router.post('/sync', wrap(async (req, res, next) => {
 | 
					router.post('/sync', wrap(async (req, res, next) => {
 | 
				
			||||||
    const timestampStr = req.body.timestamp;
 | 
					    const timestampStr = req.body.timestamp;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const timestamp = utils.parseDate(timestampStr);
 | 
					    const timestamp = utils.parseDateTime(timestampStr);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const now = new Date();
 | 
					    const now = new Date();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -58,15 +58,114 @@ router.put('/:noteId', auth.checkApiAuth, wrap(async (req, res, next) => {
 | 
				
			|||||||
}));
 | 
					}));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
router.get('/', auth.checkApiAuth, wrap(async (req, res, next) => {
 | 
					router.get('/', auth.checkApiAuth, wrap(async (req, res, next) => {
 | 
				
			||||||
    const search = '%' + utils.sanitizeSql(req.query.search) + '%';
 | 
					    let {attrFilters, searchText} = parseFilters(req.query.search);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // searching in protected notes is pointless because of encryption
 | 
					    const {query, params} = getSearchQuery(attrFilters, searchText);
 | 
				
			||||||
    const noteIds = await sql.getColumn(`SELECT noteId FROM notes 
 | 
					
 | 
				
			||||||
              WHERE isDeleted = 0 AND isProtected = 0 AND (title LIKE ? OR content LIKE ?)`, [search, search]);
 | 
					    console.log(query, params);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const noteIds = await sql.getColumn(query, params);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    res.send(noteIds);
 | 
					    res.send(noteIds);
 | 
				
			||||||
}));
 | 
					}));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function parseFilters(searchText) {
 | 
				
			||||||
 | 
					    const attrFilters = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const attrRegex = /(\b(and|or)\s+)?@(!?)([\w_-]+|"[^"]+")((=|!=|<|<=|>|>=)([\w_-]+|"[^"]+"))?/i;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let match = attrRegex.exec(searchText);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    function trimQuotes(str) { return str.startsWith('"') ? str.substr(1, str.length - 2) : str; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    while (match != null) {
 | 
				
			||||||
 | 
					        const relation = match[2] !== undefined ? match[2].toLowerCase() : 'and';
 | 
				
			||||||
 | 
					        const operator = match[3] === '!' ? 'not-exists' : 'exists';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        attrFilters.push({
 | 
				
			||||||
 | 
					            relation: relation,
 | 
				
			||||||
 | 
					            name: trimQuotes(match[4]),
 | 
				
			||||||
 | 
					            operator: match[6] !== undefined ? match[6] : operator,
 | 
				
			||||||
 | 
					            value: match[7] !== undefined ? trimQuotes(match[7]) : null
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // remove attributes from further fulltext search
 | 
				
			||||||
 | 
					        searchText = searchText.split(match[0]).join('');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        match = attrRegex.exec(searchText);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return {attrFilters, searchText};
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function getSearchQuery(attrFilters, searchText) {
 | 
				
			||||||
 | 
					    const joins = [];
 | 
				
			||||||
 | 
					    const joinParams = [];
 | 
				
			||||||
 | 
					    let where = '1';
 | 
				
			||||||
 | 
					    const whereParams = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let i = 1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (const filter of attrFilters) {
 | 
				
			||||||
 | 
					        joins.push(`LEFT JOIN attributes AS attr${i} ON attr${i}.noteId = notes.noteId AND attr${i}.name = ?`);
 | 
				
			||||||
 | 
					        joinParams.push(filter.name);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        where += " " + filter.relation + " ";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (filter.operator === 'exists') {
 | 
				
			||||||
 | 
					            where += `attr${i}.attributeId IS NOT NULL`;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        else if (filter.operator === 'not-exists') {
 | 
				
			||||||
 | 
					            where += `attr${i}.attributeId IS NULL`;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        else if (filter.operator === '=' || filter.operator === '!=') {
 | 
				
			||||||
 | 
					            where += `attr${i}.value ${filter.operator} ?`;
 | 
				
			||||||
 | 
					            whereParams.push(filter.value);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        else if ([">", ">=", "<", "<="].includes(filter.operator)) {
 | 
				
			||||||
 | 
					            const floatParam = parseFloat(filter.value);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (isNaN(floatParam)) {
 | 
				
			||||||
 | 
					                where += `attr${i}.value ${filter.operator} ?`;
 | 
				
			||||||
 | 
					                whereParams.push(filter.value);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            else {
 | 
				
			||||||
 | 
					                where += `CAST(attr${i}.value AS DECIMAL) ${filter.operator} ?`;
 | 
				
			||||||
 | 
					                whereParams.push(floatParam);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        else {
 | 
				
			||||||
 | 
					            throw new Error("Unknown operator " + filter.operator);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        i++;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let searchCondition = '';
 | 
				
			||||||
 | 
					    const searchParams = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (searchText.trim() !== '') {
 | 
				
			||||||
 | 
					        // searching in protected notes is pointless because of encryption
 | 
				
			||||||
 | 
					        searchCondition = ' AND (notes.isProtected = 0 AND (notes.title LIKE ? OR notes.content LIKE ?))';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        searchText = '%' + searchText.trim() + '%';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        searchParams.push(searchText);
 | 
				
			||||||
 | 
					        searchParams.push(searchText); // two occurences in searchCondition
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const query = `SELECT DISTINCT notes.noteId FROM notes
 | 
				
			||||||
 | 
					            ${joins.join('\r\n')}
 | 
				
			||||||
 | 
					              WHERE 
 | 
				
			||||||
 | 
					                notes.isDeleted = 0
 | 
				
			||||||
 | 
					                AND (${where}) 
 | 
				
			||||||
 | 
					                ${searchCondition}`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const params = joinParams.concat(whereParams).concat(searchParams);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return { query, params };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
router.put('/:noteId/sort', auth.checkApiAuth, wrap(async (req, res, next) => {
 | 
					router.put('/:noteId/sort', auth.checkApiAuth, wrap(async (req, res, next) => {
 | 
				
			||||||
    const noteId = req.params.noteId;
 | 
					    const noteId = req.params.noteId;
 | 
				
			||||||
    const sourceId = req.headers.source_id;
 | 
					    const sourceId = req.headers.source_id;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -45,7 +45,8 @@ async function getRecentNotes() {
 | 
				
			|||||||
        recent_notes.isDeleted = 0
 | 
					        recent_notes.isDeleted = 0
 | 
				
			||||||
        AND note_tree.isDeleted = 0
 | 
					        AND note_tree.isDeleted = 0
 | 
				
			||||||
      ORDER BY 
 | 
					      ORDER BY 
 | 
				
			||||||
        dateAccessed DESC`);
 | 
					        dateAccessed DESC
 | 
				
			||||||
 | 
					      LIMIT 200`);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module.exports = router;
 | 
					module.exports = router;
 | 
				
			||||||
@@ -19,11 +19,12 @@ router.post('/exec', auth.checkApiAuth, wrap(async (req, res, next) => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
router.get('/startup', auth.checkApiAuth, wrap(async (req, res, next) => {
 | 
					router.get('/startup', auth.checkApiAuth, wrap(async (req, res, next) => {
 | 
				
			||||||
    const noteIds = await attributes.getNoteIdsWithAttribute("run_on_startup");
 | 
					    const noteIds = await attributes.getNoteIdsWithAttribute("run_on_startup");
 | 
				
			||||||
 | 
					    const repository = new Repository(req);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const scripts = [];
 | 
					    const scripts = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for (const noteId of noteIds) {
 | 
					    for (const noteId of noteIds) {
 | 
				
			||||||
        scripts.push(await getNoteWithSubtreeScript(noteId, req));
 | 
					        scripts.push(await getNoteWithSubtreeScript(noteId, repository));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    res.send(scripts);
 | 
					    res.send(scripts);
 | 
				
			||||||
@@ -41,10 +42,10 @@ router.get('/subtree/:noteId', auth.checkApiAuth, wrap(async (req, res, next) =>
 | 
				
			|||||||
    res.send(subTreeScripts + noteScript);
 | 
					    res.send(subTreeScripts + noteScript);
 | 
				
			||||||
}));
 | 
					}));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function getNoteWithSubtreeScript(noteId, req) {
 | 
					async function getNoteWithSubtreeScript(noteId, repository) {
 | 
				
			||||||
    const noteScript = (await notes.getNoteById(noteId, req)).content;
 | 
					    const noteScript = (await repository.getNote(noteId)).content;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const subTreeScripts = await getSubTreeScripts(noteId, [noteId], req);
 | 
					    const subTreeScripts = await getSubTreeScripts(noteId, [noteId], repository);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return subTreeScripts + noteScript;
 | 
					    return subTreeScripts + noteScript;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										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]));
 | 
					    res.send(await sql.getRow("SELECT * FROM attributes WHERE attributeId = ?", [attributeId]));
 | 
				
			||||||
}));
 | 
					}));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					router.get('/api_tokens/:apiTokenId', auth.checkApiAuth, wrap(async (req, res, next) => {
 | 
				
			||||||
 | 
					    const apiTokenId = req.params.apiTokenId;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    res.send(await sql.getRow("SELECT * FROM api_tokens WHERE apiTokenId = ?", [apiTokenId]));
 | 
				
			||||||
 | 
					}));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
router.put('/notes', auth.checkApiAuth, wrap(async (req, res, next) => {
 | 
					router.put('/notes', auth.checkApiAuth, wrap(async (req, res, next) => {
 | 
				
			||||||
    await syncUpdate.updateNote(req.body.entity, req.body.sourceId);
 | 
					    await syncUpdate.updateNote(req.body.entity, req.body.sourceId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -201,4 +207,10 @@ router.put('/attributes', auth.checkApiAuth, wrap(async (req, res, next) => {
 | 
				
			|||||||
    res.send({});
 | 
					    res.send({});
 | 
				
			||||||
}));
 | 
					}));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					router.put('/api_tokens', auth.checkApiAuth, wrap(async (req, res, next) => {
 | 
				
			||||||
 | 
					    await syncUpdate.updateApiToken(req.body.entity, req.body.sourceId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    res.send({});
 | 
				
			||||||
 | 
					}));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module.exports = router;
 | 
					module.exports = router;
 | 
				
			||||||
@@ -29,8 +29,20 @@ router.get('/', auth.checkApiAuth, wrap(async (req, res, next) => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    protected_session.decryptNotes(req, notes);
 | 
					    protected_session.decryptNotes(req, notes);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const hiddenInAutocomplete = await sql.getColumn(`
 | 
				
			||||||
 | 
					      SELECT 
 | 
				
			||||||
 | 
					        DISTINCT noteId 
 | 
				
			||||||
 | 
					      FROM 
 | 
				
			||||||
 | 
					        attributes
 | 
				
			||||||
 | 
					        JOIN notes USING(noteId)
 | 
				
			||||||
 | 
					      WHERE
 | 
				
			||||||
 | 
					        attributes.name = 'hide_in_autocomplete' 
 | 
				
			||||||
 | 
					        AND attributes.isDeleted = 0
 | 
				
			||||||
 | 
					        AND notes.isDeleted = 0`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    res.send({
 | 
					    res.send({
 | 
				
			||||||
        notes: notes,
 | 
					        notes: notes,
 | 
				
			||||||
 | 
					        hiddenInAutocomplete: hiddenInAutocomplete,
 | 
				
			||||||
        start_note_path: await options.getOption('start_note_path')
 | 
					        start_note_path: await options.getOption('start_note_path')
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
}));
 | 
					}));
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -61,10 +61,8 @@ router.put('/:noteTreeId/move-before/:beforeNoteTreeId', auth.checkApiAuth, wrap
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        await sync_table.addNoteReorderingSync(beforeNote.parentNoteId, sourceId);
 | 
					        await sync_table.addNoteReorderingSync(beforeNote.parentNoteId, sourceId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const now = utils.nowDate();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        await sql.execute("UPDATE note_tree SET parentNoteId = ?, notePosition = ?, dateModified = ? WHERE noteTreeId = ?",
 | 
					        await sql.execute("UPDATE note_tree SET parentNoteId = ?, notePosition = ?, dateModified = ? WHERE noteTreeId = ?",
 | 
				
			||||||
            [beforeNote.parentNoteId, beforeNote.notePosition, now, noteTreeId]);
 | 
					            [beforeNote.parentNoteId, beforeNote.notePosition, utils.nowDate(), noteTreeId]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        await sync_table.addNoteTreeSync(noteTreeId, sourceId);
 | 
					        await sync_table.addNoteTreeSync(noteTreeId, sourceId);
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -28,6 +28,7 @@ const cleanupRoute = require('./api/cleanup');
 | 
				
			|||||||
const imageRoute = require('./api/image');
 | 
					const imageRoute = require('./api/image');
 | 
				
			||||||
const attributesRoute = require('./api/attributes');
 | 
					const attributesRoute = require('./api/attributes');
 | 
				
			||||||
const scriptRoute = require('./api/script');
 | 
					const scriptRoute = require('./api/script');
 | 
				
			||||||
 | 
					const senderRoute = require('./api/sender');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function register(app) {
 | 
					function register(app) {
 | 
				
			||||||
    app.use('/', indexRoute);
 | 
					    app.use('/', indexRoute);
 | 
				
			||||||
@@ -40,7 +41,7 @@ function register(app) {
 | 
				
			|||||||
    app.use('/api/notes', notesApiRoute);
 | 
					    app.use('/api/notes', notesApiRoute);
 | 
				
			||||||
    app.use('/api/tree', treeChangesApiRoute);
 | 
					    app.use('/api/tree', treeChangesApiRoute);
 | 
				
			||||||
    app.use('/api/notes', cloningApiRoute);
 | 
					    app.use('/api/notes', cloningApiRoute);
 | 
				
			||||||
    app.use('/api/notes', attributesRoute);
 | 
					    app.use('/api', attributesRoute);
 | 
				
			||||||
    app.use('/api/notes-history', noteHistoryApiRoute);
 | 
					    app.use('/api/notes-history', noteHistoryApiRoute);
 | 
				
			||||||
    app.use('/api/recent-changes', recentChangesApiRoute);
 | 
					    app.use('/api/recent-changes', recentChangesApiRoute);
 | 
				
			||||||
    app.use('/api/settings', settingsApiRoute);
 | 
					    app.use('/api/settings', settingsApiRoute);
 | 
				
			||||||
@@ -59,6 +60,7 @@ function register(app) {
 | 
				
			|||||||
    app.use('/api/cleanup', cleanupRoute);
 | 
					    app.use('/api/cleanup', cleanupRoute);
 | 
				
			||||||
    app.use('/api/images', imageRoute);
 | 
					    app.use('/api/images', imageRoute);
 | 
				
			||||||
    app.use('/api/script', scriptRoute);
 | 
					    app.use('/api/script', scriptRoute);
 | 
				
			||||||
 | 
					    app.use('/api/sender', senderRoute);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module.exports = {
 | 
					module.exports = {
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										13
									
								
								src/scripts/today.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/scripts/today.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,13 @@
 | 
				
			|||||||
 | 
					api.addButtonToToolbar('go-today', $('<button class="btn btn-xs" onclick="goToday();"><span class="ui-icon ui-icon-calendar"></span> Today</button>'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					window.goToday = async function() {
 | 
				
			||||||
 | 
					    const todayDateStr = formatDateISO(new Date());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const todayNoteId = await server.exec([todayDateStr], async todayDateStr => {
 | 
				
			||||||
 | 
					        return await this.getDateNoteId(todayDateStr);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    api.activateNote(todayNoteId);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					$(document).bind('keydown', "alt+t", window.goToday);
 | 
				
			||||||
@@ -40,7 +40,8 @@
 | 
				
			|||||||
                await this.createNote(parentNoteId, 'data', jsonContent, {
 | 
					                await this.createNote(parentNoteId, 'data', jsonContent, {
 | 
				
			||||||
                    json: true,
 | 
					                    json: true,
 | 
				
			||||||
                    attributes: {
 | 
					                    attributes: {
 | 
				
			||||||
                        date_data: date
 | 
					                        date_data: date,
 | 
				
			||||||
 | 
					                        hide_in_autocomplete: null
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                });
 | 
					                });
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
@@ -65,10 +66,14 @@
 | 
				
			|||||||
                });
 | 
					                });
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            data.sort((a, b) => a.date < b.date ? -1 : +1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return data;
 | 
					            return data;
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        var config = {
 | 
					        const ctx = $("#canvas")[0].getContext("2d");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        new Chart(ctx, {
 | 
				
			||||||
            type: 'line',
 | 
					            type: 'line',
 | 
				
			||||||
            data: {
 | 
					            data: {
 | 
				
			||||||
                labels: data.map(row => row.date),
 | 
					                labels: data.map(row => row.date),
 | 
				
			||||||
@@ -80,10 +85,7 @@
 | 
				
			|||||||
                    fill: false
 | 
					                    fill: false
 | 
				
			||||||
                }]
 | 
					                }]
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        };
 | 
					        });
 | 
				
			||||||
 | 
					 | 
				
			||||||
        var ctx = $("#canvas")[0].getContext("2d");
 | 
					 | 
				
			||||||
        new Chart(ctx, config);
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    $("#weight-form").submit(event => {
 | 
					    $("#weight-form").submit(event => {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,7 +3,7 @@
 | 
				
			|||||||
const build = require('./build');
 | 
					const build = require('./build');
 | 
				
			||||||
const packageJson = require('../../package');
 | 
					const packageJson = require('../../package');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const APP_DB_VERSION = 71;
 | 
					const APP_DB_VERSION = 76;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module.exports = {
 | 
					module.exports = {
 | 
				
			||||||
    app_version: packageJson.version,
 | 
					    app_version: packageJson.version,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,13 +5,23 @@ const utils = require('./utils');
 | 
				
			|||||||
const sync_table = require('./sync_table');
 | 
					const sync_table = require('./sync_table');
 | 
				
			||||||
const Repository = require('./repository');
 | 
					const Repository = require('./repository');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const BUILTIN_ATTRIBUTES = [
 | 
				
			||||||
 | 
					    'run_on_startup',
 | 
				
			||||||
 | 
					    'disable_versioning',
 | 
				
			||||||
 | 
					    'calendar_root',
 | 
				
			||||||
 | 
					    'hide_in_autocomplete'
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function getNoteAttributeMap(noteId) {
 | 
					async function getNoteAttributeMap(noteId) {
 | 
				
			||||||
    return await sql.getMap(`SELECT name, value FROM attributes WHERE noteId = ?`, [noteId]);
 | 
					    return await sql.getMap(`SELECT name, value FROM attributes WHERE noteId = ?`, [noteId]);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function getNoteIdWithAttribute(name, value) {
 | 
					async function getNoteIdWithAttribute(name, value) {
 | 
				
			||||||
    return await sql.getValue(`SELECT notes.noteId FROM notes JOIN attributes USING(noteId) 
 | 
					    return await sql.getValue(`SELECT notes.noteId FROM notes JOIN attributes USING(noteId) 
 | 
				
			||||||
          WHERE notes.isDeleted = 0 AND attributes.name = ? AND attributes.value = ?`, [name, value]);
 | 
					          WHERE notes.isDeleted = 0
 | 
				
			||||||
 | 
					                AND attributes.isDeleted = 0
 | 
				
			||||||
 | 
					                AND attributes.name = ? 
 | 
				
			||||||
 | 
					                AND attributes.value = ?`, [name, value]);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function getNotesWithAttribute(dataKey, name, value) {
 | 
					async function getNotesWithAttribute(dataKey, name, value) {
 | 
				
			||||||
@@ -21,11 +31,11 @@ async function getNotesWithAttribute(dataKey, name, value) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    if (value !== undefined) {
 | 
					    if (value !== undefined) {
 | 
				
			||||||
        notes = await repository.getEntities(`SELECT notes.* FROM notes JOIN attributes USING(noteId) 
 | 
					        notes = await repository.getEntities(`SELECT notes.* FROM notes JOIN attributes USING(noteId) 
 | 
				
			||||||
          WHERE notes.isDeleted = 0 AND attributes.name = ? AND attributes.value = ?`, [name, value]);
 | 
					          WHERE notes.isDeleted = 0 AND attributes.isDeleted = 0 AND attributes.name = ? AND attributes.value = ?`, [name, value]);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    else {
 | 
					    else {
 | 
				
			||||||
        notes = await repository.getEntities(`SELECT notes.* FROM notes JOIN attributes USING(noteId) 
 | 
					        notes = await repository.getEntities(`SELECT notes.* FROM notes JOIN attributes USING(noteId) 
 | 
				
			||||||
          WHERE notes.isDeleted = 0 AND attributes.name = ?`, [name]);
 | 
					          WHERE notes.isDeleted = 0 AND attributes.isDeleted = 0 AND attributes.name = ?`, [name]);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return notes;
 | 
					    return notes;
 | 
				
			||||||
@@ -39,7 +49,7 @@ async function getNoteWithAttribute(dataKey, name, value) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
async function getNoteIdsWithAttribute(name) {
 | 
					async function getNoteIdsWithAttribute(name) {
 | 
				
			||||||
    return await sql.getColumn(`SELECT DISTINCT notes.noteId FROM notes JOIN attributes USING(noteId) 
 | 
					    return await sql.getColumn(`SELECT DISTINCT notes.noteId FROM notes JOIN attributes USING(noteId) 
 | 
				
			||||||
          WHERE notes.isDeleted = 0 AND attributes.name = ?`, [name]);
 | 
					          WHERE notes.isDeleted = 0 AND attributes.isDeleted = 0 AND attributes.name = ? AND attributes.isDeleted = 0`, [name]);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function createAttribute(noteId, name, value = null, sourceId = null) {
 | 
					async function createAttribute(noteId, name, value = null, sourceId = null) {
 | 
				
			||||||
@@ -52,7 +62,8 @@ async function createAttribute(noteId, name, value = null, sourceId = null) {
 | 
				
			|||||||
        name: name,
 | 
					        name: name,
 | 
				
			||||||
        value: value,
 | 
					        value: value,
 | 
				
			||||||
        dateModified: now,
 | 
					        dateModified: now,
 | 
				
			||||||
        dateCreated: now
 | 
					        dateCreated: now,
 | 
				
			||||||
 | 
					        isDeleted: false
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await sync_table.addAttributeSync(attributeId, sourceId);
 | 
					    await sync_table.addAttributeSync(attributeId, sourceId);
 | 
				
			||||||
@@ -64,5 +75,6 @@ module.exports = {
 | 
				
			|||||||
    getNotesWithAttribute,
 | 
					    getNotesWithAttribute,
 | 
				
			||||||
    getNoteWithAttribute,
 | 
					    getNoteWithAttribute,
 | 
				
			||||||
    getNoteIdsWithAttribute,
 | 
					    getNoteIdsWithAttribute,
 | 
				
			||||||
    createAttribute
 | 
					    createAttribute,
 | 
				
			||||||
 | 
					    BUILTIN_ATTRIBUTES
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
@@ -10,7 +10,7 @@ const sync_mutex = require('./sync_mutex');
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
async function regularBackup() {
 | 
					async function regularBackup() {
 | 
				
			||||||
    const now = new Date();
 | 
					    const now = new Date();
 | 
				
			||||||
    const lastBackupDate = utils.parseDate(await options.getOption('last_backup_date'));
 | 
					    const lastBackupDate = utils.parseDateTime(await options.getOption('last_backup_date'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    console.log(lastBackupDate);
 | 
					    console.log(lastBackupDate);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -223,6 +223,8 @@ async function runAllChecks() {
 | 
				
			|||||||
    await runSyncRowChecks("recent_notes", "noteTreeId", errorList);
 | 
					    await runSyncRowChecks("recent_notes", "noteTreeId", errorList);
 | 
				
			||||||
    await runSyncRowChecks("images", "imageId", errorList);
 | 
					    await runSyncRowChecks("images", "imageId", errorList);
 | 
				
			||||||
    await runSyncRowChecks("note_images", "noteImageId", errorList);
 | 
					    await runSyncRowChecks("note_images", "noteImageId", errorList);
 | 
				
			||||||
 | 
					    await runSyncRowChecks("attributes", "attributeId", errorList);
 | 
				
			||||||
 | 
					    await runSyncRowChecks("api_tokens", "apiTokenId", errorList);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (errorList.length === 0) {
 | 
					    if (errorList.length === 0) {
 | 
				
			||||||
        // we run this only if basic checks passed since this assumes basic data consistency
 | 
					        // we run this only if basic checks passed since this assumes basic data consistency
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,12 +3,16 @@
 | 
				
			|||||||
const sql = require('./sql');
 | 
					const sql = require('./sql');
 | 
				
			||||||
const notes = require('./notes');
 | 
					const notes = require('./notes');
 | 
				
			||||||
const attributes = require('./attributes');
 | 
					const attributes = require('./attributes');
 | 
				
			||||||
 | 
					const utils = require('./utils');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const CALENDAR_ROOT_ATTRIBUTE = 'calendar_root';
 | 
					const CALENDAR_ROOT_ATTRIBUTE = 'calendar_root';
 | 
				
			||||||
const YEAR_ATTRIBUTE = 'year_note';
 | 
					const YEAR_ATTRIBUTE = 'year_note';
 | 
				
			||||||
const MONTH_ATTRIBUTE = 'month_note';
 | 
					const MONTH_ATTRIBUTE = 'month_note';
 | 
				
			||||||
const DATE_ATTRIBUTE = 'date_note';
 | 
					const DATE_ATTRIBUTE = 'date_note';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const DAYS = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'];
 | 
				
			||||||
 | 
					const MONTHS = ['January','February','March','April','May','June','July','August','September','October','November','December'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function createNote(parentNoteId, noteTitle, noteText) {
 | 
					async function createNote(parentNoteId, noteTitle, noteText) {
 | 
				
			||||||
    return (await notes.createNewNote(parentNoteId, {
 | 
					    return (await notes.createNewNote(parentNoteId, {
 | 
				
			||||||
        title: noteTitle,
 | 
					        title: noteTitle,
 | 
				
			||||||
@@ -72,7 +76,11 @@ async function getMonthNoteId(dateTimeStr, rootNoteId) {
 | 
				
			|||||||
        monthNoteId = await getNoteStartingWith(yearNoteId, monthNumber);
 | 
					        monthNoteId = await getNoteStartingWith(yearNoteId, monthNumber);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (!monthNoteId) {
 | 
					        if (!monthNoteId) {
 | 
				
			||||||
            monthNoteId = await createNote(yearNoteId, monthNumber);
 | 
					            const dateObj = utils.parseDate(dateTimeStr);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            const noteTitle = monthNumber + " - " + MONTHS[dateObj.getMonth()];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            monthNoteId = await createNote(yearNoteId, noteTitle);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        await attributes.createAttribute(monthNoteId, MONTH_ATTRIBUTE, monthStr);
 | 
					        await attributes.createAttribute(monthNoteId, MONTH_ATTRIBUTE, monthStr);
 | 
				
			||||||
@@ -97,7 +105,11 @@ async function getDateNoteId(dateTimeStr, rootNoteId = null) {
 | 
				
			|||||||
        dateNoteId = await getNoteStartingWith(monthNoteId, dayNumber);
 | 
					        dateNoteId = await getNoteStartingWith(monthNoteId, dayNumber);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (!dateNoteId) {
 | 
					        if (!dateNoteId) {
 | 
				
			||||||
            dateNoteId = await createNote(monthNoteId, dayNumber);
 | 
					            const dateObj = utils.parseDate(dateTimeStr);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            const noteTitle = dayNumber + " - " + DAYS[dateObj.getDay()];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            dateNoteId = await createNote(monthNoteId, noteTitle);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        await attributes.createAttribute(dateNoteId, DATE_ATTRIBUTE, dateStr);
 | 
					        await attributes.createAttribute(dateNoteId, DATE_ATTRIBUTE, dateStr);
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										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
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@@ -5,32 +5,6 @@ const sync_table = require('./sync_table');
 | 
				
			|||||||
const attributes = require('./attributes');
 | 
					const attributes = require('./attributes');
 | 
				
			||||||
const protected_session = require('./protected_session');
 | 
					const protected_session = require('./protected_session');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function updateJsonNote(noteId, data) {
 | 
					 | 
				
			||||||
    const ret = await createNewNote(noteId, {
 | 
					 | 
				
			||||||
        title: name,
 | 
					 | 
				
			||||||
        content: JSON.stringify(data),
 | 
					 | 
				
			||||||
        target: 'into',
 | 
					 | 
				
			||||||
        isProtected: false,
 | 
					 | 
				
			||||||
        type: 'code',
 | 
					 | 
				
			||||||
        mime: 'application/json'
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return ret.noteId;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function createNewJsonNote(parentNoteId, name, payload) {
 | 
					 | 
				
			||||||
    const ret = await createNewNote(parentNoteId, {
 | 
					 | 
				
			||||||
        title: name,
 | 
					 | 
				
			||||||
        content: JSON.stringify(payload),
 | 
					 | 
				
			||||||
        target: 'into',
 | 
					 | 
				
			||||||
        isProtected: false,
 | 
					 | 
				
			||||||
        type: 'code',
 | 
					 | 
				
			||||||
        mime: 'application/json'
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return ret.noteId;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function createNewNote(parentNoteId, noteOpts, dataKey, sourceId) {
 | 
					async function createNewNote(parentNoteId, noteOpts, dataKey, sourceId) {
 | 
				
			||||||
    const noteId = utils.newNoteId();
 | 
					    const noteId = utils.newNoteId();
 | 
				
			||||||
    const noteTreeId = utils.newNoteTreeId();
 | 
					    const noteTreeId = utils.newNoteTreeId();
 | 
				
			||||||
@@ -180,10 +154,10 @@ async function saveNoteHistory(noteId, dataKey, sourceId, nowStr) {
 | 
				
			|||||||
        note.isProtected = false;
 | 
					        note.isProtected = false;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const newnoteRevisionId = utils.newnoteRevisionId();
 | 
					    const newNoteRevisionId = utils.newNoteRevisionId();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await sql.insert('note_revisions', {
 | 
					    await sql.insert('note_revisions', {
 | 
				
			||||||
        noteRevisionId: newnoteRevisionId,
 | 
					        noteRevisionId: newNoteRevisionId,
 | 
				
			||||||
        noteId: noteId,
 | 
					        noteId: noteId,
 | 
				
			||||||
        // title and text should be decrypted now
 | 
					        // title and text should be decrypted now
 | 
				
			||||||
        title: oldNote.title,
 | 
					        title: oldNote.title,
 | 
				
			||||||
@@ -193,7 +167,7 @@ async function saveNoteHistory(noteId, dataKey, sourceId, nowStr) {
 | 
				
			|||||||
        dateModifiedTo: nowStr
 | 
					        dateModifiedTo: nowStr
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await sync_table.addNoteHistorySync(newnoteRevisionId, sourceId);
 | 
					    await sync_table.addNoteHistorySync(newNoteRevisionId, sourceId);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function saveNoteImages(noteId, noteText, sourceId) {
 | 
					async function saveNoteImages(noteId, noteText, sourceId) {
 | 
				
			||||||
@@ -261,7 +235,7 @@ async function updateNote(noteId, newNote, dataKey, sourceId) {
 | 
				
			|||||||
        "SELECT noteRevisionId FROM note_revisions WHERE noteId = ? AND dateModifiedTo >= ?", [noteId, historyCutoff]);
 | 
					        "SELECT noteRevisionId FROM note_revisions WHERE noteId = ? AND dateModifiedTo >= ?", [noteId, historyCutoff]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await sql.doInTransaction(async () => {
 | 
					    await sql.doInTransaction(async () => {
 | 
				
			||||||
        const msSinceDateCreated = now.getTime() - utils.parseDate(newNote.detail.dateCreated).getTime();
 | 
					        const msSinceDateCreated = now.getTime() - utils.parseDateTime(newNote.detail.dateCreated).getTime();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (attributesMap.disable_versioning !== 'true'
 | 
					        if (attributesMap.disable_versioning !== 'true'
 | 
				
			||||||
            && !existingnoteRevisionId
 | 
					            && !existingnoteRevisionId
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -54,6 +54,8 @@ function ScriptContext(noteId, dataKey) {
 | 
				
			|||||||
        return noteId;
 | 
					        return noteId;
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.createAttribute = attributes.createAttribute;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.updateEntity = this.repository.updateEntity;
 | 
					    this.updateEntity = this.repository.updateEntity;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.log = function(message) {
 | 
					    this.log = function(message) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -149,6 +149,9 @@ async function pullSync(syncContext) {
 | 
				
			|||||||
        else if (sync.entityName === 'attributes') {
 | 
					        else if (sync.entityName === 'attributes') {
 | 
				
			||||||
            await syncUpdate.updateAttribute(resp, syncContext.sourceId);
 | 
					            await syncUpdate.updateAttribute(resp, syncContext.sourceId);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					        else if (sync.entityName === 'api_tokens') {
 | 
				
			||||||
 | 
					            await syncUpdate.updateApiToken(resp, syncContext.sourceId);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
        else {
 | 
					        else {
 | 
				
			||||||
            throw new Error(`Unrecognized entity type ${sync.entityName} in sync #${sync.id}`);
 | 
					            throw new Error(`Unrecognized entity type ${sync.entityName} in sync #${sync.id}`);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -233,6 +236,9 @@ async function pushEntity(sync, syncContext) {
 | 
				
			|||||||
    else if (sync.entityName === 'attributes') {
 | 
					    else if (sync.entityName === 'attributes') {
 | 
				
			||||||
        entity = await sql.getRow('SELECT * FROM attributes WHERE attributeId = ?', [sync.entityId]);
 | 
					        entity = await sql.getRow('SELECT * FROM attributes WHERE attributeId = ?', [sync.entityId]);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    else if (sync.entityName === 'api_tokens') {
 | 
				
			||||||
 | 
					        entity = await sql.getRow('SELECT * FROM api_tokens WHERE apiTokenId = ?', [sync.entityId]);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    else {
 | 
					    else {
 | 
				
			||||||
        throw new Error(`Unrecognized entity type ${sync.entityName} in sync #${sync.id}`);
 | 
					        throw new Error(`Unrecognized entity type ${sync.entityName} in sync #${sync.id}`);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -40,6 +40,10 @@ async function addAttributeSync(attributeId, sourceId) {
 | 
				
			|||||||
    await addEntitySync("attributes", attributeId, sourceId);
 | 
					    await addEntitySync("attributes", attributeId, sourceId);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function addApiTokenSync(apiTokenId, sourceId) {
 | 
				
			||||||
 | 
					    await addEntitySync("api_tokens", apiTokenId, sourceId);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function addEntitySync(entityName, entityId, sourceId) {
 | 
					async function addEntitySync(entityName, entityId, sourceId) {
 | 
				
			||||||
    await sql.replace("sync", {
 | 
					    await sql.replace("sync", {
 | 
				
			||||||
        entityName: entityName,
 | 
					        entityName: entityName,
 | 
				
			||||||
@@ -93,6 +97,7 @@ async function fillAllSyncRows() {
 | 
				
			|||||||
    await fillSyncRows("images", "imageId");
 | 
					    await fillSyncRows("images", "imageId");
 | 
				
			||||||
    await fillSyncRows("note_images", "noteImageId");
 | 
					    await fillSyncRows("note_images", "noteImageId");
 | 
				
			||||||
    await fillSyncRows("attributes", "attributeId");
 | 
					    await fillSyncRows("attributes", "attributeId");
 | 
				
			||||||
 | 
					    await fillSyncRows("api_tokens", "apiTokenId");
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module.exports = {
 | 
					module.exports = {
 | 
				
			||||||
@@ -105,6 +110,7 @@ module.exports = {
 | 
				
			|||||||
    addImageSync,
 | 
					    addImageSync,
 | 
				
			||||||
    addNoteImageSync,
 | 
					    addNoteImageSync,
 | 
				
			||||||
    addAttributeSync,
 | 
					    addAttributeSync,
 | 
				
			||||||
 | 
					    addApiTokenSync,
 | 
				
			||||||
    addEntitySync,
 | 
					    addEntitySync,
 | 
				
			||||||
    cleanupSyncRowsForMissingEntities,
 | 
					    cleanupSyncRowsForMissingEntities,
 | 
				
			||||||
    fillAllSyncRows
 | 
					    fillAllSyncRows
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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 = {
 | 
					module.exports = {
 | 
				
			||||||
    updateNote,
 | 
					    updateNote,
 | 
				
			||||||
    updateNoteTree,
 | 
					    updateNoteTree,
 | 
				
			||||||
@@ -146,5 +160,6 @@ module.exports = {
 | 
				
			|||||||
    updateRecentNotes,
 | 
					    updateRecentNotes,
 | 
				
			||||||
    updateImage,
 | 
					    updateImage,
 | 
				
			||||||
    updateNoteImage,
 | 
					    updateNoteImage,
 | 
				
			||||||
    updateAttribute
 | 
					    updateAttribute,
 | 
				
			||||||
 | 
					    updateApiToken
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
@@ -2,6 +2,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
const sql = require('./sql');
 | 
					const sql = require('./sql');
 | 
				
			||||||
const sync_table = require('./sync_table');
 | 
					const sync_table = require('./sync_table');
 | 
				
			||||||
 | 
					const protected_session = require('./protected_session');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function validateParentChild(res, parentNoteId, childNoteId, noteTreeId = null) {
 | 
					async function validateParentChild(res, parentNoteId, childNoteId, noteTreeId = null) {
 | 
				
			||||||
    const existing = await getExistingNoteTree(parentNoteId, childNoteId);
 | 
					    const existing = await getExistingNoteTree(parentNoteId, childNoteId);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,7 +11,7 @@ function newNoteTreeId() {
 | 
				
			|||||||
    return randomString(12);
 | 
					    return randomString(12);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function newnoteRevisionId() {
 | 
					function newNoteRevisionId() {
 | 
				
			||||||
    return randomString(12);
 | 
					    return randomString(12);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -27,6 +27,10 @@ function newAttributeId() {
 | 
				
			|||||||
    return randomString(12);
 | 
					    return randomString(12);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function newApiTokenId() {
 | 
				
			||||||
 | 
					    return randomString(12);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function randomString(length) {
 | 
					function randomString(length) {
 | 
				
			||||||
    return randtoken.generate(length);
 | 
					    return randtoken.generate(length);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -39,6 +43,14 @@ function nowDate() {
 | 
				
			|||||||
    return dateStr(new Date());
 | 
					    return dateStr(new Date());
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function localDate() {
 | 
				
			||||||
 | 
					    const date = new Date();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return date.getFullYear() + "-"
 | 
				
			||||||
 | 
					        + (date.getMonth() < 9 ? "0" : "") + (date.getMonth() + 1) + "-"
 | 
				
			||||||
 | 
					        + (date.getDate() < 10 ? "0" : "") + date.getDate();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function dateStr(date) {
 | 
					function dateStr(date) {
 | 
				
			||||||
    return date.toISOString();
 | 
					    return date.toISOString();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -47,7 +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().
 | 
					 * @param str - needs to be in the ISO 8601 format "YYYY-MM-DDTHH:MM:SS.sssZ" format as outputted by dateStr().
 | 
				
			||||||
 *              also is assumed to be GMT time (as indicated by the "Z" at the end), *not* local time
 | 
					 *              also is assumed to be GMT time (as indicated by the "Z" at the end), *not* local time
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
function parseDate(str) {
 | 
					function parseDateTime(str) {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
        return new Date(Date.parse(str));
 | 
					        return new Date(Date.parse(str));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -56,6 +68,12 @@ function parseDate(str) {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function parseDate(str) {
 | 
				
			||||||
 | 
					    const datePart = str.substr(0, 10);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return parseDateTime(datePart + "T12:00:00.000Z");
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function toBase64(plainText) {
 | 
					function toBase64(plainText) {
 | 
				
			||||||
    return Buffer.from(plainText).toString('base64');
 | 
					    return Buffer.from(plainText).toString('base64');
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -115,14 +133,17 @@ module.exports = {
 | 
				
			|||||||
    randomSecureToken,
 | 
					    randomSecureToken,
 | 
				
			||||||
    randomString,
 | 
					    randomString,
 | 
				
			||||||
    nowDate,
 | 
					    nowDate,
 | 
				
			||||||
 | 
					    localDate,
 | 
				
			||||||
    dateStr,
 | 
					    dateStr,
 | 
				
			||||||
    parseDate,
 | 
					    parseDate,
 | 
				
			||||||
 | 
					    parseDateTime,
 | 
				
			||||||
    newNoteId,
 | 
					    newNoteId,
 | 
				
			||||||
    newNoteTreeId,
 | 
					    newNoteTreeId,
 | 
				
			||||||
    newnoteRevisionId,
 | 
					    newNoteRevisionId,
 | 
				
			||||||
    newImageId,
 | 
					    newImageId,
 | 
				
			||||||
    newNoteImageId,
 | 
					    newNoteImageId,
 | 
				
			||||||
    newAttributeId,
 | 
					    newAttributeId,
 | 
				
			||||||
 | 
					    newApiTokenId,
 | 
				
			||||||
    toBase64,
 | 
					    toBase64,
 | 
				
			||||||
    fromBase64,
 | 
					    fromBase64,
 | 
				
			||||||
    hmac,
 | 
					    hmac,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -17,15 +17,20 @@
 | 
				
			|||||||
          <button class="btn btn-xs" onclick="jumpToNote.showDialog();" title="CTRL+J">Jump to note</button>
 | 
					          <button class="btn btn-xs" onclick="jumpToNote.showDialog();" title="CTRL+J">Jump to note</button>
 | 
				
			||||||
          <button class="btn btn-xs" onclick="recentNotes.showDialog();" title="CTRL+E">Recent notes</button>
 | 
					          <button class="btn btn-xs" onclick="recentNotes.showDialog();" title="CTRL+E">Recent notes</button>
 | 
				
			||||||
          <button class="btn btn-xs" onclick="recentChanges.showDialog();">Recent changes</button>
 | 
					          <button class="btn btn-xs" onclick="recentChanges.showDialog();">Recent changes</button>
 | 
				
			||||||
          <button class="btn btn-xs" onclick="eventLog.showDialog();">Event log</button>
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <div id="plugin-buttons">
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <div>
 | 
					        <div>
 | 
				
			||||||
          <button class="btn btn-xs" onclick="syncNow();" title="Number of outstanding changes to be pushed to server">
 | 
					          <button class="btn btn-xs" onclick="syncNow();" title="Number of outstanding changes to be pushed to server">
 | 
				
			||||||
 | 
					            <span class="ui-icon ui-icon-refresh"></span>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            Sync now (<span id="changes-to-push-count">0</span>)
 | 
					            Sync now (<span id="changes-to-push-count">0</span>)
 | 
				
			||||||
          </button>
 | 
					          </button>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          <button class="btn btn-xs" onclick="settings.showDialog();">Settings</button>
 | 
					          <button class="btn btn-xs" onclick="settings.showDialog();">
 | 
				
			||||||
 | 
					            <span class="ui-icon ui-icon-gear"></span> Settings</button>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          <form action="logout" id="logout-button" method="POST" style="display: inline;">
 | 
					          <form action="logout" id="logout-button" method="POST" style="display: inline;">
 | 
				
			||||||
            <input type="submit" class="btn btn-xs" value="Logout">
 | 
					            <input type="submit" class="btn btn-xs" value="Logout">
 | 
				
			||||||
@@ -51,14 +56,13 @@
 | 
				
			|||||||
            <img src="images/icons/search.png" alt="Search in notes"/>
 | 
					            <img src="images/icons/search.png" alt="Search in notes"/>
 | 
				
			||||||
          </a>
 | 
					          </a>
 | 
				
			||||||
        </div>
 | 
					        </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;">
 | 
				
			||||||
          <p>
 | 
					        <div style="display: flex; align-items: center;">
 | 
				
			||||||
          <label>Search:</label>
 | 
					          <label>Search:</label>
 | 
				
			||||||
            <input name="search-text" autocomplete="off">
 | 
					          <input name="search-text" style="flex-grow: 100; margin-left: 5px; margin-right: 5px;" autocomplete="off">
 | 
				
			||||||
            <button id="reset-search-button">×</button>
 | 
					          <button id="reset-search-button" class="btn btn-sm" title="Reset search">×</button>
 | 
				
			||||||
            <span id="matches"></span>
 | 
					 | 
				
			||||||
          </p>
 | 
					 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -68,7 +72,7 @@
 | 
				
			|||||||
      <div id="parent-list" class="hide-toggle">
 | 
					      <div id="parent-list" class="hide-toggle">
 | 
				
			||||||
        <p><strong>Note locations:</strong></p>
 | 
					        <p><strong>Note locations:</strong></p>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <ul id="parent-list-list"></ul>
 | 
					        <ul id="parent-list-inner"></ul>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <div class="hide-toggle" style="grid-area: title;">
 | 
					      <div class="hide-toggle" style="grid-area: title;">
 | 
				
			||||||
@@ -138,23 +142,16 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        <div id="note-detail-render"></div>
 | 
					        <div id="note-detail-render"></div>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <div id="attribute-list">
 | 
				
			||||||
 | 
					        <button class="btn btn-sm" onclick="attributesDialog.showDialog();">Attributes:</button>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <span id="attribute-list-inner"></span>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <div id="recent-notes-dialog" title="Recent notes" style="display: none;">
 | 
					    <div id="recent-notes-dialog" title="Recent notes" style="display: none;">
 | 
				
			||||||
      <select id="recent-notes-select-box" size="20" style="width: 100%">
 | 
					      <input id="recent-notes-search-input" class="form-control"/>
 | 
				
			||||||
      </select>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      <br/><br/>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      <p>
 | 
					 | 
				
			||||||
        <button class="btn btn-sm" id="recent-notes-jump-to">Jump to <kbd>enter</kbd></button>
 | 
					 | 
				
			||||||
         
 | 
					 | 
				
			||||||
        <button class="btn btn-sm" id="recent-notes-add-link">Add link <kbd>l</kbd></button>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <button class="btn btn-sm" id="recent-notes-add-current-as-child">Add current as child <kbd>c</kbd></button>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <button class="btn btn-sm" id="recent-notes-add-recent-as-child">Add recent as child <kbd>r</kbd></button>
 | 
					 | 
				
			||||||
      </p>
 | 
					 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <div id="add-link-dialog" title="Add link" style="display: none;">
 | 
					    <div id="add-link-dialog" title="Add link" style="display: none;">
 | 
				
			||||||
@@ -363,8 +360,11 @@
 | 
				
			|||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <div id="sql-console-dialog" title="SQL console" style="display: none; padding: 20px;">
 | 
					    <div id="sql-console-dialog" title="SQL console" style="display: none; padding: 20px;">
 | 
				
			||||||
      <textarea style="width: 100%; height: 100px" id="sql-console-query"></textarea>
 | 
					      <div style="height: 150px; width: 100%; border: 1px solid #ccc; margin-bottom: 10px;" id="sql-console-query"></div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <div style="text-align: center">
 | 
				
			||||||
        <button class="btn btn-danger" id="sql-console-execute">Execute <kbd>CTRL+ENTER</kbd></button>
 | 
					        <button class="btn btn-danger" id="sql-console-execute">Execute <kbd>CTRL+ENTER</kbd></button>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <table id="sql-console-results" class="table table-striped" style="overflow: scroll; width: 100%;">
 | 
					      <table id="sql-console-results" class="table table-striped" style="overflow: scroll; width: 100%;">
 | 
				
			||||||
        <thead></thead>
 | 
					        <thead></thead>
 | 
				
			||||||
@@ -378,29 +378,40 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    <div id="attributes-dialog" title="Note attributes" style="display: none; padding: 20px;">
 | 
					    <div id="attributes-dialog" title="Note attributes" style="display: none; padding: 20px;">
 | 
				
			||||||
      <form data-bind="submit: save">
 | 
					      <form data-bind="submit: save">
 | 
				
			||||||
      <div style="display: flex; justify-content: space-between; padding: 15px; padding-top: 0;">
 | 
					      <div style="text-align: center">
 | 
				
			||||||
        <button class="btn-default" type="button" data-bind="click: addNewRow">Add new attribute</button>
 | 
					        <button class="btn btn-large" style="width: 200px;" id="save-attributes-button" type="submit">Save changes <kbd>enter</kbd></button>
 | 
				
			||||||
 | 
					 | 
				
			||||||
        <button class="btn-primary" type="submit">Save</button>
 | 
					 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <div style="height: 97%; overflow: auto">
 | 
					      <div style="height: 97%; overflow: auto">
 | 
				
			||||||
        <table id="attributes-table" class="table">
 | 
					        <table id="attributes-table" class="table">
 | 
				
			||||||
          <thead>
 | 
					          <thead>
 | 
				
			||||||
            <tr>
 | 
					            <tr>
 | 
				
			||||||
 | 
					              <th></th>
 | 
				
			||||||
              <th>ID</th>
 | 
					              <th>ID</th>
 | 
				
			||||||
              <th>Name</th>
 | 
					              <th>Name</th>
 | 
				
			||||||
              <th>Value</th>
 | 
					              <th>Value</th>
 | 
				
			||||||
 | 
					              <th></th>
 | 
				
			||||||
            </tr>
 | 
					            </tr>
 | 
				
			||||||
          </thead>
 | 
					          </thead>
 | 
				
			||||||
          <tbody data-bind="foreach: attributes">
 | 
					          <tbody data-bind="foreach: attributes">
 | 
				
			||||||
            <tr>
 | 
					            <tr data-bind="if: isDeleted == 0">
 | 
				
			||||||
              <td data-bind="text: attributeId"></td>
 | 
					              <td class="handle">
 | 
				
			||||||
 | 
					                <span class="glyphicon glyphicon-resize-vertical"></span>
 | 
				
			||||||
 | 
					                <input type="hidden" name="position" data-bind="value: position"/>
 | 
				
			||||||
 | 
					              </td>
 | 
				
			||||||
 | 
					              <!-- ID column has specific width because if it's empty its size can be deformed when dragging -->
 | 
				
			||||||
 | 
					              <td data-bind="text: attributeId" style="width: 150px;"></td>
 | 
				
			||||||
              <td>
 | 
					              <td>
 | 
				
			||||||
                <input type="text" data-bind="value: name"/>
 | 
					                <!-- Change to valueUpdate: blur is necessary because jQuery UI autocomplete hijacks change event -->
 | 
				
			||||||
 | 
					                <input type="text" class="attribute-name" data-bind="value: name, valueUpdate: 'blur',  event: { blur: $parent.attributeChanged }"/>
 | 
				
			||||||
 | 
					                <div style="color: yellowgreen" data-bind="if: $parent.isNotUnique($index())"><span class="glyphicon glyphicon-info-sign"></span> Duplicate attribute.</div>
 | 
				
			||||||
 | 
					                <div style="color: red" data-bind="if: $parent.isEmptyName($index())">Attribute name can't be empty.</div>
 | 
				
			||||||
              </td>
 | 
					              </td>
 | 
				
			||||||
              <td>
 | 
					              <td>
 | 
				
			||||||
                <input type="text" data-bind="value: value" style="width: 300px"/>
 | 
					                <input type="text" class="attribute-value" data-bind="value: value, valueUpdate: 'blur', event: { blur: $parent.attributeChanged }" style="width: 300px"/>
 | 
				
			||||||
 | 
					              </td>
 | 
				
			||||||
 | 
					              <td title="Delete" style="padding: 13px;">
 | 
				
			||||||
 | 
					                <span class="glyphicon glyphicon-trash" data-bind="click: $parent.deleteAttribute"></span>
 | 
				
			||||||
              </td>
 | 
					              </td>
 | 
				
			||||||
            </tr>
 | 
					            </tr>
 | 
				
			||||||
          </tbody>
 | 
					          </tbody>
 | 
				
			||||||
@@ -492,7 +503,7 @@
 | 
				
			|||||||
    <script src="javascripts/link.js"></script>
 | 
					    <script src="javascripts/link.js"></script>
 | 
				
			||||||
    <script src="javascripts/sync.js"></script>
 | 
					    <script src="javascripts/sync.js"></script>
 | 
				
			||||||
    <script src="javascripts/messaging.js"></script>
 | 
					    <script src="javascripts/messaging.js"></script>
 | 
				
			||||||
 | 
					    <script src="javascripts/api.js"></script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <script type="text/javascript">
 | 
					    <script type="text/javascript">
 | 
				
			||||||
      // we hide container initally because otherwise it is rendered first without CSS and then flickers into
 | 
					      // we hide container initally because otherwise it is rendered first without CSS and then flickers into
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user