mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 02:16:05 +01:00 
			
		
		
		
	Compare commits
	
		
			40 Commits
		
	
	
		
			v0.28.3
			...
			v0.29.0-be
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 6cc0dd5a80 | ||
|  | afd5f4823f | ||
|  | b0cf82c91b | ||
|  | 6a67cdd5af | ||
|  | bad7b84993 | ||
|  | d3ca6b5ae6 | ||
|  | da5009f089 | ||
|  | c08524c977 | ||
|  | f89537037e | ||
|  | c153793766 | ||
|  | 0aec5927d5 | ||
|  | 8aea9a1801 | ||
|  | 73247e3220 | ||
|  | 89344a6eda | ||
|  | 40d2e6ea83 | ||
|  | 910cfe9a17 | ||
|  | e58a80fc00 | ||
|  | 4a2319cb33 | ||
|  | 5619088c41 | ||
|  | 60271993eb | ||
|  | 6695e8b011 | ||
|  | 707df18b93 | ||
|  | 90895f1288 | ||
|  | ba1ca506af | ||
|  | f90ed99a40 | ||
|  | 67630b1a22 | ||
|  | 2c1580ea65 | ||
|  | 840a0b5f64 | ||
|  | b39f6ef7ad | ||
|  | fb27088fcd | ||
|  | 76fbff68ba | ||
|  | 54de4d236d | ||
|  | e211dd65ad | ||
|  | a87f4d8653 | ||
|  | b59c175c2e | ||
|  | 580104c4c5 | ||
|  | 0fc3053b0a | ||
|  | 929e0f69c2 | ||
|  | e70af1300a | ||
|  | 4d0e46021b | 
							
								
								
									
										
											BIN
										
									
								
								db/demo.tar
									
									
									
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								db/demo.tar
									
									
									
									
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										2309
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2309
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										35
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										35
									
								
								package.json
									
									
									
									
									
								
							| @@ -2,7 +2,7 @@ | ||||
