mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-30 09:56:36 +01:00 
			
		
		
		
	Compare commits
	
		
			22 Commits
		
	
	
		
			v0.5.1-bet
			...
			v0.5.6
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 85d32c66f2 | ||
|  | 214d2e7659 | ||
|  | f380bb7f65 | ||
|  | 0a9a032daa | ||
|  | 23a2b58b24 | ||
|  | aee64b2522 | ||
|  | 02e07ec03a | ||
|  | 3d2dc8e699 | ||
|  | c84e15c9be | ||
|  | e18d0b9fd4 | ||
|  | 52817504d1 | ||
|  | a3b31fab54 | ||
|  | bc4aa3e40a | ||
|  | 873ea67e9c | ||
|  | 2c5115003b | ||
|  | e8ed913374 | ||
|  | 5bffba4e2f | ||
|  | 05575913db | ||
|  | 31c32ff42c | ||
|  | 6a671a5c02 | ||
|  | e174aec299 | ||
|  | d1329f60c3 | 
| @@ -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" | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										190
									
								
								db/schema.sql
									
									
									
									
									
								
							
							
						
						
									
										190
									
								
								db/schema.sql
									
									
									
									
									
								
							| @@ -1,119 +1,121 @@ | |||||||
| 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, |   dateCreated TEXT NOT NULL, | ||||||
|   date_modified TEXT NOT NULL |   dateModified TEXT NOT NULL | ||||||
| ); | ); | ||||||
| CREATE INDEX attributes_note_id_index ON attributes (note_id); | CREATE UNIQUE INDEX `IDX_sync_entityName_entityId` ON `sync` ( | ||||||
| CREATE UNIQUE INDEX attributes_note_id_name_index ON attributes (note_id, name); |   `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 UNIQUE INDEX IDX_attributes_noteId_name ON attributes (noteId, name); | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ | |||||||
| 
 | 
 | ||||||
| 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; | ||||||
| @@ -24,7 +24,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'; | ||||||
| @@ -69,4 +69,4 @@ app.on('ready', () => { | |||||||
|     mainWindow = createMainWindow(); |     mainWindow = createMainWindow(); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| require('./www'); | require('./src/www'); | ||||||
| @@ -1,8 +1,9 @@ | |||||||
| { | { | ||||||
|   "name": "trilium", |   "name": "trilium", | ||||||
|   "description": "Trilium Notes", |   "description": "Trilium Notes", | ||||||
|   "version": "0.5.1-beta", |   "version": "0.5.6", | ||||||
|   "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", | ||||||
|   | |||||||
							
								
								
									
										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 | ||||||
|  |     } | ||||||
|  | })(); | ||||||
| @@ -2,7 +2,9 @@ | |||||||
|  |  | ||||||
| const attributesDialog = (function() { | const attributesDialog = (function() { | ||||||
|     const dialogEl = $("#attributes-dialog"); |     const dialogEl = $("#attributes-dialog"); | ||||||
|  |     const saveAttributesButton = $("#save-attributes-button"); | ||||||
|     const attributesModel = new AttributesModel(); |     const attributesModel = new AttributesModel(); | ||||||
|  |     let attributeNames = []; | ||||||
|  |  | ||||||
|     function AttributesModel() { |     function AttributesModel() { | ||||||
|         const self = this; |         const self = this; | ||||||
| @@ -14,38 +16,112 @@ 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)); | ||||||
|  |  | ||||||
|  |             addLastEmptyRow(); | ||||||
|  |  | ||||||
|  |             attributeNames = await server.get('attributes/names'); | ||||||
|  |  | ||||||
|  |             // attribute might not be rendered immediatelly so could not focus | ||||||
|  |             setTimeout(() => $(".attribute-name:last").focus(), 100); | ||||||
|         }; |         }; | ||||||
|  |  | ||||||
|         this.addNewRow = function() { |         function isValid() { | ||||||
|             self.attributes.push({ |             for (let attrs = self.attributes(), i = 0; i < attrs.length; i++) { | ||||||
|  |                 if (self.isEmptyName(i) || self.isNotUnique(i)) { | ||||||
|  |                     return false; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             return true; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         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 attributesToSave = self.attributes() | ||||||
|  |                 .map(attr => attr()) | ||||||
|  |                 .filter(attr => attr.attributeId !== "" || attr.name !== ""); | ||||||
|  |  | ||||||
|  |             const attributes = await server.put('notes/' + noteId + '/attributes', attributesToSave); | ||||||
|  |  | ||||||
|  |             self.attributes(attributes.map(ko.observable)); | ||||||
|  |  | ||||||
|  |             addLastEmptyRow(); | ||||||
|  |  | ||||||
|  |             showMessage("Attributes have been saved."); | ||||||
|  |  | ||||||
|  |             noteEditor.loadAttributeList(); | ||||||
|  |         }; | ||||||
|  |  | ||||||
|  |         function addLastEmptyRow() { | ||||||
|  |             const attrs = self.attributes(); | ||||||
|  |             const last = attrs.length === 0 ? null : attrs[attrs.length - 1](); | ||||||
|  |  | ||||||
|  |             if (!last || last.name.trim() !== "" || last.value !== "") { | ||||||
|  |                 self.attributes.push(ko.observable({ | ||||||
|                     attributeId: '', |                     attributeId: '', | ||||||
|                     name: '', |                     name: '', | ||||||
|                     value: '' |                     value: '' | ||||||
|             }); |                 })); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         this.attributeChanged = function (row) { | ||||||
|  |             addLastEmptyRow(); | ||||||
|  |  | ||||||
|  |             for (const attr of self.attributes()) { | ||||||
|  |                 if (row.attributeId === attr().attributeId) { | ||||||
|  |                     attr.valueHasMutated(); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|         }; |         }; | ||||||
|  |  | ||||||
|         this.save = async function() { |         this.isNotUnique = function(index) { | ||||||
|             const noteId = noteEditor.getCurrentNoteId(); |             const cur = self.attributes()[index](); | ||||||
|  |  | ||||||
|             const attributes = await server.put('notes/' + noteId + '/attributes', this.attributes()); |             if (cur.name.trim() === "") { | ||||||
|  |                 return false; | ||||||
|  |             } | ||||||
|  |  | ||||||
|             self.attributes(attributes); |             for (let attrs = self.attributes(), i = 0; i < attrs.length; i++) { | ||||||
|  |                 const attr = attrs[i](); | ||||||
|  |  | ||||||
|             showMessage("Attributes have been saved."); |                 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 !== ""); | ||||||
|  |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     async function showDialog() { |     async function showDialog() { | ||||||
|         glob.activeDialog = dialogEl; |         glob.activeDialog = dialogEl; | ||||||
|  |  | ||||||
|  |         await attributesModel.loadAttributes(); | ||||||
|  |  | ||||||
|         dialogEl.dialog({ |         dialogEl.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 +132,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 | ||||||
|     }; |     }; | ||||||
|   | |||||||
| @@ -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(); | ||||||
|   | |||||||
| @@ -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; | ||||||
| @@ -187,6 +189,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 +313,7 @@ const noteEditor = (function() { | |||||||
|         newNoteCreated, |         newNoteCreated, | ||||||
|         getEditor, |         getEditor, | ||||||
|         focus, |         focus, | ||||||
|         executeCurrentNote |         executeCurrentNote, | ||||||
|  |         loadAttributeList | ||||||
|     }; |     }; | ||||||
| })(); | })(); | ||||||
| @@ -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 = {}; | ||||||
|   | |||||||
| @@ -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; | ||||||
| } | } | ||||||
| @@ -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 noteId = ? ORDER BY 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(); | ||||||
| @@ -45,4 +46,26 @@ 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 noteId = ? ORDER BY dateCreated", [noteId])); | ||||||
| })); | })); | ||||||
|  |  | ||||||
|  | router.get('/attributes/names', auth.checkApiAuth, wrap(async (req, res, next) => { | ||||||
|  |     const names = await sql.getColumn("SELECT DISTINCT name FROM attributes"); | ||||||
|  |  | ||||||
|  |     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 name = ? AND value != '' ORDER BY value", [attributeName]); | ||||||
|  |  | ||||||
|  |     res.send(values); | ||||||
|  | })); | ||||||
|  |  | ||||||
| module.exports = router; | module.exports = router; | ||||||
| @@ -58,15 +58,112 @@ 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]); |     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 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; | ||||||
|   | |||||||
| @@ -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; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -40,7 +40,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); | ||||||
|   | |||||||
							
								
								
									
										11
									
								
								src/scripts/today.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/scripts/today.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | |||||||
|  | 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); | ||||||
|  | }; | ||||||
| @@ -65,6 +65,8 @@ | |||||||
|                 }); |                 }); | ||||||
|             } |             } | ||||||
|  |  | ||||||
|  |             data.sort((a, b) => a.date < b.date ? -1 : +1); | ||||||
|  |  | ||||||
|             return data; |             return data; | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -5,6 +5,8 @@ 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' ]; | ||||||
|  |  | ||||||
| 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]); | ||||||
| } | } | ||||||
| @@ -64,5 +66,6 @@ module.exports = { | |||||||
|     getNotesWithAttribute, |     getNotesWithAttribute, | ||||||
|     getNoteWithAttribute, |     getNoteWithAttribute, | ||||||
|     getNoteIdsWithAttribute, |     getNoteIdsWithAttribute, | ||||||
|     createAttribute |     createAttribute, | ||||||
|  |     BUILTIN_ATTRIBUTES | ||||||
| }; | }; | ||||||
| @@ -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(); | ||||||
|   | |||||||
| @@ -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,6 +142,12 @@ | |||||||
|  |  | ||||||
|         <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;"> | ||||||
| @@ -378,10 +388,8 @@ | |||||||
|  |  | ||||||
|     <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 <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"> | ||||||
| @@ -397,10 +405,14 @@ | |||||||
|             <tr> |             <tr> | ||||||
|               <td data-bind="text: attributeId"></td> |               <td data-bind="text: attributeId"></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: red" data-bind="if: $parent.isNotUnique($index())">Attribute name must be unique per note.</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> | ||||||
|             </tr> |             </tr> | ||||||
|           </tbody> |           </tbody> | ||||||
| @@ -492,7 +504,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