|   "name": "trilium", | ||||
|   "productName": "Trilium Notes", | ||||
|   "description": "Trilium Notes", | ||||
|   "version": "0.28.3", | ||||
|   "version": "0.29.0-beta", | ||||
|   "license": "AGPL-3.0-only", | ||||
|   "main": "electron.js", | ||||
|   "bin": { | ||||
| @@ -29,53 +29,54 @@ | ||||
|     "cookie-parser": "1.4.3", | ||||
|     "debug": "4.1.1", | ||||
|     "ejs": "2.6.1", | ||||
|     "electron-debug": "2.0.0", | ||||
|     "electron-dl": "1.12.0", | ||||
|     "electron-debug": "2.1.0", | ||||
|     "electron-dl": "1.13.0", | ||||
|     "electron-in-page-search": "1.3.2", | ||||
|     "express": "4.16.4", | ||||
|     "express-session": "1.15.6", | ||||
|     "file-type": "10.7.0", | ||||
|     "file-type": "10.7.1", | ||||
|     "fs-extra": "7.0.1", | ||||
|     "get-port": "4.1.0", | ||||
|     "helmet": "3.15.0", | ||||
|     "html": "1.0.0", | ||||
|     "image-type": "3.0.0", | ||||
|     "imagemin": "6.0.0", | ||||
|     "imagemin": "6.1.0", | ||||
|     "imagemin-giflossy": "5.1.10", | ||||
|     "imagemin-mozjpeg": "8.0.0", | ||||
|     "imagemin-pngquant": "6.0.0", | ||||
|     "imagemin-pngquant": "7.0.0", | ||||
|     "ini": "1.3.5", | ||||
|     "jimp": "0.6.0", | ||||
|     "mime-types": "^2.1.21", | ||||
|     "moment": "2.23.0", | ||||
|     "moment": "2.24.0", | ||||
|     "multer": "1.4.1", | ||||
|     "node-abi": "2.5.1", | ||||
|     "node-abi": "2.6.0", | ||||
|     "open": "0.0.5", | ||||
|     "rand-token": "0.4.0", | ||||
|     "rcedit": "1.1.1", | ||||
|     "rimraf": "2.6.2", | ||||
|     "rimraf": "2.6.3", | ||||
|     "sanitize-filename": "1.6.1", | ||||
|     "sax": "^1.2.4", | ||||
|     "semver": "^5.6.0", | ||||
|     "serve-favicon": "2.5.0", | ||||
|     "session-file-store": "1.2.0", | ||||
|     "simple-node-logger": "18.12.21", | ||||
|     "sqlite": "3.0.0", | ||||
|     "sqlite": "3.0.1", | ||||
|     "tar-stream": "1.6.2", | ||||
|     "turndown": "5.0.1", | ||||
|     "turndown": "5.0.3", | ||||
|     "unescape": "1.0.1", | ||||
|     "ws": "6.1.2", | ||||
|     "ws": "6.1.3", | ||||
|     "xml2js": "0.4.19" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "devtron": "1.4.0", | ||||
|     "electron": "4.0.1", | ||||
|     "electron-builder": "20.38.4", | ||||
|     "electron-compile": "6.4.3", | ||||
|     "electron": "4.0.3", | ||||
|     "electron-builder": "20.38.5", | ||||
|     "electron-compile": "6.4.4", | ||||
|     "electron-packager": "13.0.1", | ||||
|     "electron-rebuild": "1.8.2", | ||||
|     "lorem-ipsum": "1.0.6", | ||||
|     "tape": "4.9.1", | ||||
|     "xo": "0.23.0" | ||||
|     "tape": "4.9.2", | ||||
|     "xo": "0.24.0" | ||||
|   }, | ||||
|   "xo": { | ||||
|     "envs": [ | ||||
|   | ||||
| @@ -39,7 +39,7 @@ app.use((req, res, next) => { | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| app.use(bodyParser.json({limit: '50mb'})); | ||||
| app.use(bodyParser.json({limit: '500mb'})); | ||||
| app.use(bodyParser.urlencoded({extended: false})); | ||||
| app.use(cookieParser()); | ||||
| app.use(express.static(path.join(__dirname, 'public'))); | ||||
| @@ -63,6 +63,8 @@ app.use(favicon(__dirname + '/public/images/app-icons/win/icon.ico')); | ||||
|  | ||||
| require('./routes/routes').register(app); | ||||
|  | ||||
| require('./routes/custom').register(app); | ||||
|  | ||||
| // catch 404 and forward to error handler | ||||
| app.use((req, res, next) => { | ||||
|     const err = new Error('Router not found for request ' + req.url); | ||||
|   | ||||
							
								
								
									
										
											BIN
										
									
								
								src/public/images/app-icons/ios/apple-touch-icon.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/public/images/app-icons/ios/apple-touch-icon.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 3.4 KiB | 
| @@ -37,6 +37,7 @@ import noteTypeService from './services/note_type.js'; | ||||
| import linkService from './services/link.js'; | ||||
| import noteAutocompleteService from './services/note_autocomplete.js'; | ||||
| import macInit from './services/mac_init.js'; | ||||
| import cssLoader from './services/css_loader.js'; | ||||
|  | ||||
| // required for CKEditor image upload plugin | ||||
| window.glob.getCurrentNode = treeService.getCurrentNode; | ||||
| @@ -79,6 +80,10 @@ window.onerror = function (msg, url, lineNo, columnNo, error) { | ||||
|     return false; | ||||
| }; | ||||
|  | ||||
| for (const appCssNoteId of window.appCssNoteIds) { | ||||
|     cssLoader.requireCss(`/api/notes/download/${appCssNoteId}`); | ||||
| } | ||||
|  | ||||
| const wikiBaseUrl = "https://github.com/zadam/trilium/wiki/"; | ||||
|  | ||||
| $(document).on("click", "button[data-help-page]", e => { | ||||
| @@ -121,6 +126,8 @@ $("#export-note-button").click(function () { | ||||
|  | ||||
| macInit.init(); | ||||
|  | ||||
| searchNotesService.init(); // should be in front of treeService since that one manipulates address bar hash | ||||
|  | ||||
| treeService.showTree(); | ||||
|  | ||||
| entrypoints.registerEntrypoints(); | ||||
|   | ||||
| @@ -97,7 +97,7 @@ function AttributesModel() { | ||||
|         await showAttributes(attributes); | ||||
|  | ||||
|         // attribute might not be rendered immediatelly so could not focus | ||||
|         setTimeout(() => $(".attribute-type-select:last").focus(), 100); | ||||
|         setTimeout(() => $(".attribute-type-select:last").focus(), 1000); | ||||
|     }; | ||||
|  | ||||
|     this.deleteAttribute = function(data, event) { | ||||
|   | ||||
| @@ -5,6 +5,7 @@ import server from '../services/server.js'; | ||||
| import infoService from "../services/info.js"; | ||||
| import zoomService from "../services/zoom.js"; | ||||
| import utils from "../services/utils.js"; | ||||
| import cssLoader from "../services/css_loader.js"; | ||||
|  | ||||
| const $dialog = $("#options-dialog"); | ||||
|  | ||||
| @@ -50,7 +51,22 @@ addTabHandler((function() { | ||||
|     const $body = $("body"); | ||||
|     const $container = $("#container"); | ||||
|  | ||||
|     function optionsLoaded(options) { | ||||
|     async function optionsLoaded(options) { | ||||
|         const themes = [ | ||||
|             { val: 'white', title: 'White' }, | ||||
|             { val: 'dark', title: 'Dark' }, | ||||
|             { val: 'black', title: 'Black' } | ||||
|         ].concat(await server.get('options/user-themes')); | ||||
|  | ||||
|         $themeSelect.empty(); | ||||
|  | ||||
|         for (const theme of themes) { | ||||
|             $themeSelect.append($("<option>") | ||||
|                 .attr("value", theme.val) | ||||
|                 .attr("data-note-id", theme.noteId) | ||||
|                 .html(theme.title)); | ||||
|         } | ||||
|  | ||||
|         $themeSelect.val(options.theme); | ||||
|  | ||||
|         if (utils.isElectron()) { | ||||
| @@ -71,12 +87,20 @@ addTabHandler((function() { | ||||
|     $themeSelect.change(function() { | ||||
|         const newTheme = $(this).val(); | ||||
|  | ||||
|         for (const clazz of $body[0].classList) { | ||||
|         for (const clazz of Array.from($body[0].classList)) { // create copy to safely iterate over while removing classes | ||||
|             if (clazz.startsWith("theme-")) { | ||||
|                 $body.removeClass(clazz); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         const noteId = $(this).find(":selected").attr("data-note-id"); | ||||
|  | ||||
|         if (noteId) { | ||||
|             // make sure the CSS is loaded | ||||
|             // if the CSS has been loaded and then updated then the changes won't take effect though | ||||
|             cssLoader.requireCss(`/api/notes/download/${noteId}`); | ||||
|         } | ||||
|  | ||||
|         $body.addClass("theme-" + newTheme); | ||||
|  | ||||
|         server.put('options/theme/' + newTheme); | ||||
|   | ||||
							
								
								
									
										13
									
								
								src/public/javascripts/services/css_loader.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/public/javascripts/services/css_loader.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| async function requireCss(url) { | ||||
|     const css = Array | ||||
|         .from(document.querySelectorAll('link')) | ||||
|         .map(scr => scr.href); | ||||
|  | ||||
|     if (!css.includes(url)) { | ||||
|         $('head').append($('<link rel="stylesheet" type="text/css" />').attr('href', url)); | ||||
|     } | ||||
| } | ||||
|  | ||||
| export default { | ||||
|     requireCss | ||||
| } | ||||
| @@ -9,6 +9,14 @@ const dragAndDropSetup = { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         if (!data.originalEvent.ctrlKey) { | ||||
|             // keep existing selection only if CTRL key is pressed | ||||
|             for (const selectedNode of treeService.getSelectedNodes()) { | ||||
|                 selectedNode.setSelected(false); | ||||
|                 selectedNode.renderTitle(); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         node.setSelected(true); | ||||
|  | ||||
|         // this is for dragging notes into relation map | ||||
|   | ||||
| @@ -61,8 +61,15 @@ function registerEntrypoints() { | ||||
|         $("#history-back-button").click(window.history.back); | ||||
|         $("#history-forward-button").click(window.history.forward); | ||||
|  | ||||
|         utils.bindShortcut('alt+left', window.history.back); | ||||
|         utils.bindShortcut('alt+right', window.history.forward); | ||||
|         if (utils.isMac()) { | ||||
|             // Mac has a different history navigation shortcuts - https://github.com/zadam/trilium/issues/376 | ||||
|             utils.bindShortcut('meta+left', window.history.back); | ||||
|             utils.bindShortcut('meta+right', window.history.forward); | ||||
|         } | ||||
|         else { | ||||
|             utils.bindShortcut('alt+left', window.history.back); | ||||
|             utils.bindShortcut('alt+right', window.history.forward); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     utils.bindShortcut('alt+m', e => { | ||||
|   | ||||
| @@ -1,3 +1,5 @@ | ||||
| import cssLoader from './css_loader.js'; | ||||
|  | ||||
| const CKEDITOR = {"js": ["libraries/ckeditor/ckeditor.js"]}; | ||||
|  | ||||
| const CODE_MIRROR = { | ||||
| @@ -34,7 +36,7 @@ const RELATION_MAP = { | ||||
|  | ||||
| async function requireLibrary(library) { | ||||
|     if (library.css) { | ||||
|         library.css.map(cssUrl => requireCss(cssUrl)); | ||||
|         library.css.map(cssUrl => cssLoader.requireCss(cssUrl)); | ||||
|     } | ||||
|  | ||||
|     if (library.js) { | ||||
| @@ -59,16 +61,6 @@ async function requireScript(url) { | ||||
|     await loadedScriptPromises[url]; | ||||
| } | ||||
|  | ||||
| async function requireCss(url) { | ||||
|     const css = Array | ||||
|         .from(document.querySelectorAll('link')) | ||||
|         .map(scr => scr.href); | ||||
|  | ||||
|     if (!css.includes(url)) { | ||||
|         $('head').append($('<link rel="stylesheet" type="text/css" />').attr('href', url)); | ||||
|     } | ||||
| } | ||||
|  | ||||
| export default { | ||||
|     requireLibrary, | ||||
|     CKEDITOR, | ||||
|   | ||||
| @@ -107,6 +107,7 @@ function init() { | ||||
| $(document).on('click', "a[data-action='note']", goToLink); | ||||
| $(document).on('click', 'div.popover-content a, div.ui-tooltip-content a', goToLink); | ||||
| $(document).on('dblclick', '#note-detail-text a', goToLink); | ||||
| $(document).on('click', '#note-detail-text.ck-read-only a', goToLink); | ||||
| $(document).on('click', 'span.ck-button__label', e => { | ||||
|     // this is a link preview dialog from CKEditor link editing | ||||
|     // for some reason clicked element is span | ||||
|   | ||||
| @@ -30,6 +30,7 @@ const $noteIdDisplay = $("#note-id-display"); | ||||
| const $childrenOverview = $("#children-overview"); | ||||
| const $scriptArea = $("#note-detail-script-area"); | ||||
| const $savedIndicator = $("#saved-indicator"); | ||||
| const $body = $("body"); | ||||
|  | ||||
| let currentNote = null; | ||||
|  | ||||
| @@ -145,12 +146,21 @@ async function saveNoteIfChanged() { | ||||
|     $savedIndicator.fadeIn(); | ||||
| } | ||||
|  | ||||
| function setNoteBackgroundIfProtected(note) { | ||||
|     $noteDetailWrapper.toggleClass("protected", note.isProtected); | ||||
|     $protectButton.toggleClass("active", note.isProtected); | ||||
|     $protectButton.prop("disabled", note.isProtected); | ||||
|     $unprotectButton.toggleClass("active", !note.isProtected); | ||||
|     $unprotectButton.prop("disabled", !note.isProtected || !protectedSessionHolder.isProtectedSessionAvailable()); | ||||
| function updateNoteView() { | ||||
|     $noteDetailWrapper.toggleClass("protected", currentNote.isProtected); | ||||
|     $protectButton.toggleClass("active", currentNote.isProtected); | ||||
|     $protectButton.prop("disabled", currentNote.isProtected); | ||||
|     $unprotectButton.toggleClass("active", !currentNote.isProtected); | ||||
|     $unprotectButton.prop("disabled", !currentNote.isProtected || !protectedSessionHolder.isProtectedSessionAvailable()); | ||||
|  | ||||
|     for (const clazz of Array.from($body[0].classList)) { // create copy to safely iterate over while removing classes | ||||
|         if (clazz.startsWith("type-") || clazz.startsWith("mime-")) { | ||||
|             $body.removeClass(clazz); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     $body.addClass(utils.getNoteTypeClass(currentNote.type)); | ||||
|     $body.addClass(utils.getMimeTypeClass(currentNote.mime)); | ||||
| } | ||||
|  | ||||
| async function handleProtectedSession() { | ||||
| @@ -193,7 +203,7 @@ async function loadNoteDetail(noteId) { | ||||
|  | ||||
|     $noteIdDisplay.html(noteId); | ||||
|  | ||||
|     setNoteBackgroundIfProtected(currentNote); | ||||
|     updateNoteView(); | ||||
|  | ||||
|     $noteDetailWrapper.show(); | ||||
|  | ||||
| @@ -270,7 +280,7 @@ async function showChildrenOverview() { | ||||
|             text: await treeUtils.getNoteTitle(childBranch.noteId, childBranch.parentNoteId) | ||||
|         }).attr('data-action', 'note').attr('data-note-path', notePath + '/' + childBranch.noteId); | ||||
|  | ||||
|         const childEl = $('<div class="child-overview">').html(link); | ||||
|         const childEl = $('<div class="child-overview-item">').html(link); | ||||
|         $childrenOverview.append(childEl); | ||||
|     } | ||||
|  | ||||
| @@ -344,7 +354,7 @@ setInterval(saveNoteIfChanged, 3000); | ||||
| export default { | ||||
|     reload, | ||||
|     switchToNote, | ||||
|     setNoteBackgroundIfProtected, | ||||
|     updateNoteView, | ||||
|     loadNote, | ||||
|     getCurrentNote, | ||||
|     getCurrentNoteType, | ||||
|   | ||||
| @@ -5,6 +5,7 @@ import noteDetailService from "./note_detail.js"; | ||||
|  | ||||
| const $component = $('#note-detail-file'); | ||||
|  | ||||
| const $fileNoteId = $("#file-note-id"); | ||||
| const $fileName = $("#file-filename"); | ||||
| const $fileType = $("#file-filetype"); | ||||
| const $fileSize = $("#file-filesize"); | ||||
| @@ -21,6 +22,7 @@ async function show() { | ||||
|  | ||||
|     $component.show(); | ||||
|  | ||||
|     $fileNoteId.text(currentNote.noteId); | ||||
|     $fileName.text(attributeMap.originalFileName || "?"); | ||||
|     $fileSize.text((attributeMap.fileSize || "?") + " bytes"); | ||||
|     $fileType.text(currentNote.mime); | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| import libraryLoader from "./library_loader.js"; | ||||
| import noteDetailService from './note_detail.js'; | ||||
| import treeService from './tree.js'; | ||||
| import attributeService from "./attributes.js"; | ||||
|  | ||||
| const $component = $('#note-detail-text'); | ||||
|  | ||||
| @@ -19,6 +20,8 @@ async function show() { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     textEditor.isReadOnly = await isReadOnly(); | ||||
|  | ||||
|     textEditor.setData(noteDetailService.getCurrentNote().content); | ||||
|  | ||||
|     $component.show(); | ||||
| @@ -36,6 +39,12 @@ function getContent() { | ||||
|     return content; | ||||
| } | ||||
|  | ||||
| async function isReadOnly() { | ||||
|     const attributes = await attributeService.getAttributes(); | ||||
|  | ||||
|     return attributes.some(attr => attr.type === 'label' && attr.name === 'readOnly'); | ||||
| } | ||||
|  | ||||
| function focus() { | ||||
|     $component.focus(); | ||||
| } | ||||
|   | ||||
| @@ -2,6 +2,7 @@ import treeService from './tree.js'; | ||||
| import noteDetailService from './note_detail.js'; | ||||
| import server from './server.js'; | ||||
| import infoService from "./info.js"; | ||||
| import confirmDialog from "../dialogs/confirm.js"; | ||||
|  | ||||
| const $executeScriptButton = $("#execute-script-button"); | ||||
| const $toggleEditButton = $('#toggle-edit-button'); | ||||
| @@ -110,35 +111,63 @@ function NoteTypeModel() { | ||||
|         self.updateExecuteScriptButtonVisibility(); | ||||
|     } | ||||
|  | ||||
|     this.selectText = function() { | ||||
|     function confirmChangeIfContent() { | ||||
|         if (!noteDetailService.getCurrentNoteContent()) { | ||||
|             return true; | ||||
|         } | ||||
|  | ||||
|         return confirmDialog.confirm("It is not recommended to change note type when note content is not empty. Do you want to continue anyway?"); | ||||
|     } | ||||
|  | ||||
|     this.selectText = async function() { | ||||
|         if (!await confirmChangeIfContent()) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         self.type('text'); | ||||
|         self.mime(''); | ||||
|         self.mime('text/html'); | ||||
|  | ||||
|         save(); | ||||
|     }; | ||||
|  | ||||
|     this.selectRender = function() { | ||||
|     this.selectRender = async function() { | ||||
|         if (!await confirmChangeIfContent()) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         self.type('render'); | ||||
|         self.mime('text/html'); | ||||
|  | ||||
|         save(); | ||||
|     }; | ||||
|  | ||||
|     this.selectRelationMap = function() { | ||||
|     this.selectRelationMap = async function() { | ||||
|         if (!await confirmChangeIfContent()) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         self.type('relation-map'); | ||||
|         self.mime('application/json'); | ||||
|  | ||||
|         save(); | ||||
|     }; | ||||
|  | ||||
|     this.selectCode = function() { | ||||
|     this.selectCode = async function() { | ||||
|         if (!await confirmChangeIfContent()) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         self.type('code'); | ||||
|         self.mime(''); | ||||
|         self.mime('text/plain'); | ||||
|  | ||||
|         save(); | ||||
|     }; | ||||
|  | ||||
|     this.selectCodeMime = function(el) { | ||||
|     this.selectCodeMime = async function(el) { | ||||
|         if (!await confirmChangeIfContent()) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         self.type('code'); | ||||
|         self.mime(el.mime); | ||||
|  | ||||
|   | ||||
| @@ -125,7 +125,7 @@ async function protectNoteAndSendToServer() { | ||||
|  | ||||
|     treeService.setProtected(note.noteId, note.isProtected); | ||||
|  | ||||
|     noteDetailService.setNoteBackgroundIfProtected(note); | ||||
|     noteDetailService.updateNoteView(); | ||||
| } | ||||
|  | ||||
| async function unprotectNoteAndSendToServer() { | ||||
| @@ -152,7 +152,7 @@ async function unprotectNoteAndSendToServer() { | ||||
|  | ||||
|     treeService.setProtected(currentNote.noteId, currentNote.isProtected); | ||||
|  | ||||
|     noteDetailService.setNoteBackgroundIfProtected(currentNote); | ||||
|     noteDetailService.updateNoteView(); | ||||
| } | ||||
|  | ||||
| async function protectSubtree(noteId, protect) { | ||||
|   | ||||
| @@ -76,6 +76,15 @@ async function saveSearch() { | ||||
|     await treeService.activateNote(noteId); | ||||
| } | ||||
|  | ||||
| function init() { | ||||
|     const hashValue = treeService.getHashValueFromAddress(); | ||||
|  | ||||
|     if (hashValue.startsWith("search=")) { | ||||
|         showSearch(); | ||||
|         doSearch(hashValue.substr(7)); | ||||
|     } | ||||
| } | ||||
|  | ||||
| $searchInput.keyup(e => { | ||||
|     const searchText = $searchInput.val(); | ||||
|  | ||||
| @@ -100,5 +109,6 @@ export default { | ||||
|     toggleSearch, | ||||
|     resetSearch, | ||||
|     showSearch, | ||||
|     doSearch | ||||
|     doSearch, | ||||
|     init | ||||
| }; | ||||
| @@ -483,16 +483,20 @@ async function reload() { | ||||
|     await getTree().reload(notes); | ||||
| } | ||||
|  | ||||
| function getNotePathFromAddress() { | ||||
|     return document.location.hash.substr(1); // strip initial # | ||||
| function isNotePathInAddress() { | ||||
|     return getHashValueFromAddress().startsWith("root"); | ||||
| } | ||||
|  | ||||
| function getHashValueFromAddress() { | ||||
|     return document.location.hash ? document.location.hash.substr(1) : ""; // strip initial # | ||||
| } | ||||
|  | ||||
| async function loadTree() { | ||||
|     const resp = await server.get('tree'); | ||||
|     startNotePath = resp.startNotePath; | ||||
|  | ||||
|     if (document.location.hash) { | ||||
|         startNotePath = getNotePathFromAddress(); | ||||
|     if (isNotePathInAddress()) { | ||||
|         startNotePath = getHashValueFromAddress(); | ||||
|     } | ||||
|  | ||||
|     return await treeBuilder.prepareTree(resp.notes, resp.branches, resp.relations); | ||||
| @@ -707,12 +711,14 @@ utils.bindShortcut('ctrl+p', createNoteInto); | ||||
| utils.bindShortcut('ctrl+.', scrollToCurrentNote); | ||||
|  | ||||
| $(window).bind('hashchange', function() { | ||||
|     const notePath = getNotePathFromAddress(); | ||||
|     if (isNotePathInAddress()) { | ||||
|         const notePath = getHashValueFromAddress(); | ||||
|  | ||||
|     if (notePath !== '-' && getCurrentNotePath() !== notePath) { | ||||
|         console.debug("Switching to " + notePath + " because of hash change"); | ||||
|         if (notePath !== '-' && getCurrentNotePath() !== notePath) { | ||||
|             console.debug("Switching to " + notePath + " because of hash change"); | ||||
|  | ||||
|         activateNote(notePath); | ||||
|             activateNote(notePath); | ||||
|         } | ||||
|     } | ||||
| }); | ||||
|  | ||||
| @@ -745,5 +751,6 @@ export default { | ||||
|     showTree, | ||||
|     loadTree, | ||||
|     treeInitialized, | ||||
|     setExpandedToServer | ||||
|     setExpandedToServer, | ||||
|     getHashValueFromAddress | ||||
| }; | ||||
| @@ -166,26 +166,15 @@ async function getExtraClasses(note) { | ||||
|         extraClasses.push(note.cssClass); | ||||
|     } | ||||
|  | ||||
|     extraClasses.push(note.type); | ||||
|     extraClasses.push(utils.getNoteTypeClass(note.type)); | ||||
|  | ||||
|     if (note.mime) { // some notes should not have mime type (e.g. render) | ||||
|         extraClasses.push(getMimeTypeClass(note.mime)); | ||||
|         extraClasses.push(utils.getMimeTypeClass(note.mime)); | ||||
|     } | ||||
|  | ||||
|     return extraClasses.join(" "); | ||||
| } | ||||
|  | ||||
| function getMimeTypeClass(mime) { | ||||
|     const semicolonIdx = mime.indexOf(';'); | ||||
|  | ||||
|     if (semicolonIdx !== -1) { | ||||
|         // stripping everything following the semicolon | ||||
|         mime = mime.substr(0, semicolonIdx); | ||||
|     } | ||||
|  | ||||
|     return 'mime-' + mime.toLowerCase().replace(/[\W_]+/g,"-"); | ||||
| } | ||||
|  | ||||
| export default { | ||||
|     prepareTree, | ||||
|     prepareBranch, | ||||
|   | ||||
| @@ -172,6 +172,21 @@ function setCookie(name, value) { | ||||
|     document.cookie = name + "=" + (value || "")  + expires + "; path=/"; | ||||
| } | ||||
|  | ||||
| function getNoteTypeClass(type) { | ||||
|     return "type-" + type; | ||||
| } | ||||
|  | ||||
| function getMimeTypeClass(mime) { | ||||
|     const semicolonIdx = mime.indexOf(';'); | ||||
|  | ||||
|     if (semicolonIdx !== -1) { | ||||
|         // stripping everything following the semicolon | ||||
|         mime = mime.substr(0, semicolonIdx); | ||||
|     } | ||||
|  | ||||
|     return 'mime-' + mime.toLowerCase().replace(/[\W_]+/g,"-"); | ||||
| } | ||||
|  | ||||
| export default { | ||||
|     reloadApp, | ||||
|     parseDate, | ||||
| @@ -198,5 +213,7 @@ export default { | ||||
|     bindShortcut, | ||||
|     isMobile, | ||||
|     isDesktop, | ||||
|     setCookie | ||||
|     setCookie, | ||||
|     getNoteTypeClass, | ||||
|     getMimeTypeClass | ||||
| }; | ||||
							
								
								
									
										4
									
								
								src/public/libraries/ckeditor/ckeditor.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								src/public/libraries/ckeditor/ckeditor.js
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @@ -30,6 +30,7 @@ body { | ||||
|     flex-shrink: 1; | ||||
|     flex-basis: 60%; | ||||
|     margin-top: 10px; | ||||
|     font-family: var(--tree-font-family); | ||||
|     font-size: var(--tree-font-size); | ||||
| } | ||||
|  | ||||
| @@ -66,7 +67,7 @@ body { | ||||
|     justify-content: space-around; | ||||
|     padding: 10px 0 10px 0; | ||||
|     margin: 0 20px 0 10px; | ||||
|     border: 1px solid #ddd; | ||||
|     border: 1px solid var(--main-border-color); | ||||
|     border-radius: 7px; | ||||
| } | ||||
|  | ||||
| @@ -94,4 +95,13 @@ body { | ||||
|  | ||||
| #note-detail-wrapper { | ||||
|     font-size: var(--detail-font-size); | ||||
| } | ||||
|  | ||||
| ::-webkit-scrollbar { | ||||
|     width: 12px; | ||||
| } | ||||
|  | ||||
| ::-webkit-scrollbar-thumb { | ||||
|     border-radius: 3px; | ||||
|     border: 1px solid var(--main-border-color); | ||||
| } | ||||
| @@ -1,14 +1,20 @@ | ||||
| :root { | ||||
|     --main-font-family: inherit; | ||||
|     --main-font-size: normal; | ||||
|     --tree-font-family: inherit; | ||||
|     --tree-font-size: normal; | ||||
|     --detail-font-family: inherit; | ||||
|     --detail-font-size: normal; | ||||
|     --detail-text-font-family: inherit; | ||||
|  | ||||
|     --main-background-color: white; | ||||
|     --main-text-color: black; | ||||
|     --main-border-color: #ccc; | ||||
|     --accented-background-color: #eee; | ||||
|     --more-accented-background-color: #ccc; | ||||
|     --header-background-color: #f8f8f8; | ||||
|     --button-background-color: #eee; | ||||
|     --button-disabled-background-color: #ccc; | ||||
|     --button-border-color: #ddd; | ||||
|     --button-text-color: black; | ||||
|     --button-border-radius: 5px; | ||||
| @@ -27,6 +33,7 @@ | ||||
| body.theme-black { | ||||
|     --main-background-color: black; | ||||
|     --main-text-color: white; | ||||
|     --main-border-color: #ddd; | ||||
|     --accented-background-color: #222; | ||||
|     --more-accented-background-color: #444; | ||||
|     --header-background-color: black; | ||||
| @@ -53,6 +60,7 @@ body.theme-black .CodeMirror { | ||||
| body.theme-dark { | ||||
|     --main-background-color: #333; | ||||
|     --main-text-color: white; | ||||
|     --main-border-color: #ddd; | ||||
|     --accented-background-color: #555; | ||||
|     --more-accented-background-color: #777; | ||||
|     --header-background-color: #333; | ||||
| @@ -88,6 +96,7 @@ body { | ||||
|     width: 100%; | ||||
|     background-color: var(--main-background-color); | ||||
|     color: var(--main-text-color); | ||||
|     font-family: var(--main-font-family); | ||||
| } | ||||
|  | ||||
| input, select { | ||||
| @@ -157,7 +166,7 @@ ul.fancytree-container { | ||||
|  | ||||
| /* this is done to preserve correct indentation. Better solution would be preferable */ | ||||
| .fancytree-node:not(.fancytree-folder) .fancytree-expander:before { | ||||
|     color: white; | ||||
|     color: var(--main-background-color); /* setting to background color makes this invisible */ | ||||
| } | ||||
|  | ||||
| .fancytree-node.fancytree-expanded .fancytree-expander:before { | ||||
| @@ -180,6 +189,7 @@ ul.fancytree-container { | ||||
|     padding-top: 10px; | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     font-family: var(--detail-font-family); | ||||
| } | ||||
|  | ||||
| #note-detail-component-wrapper { | ||||
| @@ -211,6 +221,11 @@ ul.fancytree-container { | ||||
|     /* This is because with empty content height of editor is 0 and it's impossible to click into it */ | ||||
|     min-height: 200px; | ||||
|     overflow: auto; | ||||
|     font-family: var(--detail-text-font-family); | ||||
| } | ||||
|  | ||||
| #note-detail-text p:first-child, #note-detail-text::before { | ||||
|     margin-top: 0; | ||||
| } | ||||
|  | ||||
| /** we disable shield background when in distraction free mode because I couldn't get it to stay static | ||||
| @@ -256,21 +271,31 @@ span.fancytree-node.fancytree-active-clone:not(.fancytree-active) .fancytree-tit | ||||
| span.fancytree-active.fancytree-focused .fancytree-title { | ||||
|     color: var(--active-item-text-color) !important; | ||||
|     background-color: var(--active-item-background-color) !important; | ||||
|     border-color: #ddd !important; | ||||
|     border-color: var(--main-border-color) !important; | ||||
|     border-radius: 3px; | ||||
|     font-weight: bold; | ||||
| } | ||||
|  | ||||
| span.fancytree-active:not(.fancytree-focused) .fancytree-title, span.fancytree-selected .fancytree-title { | ||||
| span.fancytree-active:not(.fancytree-focused) .fancytree-title { | ||||
|     color: var(--hover-item-text-color) !important; | ||||
|     background-color: var(--hover-item-background-color) !important; | ||||
|     border-color: #ddd !important; | ||||
|     border-color: var(--main-border-color) !important; | ||||
|     border-radius: 3px; | ||||
|     font-weight: bold; | ||||
| } | ||||
|  | ||||
| span.fancytree-selected:not(.fancytree-active) .fancytree-title { | ||||
|     color: var(--hover-item-text-color) !important; | ||||
|     background-color: var(--hover-item-background-color) !important; | ||||
|     border-color: var(--main-border-color) !important; | ||||
|     border-radius: 3px; | ||||
|     font-style: italic; | ||||
| } | ||||
|  | ||||
| span.fancytree-node:not(.fancytree-active):hover span.fancytree-title { | ||||
|     color: var(--hover-item-text-color) !important; | ||||
|     background-color: var(--hover-item-background-color) !important; | ||||
|     border-color: #ddd !important; | ||||
|     border-color: var(--main-border-color) !important; | ||||
|     border-radius: 3px; | ||||
| } | ||||
|  | ||||
| @@ -313,13 +338,17 @@ div.ui-tooltip { | ||||
|     margin-top: 10px; | ||||
|     display: none; | ||||
|     overflow: auto; | ||||
|     border-bottom: 2px solid #ddd; | ||||
|     border-bottom: 2px solid var(--main-border-color); | ||||
| } | ||||
|  | ||||
| #search-results ul { | ||||
|     padding: 5px 5px 5px 15px; | ||||
| } | ||||
|  | ||||
| #search-text { | ||||
|     border: 1px solid var(--main-border-color); | ||||
| } | ||||
|  | ||||
| /* | ||||
| * .electron-in-page-search-window is a class specified to default | ||||
| * <webview> element for search window. | ||||
| @@ -430,6 +459,11 @@ div.ui-tooltip { | ||||
|     min-height: 200px; | ||||
| } | ||||
|  | ||||
| .CodeMirror-gutters { | ||||
|     background-color: inherit !important; | ||||
|     border-right: none; | ||||
| } | ||||
|  | ||||
| #note-id-display { | ||||
|     position: absolute; | ||||
|     right: 10px; | ||||
| @@ -450,10 +484,12 @@ div.ui-tooltip { | ||||
|     overflow: auto; | ||||
|     /* limiting the size since actual note content is more important */ | ||||
|     max-height: 30%; | ||||
|     flex-shrink: 0; | ||||
|     flex-basis: 2em; | ||||
| } | ||||
|  | ||||
| #label-list, #relation-list, #attribute-list { | ||||
|     color: #777777; | ||||
|     color: var(--muted-text-color); | ||||
|     padding: 5px; | ||||
|     display: none; | ||||
| } | ||||
| @@ -479,9 +515,8 @@ div.ui-tooltip { | ||||
|     overflow: auto; | ||||
| } | ||||
|  | ||||
| .child-overview { | ||||
| .child-overview-item { | ||||
|     font-weight: bold; | ||||
|     font-size: larger; | ||||
|     padding: 10px; | ||||
|     background: var(--accented-background-color); | ||||
|     width: 150px; | ||||
| @@ -496,7 +531,7 @@ div.ui-tooltip { | ||||
|     word-wrap: break-word; | ||||
| } | ||||
|  | ||||
| .child-overview a { | ||||
| .child-overview-item a { | ||||
|     color: var(--muted-text-color); | ||||
| } | ||||
|  | ||||
| @@ -522,7 +557,7 @@ div.ui-tooltip { | ||||
| } | ||||
|  | ||||
| .btn.active:not(.btn-primary) { | ||||
|     background-color: #ccc !important; | ||||
|     background-color: var(--button-disabled-background-color) !important; | ||||
| } | ||||
|  | ||||
| #note-path-list a.current { | ||||
| @@ -569,8 +604,12 @@ button.icon-button { | ||||
|     max-width: 100%; | ||||
| } | ||||
|  | ||||
| pre:not(.CodeMirror-line) { | ||||
|     color: var(--main-text-color) !important; | ||||
| } | ||||
|  | ||||
| #file-preview-content { | ||||
|     background-color: #f6f6f6; | ||||
|     background-color: var(--accented-background-color); | ||||
|     padding: 15px; | ||||
|     max-width: 600px; | ||||
|     max-height: 300px; | ||||
| @@ -612,7 +651,7 @@ button.icon-button { | ||||
|  | ||||
| .go-to-selected-note-button.disabled, .go-to-selected-note-button.disabled:hover { | ||||
|     cursor: inherit; | ||||
|     color: #ccc !important; | ||||
|     color: var(--button-disabled-background-color) !important; | ||||
| } | ||||
|  | ||||
| .note-autocomplete-input { | ||||
| @@ -651,7 +690,7 @@ table.promoted-attributes-in-tooltip td, table.promoted-attributes-in-tooltip th | ||||
|     max-height: 300px; | ||||
|     overflow: hidden; | ||||
|     color: var(--main-text-color); | ||||
|     border: 1px solid #ccc; | ||||
|     border: 1px solid var(--main-border-color); | ||||
|     border-radius: 5px; | ||||
|     text-align: left; | ||||
| } | ||||
| @@ -681,7 +720,7 @@ table.promoted-attributes-in-tooltip td, table.promoted-attributes-in-tooltip th | ||||
| .algolia-autocomplete .aa-dropdown-menu { | ||||
|     width: 100%; | ||||
|     background-color: var(--main-background-color); | ||||
|     border: 1px solid #999; | ||||
|     border: 1px solid var(--main-border-color); | ||||
|     border-top: none; | ||||
|     z-index: 2000 !important; | ||||
|     max-height: 500px; | ||||
| @@ -768,9 +807,9 @@ div[data-notify="container"] { | ||||
| #saved-indicator { | ||||
|     position: absolute; | ||||
|     right: 10px; | ||||
|     top: 11px; | ||||
|     top: -7px; | ||||
|     font-size: 150%; | ||||
|     color: #777; | ||||
|     color: var(--main-text-color); | ||||
|     z-index: 100; | ||||
| } | ||||
|  | ||||
| @@ -807,7 +846,7 @@ div[data-notify="container"] { | ||||
|  | ||||
|     margin: var(--ck-spacing-large) 0; | ||||
|  | ||||
|     color: #aaa; | ||||
|     color: var(--muted-text-color); | ||||
| } | ||||
|  | ||||
| .fancytree-loading span.fancytree-expander { | ||||
|   | ||||
| @@ -34,8 +34,7 @@ async function uploadFile(req) { | ||||
|     }; | ||||
| } | ||||
|  | ||||
| async function downloadFile(req, res) { | ||||
|     const noteId = req.params.noteId; | ||||
| async function downloadNoteFile(noteId, res) { | ||||
|     const note = await repository.getNote(noteId); | ||||
|  | ||||
|     if (!note) { | ||||
| @@ -43,8 +42,7 @@ async function downloadFile(req, res) { | ||||
|     } | ||||
|  | ||||
|     if (note.isProtected && !protectedSessionService.isProtectedSessionAvailable()) { | ||||
|         res.status(401).send("Protected session not available"); | ||||
|         return; | ||||
|         return res.status(401).send("Protected session not available"); | ||||
|     } | ||||
|  | ||||
|     const originalFileName = await note.getLabel('originalFileName'); | ||||
| @@ -56,7 +54,15 @@ async function downloadFile(req, res) { | ||||
|     res.send(note.content); | ||||
| } | ||||
|  | ||||
| async function downloadFile(req, res) { | ||||
|     const noteId = req.params.noteId; | ||||
|  | ||||
|     return await downloadNoteFile(noteId, res); | ||||
|  | ||||
| } | ||||
|  | ||||
| module.exports = { | ||||
|     uploadFile, | ||||
|     downloadFile | ||||
|     downloadFile, | ||||
|     downloadNoteFile | ||||
| }; | ||||
| @@ -1,8 +1,8 @@ | ||||
| "use strict"; | ||||
|  | ||||
| const sql = require('../../services/sql'); | ||||
| const optionService = require('../../services/options'); | ||||
| const log = require('../../services/log'); | ||||
| const attributes = require('../../services/attributes'); | ||||
|  | ||||
| // options allowed to be updated directly in options dialog | ||||
| const ALLOWED_OPTIONS = ['protectedSessionTimeout', 'noteRevisionSnapshotTimeInterval', | ||||
| @@ -42,8 +42,31 @@ async function update(name, value) { | ||||
|     return true; | ||||
| } | ||||
|  | ||||
| async function getUserThemes() { | ||||
|     const notes = await attributes.getNotesWithLabel('appTheme'); | ||||
|  | ||||
|     const ret = []; | ||||
|  | ||||
|     for (const note of notes) { | ||||
|         let value = await note.getLabelValue('appTheme'); | ||||
|  | ||||
|         if (!value) { | ||||
|             value = note.title.toLowerCase().replace(/[^a-z0-9]/gi, '-'); | ||||
|         } | ||||
|  | ||||
|         ret.push({ | ||||
|             val: value, | ||||
|             title: note.title, | ||||
|             noteId: note.noteId | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     return ret; | ||||
| } | ||||
|  | ||||
| module.exports = { | ||||
|     getOptions, | ||||
|     updateOption, | ||||
|     updateOptions | ||||
|     updateOptions, | ||||
|     getUserThemes | ||||
| }; | ||||
| @@ -19,7 +19,7 @@ async function exec(req) { | ||||
| async function run(req) { | ||||
|     const note = await repository.getNote(req.params.noteId); | ||||
|  | ||||
|     const result = await scriptService.executeNote(note, note); | ||||
|     const result = await scriptService.executeNote(note, { originEntity: note }); | ||||
|  | ||||
|     return { executionResult: result }; | ||||
| } | ||||
|   | ||||
							
								
								
									
										66
									
								
								src/routes/custom.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								src/routes/custom.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,66 @@ | ||||
| const repository = require('../services/repository'); | ||||
| const log = require('../services/log'); | ||||
| const fileUploadService = require('./api/file_upload'); | ||||
| const scriptService = require('../services/script'); | ||||
|  | ||||
| function register(router) { | ||||
|     router.all('/custom/:path*', async (req, res, next) => { | ||||
|         // express puts content after first slash into 0 index element | ||||
|         const path = req.params.path + req.params[0]; | ||||
|  | ||||
|         const attrs = await repository.getEntities("SELECT * FROM attributes WHERE isDeleted = 0 AND type = 'label' AND name IN ('customRequestHandler', 'customResourceProvider')"); | ||||
|  | ||||
|         for (const attr of attrs) { | ||||
|             const regex = new RegExp(attr.value); | ||||
|             let match; | ||||
|  | ||||
|             try { | ||||
|                 match = path.match(regex); | ||||
|             } | ||||
|             catch (e) { | ||||
|                 log.error(`Testing path for label ${attr.attributeId}, regex=${attr.value} failed with error ` + e.stack); | ||||
|                 continue; | ||||
|             } | ||||
|  | ||||
|             if (!match) { | ||||
|                 continue; | ||||
|             } | ||||
|  | ||||
|             if (attr.name === 'customRequestHandler') { | ||||
|                 const note = await attr.getNote(); | ||||
|  | ||||
|                 log.info(`Handling custom request "${path}" with note ${note.noteId}`); | ||||
|  | ||||
|                 try { | ||||
|                     await scriptService.executeNote(note, { | ||||
|                         pathParams: match.slice(1), | ||||
|                         req, | ||||
|                         res | ||||
|                     }); | ||||
|                 } | ||||
|                 catch (e) { | ||||
|                     log.error(`Custom handler ${note.noteId} failed with ${e.message}`); | ||||
|  | ||||
|                     res.status(500).send(e.message); | ||||
|                 } | ||||
|             } | ||||
|             else if (attr.name === 'customResourceProvider') { | ||||
|                 await fileUploadService.downloadNoteFile(attr.noteId, res); | ||||
|             } | ||||
|             else { | ||||
|                 throw new Error("Unrecognized attribute name " + attr.name); | ||||
|             } | ||||
|  | ||||
|             return; // only first handler is executed | ||||
|         } | ||||
|  | ||||
|         const message = `No handler matched for custom ${path} request.`; | ||||
|  | ||||
|         log.info(message); | ||||
|         res.status(404).send(message); | ||||
|     }); | ||||
| } | ||||
|  | ||||
| module.exports = { | ||||
|     register | ||||
| }; | ||||
| @@ -22,22 +22,13 @@ async function index(req, res) { | ||||
|         sourceId: await sourceIdService.generateSourceId(), | ||||
|         maxSyncIdAtLoad: await sql.getValue("SELECT MAX(id) FROM sync"), | ||||
|         instanceName: config.General ? config.General.instanceName : null, | ||||
|         appCss: await getAppCss() | ||||
|         appCssNoteIds: await getAppCssNoteIds() | ||||
|     }); | ||||
| } | ||||
|  | ||||
| async function getAppCss() { | ||||
|     let css = ''; | ||||
|     const notes = attributeService.getNotesWithLabel('appCss'); | ||||
|  | ||||
|     for (const note of await notes) { | ||||
|         css += `/* ${note.noteId} */ | ||||
| ${note.content} | ||||
|  | ||||
| `; | ||||
|     } | ||||
|  | ||||
|     return css; | ||||
| async function getAppCssNoteIds() { | ||||
|     return (await attributeService.getNotesWithLabels(['appCss', 'appTheme'])) | ||||
|         .map(note => note.noteId); | ||||
| } | ||||
|  | ||||
| module.exports = { | ||||
|   | ||||
| @@ -135,6 +135,8 @@ function register(app) { | ||||
|         filesRoute.uploadFile, apiResultHandler); | ||||
|  | ||||
|     route(GET, '/api/notes/:noteId/download', [auth.checkApiAuthOrElectron], filesRoute.downloadFile); | ||||
|     // this "hacky" path is used for easier referencing of CSS resources | ||||
|     route(GET, '/api/notes/download/:noteId', [auth.checkApiAuthOrElectron], filesRoute.downloadFile); | ||||
|  | ||||
|     apiRoute(GET, '/api/notes/:noteId/attributes', attributesRoute.getEffectiveNoteAttributes); | ||||
|     apiRoute(PUT, '/api/notes/:noteId/attributes', attributesRoute.updateNoteAttributes); | ||||
| @@ -153,6 +155,7 @@ function register(app) { | ||||
|     apiRoute(GET, '/api/options', optionsApiRoute.getOptions); | ||||
|     apiRoute(PUT, '/api/options/:name/:value*', optionsApiRoute.updateOption); | ||||
|     apiRoute(PUT, '/api/options', optionsApiRoute.updateOptions); | ||||
|     apiRoute(GET, '/api/options/user-themes', optionsApiRoute.getUserThemes); | ||||
|  | ||||
|     apiRoute(POST, '/api/password/change', passwordApiRoute.changePassword); | ||||
|  | ||||
|   | ||||
| @@ -15,8 +15,12 @@ const BUILTIN_ATTRIBUTES = [ | ||||
|     { type: 'label', name: 'manualTransactionHandling' }, | ||||
|     { type: 'label', name: 'disableInclusion' }, | ||||
|     { type: 'label', name: 'appCss' }, | ||||
|     { type: 'label', name: 'appTheme' }, | ||||
|     { type: 'label', name: 'hideChildrenOverview' }, | ||||
|     { type: 'label', name: 'hidePromotedAttributes' }, | ||||
|     { type: 'label', name: 'readOnly' }, | ||||
|     { type: 'label', name: 'customRequestHandler' }, | ||||
|     { type: 'label', name: 'customResourceProvider' }, | ||||
|  | ||||
|     // relation names | ||||
|     { type: 'relation', name: 'runOnNoteView' }, | ||||
| @@ -43,6 +47,13 @@ async function getNotesWithLabel(name, value) { | ||||
|           WHERE notes.isDeleted = 0 AND attributes.isDeleted = 0 AND attributes.name = ? ${valueCondition} ORDER BY position`, params); | ||||
| } | ||||
|  | ||||
| async function getNotesWithLabels(names) { | ||||
|     const questionMarks = names.map(() => "?").join(", "); | ||||
|  | ||||
|     return await repository.getEntities(`SELECT notes.* FROM notes JOIN attributes USING(noteId)  | ||||
|           WHERE notes.isDeleted = 0 AND attributes.isDeleted = 0 AND attributes.name IN (${questionMarks}) ORDER BY position`, names); | ||||
| } | ||||
|  | ||||
| async function getNoteWithLabel(name, value) { | ||||
|     const notes = await getNotesWithLabel(name, value); | ||||
|  | ||||
| @@ -85,6 +96,7 @@ async function getAttributeNames(type, nameLike) { | ||||
|  | ||||
| module.exports = { | ||||
|     getNotesWithLabel, | ||||
|     getNotesWithLabels, | ||||
|     getNoteWithLabel, | ||||
|     createLabel, | ||||
|     createAttribute, | ||||
|   | ||||
| @@ -19,13 +19,17 @@ const appInfo = require('./app_info'); | ||||
|  * @constructor | ||||
|  * @hideconstructor | ||||
|  */ | ||||
| function BackendScriptApi(startNote, currentNote, originEntity) { | ||||
| function BackendScriptApi(currentNote, apiParams) { | ||||
|     /** @property {Note} note where script started executing */ | ||||
|     this.startNote = startNote; | ||||
|     this.startNote = apiParams.startNote; | ||||
|     /** @property {Note} note where script is currently executing */ | ||||
|     this.currentNote = currentNote; | ||||
|     /** @property {Entity} entity whose event triggered this executions */ | ||||
|     this.originEntity = originEntity; | ||||
|     this.originEntity = apiParams.originEntity; | ||||
|  | ||||
|     for (const key in apiParams) { | ||||
|         this[key] = apiParams[key]; | ||||
|     } | ||||
|  | ||||
|     this.axios = axios; | ||||
|  | ||||
| @@ -169,6 +173,23 @@ function BackendScriptApi(startNote, currentNote, originEntity) { | ||||
|      */ | ||||
|     this.createNote = noteService.createNote; | ||||
|  | ||||
|     /** | ||||
|      * Creates new note according to given params and force all connected clients to refresh their tree. | ||||
|      * | ||||
|      * @method | ||||
|      * | ||||
|      * @param {string} parentNoteId - create new note under this parent | ||||
|      * @param {string} title | ||||
|      * @param {string} [content=""] | ||||
|      * @param {CreateNoteExtraOptions} [extraOptions={}] | ||||
|      * @returns {Promise<{note: Note, branch: Branch}>} object contains newly created entities note and branch | ||||
|      */ | ||||
|     this.createNoteAndRefresh = async function(parentNoteId, title, content, extraOptions) { | ||||
|         await noteService.createNote(parentNoteId, title, content, extraOptions); | ||||
|  | ||||
|         messagingService.refreshTree(); | ||||
|     }; | ||||
|  | ||||
|     /** | ||||
|      * Log given message to trilium logs. | ||||
|      * | ||||
| @@ -234,7 +255,7 @@ function BackendScriptApi(startNote, currentNote, originEntity) { | ||||
|      * | ||||
|      * @returns {Promise<void>} | ||||
|      */ | ||||
|     this.refreshTree = () => messagingService.sendMessageToAllClients({ type: 'refresh-tree' }); | ||||
|     this.refreshTree = messagingService.refreshTree; | ||||
|  | ||||
|     /** | ||||
|      * @return {{syncVersion, appVersion, buildRevision, dbVersion, dataDirectory, buildDate}|*} - object representing basic info about running Trilium version | ||||
|   | ||||
| @@ -1 +1 @@ | ||||
| module.exports = { buildDate:"2019-01-22T23:01:32+01:00", buildRevision: "2ac560c56e2d347fccc0ad51b8d62999408a7f74" }; | ||||
| module.exports = { buildDate:"2019-02-03T15:44:19+01:00", buildRevision: "afd5f4823f1f605300f906a61e8822e857b9ee5f" }; | ||||
|   | ||||
| @@ -5,23 +5,39 @@ const sqlInit = require('./sql_init'); | ||||
| const log = require('./log'); | ||||
| const messagingService = require('./messaging'); | ||||
| const syncMutexService = require('./sync_mutex'); | ||||
| const repository = require('./repository.js'); | ||||
| const repository = require('./repository'); | ||||
| const cls = require('./cls'); | ||||
| const syncTableService = require('./sync_table'); | ||||
| const Branch = require('../entities/branch'); | ||||
|  | ||||
| async function runCheck(query, errorText, errorList) { | ||||
|     const result = await sql.getColumn(query); | ||||
| let unrecoverableConsistencyErrors = false; | ||||
| let fixedIssues = false; | ||||
|  | ||||
|     if (result.length > 0) { | ||||
|         const resultText = result.map(val => "'" + val + "'").join(', '); | ||||
| async function findIssues(query, errorCb) { | ||||
|     const results = await sql.getRows(query); | ||||
|  | ||||
|         const err = errorText + ": " + resultText; | ||||
|         errorList.push(err); | ||||
|     for (const res of results) { | ||||
|         logError(errorCb(res)); | ||||
|  | ||||
|         log.error(err); | ||||
|         unrecoverableConsistencyErrors = true; | ||||
|     } | ||||
|  | ||||
|     return results; | ||||
| } | ||||
|  | ||||
| async function checkTreeCycles(errorList) { | ||||
| async function findAndFixIssues(query, fixerCb) { | ||||
|     const results = await sql.getRows(query); | ||||
|  | ||||
|     for (const res of results) { | ||||
|         await fixerCb(res); | ||||
|  | ||||
|         fixedIssues = true; | ||||
|     } | ||||
|  | ||||
|     return results; | ||||
| } | ||||
|  | ||||
| async function checkTreeCycles() { | ||||
|     const childToParents = {}; | ||||
|     const rows = await sql.getRows("SELECT noteId, parentNoteId FROM branches WHERE isDeleted = 0"); | ||||
|  | ||||
| @@ -33,25 +49,29 @@ async function checkTreeCycles(errorList) { | ||||
|         childToParents[childNoteId].push(parentNoteId); | ||||
|     } | ||||
|  | ||||
|     function checkTreeCycle(noteId, path, errorList) { | ||||
|     function checkTreeCycle(noteId, path) { | ||||
|         if (noteId === 'root') { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if (!childToParents[noteId] || childToParents[noteId].length === 0) { | ||||
|             errorList.push(`No parents found for noteId=${noteId}`); | ||||
|             logError(`No parents found for note ${noteId}`); | ||||
|  | ||||
|             unrecoverableConsistencyErrors = true; | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         for (const parentNoteId of childToParents[noteId]) { | ||||
|             if (path.includes(parentNoteId)) { | ||||
|                 errorList.push(`Tree cycle detected at parent-child relationship: ${parentNoteId} - ${noteId}, whole path: ${path}`); | ||||
|                 logError(`Tree cycle detected at parent-child relationship: ${parentNoteId} - ${noteId}, whole path: ${path}`); | ||||
|  | ||||
|                 unrecoverableConsistencyErrors = true; | ||||
|             } | ||||
|             else { | ||||
|                 const newPath = path.slice(); | ||||
|                 newPath.push(noteId); | ||||
|  | ||||
|                 checkTreeCycle(parentNoteId, newPath, errorList); | ||||
|                 checkTreeCycle(parentNoteId, newPath); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| @@ -59,279 +79,364 @@ async function checkTreeCycles(errorList) { | ||||
|     const noteIds = Object.keys(childToParents); | ||||
|  | ||||
|     for (const noteId of noteIds) { | ||||
|         checkTreeCycle(noteId, [], errorList); | ||||
|         checkTreeCycle(noteId, []); | ||||
|     } | ||||
|  | ||||
|     if (childToParents['root'].length !== 1 || childToParents['root'][0] !== 'none') { | ||||
|         errorList.push('Incorrect root parent: ' + JSON.stringify(childToParents['root'])); | ||||
|         logError('Incorrect root parent: ' + JSON.stringify(childToParents['root'])); | ||||
|         unrecoverableConsistencyErrors = true; | ||||
|     } | ||||
| } | ||||
|  | ||||
| async function runSyncRowChecks(table, key, errorList) { | ||||
|     await runCheck(` | ||||
|         SELECT  | ||||
|           ${key}  | ||||
|         FROM  | ||||
|           ${table}  | ||||
|           LEFT JOIN sync ON sync.entityName = '${table}' AND entityId = ${key}  | ||||
|         WHERE  | ||||
|           sync.id IS NULL AND ` + (table === 'options' ? 'isSynced = 1' : '1'), | ||||
|         `Missing sync records for ${key} in table ${table}`, errorList); | ||||
| async function findBrokenReferenceIssues() { | ||||
|     await findIssues(` | ||||
|           SELECT branchId, branches.noteId | ||||
|           FROM branches LEFT JOIN notes USING(noteId) | ||||
|           WHERE notes.noteId IS NULL`, | ||||
|         ({branchId, noteId}) => `Branch ${branchId} references missing note ${noteId}`); | ||||
|  | ||||
|     await runCheck(` | ||||
|         SELECT  | ||||
|           entityId  | ||||
|         FROM  | ||||
|           sync  | ||||
|           LEFT JOIN ${table} ON entityId = ${key}  | ||||
|         WHERE  | ||||
|           sync.entityName = '${table}'  | ||||
|           AND ${key} IS NULL`, | ||||
|         `Missing ${table} records for existing sync rows`, errorList); | ||||
|     await findIssues(` | ||||
|           SELECT branchId, branches.noteId AS parentNoteId | ||||
|           FROM branches LEFT JOIN notes ON notes.noteId = branches.parentNoteId | ||||
|           WHERE branches.branchId != 'root' AND notes.noteId IS NULL`, | ||||
|         ({branchId, noteId}) => `Branch ${branchId} references missing parent note ${noteId}`); | ||||
|  | ||||
|     await findIssues(` | ||||
|           SELECT attributeId, attributes.noteId | ||||
|           FROM attributes LEFT JOIN notes USING(noteId) | ||||
|           WHERE notes.noteId IS NULL`, | ||||
|         ({attributeId, noteId}) => `Attribute ${attributeId} references missing source note ${noteId}`); | ||||
|  | ||||
|     // empty targetNoteId for relations is a special fixable case so not covered here | ||||
|     await findIssues(` | ||||
|           SELECT attributeId, attributes.noteId | ||||
|           FROM attributes LEFT JOIN notes ON notes.noteId = attributes.value | ||||
|           WHERE attributes.type = 'relation' AND attributes.value != '' AND notes.noteId IS NULL`, | ||||
|         ({attributeId, noteId}) => `Relation ${attributeId} references missing note ${noteId}`); | ||||
|  | ||||
|     await findIssues(` | ||||
|           SELECT linkId, links.noteId | ||||
|           FROM links LEFT JOIN notes USING(noteId) | ||||
|           WHERE notes.noteId IS NULL`, | ||||
|         ({linkId, noteId}) => `Link ${linkId} references missing source note ${noteId}`); | ||||
|  | ||||
|     await findIssues(` | ||||
|           SELECT linkId, links.noteId | ||||
|           FROM links LEFT JOIN notes ON notes.noteId = links.targetNoteId | ||||
|           WHERE notes.noteId IS NULL`, | ||||
|         ({linkId, noteId}) => `Link ${linkId} references missing target note ${noteId}`); | ||||
|  | ||||
|     await findIssues(` | ||||
|           SELECT noteRevisionId, note_revisions.noteId | ||||
|           FROM note_revisions LEFT JOIN notes USING(noteId) | ||||
|           WHERE notes.noteId IS NULL`, | ||||
|         ({noteRevisionId, noteId}) => `Note revision ${noteRevisionId} references missing note ${noteId}`); | ||||
| } | ||||
|  | ||||
| async function fixEmptyRelationTargets(errorList) { | ||||
|     const emptyRelations = await repository.getEntities("SELECT * FROM attributes WHERE isDeleted = 0 AND type = 'relation' AND value = ''"); | ||||
| async function findExistencyIssues() { | ||||
|     // principle for fixing inconsistencies is that if the note itself is deleted (isDeleted=true) then all related entities should be also deleted (branches, links, attributes) | ||||
|     // but if note is not deleted, then at least one branch should exist. | ||||
|  | ||||
|     for (const relation of emptyRelations) { | ||||
|         relation.isDeleted = true; | ||||
|         await relation.save(); | ||||
|     // the order here is important - first we might need to delete inconsistent branches and after that | ||||
|     // another check might create missing branch | ||||
|     await findAndFixIssues(` | ||||
|           SELECT | ||||
|             branchId, noteId | ||||
|           FROM | ||||
|             branches | ||||
|               JOIN notes USING(noteId) | ||||
|           WHERE | ||||
|             notes.isDeleted = 1 | ||||
|             AND branches.isDeleted = 0`, | ||||
|         async ({branchId, noteId}) => { | ||||
|             const branch = await repository.getBranch(branchId); | ||||
|             branch.isDeleted = true; | ||||
|             await branch.save(); | ||||
|  | ||||
|         errorList.push(`Relation ${relation.attributeId} of name "${relation.name} has empty target. Autofixed.`); | ||||
|     } | ||||
| } | ||||
|             logFix(`Branch ${branchId} has been deleted since associated note ${noteId} is deleted.`); | ||||
|         }); | ||||
|  | ||||
| async function fixUndeletedBranches() { | ||||
|     const undeletedBranches = await sql.getRows(` | ||||
|           SELECT  | ||||
|             branchId, noteId  | ||||
|           FROM  | ||||
|             branches  | ||||
|             JOIN notes USING(noteId)  | ||||
|           WHERE  | ||||
|             notes.isDeleted = 1  | ||||
|             AND branches.isDeleted = 0`); | ||||
|  | ||||
|     for (const {branchId, noteId} of undeletedBranches) { | ||||
|     await findAndFixIssues(` | ||||
|       SELECT | ||||
|         branchId, parentNoteId | ||||
|       FROM | ||||
|         branches | ||||
|         JOIN notes AS parentNote ON parentNote.noteId = branches.parentNoteId | ||||
|       WHERE | ||||
|         parentNote.isDeleted = 1 | ||||
|         AND branches.isDeleted = 0 | ||||
|     `, async ({branchId, parentNoteId}) => { | ||||
|         const branch = await repository.getBranch(branchId); | ||||
|         branch.isDeleted = true; | ||||
|         await branch.save(); | ||||
|  | ||||
|         log.info(`Branch ${branchId} has been deleted since associated note ${noteId} is deleted.`); | ||||
|     } | ||||
|         logFix(`Branch ${branchId} has been deleted since associated parent note ${parentNoteId} is deleted.`); | ||||
|     }); | ||||
|  | ||||
|     await findAndFixIssues(` | ||||
|       SELECT | ||||
|         DISTINCT notes.noteId | ||||
|       FROM | ||||
|         notes | ||||
|         LEFT JOIN branches ON notes.noteId = branches.noteId AND branches.isDeleted = 0 | ||||
|       WHERE | ||||
|         notes.isDeleted = 0 | ||||
|         AND branches.branchId IS NULL | ||||
|     `, async ({noteId}) => { | ||||
|         const branch = await new Branch({ | ||||
|             parentNoteId: 'root', | ||||
|             noteId: noteId, | ||||
|             prefix: 'recovered' | ||||
|         }).save(); | ||||
|  | ||||
|         logFix(`Created missing branch ${branch.branchId} for note ${noteId}`); | ||||
|     }); | ||||
|  | ||||
|     // there should be a unique relationship between note and its parent | ||||
|     await findAndFixIssues(` | ||||
|       SELECT | ||||
|         noteId, parentNoteId | ||||
|       FROM | ||||
|         branches | ||||
|       WHERE | ||||
|         branches.isDeleted = 0 | ||||
|       GROUP BY | ||||
|         branches.parentNoteId, | ||||
|         branches.noteId | ||||
|       HAVING | ||||
|         COUNT(*) > 1`, | ||||
|     async ({noteId, parentNoteId}) => { | ||||
|         const branches = await repository.getEntities(`SELECT * FROM branches WHERE noteId = ? and parentNoteId = ? and isDeleted = 1`, [noteId, parentNoteId]); | ||||
|  | ||||
|         // it's not necessarily "original" branch, it's just the only one which will survive | ||||
|         const origBranch = branches.get(0); | ||||
|  | ||||
|         // delete all but the first branch | ||||
|         for (const branch of branches.slice(1)) { | ||||
|             branch.isDeleted = true; | ||||
|             await branch.save(); | ||||
|  | ||||
|             logFix(`Removing branch ${branch.branchId} since it's parent-child duplicate of branch ${origBranch.branchId}`); | ||||
|         } | ||||
|     }); | ||||
| } | ||||
|  | ||||
| async function runAllChecks() { | ||||
|     const errorList = []; | ||||
|  | ||||
|     await runCheck(` | ||||
|           SELECT  | ||||
|             noteId  | ||||
|           FROM  | ||||
|             notes  | ||||
|             LEFT JOIN branches USING(noteId)  | ||||
|           WHERE  | ||||
|             noteId != 'root'  | ||||
|             AND branches.branchId IS NULL`, | ||||
|         "Missing branches records for following note IDs", errorList); | ||||
|  | ||||
|     await runCheck(` | ||||
|           SELECT  | ||||
|             branchId || ' > ' || branches.noteId  | ||||
|           FROM  | ||||
|             branches  | ||||
|             LEFT JOIN notes USING(noteId)  | ||||
|           WHERE  | ||||
|             notes.noteId IS NULL`, | ||||
|         "Missing notes records for following branch ID > note ID", errorList); | ||||
|  | ||||
|     await fixUndeletedBranches(); | ||||
|  | ||||
|     await runCheck(` | ||||
|           SELECT  | ||||
|             child.branchId | ||||
|           FROM  | ||||
|             branches AS child | ||||
|           WHERE  | ||||
|             child.isDeleted = 0 | ||||
|             AND child.parentNoteId != 'none' | ||||
|             AND (SELECT COUNT(*) FROM branches AS parent WHERE parent.noteId = child.parentNoteId  | ||||
|                                                                  AND parent.isDeleted = 0) = 0`, | ||||
|         "All parent branches are deleted but child branch is not for these child branch IDs", errorList); | ||||
|  | ||||
|     // we do extra JOIN to eliminate orphan notes without branches (which are reported separately) | ||||
|     await runCheck(` | ||||
|           SELECT | ||||
|             DISTINCT noteId | ||||
|           FROM | ||||
|             notes | ||||
|             JOIN branches USING(noteId) | ||||
| async function findLogicIssues() { | ||||
|     await findIssues( ` | ||||
|           SELECT noteId, type  | ||||
|           FROM notes  | ||||
|           WHERE | ||||
|             (SELECT COUNT(*) FROM branches WHERE notes.noteId = branches.noteId AND branches.isDeleted = 0) = 0 | ||||
|             AND notes.isDeleted = 0 | ||||
|     `, 'No undeleted branches for note IDs', errorList); | ||||
|             isDeleted = 0     | ||||
|             AND type NOT IN ('text', 'code', 'render', 'file', 'image', 'search', 'relation-map')`, | ||||
|         ({noteId, type}) => `Note ${noteId} has invalid type=${type}`); | ||||
|  | ||||
|     await runCheck(` | ||||
|           SELECT  | ||||
|             child.parentNoteId || ' > ' || child.noteId  | ||||
|           FROM branches  | ||||
|             AS child  | ||||
|             LEFT JOIN branches AS parent ON parent.noteId = child.parentNoteId  | ||||
|           WHERE  | ||||
|             parent.noteId IS NULL  | ||||
|             AND child.parentNoteId != 'none'`, | ||||
|         "Not existing parent in the following parent > child relations", errorList); | ||||
|  | ||||
|     await runCheck(` | ||||
|           SELECT  | ||||
|             noteRevisionId || ' > ' || note_revisions.noteId  | ||||
|           FROM  | ||||
|             note_revisions LEFT JOIN notes USING(noteId)  | ||||
|           WHERE  | ||||
|             notes.noteId IS NULL`, | ||||
|         "Missing notes records for following note revision ID > note ID", errorList); | ||||
|  | ||||
|     await runCheck(` | ||||
|           SELECT  | ||||
|             branches.parentNoteId || ' > ' || branches.noteId  | ||||
|           FROM  | ||||
|             branches  | ||||
|           WHERE  | ||||
|             branches.isDeleted = 0 | ||||
|           GROUP BY  | ||||
|             branches.parentNoteId, | ||||
|             branches.noteId | ||||
|           HAVING  | ||||
|             COUNT(*) > 1`, | ||||
|         "Duplicate undeleted parent note <-> note relationship - parent note ID > note ID", errorList); | ||||
|  | ||||
|     await runCheck(` | ||||
|           SELECT  | ||||
|             noteId | ||||
|           FROM  | ||||
|             notes | ||||
|           WHERE  | ||||
|             type != 'text'  | ||||
|             AND type != 'code'  | ||||
|             AND type != 'render'  | ||||
|             AND type != 'file'  | ||||
|             AND type != 'image'  | ||||
|             AND type != 'search'  | ||||
|             AND type != 'relation-map'`, | ||||
|         "Note has invalid type", errorList); | ||||
|  | ||||
|     await runCheck(` | ||||
|           SELECT | ||||
|             noteId | ||||
|           FROM | ||||
|             notes | ||||
|     await findIssues(` | ||||
|           SELECT noteId | ||||
|           FROM notes | ||||
|           WHERE | ||||
|             isDeleted = 0 | ||||
|             AND content IS NULL`, | ||||
|         "Note content is null even though it is not deleted", errorList); | ||||
|         ({noteId}) => `Note ${noteId} content is null even though it is not deleted`); | ||||
|  | ||||
|     await runCheck(` | ||||
|           SELECT  | ||||
|             parentNoteId | ||||
|     await findIssues(` | ||||
|           SELECT parentNoteId | ||||
|           FROM  | ||||
|             branches | ||||
|             JOIN notes ON notes.noteId = branches.parentNoteId | ||||
|           WHERE | ||||
|             notes.isDeleted = 0 | ||||
|             AND notes.type == 'search' | ||||
|             AND branches.isDeleted = 0`, | ||||
|         ({parentNoteId}) => `Search note ${parentNoteId} has children`); | ||||
|  | ||||
|     await findAndFixIssues(` | ||||
|           SELECT attributeId  | ||||
|           FROM attributes  | ||||
|           WHERE  | ||||
|             type == 'search'`, | ||||
|         "Search note has children", errorList); | ||||
|             isDeleted = 0  | ||||
|             AND type = 'relation'  | ||||
|             AND value = ''`, | ||||
|         async ({attributeId}) => { | ||||
|             const relation = await repository.getAttribute(attributeId); | ||||
|             relation.isDeleted = true; | ||||
|             await relation.save(); | ||||
|  | ||||
|     await fixEmptyRelationTargets(errorList); | ||||
|             logFix(`Removed relation ${relation.attributeId} of name "${relation.name} with empty target.`); | ||||
|         }); | ||||
|  | ||||
|     await runCheck(` | ||||
|     await findIssues(` | ||||
|           SELECT  | ||||
|             attributeId | ||||
|           FROM  | ||||
|             attributes | ||||
|             attributeId, | ||||
|             type | ||||
|           FROM attributes | ||||
|           WHERE  | ||||
|             type != 'label'  | ||||
|             isDeleted = 0     | ||||
|             AND type != 'label'  | ||||
|             AND type != 'label-definition'  | ||||
|             AND type != 'relation' | ||||
|             AND type != 'relation-definition'`, | ||||
|         "Attribute has invalid type", errorList); | ||||
|         ({attributeId, type}) => `Attribute ${attributeId} has invalid type '${type}'`); | ||||
|  | ||||
|     await runCheck(` | ||||
|           SELECT  | ||||
|             attributeId | ||||
|           FROM  | ||||
|             attributes | ||||
|             LEFT JOIN notes ON attributes.noteId = notes.noteId AND notes.isDeleted = 0 | ||||
|           WHERE | ||||
|             attributes.isDeleted = 0 | ||||
|             AND notes.noteId IS NULL`, | ||||
|         "Attribute reference to the owning note is broken", errorList); | ||||
|  | ||||
|     await runCheck(` | ||||
|     await findAndFixIssues(` | ||||
|           SELECT | ||||
|             attributeId | ||||
|             attributeId, | ||||
|             attributes.noteId | ||||
|           FROM | ||||
|             attributes | ||||
|             LEFT JOIN notes AS targetNote ON attributes.value = targetNote.noteId AND targetNote.isDeleted = 0 | ||||
|             JOIN notes ON attributes.noteId = notes.noteId | ||||
|           WHERE | ||||
|             attributes.isDeleted = 0 | ||||
|             AND notes.isDeleted = 1`, | ||||
|         async ({attributeId, noteId}) => { | ||||
|             const attribute = await repository.getAttribute(attributeId); | ||||
|             attribute.isDeleted = true; | ||||
|             await attribute.save(); | ||||
|  | ||||
|             logFix(`Removed attribute ${attributeId} because owning note ${noteId} is also deleted.`); | ||||
|         }); | ||||
|  | ||||
|     await findAndFixIssues(` | ||||
|           SELECT | ||||
|             attributeId, | ||||
|             attributes.value AS targetNoteId | ||||
|           FROM | ||||
|             attributes | ||||
|             JOIN notes ON attributes.value = notes.noteId | ||||
|           WHERE | ||||
|             attributes.type = 'relation' | ||||
|             AND attributes.isDeleted = 0 | ||||
|             AND targetNote.noteId IS NULL`, | ||||
|         "Relation reference to the target note is broken", errorList); | ||||
|             AND notes.isDeleted = 1`, | ||||
|         async ({attributeId, targetNoteId}) => { | ||||
|             const attribute = await repository.getAttribute(attributeId); | ||||
|             attribute.isDeleted = true; | ||||
|             await attribute.save(); | ||||
|  | ||||
|     await runCheck(` | ||||
|           SELECT  | ||||
|             linkId | ||||
|           FROM  | ||||
|             links | ||||
|           WHERE  | ||||
|             type != 'image' | ||||
|             AND type != 'hyper' | ||||
|             AND type != 'relation-map'`, | ||||
|         "Link type is invalid", errorList); | ||||
|             logFix(`Removed attribute ${attributeId} because target note ${targetNoteId} is also deleted.`); | ||||
|         }); | ||||
|  | ||||
|     await runCheck(` | ||||
|           SELECT  | ||||
|             linkId | ||||
|           FROM  | ||||
|     await findIssues(` | ||||
|           SELECT linkId | ||||
|           FROM links | ||||
|           WHERE type NOT IN ('image', 'hyper', 'relation-map')`, | ||||
|         ({linkId, type}) => `Link ${linkId} has invalid type '${type}'`); | ||||
|  | ||||
|     await findAndFixIssues(` | ||||
|           SELECT | ||||
|             linkId, | ||||
|             links.noteId AS sourceNoteId | ||||
|           FROM | ||||
|             links | ||||
|             LEFT JOIN notes AS sourceNote ON sourceNote.noteId = links.noteId AND sourceNote.isDeleted = 0 | ||||
|             LEFT JOIN notes AS targetNote ON targetNote.noteId = links.noteId AND targetNote.isDeleted = 0 | ||||
|           WHERE  | ||||
|               JOIN notes AS sourceNote ON sourceNote.noteId = links.noteId | ||||
|           WHERE | ||||
|             links.isDeleted = 0 | ||||
|             AND (sourceNote.noteId IS NULL | ||||
|                  OR targetNote.noteId IS NULL)`, | ||||
|         "Link to source/target note link is broken", errorList); | ||||
|             AND sourceNote.isDeleted = 1`, | ||||
|         async ({linkId, sourceNoteId}) => { | ||||
|             const link = await repository.getLink(linkId); | ||||
|             link.isDeleted = true; | ||||
|             await link.save(); | ||||
|  | ||||
|     await runSyncRowChecks("notes", "noteId", errorList); | ||||
|     await runSyncRowChecks("note_revisions", "noteRevisionId", errorList); | ||||
|     await runSyncRowChecks("branches", "branchId", errorList); | ||||
|     await runSyncRowChecks("recent_notes", "branchId", errorList); | ||||
|     await runSyncRowChecks("attributes", "attributeId", errorList); | ||||
|     await runSyncRowChecks("api_tokens", "apiTokenId", errorList); | ||||
|     await runSyncRowChecks("options", "name", errorList); | ||||
|             logFix(`Removed link ${linkId} because source note ${sourceNoteId} is also deleted.`); | ||||
|         }); | ||||
|  | ||||
|     if (errorList.length === 0) { | ||||
|     await findAndFixIssues(` | ||||
|           SELECT  | ||||
|             linkId, | ||||
|             links.targetNoteId | ||||
|           FROM  | ||||
|             links | ||||
|             JOIN notes AS targetNote ON targetNote.noteId = links.targetNoteId | ||||
|           WHERE | ||||
|             links.isDeleted = 0 | ||||
|             AND targetNote.isDeleted = 1`, | ||||
|         async ({linkId, targetNoteId}) => { | ||||
|             const link = await repository.getLink(linkId); | ||||
|             link.isDeleted = true; | ||||
|             await link.save(); | ||||
|  | ||||
|             logFix(`Removed link ${linkId} because target note ${targetNoteId} is also deleted.`); | ||||
|         }); | ||||
| } | ||||
|  | ||||
| async function runSyncRowChecks(entityName, key) { | ||||
|     await findAndFixIssues(` | ||||
|         SELECT  | ||||
|           ${key} as entityId | ||||
|         FROM  | ||||
|           ${entityName}  | ||||
|           LEFT JOIN sync ON sync.entityName = '${entityName}' AND entityId = ${key}  | ||||
|         WHERE  | ||||
|           sync.id IS NULL AND ` + (entityName === 'options' ? 'isSynced = 1' : '1'), | ||||
|         async ({entityId}) => { | ||||
|             await syncTableService.addEntitySync(entityName, entityId); | ||||
|  | ||||
|             logFix(`Created missing sync record entityName=${entityName}, entityId=${entityId}`); | ||||
|         }); | ||||
|  | ||||
|     await findAndFixIssues(` | ||||
|         SELECT  | ||||
|           id, entityId | ||||
|         FROM  | ||||
|           sync  | ||||
|           LEFT JOIN ${entityName} ON entityId = ${key}  | ||||
|         WHERE  | ||||
|           sync.entityName = '${entityName}'  | ||||
|           AND ${key} IS NULL`, | ||||
|         async ({id, entityId}) => { | ||||
|  | ||||
|             await sql.execute("DELETE FROM sync WHERE entityName = ? AND entityId = ?", [entityName, entityId]); | ||||
|  | ||||
|             logFix(`Deleted extra sync record id=${id}, entityName=${entityName}, entityId=${entityId}`); | ||||
|         }); | ||||
| } | ||||
|  | ||||
| async function findSyncRowsIssues() { | ||||
|     await runSyncRowChecks("notes", "noteId"); | ||||
|     await runSyncRowChecks("note_revisions", "noteRevisionId"); | ||||
|     await runSyncRowChecks("branches", "branchId"); | ||||
|     await runSyncRowChecks("recent_notes", "branchId"); | ||||
|     await runSyncRowChecks("attributes", "attributeId"); | ||||
|     await runSyncRowChecks("api_tokens", "apiTokenId"); | ||||
|     await runSyncRowChecks("options", "name"); | ||||
| } | ||||
|  | ||||
| async function runAllChecks() { | ||||
|     unrecoverableConsistencyErrors = false; | ||||
|     fixedIssues = false; | ||||
|  | ||||
|     await findBrokenReferenceIssues(); | ||||
|  | ||||
|     await findExistencyIssues(); | ||||
|  | ||||
|     await findLogicIssues(); | ||||
|  | ||||
|     await findSyncRowsIssues(); | ||||
|  | ||||
|     if (unrecoverableConsistencyErrors) { | ||||
|         // we run this only if basic checks passed since this assumes basic data consistency | ||||
|  | ||||
|         await checkTreeCycles(errorList); | ||||
|         await checkTreeCycles(); | ||||
|     } | ||||
|  | ||||
|     return errorList; | ||||
|     return !unrecoverableConsistencyErrors; | ||||
| } | ||||
|  | ||||
| async function runChecks() { | ||||
|     let errorList; | ||||
|     let elapsedTimeMs; | ||||
|  | ||||
|     await syncMutexService.doExclusively(async () => { | ||||
|         const startTime = new Date(); | ||||
|  | ||||
|         errorList = await runAllChecks(); | ||||
|         await runAllChecks(); | ||||
|  | ||||
|         elapsedTimeMs = new Date().getTime() - startTime.getTime(); | ||||
|     }); | ||||
|  | ||||
|     if (errorList.length > 0) { | ||||
|         log.info(`Consistency checks failed (took ${elapsedTimeMs}ms) with these errors: ` + JSON.stringify(errorList)); | ||||
|     if (fixedIssues) { | ||||
|         messagingService.refreshTree(); | ||||
|     } | ||||
|  | ||||
|     if (unrecoverableConsistencyErrors) { | ||||
|         log.info(`Consistency checks failed (took ${elapsedTimeMs}ms)`); | ||||
|  | ||||
|         messagingService.sendMessageToAllClients({type: 'consistency-checks-failed'}); | ||||
|     } | ||||
| @@ -340,13 +445,19 @@ async function runChecks() { | ||||
|     } | ||||
| } | ||||
|  | ||||
| function logFix(message) { | ||||
|     log.info("Consistency issue fixed: " + message); | ||||
| } | ||||
|  | ||||
| function logError(message) { | ||||
|     log.info("Consistency error: " + message); | ||||
| } | ||||
|  | ||||
| sqlInit.dbReady.then(() => { | ||||
|     setInterval(cls.wrap(runChecks), 60 * 60 * 1000); | ||||
|  | ||||
|     // kickoff backup immediately | ||||
|     setTimeout(cls.wrap(runChecks), 10000); | ||||
|     // kickoff checks soon after startup (to not block the initial load) | ||||
|     setTimeout(cls.wrap(runChecks), 10 * 1000); | ||||
| }); | ||||
|  | ||||
| module.exports = { | ||||
|     runChecks | ||||
| }; | ||||
| module.exports = {}; | ||||
| @@ -20,6 +20,10 @@ async function exportSingleNote(branch, format, res) { | ||||
|  | ||||
|     if (note.type === 'text') { | ||||
|         if (format === 'html') { | ||||
|             if (!note.content.toLowerCase().includes("<html")) { | ||||
|                 note.content = '<html><head><meta charset="utf-8"></head><body>' + note.content + '</body></html>'; | ||||
|             } | ||||
|  | ||||
|             payload = html.prettyPrint(note.content, {indent_size: 2}); | ||||
|             extension = 'html'; | ||||
|             mime = 'text/html'; | ||||
|   | ||||
| @@ -74,11 +74,10 @@ async function exportToTar(branch, format, res) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         const baseFileName = branch.prefix ? (branch.prefix + ' - ' + note.title) : note.title; | ||||
|         const baseFileName = sanitize(branch.prefix ? (branch.prefix + ' - ' + note.title) : note.title); | ||||
|  | ||||
|         if (note.noteId in noteIdToMeta) { | ||||
|             const sanitizedFileName = sanitize(baseFileName + ".clone"); | ||||
|             const fileName = getUniqueFilename(existingFileNames, sanitizedFileName); | ||||
|             const fileName = getUniqueFilename(existingFileNames, baseFileName + ".clone"); | ||||
|  | ||||
|             return { | ||||
|                 isClone: true, | ||||
| @@ -150,6 +149,10 @@ async function exportToTar(branch, format, res) { | ||||
|  | ||||
|     function prepareContent(note, format) { | ||||
|         if (format === 'html') { | ||||
|             if (!note.content.toLowerCase().includes("<html")) { | ||||
|                 note.content = '<html><head><meta charset="utf-8"></head><body>' + note.content + '</body></html>'; | ||||
|             } | ||||
|  | ||||
|             return html.prettyPrint(note.content, {indent_size: 2}); | ||||
|         } | ||||
|         else if (format === 'markdown') { | ||||
|   | ||||
| @@ -12,7 +12,7 @@ async function runAttachedRelations(note, relationName, originEntity) { | ||||
|         const scriptNote = await relation.getTargetNote(); | ||||
|  | ||||
|         if (scriptNote) { | ||||
|             await scriptService.executeNote(scriptNote, originEntity); | ||||
|             await scriptService.executeNoteNoException(scriptNote, { originEntity }); | ||||
|         } | ||||
|         else { | ||||
|             log.error(`Target note ${relation.value} of atttribute ${relation.attributeId} has not been found.`); | ||||
| @@ -30,7 +30,7 @@ eventService.subscribe(eventService.NOTE_TITLE_CHANGED, async note => { | ||||
|             if (await parent.hasLabel("sorted")) { | ||||
|                 await treeService.sortNotesAlphabetically(parent.noteId); | ||||
|  | ||||
|                 messagingService.sendMessageToAllClients({ type: 'refresh-tree' }); | ||||
|                 messagingService.refreshTree(); | ||||
|                 break; // sending the message once is enough | ||||
|             } | ||||
|         } | ||||
|   | ||||
| @@ -49,6 +49,10 @@ async function sendMessage(client, message) { | ||||
|     } | ||||
| } | ||||
|  | ||||
| async function refreshTree() { | ||||
|     await sendMessageToAllClients({ type: 'refresh-tree' }); | ||||
| } | ||||
|  | ||||
| async function sendMessageToAllClients(message) { | ||||
|     const jsonStr = JSON.stringify(message); | ||||
|  | ||||
| @@ -76,5 +80,6 @@ async function sendPing(client, lastSentSyncId) { | ||||
|  | ||||
| module.exports = { | ||||
|     init, | ||||
|     refreshTree, | ||||
|     sendMessageToAllClients | ||||
| }; | ||||
| @@ -57,6 +57,11 @@ async function getOption(name) { | ||||
|     return await getEntity("SELECT * FROM options WHERE name = ?", [name]); | ||||
| } | ||||
|  | ||||
| /** @returns {Link|null} */ | ||||
| async function getLink(linkId) { | ||||
|     return await getEntity("SELECT * FROM links WHERE linkId = ?", [linkId]); | ||||
| } | ||||
|  | ||||
| async function updateEntity(entity) { | ||||
|     const entityName = entity.constructor.entityName; | ||||
|     const primaryKeyName = entity.constructor.primaryKeyName; | ||||
| @@ -119,6 +124,7 @@ module.exports = { | ||||
|     getBranch, | ||||
|     getAttribute, | ||||
|     getOption, | ||||
|     getLink, | ||||
|     updateEntity, | ||||
|     setEntityConstructor | ||||
| }; | ||||
| @@ -17,7 +17,7 @@ async function runNotesWithLabel(runAttrValue) { | ||||
|           AND notes.isDeleted = 0`, [runAttrValue]); | ||||
|  | ||||
|     for (const note of notes) { | ||||
|         scriptService.executeNote(note, note); | ||||
|         scriptService.executeNoteNoException(note, { originEntity: note }); | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -5,37 +5,48 @@ const cls = require('./cls'); | ||||
| const sourceIdService = require('./source_id'); | ||||
| const log = require('./log'); | ||||
|  | ||||
| async function executeNote(note, originEntity) { | ||||
| async function executeNote(note, apiParams) { | ||||
|     if (!note.isJavaScript() || note.getScriptEnv() !== 'backend' || !note.isContentAvailable) { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     const bundle = await getScriptBundle(note); | ||||
|  | ||||
|     await executeBundle(bundle, note, originEntity); | ||||
|     await executeBundle(bundle, apiParams); | ||||
| } | ||||
|  | ||||
| async function executeBundle(bundle, startNote, originEntity = null) { | ||||
|     if (!startNote) { | ||||
| async function executeNoteNoException(note, apiParams) { | ||||
|     try { | ||||
|         await executeNote(note, apiParams); | ||||
|     } | ||||
|     catch (e) { | ||||
|         // just swallow, exception is logged already in executeNote | ||||
|     } | ||||
| } | ||||
|  | ||||
| async function executeBundle(bundle, apiParams = {}) { | ||||
|     if (!apiParams.startNote) { | ||||
|         // this is the default case, the only exception is when we want to preserve frontend startNote | ||||
|         startNote = bundle.note; | ||||
|         apiParams.startNote = bundle.note; | ||||
|     } | ||||
|  | ||||
|     // last \r\n is necessary if script contains line comment on its last line | ||||
|     const script = "async function() {\r\n" + bundle.script + "\r\n}"; | ||||
|  | ||||
|     const ctx = new ScriptContext(startNote, bundle.allNotes, originEntity); | ||||
|     const ctx = new ScriptContext(bundle.allNotes, apiParams); | ||||
|  | ||||
|     try { | ||||
|         if (await bundle.note.hasLabel('manualTransactionHandling')) { | ||||
|             return await execute(ctx, script, ''); | ||||
|             return await execute(ctx, script); | ||||
|         } | ||||
|         else { | ||||
|             return await sql.transactional(async () => await execute(ctx, script, '')); | ||||
|             return await sql.transactional(async () => await execute(ctx, script)); | ||||
|         } | ||||
|     } | ||||
|     catch (e) { | ||||
|         log.error(`Execution of script "${bundle.note.title}" (${bundle.note.noteId}) failed with error: ${e.message}`); | ||||
|  | ||||
|         throw e; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @@ -57,11 +68,11 @@ async function executeScript(script, params, startNoteId, currentNoteId, originE | ||||
|     return await executeBundle(bundle, startNote, originEntity); | ||||
| } | ||||
|  | ||||
| async function execute(ctx, script, paramsStr) { | ||||
| async function execute(ctx, script, params = []) { | ||||
|     // scripts run as "server" sourceId so clients recognize the changes as "foreign" and update themselves | ||||
|     cls.namespace.set('sourceId', sourceIdService.getCurrentSourceId()); | ||||
|  | ||||
|     return await (function() { return eval(`const apiContext = this;\r\n(${script}\r\n)(${paramsStr})`); }.call(ctx)); | ||||
|     return await (function() { return eval(`const apiContext = this;\r\n(${script}\r\n)()`); }.call(ctx)); | ||||
| } | ||||
|  | ||||
| function getParams(params) { | ||||
| @@ -168,6 +179,7 @@ function sanitizeVariableName(str) { | ||||
|  | ||||
| module.exports = { | ||||
|     executeNote, | ||||
|     executeNoteNoException, | ||||
|     executeScript, | ||||
|     getScriptBundleForFrontend | ||||
| }; | ||||
| @@ -1,10 +1,10 @@ | ||||
| const utils = require('./utils'); | ||||
| const BackendScriptApi = require('./backend_script_api'); | ||||
|  | ||||
| function ScriptContext(startNote, allNotes, originEntity = null) { | ||||
| function ScriptContext(allNotes, apiParams = {}) { | ||||
|     this.modules = {}; | ||||
|     this.notes = utils.toObject(allNotes, note => [note.noteId, note]); | ||||
|     this.apis = utils.toObject(allNotes, note => [note.noteId, new BackendScriptApi(startNote, note, originEntity)]); | ||||
|     this.apis = utils.toObject(allNotes, note => [note.noteId, new BackendScriptApi(note, apiParams)]); | ||||
|     this.require = moduleNoteIds => { | ||||
|         return moduleName => { | ||||
|             const candidates = allNotes.filter(note => moduleNoteIds.includes(note.noteId)); | ||||
|   | ||||
| @@ -76,7 +76,7 @@ | ||||
|  | ||||
|         <div id="search-box"> | ||||
|             <div style="display: flex; align-items: center; flex-wrap: wrap;"> | ||||
|                 <input name="search-text" placeholder="Search text, labels" style="flex-grow: 100; margin-left: 5px; margin-right: 5px; flex-basis: 5em; min-width: 0;" autocomplete="off"> | ||||
|                 <input name="search-text" id="search-text" placeholder="Search text, labels" style="flex-grow: 100; margin-left: 5px; margin-right: 5px; flex-basis: 5em; min-width: 0;" autocomplete="off"> | ||||
|                 <button id="do-search-button" class="btn btn-sm icon-button jam jam-search" title="Search (enter)"></button> | ||||
|  | ||||
|                   | ||||
| @@ -212,6 +212,7 @@ | ||||
|         maxSyncIdAtLoad: <%= maxSyncIdAtLoad %>, | ||||
|         instanceName: '<%= instanceName %>' | ||||
|     }; | ||||
|     window.appCssNoteIds = <%- JSON.stringify(appCssNoteIds) %>; | ||||
| </script> | ||||
|  | ||||
| <!-- Required for correct loading of scripts in Electron --> | ||||
| @@ -247,9 +248,5 @@ | ||||
|     // final form which is pretty ugly. | ||||
|     $("#container").show(); | ||||
| </script> | ||||
|  | ||||
| <style type="text/css"> | ||||
|     <%= appCss %> | ||||
| </style> | ||||
| </body> | ||||
| </html> | ||||
|   | ||||
| @@ -1,5 +1,9 @@ | ||||
| <div id="note-detail-file" class="note-detail-component"> | ||||
|     <table id="file-table"> | ||||
|         <tr> | ||||
|             <th>Note ID:</th> | ||||
|             <td id="file-note-id"></td> | ||||
|         </tr> | ||||
|         <tr> | ||||
|             <th>Original file name:</th> | ||||
|             <td id="file-filename"></td> | ||||
| @@ -19,7 +23,7 @@ | ||||
|             </td> | ||||
|         </tr> | ||||
|         <tr> | ||||
|             <td> | ||||
|             <td colspan="2"> | ||||
|                 <button id="file-download" class="btn btn-primary" type="button">Download</button> | ||||
|                   | ||||
|                 <button id="file-open" class="btn btn-primary" type="button">Open</button> | ||||
|   | ||||
| @@ -40,11 +40,7 @@ | ||||
|                             <form> | ||||
|                                 <div class="form-group"> | ||||
|                                     <label for="theme-select">Theme</label> | ||||
|                                     <select class="form-control" id="theme-select"> | ||||
|                                         <option value="white">White</option> | ||||
|                                         <option value="dark">Dark</option> | ||||
|                                         <option value="black">Black</option> | ||||
|                                     </select> | ||||
|                                     <select class="form-control" id="theme-select"></select> | ||||
|                                 </div> | ||||
|  | ||||
|                                 <div class="form-group"> | ||||
|   | ||||
| @@ -4,6 +4,7 @@ | ||||
|     <meta charset="utf-8"> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> | ||||
|     <title>Login</title> | ||||
|     <link rel="apple-touch-icon" sizes="180x180" href="/images/app-icons/ios/apple-touch-icon.png"> | ||||
| </head> | ||||
| <body> | ||||
| <div class="container"> | ||||
| @@ -71,7 +72,7 @@ | ||||
|         document.cookie = name + "=" + (value || "")  + expires + "; path=/"; | ||||
|     } | ||||
| </script> | ||||
|  | ||||
| <link href="libraries/bootstrap/css/bootstrap.min.css" rel="stylesheet"> | ||||
| </body> | ||||
|  | ||||
| <link href="libraries/bootstrap/css/bootstrap.min.css" rel="stylesheet"> | ||||
| </body> | ||||
| </html> | ||||
| @@ -4,6 +4,7 @@ | ||||
|     <meta charset="utf-8"> | ||||
|     <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> | ||||
|     <title>Trilium Notes</title> | ||||
|     <link rel="apple-touch-icon" sizes="180x180" href="/images/app-icons/ios/apple-touch-icon.png"> | ||||
| </head> | ||||
| <body class="mobile"> | ||||
| <div class="row" id="container-row" style="display: none;"> | ||||
| @@ -101,7 +102,7 @@ | ||||
| <script type="text/javascript"> | ||||
|     // we hide container initally because otherwise it is rendered first without CSS and then flickers into | ||||
|     // final form which is pretty ugly. | ||||
|     $("#container-row").show(); | ||||
| </script> | ||||
| </body> | ||||
|     $("#container-row").show(); | ||||
| </script> | ||||
| </body> | ||||
| </html> | ||||
							
								
								
									
										6
									
								
								src/www
									
									
									
									
									
								
							
							
						
						
									
										6
									
								
								src/www
									
									
									
									
									
								
							| @@ -20,6 +20,12 @@ const messagingService = require('./services/messaging'); | ||||
| const utils = require('./services/utils'); | ||||
| const sqlInit = require('./services/sql_init'); | ||||
| const port = require('./services/port'); | ||||
| const semver = require('semver'); | ||||
|  | ||||
| if (!semver.satisfies(process.version, ">=10.5.0")) { | ||||
|     console.error("Trilium only supports node.js 10.5 and later"); | ||||
|     process.exit(1); | ||||
| } | ||||
|  | ||||
| let httpServer; | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user