mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 18:36:30 +01:00 
			
		
		
		
	Compare commits
	
		
			70 Commits
		
	
	
		
			v0.46.1-be
			...
			v0.46.5
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | fdce218e88 | ||
|  | 6c8d20288d | ||
|  | 88d04772c4 | ||
|  | 9fd26a9b9f | ||
|  | 98f02c3c9a | ||
|  | 584fea1992 | ||
|  | af50a1ec52 | ||
|  | 4e76d1fa85 | ||
|  | 03a11e6f77 | ||
|  | e1a16b4a9f | ||
|  | 12b468d3dc | ||
|  | 6f901e6852 | ||
|  | 09e9ac4d00 | ||
|  | a33ac65fdf | ||
|  | f8fb071a6f | ||
|  | a654078e56 | ||
|  | fba68681aa | ||
|  | 50c84e0f5f | ||
|  | f27370d44f | ||
|  | c6c9202c00 | ||
|  | 2cafda5f66 | ||
|  | 873953cbaf | ||
|  | d51744ce19 | ||
|  | 7df8c940b6 | ||
|  | 9bac2a4819 | ||
|  | 88147f7a0a | ||
|  | ca77211b38 | ||
|  | 4606e8d118 | ||
|  | 9f002fa802 | ||
|  | 060d4fc27b | ||
|  | bf0fbe201e | ||
|  | 721e5da672 | ||
|  | 8192b51b8a | ||
|  | 73514a63d8 | ||
|  | f8c310eb8f | ||
|  | b9422b0efd | ||
|  | 14ced949a9 | ||
|  | 5b5c2a2dbb | ||
|  | 2c958eaacb | ||
|  | 4aa27b6033 | ||
|  | 89a0c5a1c9 | ||
|  | 78e48095e6 | ||
|  | 02016ed031 | ||
|  | cb91dadeca | ||
|  | 2c755bcc38 | ||
|  | 3c7a6bc1e4 | ||
|  | 3fe87259e2 | ||
|  | d476dfc53b | ||
|  | 19821b634f | ||
|  | cde41b268e | ||
|  | 1c59bc4d3c | ||
|  | cb6d35236c | ||
|  | 7572ee284b | ||
|  | d0eaf623a8 | ||
|  | 25c2db6c3a | ||
|  | 5a173ff14e | ||
|  | 7fab75b085 | ||
|  | 0f065536d0 | ||
|  | d0747abded | ||
|  | f62b4a581e | ||
|  | dcf1c62ec1 | ||
|  | 93d55b3e7b | ||
|  | 90c6852423 | ||
|  | 208baa56e9 | ||
|  | ddf8438b22 | ||
|  | 6008dc891f | ||
|  | cc887a00f2 | ||
|  | fbbd51d0b1 | ||
|  | 081b8b126a | ||
|  | 7a6bb81345 | 
| @@ -55,3 +55,7 @@ npm run start-server | ||||
| * [FancyTree](https://github.com/mar10/fancytree) - very feature rich tree library without real competition. Trilium Notes would not be the same without it. | ||||
| * [CodeMirror](https://github.com/codemirror/CodeMirror) - code editor with support for huge amount of languages | ||||
| * [jsPlumb](https://github.com/jsplumb/jsplumb) - visual connectivity library without competition. Used in [relation maps](https://github.com/zadam/trilium/wiki/Relation-map) and [link maps](https://github.com/zadam/trilium/wiki/Link-map) | ||||
|  | ||||
| ## License | ||||
|  | ||||
| This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. | ||||
|   | ||||
							
								
								
									
										947
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										947
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										20
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								package.json
									
									
									
									
									
								
							| @@ -2,7 +2,7 @@ | ||||
|   "name": "trilium", | ||||
|   "productName": "Trilium Notes", | ||||
|   "description": "Trilium Notes", | ||||
|   "version": "0.46.1-beta", | ||||
|   "version": "0.46.5", | ||||
|   "license": "AGPL-3.0-only", | ||||
|   "main": "electron.js", | ||||
|   "bin": { | ||||
| @@ -24,7 +24,7 @@ | ||||
|     "test-all": "npm run test && npm run test-es6" | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "async-mutex": "0.3.0", | ||||
|     "async-mutex": "0.3.1", | ||||
|     "axios": "0.21.1", | ||||
|     "better-sqlite3": "7.1.2", | ||||
|     "body-parser": "1.19.0", | ||||
| @@ -35,7 +35,7 @@ | ||||
|     "dayjs": "1.10.4", | ||||
|     "ejs": "3.1.6", | ||||
|     "electron-debug": "3.2.0", | ||||
|     "electron-dl": "3.1.0", | ||||
|     "electron-dl": "3.2.0", | ||||
|     "electron-find": "1.0.6", | ||||
|     "electron-window-state": "5.0.3", | ||||
|     "express": "4.17.1", | ||||
| @@ -51,10 +51,10 @@ | ||||
|     "is-animated": "^2.0.1", | ||||
|     "is-svg": "4.2.1", | ||||
|     "jimp": "0.16.1", | ||||
|     "jsdom": "^16.4.0", | ||||
|     "jsdom": "16.5.0", | ||||
|     "mime-types": "2.1.29", | ||||
|     "multer": "1.4.2", | ||||
|     "node-abi": "2.19.3", | ||||
|     "node-abi": "2.21.0", | ||||
|     "open": "7.4.2", | ||||
|     "portscanner": "2.2.0", | ||||
|     "rand-token": "1.0.1", | ||||
| @@ -70,16 +70,16 @@ | ||||
|     "striptags": "3.1.1", | ||||
|     "tmp": "^0.2.1", | ||||
|     "turndown": "7.0.0", | ||||
|     "turndown-plugin-gfm": "1.0.2", | ||||
|     "joplin-turndown-plugin-gfm": "1.0.12", | ||||
|     "unescape": "1.0.1", | ||||
|     "ws": "7.4.3", | ||||
|     "ws": "7.4.4", | ||||
|     "yauzl": "2.10.0", | ||||
|     "yazl": "2.5.1" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "cross-env": "7.0.3", | ||||
|     "electron": "9.4.3", | ||||
|     "electron-builder": "22.9.1", | ||||
|     "electron": "9.4.4", | ||||
|     "electron-builder": "22.10.5", | ||||
|     "electron-packager": "15.2.0", | ||||
|     "electron-rebuild": "2.3.5", | ||||
|     "esm": "3.2.25", | ||||
| @@ -87,7 +87,7 @@ | ||||
|     "jsdoc": "3.6.6", | ||||
|     "lorem-ipsum": "2.0.3", | ||||
|     "rcedit": "3.0.0", | ||||
|     "webpack": "5.23.0", | ||||
|     "webpack": "5.24.4", | ||||
|     "webpack-cli": "4.5.0" | ||||
|   }, | ||||
|   "optionalDependencies": { | ||||
|   | ||||
| @@ -41,7 +41,7 @@ class Entity { | ||||
|     } | ||||
|  | ||||
|     getUtcDateChanged() { | ||||
|         return this.utcDateModified; | ||||
|         return this.utcDateModified || this.utcDateCreated; | ||||
|     } | ||||
|  | ||||
|     get repository() { | ||||
|   | ||||
| @@ -681,7 +681,7 @@ class Note extends Entity { | ||||
|      * Update's given relation's value or creates it if it doesn't exist | ||||
|      * | ||||
|      * @param {string} name - relation name | ||||
|      * @param {string} [value] - relation value (noteId) | ||||
|      * @param {string} value - relation value (noteId) | ||||
|      */ | ||||
|     setRelation(name, value) { return this.setAttribute(RELATION, name, value); } | ||||
|  | ||||
|   | ||||
| @@ -129,7 +129,7 @@ ws.subscribeToMessages(async message => { | ||||
|         toastService.showPersistent(makeToast(message.taskId, "Export in progress: " + message.progressCount)); | ||||
|     } | ||||
|     else if (message.type === 'task-succeeded') { | ||||
|         const toast = makeToast(message.taskId, "Import finished successfully."); | ||||
|         const toast = makeToast(message.taskId, "Export finished successfully."); | ||||
|         toast.closeAfter = 5000; | ||||
|  | ||||
|         toastService.showPersistent(toast); | ||||
|   | ||||
| @@ -28,8 +28,18 @@ const TPL = ` | ||||
|             </select> | ||||
|         </div> | ||||
|     </div> | ||||
|      | ||||
|     <div class="form-group row"> | ||||
|         <div class="col-4"> | ||||
|             <label for="heading-style">Heading style</label> | ||||
|             <select class="form-control" id="heading-style"> | ||||
|                 <option value="plain">Plain</option> | ||||
|                 <option value="markdown">Markdown-style</option> | ||||
|             </select> | ||||
|         </div> | ||||
|     </div> | ||||
|  | ||||
|     <p>Zooming can be controlled with CTRL-+ and CTRL-= shortcuts as well.</p> | ||||
|     <p>Zooming can be controlled with CTRL+- and CTRL+= shortcuts as well.</p> | ||||
|  | ||||
|     <h4>Font sizes</h4> | ||||
|  | ||||
| @@ -78,6 +88,7 @@ export default class ApperanceOptions { | ||||
|         this.$themeSelect = $("#theme-select"); | ||||
|         this.$zoomFactorSelect = $("#zoom-factor-select"); | ||||
|         this.$nativeTitleBarSelect = $("#native-title-bar-select"); | ||||
|         this.$headingStyle = $("#heading-style"); | ||||
|         this.$mainFontSize = $("#main-font-size"); | ||||
|         this.$treeFontSize = $("#tree-font-size"); | ||||
|         this.$detailFontSize = $("#detail-font-size"); | ||||
| @@ -86,11 +97,7 @@ export default class ApperanceOptions { | ||||
|         this.$themeSelect.on('change', () => { | ||||
|             const newTheme = this.$themeSelect.val(); | ||||
|  | ||||
|             for (const clazz of Array.from(this.$body[0].classList)) { // create copy to safely iterate over while removing classes | ||||
|                 if (clazz.startsWith("theme-")) { | ||||
|                     this.$body.removeClass(clazz); | ||||
|                 } | ||||
|             } | ||||
|             this.toggleBodyClass("theme-", newTheme); | ||||
|  | ||||
|             const noteId = $(this).find(":selected").attr("data-note-id"); | ||||
|  | ||||
| @@ -100,8 +107,6 @@ export default class ApperanceOptions { | ||||
|                 libraryLoader.requireCss(`api/notes/download/${noteId}`); | ||||
|             } | ||||
|  | ||||
|             this.$body.addClass("theme-" + newTheme); | ||||
|  | ||||
|             server.put('options/theme/' + newTheme); | ||||
|         }); | ||||
|  | ||||
| @@ -113,6 +118,14 @@ export default class ApperanceOptions { | ||||
|             server.put('options/nativeTitleBarVisible/' + nativeTitleBarVisible); | ||||
|         }); | ||||
|  | ||||
|         this.$headingStyle.on('change', () => { | ||||
|             const newHeadingStyle = this.$headingStyle.val(); | ||||
|  | ||||
|             this.toggleBodyClass("heading-style-", newHeadingStyle); | ||||
|  | ||||
|             server.put('options/headingStyle/' + newHeadingStyle); | ||||
|         }); | ||||
|  | ||||
|         this.$mainFontSize.on('change', async () => { | ||||
|             await server.put('options/mainFontSize/' + this.$mainFontSize.val()); | ||||
|  | ||||
| @@ -132,6 +145,16 @@ export default class ApperanceOptions { | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     toggleBodyClass(prefix, value) { | ||||
|         for (const clazz of Array.from(this.$body[0].classList)) { // create copy to safely iterate over while removing classes | ||||
|             if (clazz.startsWith(prefix)) { | ||||
|                 this.$body.removeClass(clazz); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         this.$body.addClass(prefix + value); | ||||
|     } | ||||
|  | ||||
|     async optionsLoaded(options) { | ||||
|         const themes = [ | ||||
|             { val: 'white', title: 'White' }, | ||||
| @@ -159,6 +182,8 @@ export default class ApperanceOptions { | ||||
|  | ||||
|         this.$nativeTitleBarSelect.val(options.nativeTitleBarVisible === 'true' ? 'show' : 'hide'); | ||||
|  | ||||
|         this.$headingStyle.val(options.headingStyle); | ||||
|  | ||||
|         this.$mainFontSize.val(options.mainFontSize); | ||||
|         this.$treeFontSize.val(options.treeFontSize); | ||||
|         this.$detailFontSize.val(options.detailFontSize); | ||||
| @@ -169,4 +194,4 @@ export default class ApperanceOptions { | ||||
|         this.$body.get(0).style.setProperty("--tree-font-size", this.$treeFontSize.val() + "%"); | ||||
|         this.$body.get(0).style.setProperty("--detail-font-size", this.$detailFontSize.val() + "%"); | ||||
|     } | ||||
| } | ||||
| } | ||||
|   | ||||
							
								
								
									
										24
									
								
								src/public/app/dialogs/sort_child_notes.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								src/public/app/dialogs/sort_child_notes.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| import server from "../services/server.js"; | ||||
| import utils from "../services/utils.js"; | ||||
|  | ||||
| const $dialog = $("#sort-child-notes-dialog"); | ||||
| const $form = $("#sort-child-notes-form"); | ||||
|  | ||||
| let parentNoteId = null; | ||||
|  | ||||
| $form.on('submit', async () => { | ||||
|     const sortBy = $form.find("input[name='sort-by']:checked").val(); | ||||
|     const sortDirection = $form.find("input[name='sort-direction']:checked").val(); | ||||
|  | ||||
|     await server.put(`notes/${parentNoteId}/sort-children`, {sortBy, sortDirection}); | ||||
|  | ||||
|     utils.closeActiveDialog(); | ||||
| }); | ||||
|  | ||||
| export async function showDialog(noteId) { | ||||
|     parentNoteId = noteId; | ||||
|  | ||||
|     utils.openDialog($dialog); | ||||
|  | ||||
|     $form.find('input:first').focus(); | ||||
| } | ||||
| @@ -2,6 +2,7 @@ import server from '../services/server.js'; | ||||
| import noteAttributeCache from "../services/note_attribute_cache.js"; | ||||
| import ws from "../services/ws.js"; | ||||
| import options from "../services/options.js"; | ||||
| import treeCache from "../services/tree_cache.js"; | ||||
|  | ||||
| const LABEL = 'label'; | ||||
| const RELATION = 'relation'; | ||||
| @@ -158,6 +159,26 @@ class NoteShort { | ||||
|         return this.treeCache.getNotesFromCache(this.parents); | ||||
|     } | ||||
|  | ||||
|     // will sort the parents so that non-search & non-archived are first and archived at the end | ||||
|     // this is done so that non-search & non-archived paths are always explored as first when looking for note path | ||||
|     resortParents() { | ||||
|         this.parents.sort((aNoteId, bNoteId) => { | ||||
|             const aBranchId = this.parentToBranch[aNoteId]; | ||||
|  | ||||
|             if (aBranchId && aBranchId.startsWith('virt-')) { | ||||
|                 return 1; | ||||
|             } | ||||
|  | ||||
|             const aNote = this.treeCache.getNoteFromCache([aNoteId]); | ||||
|  | ||||
|             if (aNote.hasLabel('archived')) { | ||||
|                 return 1; | ||||
|             } | ||||
|  | ||||
|             return -1; | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     /** @returns {string[]} */ | ||||
|     getChildNoteIds() { | ||||
|         return this.children; | ||||
| @@ -233,6 +254,72 @@ class NoteShort { | ||||
|         return noteAttributeCache.attributes[this.noteId]; | ||||
|     } | ||||
|  | ||||
|     getAllNotePaths(encounteredNoteIds = null) { | ||||
|         if (this.noteId === 'root') { | ||||
|             return [['root']]; | ||||
|         } | ||||
|  | ||||
|         if (!encounteredNoteIds) { | ||||
|             encounteredNoteIds = new Set(); | ||||
|         } | ||||
|  | ||||
|         encounteredNoteIds.add(this.noteId); | ||||
|  | ||||
|         const parentNotes = this.getParentNotes(); | ||||
|         let paths; | ||||
|  | ||||
|         if (parentNotes.length === 1) { // optimization for the most common case | ||||
|             if (encounteredNoteIds.has(parentNotes[0].noteId)) { | ||||
|                 return []; | ||||
|             } | ||||
|             else { | ||||
|                 paths = parentNotes[0].getAllNotePaths(encounteredNoteIds); | ||||
|             } | ||||
|         } | ||||
|         else { | ||||
|             paths = []; | ||||
|  | ||||
|             for (const parentNote of parentNotes) { | ||||
|                 if (encounteredNoteIds.has(parentNote.noteId)) { | ||||
|                     continue; | ||||
|                 } | ||||
|  | ||||
|                 const newSet = new Set(encounteredNoteIds); | ||||
|  | ||||
|                 paths.push(...parentNote.getAllNotePaths(newSet)); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         for (const path of paths) { | ||||
|             path.push(this.noteId); | ||||
|         } | ||||
|  | ||||
|         return paths; | ||||
|     } | ||||
|  | ||||
|     getSortedNotePaths(hoistedNotePath = 'root') { | ||||
|         const notePaths = this.getAllNotePaths().map(path => ({ | ||||
|             notePath: path, | ||||
|             isInHoistedSubTree: path.includes(hoistedNotePath), | ||||
|             isArchived: path.find(noteId => treeCache.notes[noteId].hasLabel('archived')), | ||||
|             isSearch: path.find(noteId => treeCache.notes[noteId].type === 'search') | ||||
|         })); | ||||
|  | ||||
|         notePaths.sort((a, b) => { | ||||
|             if (a.isInHoistedSubTree !== b.isInHoistedSubTree) { | ||||
|                 return a.isInHoistedSubTree ? -1 : 1; | ||||
|             } else if (a.isSearch !== b.isSearch) { | ||||
|                 return a.isSearch ? 1 : -1; | ||||
|             } else if (a.isArchived !== b.isArchived) { | ||||
|                 return a.isArchived ? 1 : -1; | ||||
|             } else { | ||||
|                 return a.notePath.length - b.notePath.length; | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         return notePaths; | ||||
|     } | ||||
|  | ||||
|     __filterAttrs(attributes, type, name) { | ||||
|         if (!type && !name) { | ||||
|             return attributes; | ||||
| @@ -522,7 +609,7 @@ class NoteShort { | ||||
|             }); | ||||
|     } | ||||
|  | ||||
|     hasAncestor(ancestorNote, visitedNoteIds) { | ||||
|     hasAncestor(ancestorNote, visitedNoteIds = null) { | ||||
|         if (this.noteId === ancestorNote.noteId) { | ||||
|             return true; | ||||
|         } | ||||
|   | ||||
| @@ -12,6 +12,7 @@ import keyboardActionsService from "./keyboard_actions.js"; | ||||
| import MobileScreenSwitcherExecutor from "../widgets/mobile_widgets/mobile_screen_switcher.js"; | ||||
| import MainTreeExecutors from "./main_tree_executors.js"; | ||||
| import protectedSessionHolder from "./protected_session_holder.js"; | ||||
| import toast from "./toast.js"; | ||||
|  | ||||
| class AppContext extends Component { | ||||
|     constructor(isMainWindow) { | ||||
| @@ -19,6 +20,7 @@ class AppContext extends Component { | ||||
|  | ||||
|         this.isMainWindow = isMainWindow; | ||||
|         this.executors = []; | ||||
|         this.beforeUnloadListeners = []; | ||||
|     } | ||||
|  | ||||
|     setLayout(layout) { | ||||
| @@ -104,6 +106,15 @@ class AppContext extends Component { | ||||
|     getComponentByEl(el) { | ||||
|         return $(el).closest(".component").prop('component'); | ||||
|     } | ||||
|  | ||||
|     addBeforeUnloadListener(obj) { | ||||
|         if (typeof WeakRef !== "function") { | ||||
|             // older browsers don't support WeakRef | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         this.beforeUnloadListeners.push(new WeakRef(obj)); | ||||
|     } | ||||
| } | ||||
|  | ||||
| const appContext = new AppContext(window.glob.isMainWindow); | ||||
| @@ -112,7 +123,29 @@ const appContext = new AppContext(window.glob.isMainWindow); | ||||
| $(window).on('beforeunload', () => { | ||||
|     protectedSessionHolder.resetSessionCookie(); | ||||
|  | ||||
|     appContext.triggerEvent('beforeUnload'); | ||||
|     let allSaved = true; | ||||
|  | ||||
|     appContext.beforeUnloadListeners = appContext.beforeUnloadListeners.filter(wr => !!wr.deref()); | ||||
|  | ||||
|     for (const weakRef of appContext.beforeUnloadListeners) { | ||||
|         const component = weakRef.deref(); | ||||
|  | ||||
|         if (!component) { | ||||
|             continue; | ||||
|         } | ||||
|  | ||||
|         if (!component.beforeUnloadEvent()) { | ||||
|             console.log(`Component ${component.componentId} is not finished saving its state.`); | ||||
|  | ||||
|             toast.showMessage("Please wait for a couple of seconds for the save to finish, then you can try again.", 10000); | ||||
|  | ||||
|             allSaved = false; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     if (!allSaved) { | ||||
|         return "some string"; | ||||
|     } | ||||
| }); | ||||
|  | ||||
| function isNotePathInAddress() { | ||||
|   | ||||
| @@ -79,6 +79,15 @@ async function renderAttributes(attributes, renderIsInheritable) { | ||||
|     return $container; | ||||
| } | ||||
|  | ||||
| const HIDDEN_ATTRIBUTES = [ | ||||
|     'originalFileName', | ||||
|     'template', | ||||
|     'cssClass', | ||||
|     'iconClass', | ||||
|     'pageSize', | ||||
|     'viewType' | ||||
| ]; | ||||
|  | ||||
| async function renderNormalAttributes(note) { | ||||
|     const promotedDefinitionAttributes = note.getPromotedDefinitionAttributes(); | ||||
|     let attrs = note.getAttributes(); | ||||
| @@ -90,6 +99,7 @@ async function renderNormalAttributes(note) { | ||||
|         attrs = attrs.filter( | ||||
|             attr => !attr.isDefinition() | ||||
|                  && !attr.isAutoLink | ||||
|                  && !HIDDEN_ATTRIBUTES.includes(attr.name) | ||||
|                  && attr.noteId === note.noteId | ||||
|         ); | ||||
|     } | ||||
|   | ||||
| @@ -125,7 +125,7 @@ async function deleteNotes(branchIdsToDelete) { | ||||
| } | ||||
|  | ||||
| async function moveNodeUpInHierarchy(node) { | ||||
|     if (hoistedNoteService.isRootNode(node) | ||||
|     if (hoistedNoteService.isHoistedNode(node) | ||||
|         || hoistedNoteService.isTopLevelNode(node) | ||||
|         || node.getParent().data.noteType === 'search') { | ||||
|         return; | ||||
|   | ||||
| @@ -18,6 +18,8 @@ async function getTodayNote() { | ||||
| async function getDateNote(date) { | ||||
|     const note = await server.get('date-notes/date/' + date, "date-note"); | ||||
|  | ||||
|     await ws.waitForMaxKnownEntityChangeId(); | ||||
|  | ||||
|     return await treeCache.getNote(note.noteId); | ||||
| } | ||||
|  | ||||
| @@ -25,6 +27,8 @@ async function getDateNote(date) { | ||||
| async function getMonthNote(month) { | ||||
|     const note = await server.get('date-notes/month/' + month, "date-note"); | ||||
|  | ||||
|     await ws.waitForMaxKnownEntityChangeId(); | ||||
|  | ||||
|     return await treeCache.getNote(note.noteId); | ||||
| } | ||||
|  | ||||
| @@ -32,6 +36,8 @@ async function getMonthNote(month) { | ||||
| async function getYearNote(year) { | ||||
|     const note = await server.get('date-notes/year/' + year, "date-note"); | ||||
|  | ||||
|     await ws.waitForMaxKnownEntityChangeId(); | ||||
|  | ||||
|     return await treeCache.getNote(note.noteId); | ||||
| } | ||||
|  | ||||
| @@ -39,21 +45,14 @@ async function getYearNote(year) { | ||||
| async function createSqlConsole() { | ||||
|     const note = await server.post('sql-console'); | ||||
|  | ||||
|     await ws.waitForMaxKnownEntityChangeId(); | ||||
|  | ||||
|     return await treeCache.getNote(note.noteId); | ||||
| } | ||||
|  | ||||
| /** @return {NoteShort} */ | ||||
| async function createSearchNote(opts = {}) { | ||||
|     const note = await server.post('search-note'); | ||||
|  | ||||
|     const attrsToUpdate = [ | ||||
|         opts.ancestorNoteId ? { type: 'relation', name: 'ancestor', value: opts.ancestorNoteId } : undefined, | ||||
|         { type: 'label', name: 'searchString', value: opts.searchString } | ||||
|     ].filter(attr => !!attr); | ||||
|  | ||||
|     if (attrsToUpdate.length > 0) { | ||||
|         await server.put(`notes/${note.noteId}/attributes`, attrsToUpdate); | ||||
|     } | ||||
|     const note = await server.post('search-note', opts); | ||||
|  | ||||
|     await ws.waitForMaxKnownEntityChangeId(); | ||||
|  | ||||
|   | ||||
| @@ -74,7 +74,11 @@ export default class Entrypoints extends Component { | ||||
|  | ||||
|         await ws.waitForMaxKnownEntityChangeId(); | ||||
|  | ||||
|         await appContext.tabManager.openTabWithNote(note.noteId, true); | ||||
|         const hoistedNoteId = appContext.tabManager.getActiveTabContext() | ||||
|             ? appContext.tabManager.getActiveTabContext().hoistedNoteId | ||||
|             : 'root'; | ||||
|  | ||||
|         await appContext.tabManager.openTabWithNote(note.noteId, true, null, hoistedNoteId); | ||||
|  | ||||
|         appContext.triggerEvent('focusAndSelectTitle'); | ||||
|     } | ||||
| @@ -182,8 +186,6 @@ export default class Entrypoints extends Component { | ||||
|         utils.reloadApp(); | ||||
|     } | ||||
|  | ||||
|     createTopLevelNoteCommand() { noteCreateService.createNewTopLevelNote(); } | ||||
|  | ||||
|     async openInWindowCommand({notePath, hoistedNoteId}) { | ||||
|         if (!hoistedNoteId) { | ||||
|             hoistedNoteId = 'root'; | ||||
|   | ||||
| @@ -166,8 +166,7 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain | ||||
|         }, "script"); | ||||
|  | ||||
|         if (ret.success) { | ||||
|             // wait until all the changes done in the script has been synced to frontend before continuing | ||||
|             await ws.waitForEntityChangeId(ret.maxEntityChangeId); | ||||
|             await ws.waitForMaxKnownEntityChangeId(); | ||||
|  | ||||
|             return ret.executionResult; | ||||
|         } | ||||
|   | ||||
| @@ -16,19 +16,17 @@ async function unhoist() { | ||||
| } | ||||
|  | ||||
| function isTopLevelNode(node) { | ||||
|     return isRootNode(node.getParent()); | ||||
|     return isHoistedNode(node.getParent()); | ||||
| } | ||||
|  | ||||
| function isRootNode(node) { | ||||
| function isHoistedNode(node) { | ||||
|     // even though check for 'root' should not be necessary, we keep it just in case | ||||
|     return node.data.noteId === "root" | ||||
|         || node.data.noteId === getHoistedNoteId(); | ||||
| } | ||||
|  | ||||
| async function checkNoteAccess(notePath, tabContext) { | ||||
|     // notePath argument can contain only noteId which is not good when hoisted since | ||||
|     // then we need to check the whole note path | ||||
|     const resolvedNotePath = await treeService.resolveNotePath(notePath); | ||||
|     const resolvedNotePath = await treeService.resolveNotePath(notePath, tabContext.hoistedNoteId); | ||||
|  | ||||
|     if (!resolvedNotePath) { | ||||
|         console.log("Cannot activate " + notePath); | ||||
| @@ -37,7 +35,7 @@ async function checkNoteAccess(notePath, tabContext) { | ||||
|  | ||||
|     const hoistedNoteId = tabContext.hoistedNoteId; | ||||
|  | ||||
|     if (hoistedNoteId !== 'root' && !resolvedNotePath.includes(hoistedNoteId)) { | ||||
|     if (!resolvedNotePath.includes(hoistedNoteId)) { | ||||
|         const confirmDialog = await import('../dialogs/confirm.js'); | ||||
|  | ||||
|         if (!await confirmDialog.confirm("Requested note is outside of hoisted note subtree and you must unhoist to access the note. Do you want to proceed with unhoisting?")) { | ||||
| @@ -55,6 +53,6 @@ export default { | ||||
|     getHoistedNoteId, | ||||
|     unhoist, | ||||
|     isTopLevelNode, | ||||
|     isRootNode, | ||||
|     isHoistedNode, | ||||
|     checkNoteAccess | ||||
| } | ||||
|   | ||||
| @@ -117,7 +117,8 @@ export default class LinkMap { | ||||
|  | ||||
|             const $noteBox = $("<div>") | ||||
|                 .addClass("note-box") | ||||
|                 .prop("id", noteBoxId); | ||||
|                 .prop("id", noteBoxId) | ||||
|                 .addClass(note.getCssClass()); | ||||
|  | ||||
|             const $link = $linkTitles[noteId]; | ||||
|  | ||||
|   | ||||
| @@ -27,28 +27,28 @@ export default class MainTreeExecutors extends Component { | ||||
|     } | ||||
|  | ||||
|     async createNoteIntoCommand() { | ||||
|         const activeNote = appContext.tabManager.getActiveTabNote(); | ||||
|         const activeTabContext = appContext.tabManager.getActiveTabContext(); | ||||
|  | ||||
|         if (!activeNote) { | ||||
|         if (!activeTabContext) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         await noteCreateService.createNote(activeNote.noteId, { | ||||
|             isProtected: activeNote.isProtected, | ||||
|         await noteCreateService.createNote(activeTabContext.notePath, { | ||||
|             isProtected: activeTabContext.note.isProtected, | ||||
|             saveSelection: false | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     async createNoteAfterCommand() { | ||||
|         const node = this.tree.getActiveNode(); | ||||
|         const parentNoteId = node.data.parentNoteId; | ||||
|         const parentNotePath = treeService.getNotePath(node.getParent()); | ||||
|         const isProtected = await treeService.getParentProtectedStatus(node); | ||||
|  | ||||
|         if (node.data.noteId === 'root' || node.data.noteId === hoistedNoteService.getHoistedNoteId()) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         await noteCreateService.createNote(parentNoteId, { | ||||
|         await noteCreateService.createNote(parentNotePath, { | ||||
|             target: 'after', | ||||
|             targetBranchId: node.data.branchId, | ||||
|             isProtected: isProtected, | ||||
|   | ||||
| @@ -1,19 +1,13 @@ | ||||
| import hoistedNoteService from "./hoisted_note.js"; | ||||
| import appContext from "./app_context.js"; | ||||
| import utils from "./utils.js"; | ||||
| import protectedSessionHolder from "./protected_session_holder.js"; | ||||
| import server from "./server.js"; | ||||
| import ws from "./ws.js"; | ||||
| import treeCache from "./tree_cache.js"; | ||||
| import treeService from "./tree.js"; | ||||
| import toastService from "./toast.js"; | ||||
|  | ||||
| async function createNewTopLevelNote() { | ||||
|     const hoistedNoteId = hoistedNoteService.getHoistedNoteId(); | ||||
|  | ||||
|     await createNote(hoistedNoteId); | ||||
| } | ||||
|  | ||||
| async function createNote(parentNoteId, options = {}) { | ||||
| async function createNote(parentNotePath, options = {}) { | ||||
|     options = Object.assign({ | ||||
|         activate: true, | ||||
|         focus: 'title', | ||||
| @@ -36,6 +30,8 @@ async function createNote(parentNoteId, options = {}) { | ||||
|  | ||||
|     const newNoteName = options.title || "new note"; | ||||
|  | ||||
|     const parentNoteId = treeService.getNoteIdFromNotePath(parentNotePath); | ||||
|  | ||||
|     const {note, branch} = await server.post(`notes/${parentNoteId}/children?target=${options.target}&targetBranchId=${options.targetBranchId}`, { | ||||
|         title: newNoteName, | ||||
|         content: options.content || "", | ||||
| @@ -53,7 +49,7 @@ async function createNote(parentNoteId, options = {}) { | ||||
|  | ||||
|     if (options.activate) { | ||||
|         const activeTabContext = appContext.tabManager.getActiveTabContext(); | ||||
|         await activeTabContext.setNote(note.noteId); | ||||
|         await activeTabContext.setNote(`${parentNotePath}/${note.noteId}`); | ||||
|  | ||||
|         if (options.focus === 'title') { | ||||
|             appContext.triggerEvent('focusAndSelectTitle'); | ||||
| @@ -88,12 +84,13 @@ function parseSelectedHtml(selectedHtml) { | ||||
|     } | ||||
| } | ||||
|  | ||||
| async function duplicateSubtree(noteId, parentNoteId) { | ||||
| async function duplicateSubtree(noteId, parentNotePath) { | ||||
|     const parentNoteId = treeService.getNoteIdFromNotePath(parentNotePath); | ||||
|     const {note} = await server.post(`notes/${noteId}/duplicate/${parentNoteId}`); | ||||
|  | ||||
|     await ws.waitForMaxKnownEntityChangeId(); | ||||
|  | ||||
|     await appContext.tabManager.activateOrOpenNote(note.noteId); | ||||
|     await appContext.tabManager.activateOrOpenNote(`${parentNotePath}/${note.noteId}`); | ||||
|  | ||||
|     const origNote = await treeCache.getNote(noteId); | ||||
|     toastService.showMessage(`Note "${origNote.title}" has been duplicated`); | ||||
| @@ -101,6 +98,5 @@ async function duplicateSubtree(noteId, parentNoteId) { | ||||
|  | ||||
| export default { | ||||
|     createNote, | ||||
|     createNewTopLevelNote, | ||||
|     duplicateSubtree | ||||
| }; | ||||
|   | ||||
| @@ -56,6 +56,14 @@ const TPL = ` | ||||
|      | ||||
|     .note-book-title { | ||||
|         margin-bottom: 0; | ||||
|         word-break: break-all; | ||||
|     } | ||||
|      | ||||
|     /* not-expanded title is limited to one line only */ | ||||
|     .note-book-card:not(.expanded) .note-book-title { | ||||
|         overflow: hidden; | ||||
|         white-space: nowrap; | ||||
|         text-overflow: ellipsis; | ||||
|     } | ||||
|      | ||||
|     .note-book-title .rendered-note-attributes { | ||||
| @@ -148,7 +156,7 @@ class NoteListRenderer { | ||||
|     /* | ||||
|      * We're using noteIds so that it's not necessary to load all notes at once when paging | ||||
|      */ | ||||
|     constructor($parent, parentNote, noteIds) { | ||||
|     constructor($parent, parentNote, noteIds, showNotePath = false) { | ||||
|         this.$noteList = $(TPL); | ||||
|  | ||||
|         // note list must be added to the DOM immediatelly, otherwise some functionality scripting (canvas) won't work | ||||
| @@ -200,6 +208,8 @@ class NoteListRenderer { | ||||
|  | ||||
|             await this.renderList(); | ||||
|         }); | ||||
|  | ||||
|         this.showNotePath = showNotePath; | ||||
|     } | ||||
|  | ||||
|     /** @return {Set<string>} list of noteIds included (images, included notes) into a parent note and which | ||||
| @@ -298,7 +308,7 @@ class NoteListRenderer { | ||||
|             .append( | ||||
|                 $('<h5 class="note-book-title">') | ||||
|                     .append($expander) | ||||
|                     .append(await linkService.createNoteLink(note.noteId, {showTooltip: false})) | ||||
|                     .append(await linkService.createNoteLink(note.noteId, {showTooltip: false, showNotePath: this.showNotePath})) | ||||
|                     .append($renderedAttributes) | ||||
|             ); | ||||
|  | ||||
|   | ||||
| @@ -15,11 +15,27 @@ export default class SpacedUpdate { | ||||
|  | ||||
|     async updateNowIfNecessary() { | ||||
|         if (this.changed) { | ||||
|             this.changed = false; | ||||
|             await this.updater(); | ||||
|             this.changed = false; // optimistic... | ||||
|  | ||||
|             try { | ||||
|                 await this.updater(); | ||||
|             } | ||||
|             catch (e) { | ||||
|                 this.changed = true; | ||||
|  | ||||
|                 throw e; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     isAllSavedAndTriggerUpdate() { | ||||
|         const allSaved = !this.changed; | ||||
|  | ||||
|         this.updateNowIfNecessary(); | ||||
|  | ||||
|         return allSaved; | ||||
|     } | ||||
|  | ||||
|     triggerUpdate() { | ||||
|         if (!this.changed) { | ||||
|             return; | ||||
|   | ||||
| @@ -79,7 +79,7 @@ class TabContext extends Component { | ||||
|             return inputNotePath; | ||||
|         } | ||||
|  | ||||
|         const resolvedNotePath = await treeService.resolveNotePath(inputNotePath); | ||||
|         const resolvedNotePath = await treeService.resolveNotePath(inputNotePath, this.hoistedNoteId); | ||||
|  | ||||
|         if (!resolvedNotePath) { | ||||
|             logError(`Cannot resolve note path ${inputNotePath}`); | ||||
|   | ||||
| @@ -27,6 +27,8 @@ export default class TabManager extends Component { | ||||
|                 openTabs: JSON.stringify(openTabs) | ||||
|             }); | ||||
|         }); | ||||
|  | ||||
|         appContext.addBeforeUnloadListener(this); | ||||
|     } | ||||
|  | ||||
|     /** @type {TabContext[]} */ | ||||
| @@ -203,7 +205,7 @@ export default class TabManager extends Component { | ||||
|         let hoistedNoteId = 'root'; | ||||
|  | ||||
|         if (tabContext) { | ||||
|             const resolvedNotePath = await treeService.resolveNotePath(notePath); | ||||
|             const resolvedNotePath = await treeService.resolveNotePath(notePath, tabContext.hoistedNoteId); | ||||
|  | ||||
|             if (resolvedNotePath.includes(tabContext.hoistedNoteId)) { | ||||
|                 hoistedNoteId = tabContext.hoistedNoteId; | ||||
| @@ -329,6 +331,8 @@ export default class TabManager extends Component { | ||||
|  | ||||
|     beforeUnloadEvent() { | ||||
|         this.tabsUpdate.updateNowIfNecessary(); | ||||
|  | ||||
|         return true; // don't block closing the tab, this metadata is not that important | ||||
|     } | ||||
|  | ||||
|     openNewTabCommand() { | ||||
|   | ||||
| @@ -8,8 +8,8 @@ import appContext from "./app_context.js"; | ||||
| /** | ||||
|  * @return {string|null} | ||||
|  */ | ||||
| async function resolveNotePath(notePath) { | ||||
|     const runPath = await resolveNotePathToSegments(notePath); | ||||
| async function resolveNotePath(notePath, hoistedNoteId = 'root') { | ||||
|     const runPath = await resolveNotePathToSegments(notePath, hoistedNoteId); | ||||
|  | ||||
|     return runPath ? runPath.join("/") : null; | ||||
| } | ||||
| @@ -21,7 +21,7 @@ async function resolveNotePath(notePath) { | ||||
|  * | ||||
|  * @return {string[]} | ||||
|  */ | ||||
| async function resolveNotePathToSegments(notePath, logErrors = true) { | ||||
| async function resolveNotePathToSegments(notePath, hoistedNoteId = 'root', logErrors = true) { | ||||
|     utils.assertArguments(notePath); | ||||
|  | ||||
|     // we might get notePath with the tabId suffix, remove it if present | ||||
| @@ -37,7 +37,7 @@ async function resolveNotePathToSegments(notePath, logErrors = true) { | ||||
|         path.push('root'); | ||||
|     } | ||||
|  | ||||
|     const effectivePath = []; | ||||
|     const effectivePathSegments = []; | ||||
|     let childNoteId = null; | ||||
|     let i = 0; | ||||
|  | ||||
| @@ -56,6 +56,8 @@ async function resolveNotePathToSegments(notePath, logErrors = true) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             child.resortParents(); | ||||
|  | ||||
|             const parents = child.getParentNotes(); | ||||
|  | ||||
|             if (!parents.length) { | ||||
| @@ -73,13 +75,13 @@ async function resolveNotePathToSegments(notePath, logErrors = true) { | ||||
|                     console.log(utils.now(), `Did not find parent ${parentNoteId} (${parent ? parent.title : 'n/a'}) for child ${childNoteId} (${child.title}), available parents: ${parents.map(p => `${p.noteId} (${p.title})`)}`); | ||||
|                 } | ||||
|  | ||||
|                 const someNotePath = getSomeNotePath(parents[0]); | ||||
|                 const someNotePath = getSomeNotePath(child, hoistedNoteId); | ||||
|  | ||||
|                 if (someNotePath) { // in case it's root the path may be empty | ||||
|                     const pathToRoot = someNotePath.split("/").reverse(); | ||||
|                     const pathToRoot = someNotePath.split("/").reverse().slice(1); | ||||
|  | ||||
|                     for (const noteId of pathToRoot) { | ||||
|                         effectivePath.push(noteId); | ||||
|                         effectivePathSegments.push(noteId); | ||||
|                     } | ||||
|                 } | ||||
|  | ||||
| @@ -87,36 +89,37 @@ async function resolveNotePathToSegments(notePath, logErrors = true) { | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         effectivePath.push(parentNoteId); | ||||
|         effectivePathSegments.push(parentNoteId); | ||||
|         childNoteId = parentNoteId; | ||||
|     } | ||||
|  | ||||
|     return effectivePath.reverse(); | ||||
|     effectivePathSegments.reverse(); | ||||
|  | ||||
|     if (effectivePathSegments.includes(hoistedNoteId)) { | ||||
|         return effectivePathSegments; | ||||
|     } | ||||
|     else { | ||||
|         const note = await treeCache.getNote(getNoteIdFromNotePath(notePath)); | ||||
|  | ||||
|         const someNotePathSegments = getSomeNotePathSegments(note, hoistedNoteId); | ||||
|  | ||||
|         // if there isn't actually any note path with hoisted note then return the original resolved note path | ||||
|         return someNotePathSegments.includes(hoistedNoteId) ? someNotePathSegments : effectivePathSegments; | ||||
|     } | ||||
| } | ||||
|  | ||||
| function getSomeNotePath(note) { | ||||
| function getSomeNotePathSegments(note, hoistedNotePath = 'root') { | ||||
|     utils.assertArguments(note); | ||||
|  | ||||
|     const path = []; | ||||
|     const notePaths = note.getSortedNotePaths(hoistedNotePath); | ||||
|  | ||||
|     let cur = note; | ||||
|     return notePaths[0].notePath; | ||||
| } | ||||
|  | ||||
|     while (cur.noteId !== 'root') { | ||||
|         path.push(cur.noteId); | ||||
| function getSomeNotePath(note, hoistedNotePath = 'root') { | ||||
|     const notePath = getSomeNotePathSegments(note, hoistedNotePath); | ||||
|  | ||||
|         const parents = cur.getParentNotes().filter(note => note.type !== 'search'); | ||||
|  | ||||
|         if (!parents.length) { | ||||
|             logError(`Can't find parents for note ${cur.noteId}`); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         cur = parents[0]; | ||||
|     } | ||||
|  | ||||
|     path.push('root'); | ||||
|  | ||||
|     return path.reverse().join('/'); | ||||
|     return notePath.join('/'); | ||||
| } | ||||
|  | ||||
| async function sortAlphabetically(noteId) { | ||||
| @@ -136,7 +139,7 @@ ws.subscribeToMessages(message => { | ||||
| }); | ||||
|  | ||||
| function getParentProtectedStatus(node) { | ||||
|     return hoistedNoteService.isRootNode(node) ? 0 : node.getParent().data.isProtected; | ||||
|     return hoistedNoteService.isHoistedNode(node) ? 0 : node.getParent().data.isProtected; | ||||
| } | ||||
|  | ||||
| function getNoteIdFromNotePath(notePath) { | ||||
| @@ -196,7 +199,7 @@ function getNotePath(node) { | ||||
|  | ||||
|     const path = []; | ||||
|  | ||||
|     while (node && !hoistedNoteService.isRootNode(node)) { | ||||
|     while (node) { | ||||
|         if (node.data.noteId) { | ||||
|             path.push(node.data.noteId); | ||||
|         } | ||||
| @@ -204,10 +207,6 @@ function getNotePath(node) { | ||||
|         node = node.getParent(); | ||||
|     } | ||||
|  | ||||
|     if (node) { // null node can happen directly after unhoisting when tree is still hoisted but option has been changed already | ||||
|         path.push(node.data.noteId); // root or hoisted noteId | ||||
|     } | ||||
|  | ||||
|     return path.reverse().join("/"); | ||||
| } | ||||
|  | ||||
| @@ -311,6 +310,7 @@ export default { | ||||
|     resolveNotePath, | ||||
|     resolveNotePathToSegments, | ||||
|     getSomeNotePath, | ||||
|     getSomeNotePathSegments, | ||||
|     getParentProtectedStatus, | ||||
|     getNotePath, | ||||
|     getNoteIdFromNotePath, | ||||
|   | ||||
| @@ -75,7 +75,7 @@ class TreeContextMenu { | ||||
|                     { title: 'Expand subtree <kbd data-command="expandSubtree"></kbd>', command: "expandSubtree", uiIcon: "expand", enabled: noSelectedNotes }, | ||||
|                     { title: 'Collapse subtree <kbd data-command="collapseSubtree"></kbd>', command: "collapseSubtree", uiIcon: "collapse", enabled: noSelectedNotes }, | ||||
|                     { title: "Force note sync", command: "forceNoteSync", uiIcon: "refresh", enabled: noSelectedNotes }, | ||||
|                     { title: 'Sort alphabetically <kbd data-command="sortChildNotes"></kbd>', command: "sortChildNotes", uiIcon: "empty", enabled: noSelectedNotes && notSearch }, | ||||
|                     { title: 'Sort by ... <kbd data-command="sortChildNotes"></kbd>', command: "sortChildNotes", uiIcon: "empty", enabled: noSelectedNotes && notSearch }, | ||||
|                     { title: 'Recent changes in subtree', command: "recentChangesInSubtree", uiIcon: "history", enabled: noSelectedNotes } | ||||
|                 ] }, | ||||
|             { title: "----" }, | ||||
| @@ -112,10 +112,10 @@ class TreeContextMenu { | ||||
|             appContext.tabManager.openTabWithNoteWithHoisting(notePath); | ||||
|         } | ||||
|         else if (command === "insertNoteAfter") { | ||||
|             const parentNoteId = this.node.data.parentNoteId; | ||||
|             const parentNotePath = treeService.getNotePath(this.node.getParent()); | ||||
|             const isProtected = await treeService.getParentProtectedStatus(this.node); | ||||
|  | ||||
|             noteCreateService.createNote(parentNoteId, { | ||||
|             noteCreateService.createNote(parentNotePath, { | ||||
|                 target: 'after', | ||||
|                 targetBranchId: this.node.data.branchId, | ||||
|                 type: type, | ||||
| @@ -123,14 +123,14 @@ class TreeContextMenu { | ||||
|             }); | ||||
|         } | ||||
|         else if (command === "insertChildNote") { | ||||
|             noteCreateService.createNote(noteId, { | ||||
|             const parentNotePath = treeService.getNotePath(this.node); | ||||
|  | ||||
|             noteCreateService.createNote(parentNotePath, { | ||||
|                 type: type, | ||||
|                 isProtected: this.node.data.isProtected | ||||
|             }); | ||||
|         } | ||||
|         else { | ||||
|             console.log("Triggering", command, notePath); | ||||
|  | ||||
|             this.treeWidget.triggerCommand(command, {node: this.node, notePath: notePath}); | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -193,6 +193,10 @@ function getNoteTypeClass(type) { | ||||
| } | ||||
|  | ||||
| function getMimeTypeClass(mime) { | ||||
|     if (!mime) { | ||||
|         return ""; | ||||
|     } | ||||
|  | ||||
|     const semicolonIdx = mime.indexOf(';'); | ||||
|  | ||||
|     if (semicolonIdx !== -1) { | ||||
| @@ -296,9 +300,13 @@ function dynamicRequire(moduleName) { | ||||
|     } | ||||
| } | ||||
|  | ||||
| function timeLimit(promise, limitMs) { | ||||
| function timeLimit(promise, limitMs, errorMessage) { | ||||
|     if (!promise || !promise.then) { // it's not actually a promise | ||||
|         return promise; | ||||
|     } | ||||
|  | ||||
|     // better stack trace if created outside of promise | ||||
|     const error = new Error('Process exceeded time limit ' + limitMs); | ||||
|     const error = new Error(errorMessage || `Process exceeded time limit ${limitMs}`); | ||||
|  | ||||
|     return new Promise((res, rej) => { | ||||
|         let resolved = false; | ||||
|   | ||||
| @@ -43,7 +43,6 @@ const processedEntityChangeIds = new Set(); | ||||
| function logRows(entityChanges) { | ||||
|     const filteredRows = entityChanges.filter(row => | ||||
|         !processedEntityChangeIds.has(row.id) | ||||
|         && row.entityName !== 'recent_notes' | ||||
|         && (row.entityName !== 'options' || row.entityId !== 'openTabs')); | ||||
|  | ||||
|     if (filteredRows.length > 0) { | ||||
| @@ -103,7 +102,7 @@ function waitForEntityChangeId(desiredEntityChangeId) { | ||||
|         return Promise.resolve(); | ||||
|     } | ||||
|  | ||||
|     console.debug("Waiting for", desiredEntityChangeId, 'current is', lastProcessedEntityChangeId); | ||||
|     console.debug(`Waiting for ${desiredEntityChangeId}, last processed is ${lastProcessedEntityChangeId}, last accepted ${lastAcceptedEntityChangeId}`); | ||||
|  | ||||
|     return new Promise((res, rej) => { | ||||
|         entityChangeIdReachedListeners.push({ | ||||
| @@ -127,7 +126,7 @@ function checkEntityChangeIdListeners() { | ||||
|         .filter(l => l.desiredEntityChangeId > lastProcessedEntityChangeId); | ||||
|  | ||||
|     entityChangeIdReachedListeners.filter(l => Date.now() > l.start - 60000) | ||||
|         .forEach(l => console.log(`Waiting for entityChangeId ${l.desiredEntityChangeId} while current is ${lastProcessedEntityChangeId} for ${Math.floor((Date.now() - l.start) / 1000)}s`)); | ||||
|         .forEach(l => console.log(`Waiting for entityChangeId ${l.desiredEntityChangeId} while last processed is ${lastProcessedEntityChangeId} (last accepted ${lastAcceptedEntityChangeId}) for ${Math.floor((Date.now() - l.start) / 1000)}s`)); | ||||
| } | ||||
|  | ||||
| async function runSafely(syncHandler, syncData) { | ||||
| @@ -230,25 +229,6 @@ subscribeToMessages(message => { | ||||
| }); | ||||
|  | ||||
| async function processEntityChanges(entityChanges) { | ||||
|     const missingNoteIds = []; | ||||
|  | ||||
|     for (const {entityName, entity} of entityChanges) { | ||||
|         if (entityName === 'branches' && !(entity.parentNoteId in treeCache.notes)) { | ||||
|             missingNoteIds.push(entity.parentNoteId); | ||||
|         } | ||||
|         else if (entityName === 'attributes' | ||||
|               && entity.type === 'relation' | ||||
|               && entity.name === 'template' | ||||
|               && !(entity.noteId in treeCache.notes)) { | ||||
|  | ||||
|             missingNoteIds.push(entity.value); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     if (missingNoteIds.length > 0) { | ||||
|         await treeCache.reloadNotes(missingNoteIds); | ||||
|     } | ||||
|  | ||||
|     const loadResults = new LoadResults(treeCache); | ||||
|  | ||||
|     for (const ec of entityChanges.filter(ec => ec.entityName === 'notes')) { | ||||
| @@ -391,6 +371,25 @@ async function processEntityChanges(entityChanges) { | ||||
|         loadResults.addOption(ec.entity.name); | ||||
|     } | ||||
|  | ||||
|     const missingNoteIds = []; | ||||
|  | ||||
|     for (const {entityName, entity} of entityChanges) { | ||||
|         if (entityName === 'branches' && !(entity.parentNoteId in treeCache.notes)) { | ||||
|             missingNoteIds.push(entity.parentNoteId); | ||||
|         } | ||||
|         else if (entityName === 'attributes' | ||||
|             && entity.type === 'relation' | ||||
|             && entity.name === 'template' | ||||
|             && !(entity.value in treeCache.notes)) { | ||||
|  | ||||
|             missingNoteIds.push(entity.value); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     if (missingNoteIds.length > 0) { | ||||
|         await treeCache.reloadNotes(missingNoteIds); | ||||
|     } | ||||
|  | ||||
|     if (!loadResults.isEmpty()) { | ||||
|         if (loadResults.hasAttributeRelatedChanges()) { | ||||
|             noteAttributeCache.invalidate(); | ||||
|   | ||||
| @@ -194,9 +194,18 @@ const ATTR_HELP = { | ||||
|         "appTheme": "marks CSS notes which are full Trilium themes and are thus available in Trilium options.", | ||||
|         "cssClass": "value of this label is then added as CSS class to the node representing given note in the tree. This can be useful for advanced theming. Can be used in template notes.", | ||||
|         "iconClass": "value of this label is added as a CSS class to the icon on the tree which can help visually distinguish the notes in the tree. Example might be bx bx-home - icons are taken from boxicons. Can be used in template notes.", | ||||
|         "bookZoomLevel": 'applies only to book note and sets the "zoom level" (how many notes fit on 1 row)', | ||||
|         "pageSize": "number of items per page in note listing", | ||||
|         "customRequestHandler": 'see <a href="javascript:" data-help-page="Custom request handler">Custom request handler</a>', | ||||
|         "customResourceProvider": 'see <a href="javascript:" data-help-page="Custom request handler">Custom request handler</a>' | ||||
|         "customResourceProvider": 'see <a href="javascript:" data-help-page="Custom request handler">Custom request handler</a>', | ||||
|         "widget": "marks this note as a custom widget which will be added to the Trilium component tree", | ||||
|         "workspace": "marks this note as a workspace which allows easy hoisting", | ||||
|         "workspaceIconClass": "defines box icon CSS class which will be used in tab when hoisted to this note", | ||||
|         "workspaceTabBackgroundColor": "CSS color used in the note tab when hoisted to this note", | ||||
|         "searchHome": "new search notes will be created as children of this note", | ||||
|         "hoistedSearchHome": "new search notes will be created as children of this note when hoisted to some ancestor of this note", | ||||
|         "inbox": "default inbox location for new notes", | ||||
|         "hoistedInbox": "default inbox location for new notes when hoisted to some ancestor of this note", | ||||
|         "sqlConsoleHome": "default location of SQL console notes", | ||||
|     }, | ||||
|     "relation": { | ||||
|         "runOnNoteCreation": "executes when note is created on backend", | ||||
|   | ||||
| @@ -491,7 +491,7 @@ export default class AttributeEditorWidget extends TabAwareWidget { | ||||
|     } | ||||
|  | ||||
|     async createNoteForReferenceLink(title) { | ||||
|         const {note} = await noteCreateService.createNote(this.noteId, { | ||||
|         const {note} = await noteCreateService.createNote(this.notePath, { | ||||
|             activate: false, | ||||
|             title: title | ||||
|         }); | ||||
|   | ||||
| @@ -12,6 +12,10 @@ const TPL = ` | ||||
|             text-overflow: ellipsis; | ||||
|         } | ||||
|     </style> | ||||
|      | ||||
|     <div class="no-edited-notes-found">No edited notes on this day yet ...</div> | ||||
|      | ||||
|     <div class="edited-notes-list"></div> | ||||
| </div> | ||||
| `; | ||||
|  | ||||
| @@ -31,18 +35,20 @@ export default class EditedNotesWidget extends CollapsibleWidget { | ||||
|  | ||||
|     async doRenderBody() { | ||||
|         this.$body.html(TPL); | ||||
|         this.$editedNotes = this.$body.find('.edited-notes-widget'); | ||||
|         this.$list = this.$body.find('.edited-notes-list'); | ||||
|         this.$noneFound = this.$body.find('.no-edited-notes-found'); | ||||
|     } | ||||
|  | ||||
|     async refreshWithNote(note) { | ||||
|         // remember which title was when we found the similar notes | ||||
|         this.title = note.title; | ||||
|         let editedNotes = await server.get('edited-notes/' + note.getLabelValue("dateNote")); | ||||
|  | ||||
|         editedNotes = editedNotes.filter(n => n.noteId !== note.noteId); | ||||
|  | ||||
|         this.$list.empty(); | ||||
|         this.$noneFound.hide(); | ||||
|  | ||||
|         if (editedNotes.length === 0) { | ||||
|             this.$body.text("No edited notes on this day yet ..."); | ||||
|             this.$noneFound.show(); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
| @@ -50,8 +56,6 @@ export default class EditedNotesWidget extends CollapsibleWidget { | ||||
|  | ||||
|         await treeCache.getNotes(noteIds, true); // preload all at once | ||||
|  | ||||
|         const $list = $('<div>'); // not using <ul> because it's difficult to style correctly with text-overflow | ||||
|  | ||||
|         for (const editedNote of editedNotes) { | ||||
|             const $item = $('<div class="edited-note-line">'); | ||||
|  | ||||
| @@ -67,9 +71,7 @@ export default class EditedNotesWidget extends CollapsibleWidget { | ||||
|                 $item.append(editedNote.notePath ? await linkService.createNoteLink(editedNote.notePath.join("/"), {showNotePath: true}) : editedNote.title); | ||||
|             } | ||||
|  | ||||
|             $list.append($item); | ||||
|             this.$list.append($item); | ||||
|         } | ||||
|  | ||||
|         this.$editedNotes.empty().append($list); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -91,7 +91,13 @@ export default class Component { | ||||
|             console.log(`Call to ${fun.name} in ${this.componentId} took ${took}ms`); | ||||
|         } | ||||
|  | ||||
|         await promise; | ||||
|         if (glob.isDev) { | ||||
|             await utils.timeLimit(promise, 20000, `Time limit failed on ${this.constructor.name} with ${fun.name}`); | ||||
|         } | ||||
|         else { | ||||
|             // cheaper and in non-dev the extra reporting is lost anyway through reload | ||||
|             await promise; | ||||
|         } | ||||
|  | ||||
|         return true; | ||||
|     } | ||||
|   | ||||
| @@ -26,7 +26,7 @@ class MobileDetailMenuWidget extends BasicWidget { | ||||
|                 ], | ||||
|                 selectMenuItemHandler: async ({command}) => { | ||||
|                     if (command === "insertChildNote") { | ||||
|                         noteCreateService.createNote(note.noteId); | ||||
|                         noteCreateService.createNote(appContext.tabManager.getActiveTabNotePath()); | ||||
|                     } | ||||
|                     else if (command === "delete") { | ||||
|                         const notePath = appContext.tabManager.getActiveTabNotePath(); | ||||
|   | ||||
| @@ -13,7 +13,7 @@ const WIDGET_TPL = ` | ||||
|     } | ||||
|     </style> | ||||
|  | ||||
|     <a data-trigger-command="createTopLevelNote" title="Create new top level note" class="icon-action bx bx-folder-plus"></a> | ||||
|     <a data-trigger-command="createNoteIntoInbox" title="New note" class="icon-action bx bx-folder-plus"></a> | ||||
|  | ||||
|     <a data-trigger-command="collapseTree" title="Collapse note tree" class="icon-action bx bx-layer-minus"></a> | ||||
|  | ||||
|   | ||||
| @@ -65,6 +65,8 @@ export default class NoteDetailWidget extends TabAwareWidget { | ||||
|  | ||||
|             await server.put('notes/' + noteId, dto, this.componentId); | ||||
|         }); | ||||
|  | ||||
|         appContext.addBeforeUnloadListener(this); | ||||
|     } | ||||
|  | ||||
|     isEnabled() { | ||||
| @@ -276,7 +278,7 @@ export default class NoteDetailWidget extends TabAwareWidget { | ||||
|  | ||||
|             const label = attrs.find(attr => | ||||
|                 attr.type === 'label' | ||||
|                 && ['readOnly', 'autoReadOnlyDisabled', 'cssClass', 'bookZoomLevel'].includes(attr.name) | ||||
|                 && ['readOnly', 'autoReadOnlyDisabled', 'cssClass', 'displayRelations'].includes(attr.name) | ||||
|                 && attr.isAffecting(this.note)); | ||||
|  | ||||
|             const relation = attrs.find(attr => | ||||
| @@ -293,7 +295,7 @@ export default class NoteDetailWidget extends TabAwareWidget { | ||||
|     } | ||||
|  | ||||
|     beforeUnloadEvent() { | ||||
|         this.spacedUpdate.updateNowIfNecessary(); | ||||
|         return this.spacedUpdate.isAllSavedAndTriggerUpdate(); | ||||
|     } | ||||
|  | ||||
|     textPreviewDisabledEvent({tabContext}) { | ||||
| @@ -316,7 +318,7 @@ export default class NoteDetailWidget extends TabAwareWidget { | ||||
|         } | ||||
|  | ||||
|         // without await as this otherwise causes deadlock through component mutex | ||||
|         noteCreateService.createNote(note.noteId, { | ||||
|         noteCreateService.createNote(appContext.tabManager.getActiveTabNotePath(), { | ||||
|             isProtected: note.isProtected, | ||||
|             saveSelection: true | ||||
|         }); | ||||
|   | ||||
| @@ -16,6 +16,7 @@ const TPL = ` | ||||
|         border: 1px solid transparent; | ||||
|         cursor: pointer; | ||||
|         padding: 6px; | ||||
|         color: var(--main-text-color); | ||||
|     } | ||||
|      | ||||
|     .note-icon-container button.note-icon:hover { | ||||
|   | ||||
| @@ -15,9 +15,21 @@ const TPL = ` | ||||
|     } | ||||
|      | ||||
|     .note-path-list { | ||||
|         max-height: 600px; | ||||
|         max-height: 700px; | ||||
|         overflow-y: auto; | ||||
|     } | ||||
|      | ||||
|     .note-path-list .path-current { | ||||
|         font-weight: bold; | ||||
|     } | ||||
|      | ||||
|     .note-path-list .path-archived { | ||||
|         color: var(--muted-text-color) !important; | ||||
|     } | ||||
|      | ||||
|     .note-path-list .path-search { | ||||
|         font-style: italic; | ||||
|     } | ||||
|     </style> | ||||
|      | ||||
|     <button class="btn dropdown-toggle note-path-list-button bx bx-collection" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" title="Note paths"></button> | ||||
| @@ -43,20 +55,12 @@ export default class NotePathsWidget extends TabAwareWidget { | ||||
|         ); | ||||
|  | ||||
|         if (this.noteId === 'root') { | ||||
|             await this.addPath('root', true); | ||||
|             await this.addPath('root'); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         const pathSegments = treeService.parseNotePath(this.notePath); | ||||
|         const activeNoteParentNoteId = pathSegments[pathSegments.length - 2]; // we know this is not root so there must be a parent | ||||
|  | ||||
|         for (const parentNote of this.note.getParentNotes()) { | ||||
|             const parentNotePath = treeService.getSomeNotePath(parentNote); | ||||
|             // this is to avoid having root notes leading '/' | ||||
|             const notePath = parentNotePath ? (parentNotePath + '/' + this.noteId) : this.noteId; | ||||
|             const isCurrent = activeNoteParentNoteId === parentNote.noteId; | ||||
|  | ||||
|             await this.addPath(notePath, isCurrent); | ||||
|         for (const notePathRecord of this.note.getSortedNotePaths(this.hoistedNoteId)) { | ||||
|             await this.addPath(notePathRecord); | ||||
|         } | ||||
|  | ||||
|         const cloneLink = $("<div>") | ||||
| @@ -70,7 +74,9 @@ export default class NotePathsWidget extends TabAwareWidget { | ||||
|         this.$notePathList.append(cloneLink); | ||||
|     } | ||||
|  | ||||
|     async addPath(notePath, isCurrent) { | ||||
|     async addPath(notePathRecord) { | ||||
|         const notePath = notePathRecord.notePath.join('/'); | ||||
|  | ||||
|         const title = await treeService.getNotePathTitle(notePath); | ||||
|  | ||||
|         const $noteLink = await linkService.createNoteLink(notePath, {title}); | ||||
| @@ -82,8 +88,33 @@ export default class NotePathsWidget extends TabAwareWidget { | ||||
|             .find('a') | ||||
|             .addClass("no-tooltip-preview"); | ||||
|  | ||||
|         if (isCurrent) { | ||||
|             $noteLink.addClass("current"); | ||||
|         const icons = []; | ||||
|  | ||||
|         if (this.notePath === notePath) { | ||||
|             $noteLink.addClass("path-current"); | ||||
|         } | ||||
|  | ||||
|         if (notePathRecord.isInHoistedSubTree) { | ||||
|             $noteLink.addClass("path-in-hoisted-subtree"); | ||||
|         } | ||||
|         else { | ||||
|             icons.push(`<span class="bx bx-trending-up" title="This path is outside of hoisted note and you would have to unhoist."></span>`); | ||||
|         } | ||||
|  | ||||
|         if (notePathRecord.isArchived) { | ||||
|             $noteLink.addClass("path-archived"); | ||||
|  | ||||
|             icons.push(`<span class="bx bx-archive" title="Archived"></span>`); | ||||
|         } | ||||
|  | ||||
|         if (notePathRecord.isSearch) { | ||||
|             $noteLink.addClass("path-search"); | ||||
|  | ||||
|             icons.push(`<span class="bx bx-search" title="Search"></span>`); | ||||
|         } | ||||
|  | ||||
|         if (icons.length > 0) { | ||||
|             $noteLink.append(` ${icons.join(' ')}`); | ||||
|         } | ||||
|  | ||||
|         this.$notePathList.append($noteLink); | ||||
| @@ -96,4 +127,10 @@ export default class NotePathsWidget extends TabAwareWidget { | ||||
|             this.refresh(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     async refresh() { | ||||
|         await super.refresh(); | ||||
|  | ||||
|         this.$widget.find('.dropdown-toggle').dropdown('hide'); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -3,6 +3,7 @@ import utils from "../services/utils.js"; | ||||
| import protectedSessionHolder from "../services/protected_session_holder.js"; | ||||
| import server from "../services/server.js"; | ||||
| import SpacedUpdate from "../services/spaced_update.js"; | ||||
| import appContext from "../services/app_context.js"; | ||||
|  | ||||
| const TPL = ` | ||||
| <div class="note-title-container"> | ||||
| @@ -37,6 +38,8 @@ export default class NoteTitleWidget extends TabAwareWidget { | ||||
|  | ||||
|             await server.put(`notes/${this.noteId}/change-title`, {title}); | ||||
|         }); | ||||
|  | ||||
|         appContext.addBeforeUnloadListener(this); | ||||
|     } | ||||
|  | ||||
|     doRender() { | ||||
| @@ -101,6 +104,6 @@ export default class NoteTitleWidget extends TabAwareWidget { | ||||
|     } | ||||
|  | ||||
|     beforeUnloadEvent() { | ||||
|         this.spacedUpdate.updateNowIfNecessary(); | ||||
|         return this.spacedUpdate.isAllSavedAndTriggerUpdate(); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -203,8 +203,9 @@ export default class NoteTreeWidget extends TabAwareWidget { | ||||
|         this.$tree.on("mousedown", ".refresh-search-button", e => this.refreshSearch(e)); | ||||
|         this.$tree.on("mousedown", ".add-note-button", e => { | ||||
|             const node = $.ui.fancytree.getNode(e); | ||||
|             const parentNotePath = treeService.getNotePath(node); | ||||
|  | ||||
|             noteCreateService.createNote(node.data.noteId, { | ||||
|             noteCreateService.createNote(parentNotePath, { | ||||
|                 isProtected: node.data.isProtected | ||||
|             }); | ||||
|         }); | ||||
| @@ -476,7 +477,7 @@ export default class NoteTreeWidget extends TabAwareWidget { | ||||
|  | ||||
|                         let childNoteIds = note.getChildNoteIds(); | ||||
|  | ||||
|                         if (childNoteIds.length > MAX_SEARCH_RESULTS_IN_TREE) { | ||||
|                         if (note.type === 'search' && childNoteIds.length > MAX_SEARCH_RESULTS_IN_TREE) { | ||||
|                             childNoteIds = childNoteIds.slice(0, MAX_SEARCH_RESULTS_IN_TREE); | ||||
|                         } | ||||
|  | ||||
| @@ -594,7 +595,7 @@ export default class NoteTreeWidget extends TabAwareWidget { | ||||
|  | ||||
|         let childBranches = parentNote.getFilteredChildBranches(); | ||||
|  | ||||
|         if (childBranches.length > MAX_SEARCH_RESULTS_IN_TREE) { | ||||
|         if (parentNote.type === 'search' && childBranches.length > MAX_SEARCH_RESULTS_IN_TREE) { | ||||
|             childBranches = childBranches.slice(0, MAX_SEARCH_RESULTS_IN_TREE); | ||||
|         } | ||||
|  | ||||
| @@ -797,19 +798,17 @@ export default class NoteTreeWidget extends TabAwareWidget { | ||||
|             const node = await this.expandToNote(activeContext.notePath); | ||||
|  | ||||
|             await node.makeVisible({scrollIntoView: true}); | ||||
|             node.setFocus(true); | ||||
|             node.setActive(true, {noEvents: true, noFocus: false}); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** @return {FancytreeNode} */ | ||||
|     async getNodeFromPath(notePath, expand = false, logErrors = true) { | ||||
|         utils.assertArguments(notePath); | ||||
|         /** @let {FancytreeNode} */ | ||||
|         let parentNode = this.getNodesByNoteId('root')[0]; | ||||
|  | ||||
|         const hoistedNoteId = hoistedNoteService.getHoistedNoteId(); | ||||
|         /** @const {FancytreeNode} */ | ||||
|         let parentNode = null; | ||||
|  | ||||
|         const resolvedNotePathSegments = await treeService.resolveNotePathToSegments(notePath, logErrors); | ||||
|         let resolvedNotePathSegments = await treeService.resolveNotePathToSegments(notePath, this.hoistedNoteId, logErrors); | ||||
|  | ||||
|         if (!resolvedNotePathSegments) { | ||||
|             if (logErrors) { | ||||
| @@ -819,14 +818,9 @@ export default class NoteTreeWidget extends TabAwareWidget { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         resolvedNotePathSegments = resolvedNotePathSegments.slice(1); | ||||
|  | ||||
|         for (const childNoteId of resolvedNotePathSegments) { | ||||
|             if (childNoteId === hoistedNoteId) { | ||||
|                 // there must be exactly one node with given hoistedNoteId | ||||
|                 parentNode = this.getNodesByNoteId(childNoteId)[0]; | ||||
|  | ||||
|                 continue; | ||||
|             } | ||||
|  | ||||
|             // we expand only after hoisted note since before then nodes are not actually present in the tree | ||||
|             if (parentNode) { | ||||
|                 if (!parentNode.isLoaded()) { | ||||
| @@ -857,7 +851,7 @@ export default class NoteTreeWidget extends TabAwareWidget { | ||||
|                             // these are real notes with real notePath, user can display them in a detail | ||||
|                             // but they don't have a node in the tree | ||||
|  | ||||
|                             ws.logError(`Can't find node for child node of noteId=${childNoteId} for parent of noteId=${parentNode.data.noteId} and hoistedNoteId=${hoistedNoteId}, requested path is ${notePath}`); | ||||
|                             ws.logError(`Can't find node for child node of noteId=${childNoteId} for parent of noteId=${parentNode.data.noteId} and hoistedNoteId=${hoistedNoteService.getHoistedNoteId()}, requested path is ${notePath}`); | ||||
|                         } | ||||
|  | ||||
|                         return; | ||||
| @@ -999,6 +993,7 @@ export default class NoteTreeWidget extends TabAwareWidget { | ||||
|         const activeNodeFocused = activeNode && activeNode.hasFocus(); | ||||
|         const nextNode = activeNode ? (activeNode.getNextSibling() || activeNode.getPrevSibling() || activeNode.getParent()) : null; | ||||
|         const activeNotePath = activeNode ? treeService.getNotePath(activeNode) : null; | ||||
|  | ||||
|         const nextNotePath = nextNode ? treeService.getNotePath(nextNode) : null; | ||||
|         const activeNoteId = activeNode ? activeNode.data.noteId : null; | ||||
|  | ||||
| @@ -1030,6 +1025,9 @@ export default class NoteTreeWidget extends TabAwareWidget { | ||||
|         } | ||||
|  | ||||
|         for (const branch of loadResults.getBranches()) { | ||||
|             // adding noteId itself to update all potential clones | ||||
|             noteIdsToUpdate.add(branch.noteId); | ||||
|  | ||||
|             for (const node of this.getNodesByBranchId(branch.branchId)) { | ||||
|                 if (branch.isDeleted) { | ||||
|                     if (node.isActive()) { | ||||
| @@ -1048,9 +1046,6 @@ export default class NoteTreeWidget extends TabAwareWidget { | ||||
|  | ||||
|                     noteIdsToUpdate.add(branch.parentNoteId); | ||||
|                 } | ||||
|                 else { | ||||
|                     noteIdsToUpdate.add(branch.noteId); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             if (!branch.isDeleted) { | ||||
| @@ -1119,23 +1114,32 @@ export default class NoteTreeWidget extends TabAwareWidget { | ||||
|             } | ||||
|  | ||||
|             if (node) { | ||||
|                 node.setActive(true, {noEvents: true, noFocus: true}); | ||||
|                 node.setActive(true, {noEvents: true, noFocus: !activeNodeFocused}); | ||||
|  | ||||
|                 if (activeNodeFocused) { | ||||
|                     node.setFocus(true); | ||||
|                 } | ||||
|             } | ||||
|             else { | ||||
|                 // this is used when original note has been deleted and we want to move the focus to the note above/below | ||||
|                 node = await this.expandToNote(nextNotePath, false); | ||||
|  | ||||
|                 if (node) { | ||||
|                     await appContext.tabManager.getActiveTabContext().setNote(nextNotePath); | ||||
|                     // FIXME: this is conceptually wrong | ||||
|                     //        here note tree is responsible for updating global state of the application | ||||
|                     //        this should be done by tabcontext / tabmanager and note tree should only listen to | ||||
|                     //        changes in active note and just set the "active" state | ||||
|                     // We don't await since that can bring up infinite cycles when e.g. custom widget does some backend requests which wait for max sync ID processed | ||||
|                     appContext.tabManager.getActiveTabContext().setNote(nextNotePath).then(() => { | ||||
|                         const newActiveNode = this.getActiveNode(); | ||||
|  | ||||
|                         // return focus if the previously active node was also focused | ||||
|                         if (newActiveNode && activeNodeFocused) {console.log("FOCUSING!!!"); | ||||
|                             newActiveNode.setFocus(true); | ||||
|                         } | ||||
|                     }); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             const newActiveNode = this.getActiveNode(); | ||||
|  | ||||
|             // return focus if the previously active node was also focused | ||||
|             if (newActiveNode && activeNodeFocused) { | ||||
|                 await newActiveNode.setFocus(true); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         if (noteIdsToReload.size > 0 || noteIdsToUpdate.size > 0) { | ||||
| @@ -1202,13 +1206,20 @@ export default class NoteTreeWidget extends TabAwareWidget { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     filterHoistedBranch() { | ||||
|     async filterHoistedBranch() { | ||||
|         if (this.tabContext) { | ||||
|             // make sure the hoisted node is loaded (can be unloaded e.g. after tree collapse in another tab) | ||||
|             const hoistedNotePath = await treeService.resolveNotePath(this.tabContext.hoistedNoteId); | ||||
|             await this.getNodeFromPath(hoistedNotePath); | ||||
|  | ||||
|             if (this.tabContext.hoistedNoteId === 'root') { | ||||
|                 this.tree.clearFilter(); | ||||
|             } | ||||
|             else { | ||||
|                 this.tree.filterBranches(node => node.data.noteId === this.tabContext.hoistedNoteId); | ||||
|                 // hack when hoisted note is cloned then it could be filtered multiple times while we want only 1 | ||||
|                 this.tree.filterBranches(node => | ||||
|                     node.data.noteId === this.tabContext.hoistedNoteId // optimization to not having always resolve the node path | ||||
|                     && treeService.getNotePath(node) === hoistedNotePath); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| @@ -1365,7 +1376,7 @@ export default class NoteTreeWidget extends TabAwareWidget { | ||||
|     } | ||||
|  | ||||
|     sortChildNotesCommand({node}) { | ||||
|         treeService.sortAlphabetically(node.data.noteId); | ||||
|         import("../dialogs/sort_child_notes.js").then(d => d.showDialog(node.data.noteId)); | ||||
|     } | ||||
|  | ||||
|     async recentChangesInSubtreeCommand({node}) { | ||||
|   | ||||
| @@ -49,7 +49,11 @@ export default class QuickSearchWidget extends BasicWidget { | ||||
|         this.$widget.find('.input-group-append').on('shown.bs.dropdown', () => this.search()); | ||||
|  | ||||
|         utils.bindElShortcut(this.$searchString, 'return', () => { | ||||
|             this.$dropdownToggle.dropdown('show'); | ||||
|             if (this.$dropdownMenu.is(":visible")) { | ||||
|                 this.search(); // just update already visible dropdown | ||||
|             } else { | ||||
|                 this.$dropdownToggle.dropdown('show'); | ||||
|             } | ||||
|  | ||||
|             this.$searchString.focus(); | ||||
|         }); | ||||
| @@ -90,12 +94,18 @@ export default class QuickSearchWidget extends BasicWidget { | ||||
|             const $link = await linkService.createNoteLink(note.noteId, {showNotePath: true}); | ||||
|             $link.addClass('dropdown-item'); | ||||
|             $link.attr("tabIndex", "0"); | ||||
|             $link.on('click', () => this.$dropdownToggle.dropdown("hide")); | ||||
|             $link.on('click', e => { | ||||
|                 this.$dropdownToggle.dropdown("hide"); | ||||
|  | ||||
|                 if (!e.target || e.target.nodeName !== 'A') { | ||||
|                     // click on the link is handled by link handling but we want the whole item clickable | ||||
|                     appContext.tabManager.getActiveTabContext().setNote(note.noteId); | ||||
|                 } | ||||
|             }); | ||||
|             utils.bindElShortcut($link, 'return', () => { | ||||
|                 $link.find('a').trigger({ | ||||
|                     type: 'click', | ||||
|                     which: 1 // left click | ||||
|                 }); | ||||
|                 this.$dropdownToggle.dropdown("hide"); | ||||
|  | ||||
|                 appContext.tabManager.getActiveTabContext().setNote(note.noteId); | ||||
|             }); | ||||
|  | ||||
|             this.$dropdownMenu.append($link); | ||||
|   | ||||
| @@ -52,7 +52,7 @@ export default class SearchResultWidget extends TabAwareWidget { | ||||
|         this.$noResults.toggle(note.getChildNoteIds().length === 0 && !!note.searchResultsLoaded); | ||||
|         this.$notExecutedYet.toggle(!note.searchResultsLoaded); | ||||
|  | ||||
|         const noteListRenderer = new NoteListRenderer(this.$content, note, note.getChildNoteIds()); | ||||
|         const noteListRenderer = new NoteListRenderer(this.$content, note, note.getChildNoteIds(), true); | ||||
|         await noteListRenderer.renderList(); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -22,6 +22,10 @@ export default class TabAwareWidget extends BasicWidget { | ||||
|         return this.tabContext && this.tabContext.notePath; | ||||
|     } | ||||
|  | ||||
|     get hoistedNoteId() { | ||||
|         return this.tabContext && this.tabContext.hoistedNoteId; | ||||
|     } | ||||
|  | ||||
|     isEnabled() { | ||||
|         return !!this.note; | ||||
|     } | ||||
|   | ||||
| @@ -83,4 +83,10 @@ export default class InheritedAttributesWidget extends TabAwareWidget { | ||||
|     getInheritedAttributes(note) { | ||||
|         return note.getAttributes().filter(attr => attr.noteId !== this.noteId); | ||||
|     } | ||||
|  | ||||
|     entitiesReloadedEvent({loadResults}) { | ||||
|         if (loadResults.getAttributes(this.componentId).find(attr => attr.isAffecting(this.note))) { | ||||
|             this.refresh(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -49,15 +49,16 @@ const TPL = ` | ||||
|     } | ||||
|           | ||||
|     .note-detail-editable-text h2 { font-size: 1.8em; }  | ||||
|     .note-detail-editable-text h2::before { content: "##\\2004"; color: var(--muted-text-color); } | ||||
|     .note-detail-editable-text h3 { font-size: 1.6em; } | ||||
|     .note-detail-editable-text h3::before { content: "###\\2004"; color: var(--muted-text-color); } | ||||
|     .note-detail-editable-text h4 { font-size: 1.4em; } | ||||
|     .note-detail-editable-text h4:not(.include-note-title)::before { content: "####\\2004"; color: var(--muted-text-color); } | ||||
|     .note-detail-editable-text h5 { font-size: 1.2em; } | ||||
|     .note-detail-editable-text h5::before { content: "#####\\2004"; color: var(--muted-text-color); } | ||||
|     .note-detail-editable-text h6 { font-size: 1.1em; } | ||||
|     .note-detail-editable-text h6::before { content: "######\\2004"; color: var(--muted-text-color); } | ||||
|      | ||||
|     body.heading-style-markdown .note-detail-editable-text h2::before { content: "##\\2004"; color: var(--muted-text-color); } | ||||
|     body.heading-style-markdown .note-detail-editable-text h3::before { content: "###\\2004"; color: var(--muted-text-color); } | ||||
|     body.heading-style-markdown .note-detail-editable-text h4:not(.include-note-title)::before { content: "####\\2004"; color: var(--muted-text-color); } | ||||
|     body.heading-style-markdown .note-detail-editable-text h5::before { content: "#####\\2004"; color: var(--muted-text-color); } | ||||
|     body.heading-style-markdown .note-detail-editable-text h6::before { content: "######\\2004"; color: var(--muted-text-color); } | ||||
|      | ||||
|     .note-detail-editable-text-editor { | ||||
|         padding-top: 10px; | ||||
| @@ -274,7 +275,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget { | ||||
|     } | ||||
|  | ||||
|     async createNoteForReferenceLink(title) { | ||||
|         const {note} = await noteCreateService.createNote(this.noteId, { | ||||
|         const {note} = await noteCreateService.createNote(this.notePath, { | ||||
|             activate: false, | ||||
|             title: title | ||||
|         }); | ||||
|   | ||||
| @@ -14,16 +14,11 @@ const TPL = ` | ||||
|     .note-detail-readonly-text h5 { font-size: 1.2em; } | ||||
|     .note-detail-readonly-text h6 { font-size: 1.1em; } | ||||
|      | ||||
|     .note-detail-readonly-text h2 { font-size: 1.8em; }  | ||||
|     .note-detail-readonly-text h2::before { content: "##\\2004"; color: var(--muted-text-color); } | ||||
|     .note-detail-readonly-text h3 { font-size: 1.6em; } | ||||
|     .note-detail-readonly-text h3::before { content: "###\\2004"; color: var(--muted-text-color); } | ||||
|     .note-detail-readonly-text h4 { font-size: 1.4em; } | ||||
|     .note-detail-readonly-text h4:not(.include-note-title)::before { content: "####\\2004"; color: var(--muted-text-color); } | ||||
|     .note-detail-readonly-text h5 { font-size: 1.2em; } | ||||
|     .note-detail-readonly-text h5::before { content: "#####\\2004"; color: var(--muted-text-color); } | ||||
|     .note-detail-readonly-text h6 { font-size: 1.1em; } | ||||
|     .note-detail-readonly-text h6::before { content: "######\\2004"; color: var(--muted-text-color); } | ||||
|     body.heading-style-markdown .note-detail-readonly-text h2::before { content: "##\\2004"; color: var(--muted-text-color); } | ||||
|     body.heading-style-markdown .note-detail-readonly-text h3::before { content: "###\\2004"; color: var(--muted-text-color); } | ||||
|     body.heading-style-markdown .note-detail-readonly-text h4:not(.include-note-title)::before { content: "####\\2004"; color: var(--muted-text-color); } | ||||
|     body.heading-style-markdown .note-detail-readonly-text h5::before { content: "#####\\2004"; color: var(--muted-text-color); } | ||||
|     body.heading-style-markdown .note-detail-readonly-text h6::before { content: "######\\2004"; color: var(--muted-text-color); } | ||||
|      | ||||
|     .note-detail-readonly-text { | ||||
|         padding-left: 22px; | ||||
|   | ||||
| @@ -7,6 +7,7 @@ import attributeAutocompleteService from "../../services/attribute_autocomplete. | ||||
| import TypeWidget from "./type_widget.js"; | ||||
| import appContext from "../../services/app_context.js"; | ||||
| import utils from "../../services/utils.js"; | ||||
| import treeCache from "../../services/tree_cache.js"; | ||||
|  | ||||
| const uniDirectionalOverlays = [ | ||||
|     [ "Arrow", { | ||||
| @@ -285,7 +286,7 @@ export default class RelationMapTypeWidget extends TypeWidget { | ||||
|  | ||||
|     async loadNotesAndRelations() { | ||||
|         const noteIds = this.mapData.notes.map(note => note.noteId); | ||||
|         const data = await server.post("notes/relation-map", {noteIds}); | ||||
|         const data = await server.post("notes/relation-map", {noteIds, relationMapNoteId: this.noteId}); | ||||
|  | ||||
|         this.relations = []; | ||||
|  | ||||
| @@ -531,8 +532,11 @@ export default class RelationMapTypeWidget extends TypeWidget { | ||||
|             linkService.goToLink(e); | ||||
|         }); | ||||
|  | ||||
|         const note = await treeCache.getNote(noteId); | ||||
|  | ||||
|         const $noteBox = $("<div>") | ||||
|             .addClass("note-box") | ||||
|             .addClass(note.getCssClass()) | ||||
|             .prop("id", this.noteIdToId(noteId)) | ||||
|             .append($("<span>").addClass("title").append($link)) | ||||
|             .append($("<div>").addClass("endpoint").attr("title", "Start dragging relations from here and drop them on another note.")) | ||||
|   | ||||
| @@ -106,7 +106,7 @@ function processContent(images, note, content) { | ||||
|         for (const {src, dataUrl, imageId} of images) { | ||||
|             const filename = path.basename(src); | ||||
|  | ||||
|             if (!dataUrl.startsWith("data:image")) { | ||||
|             if (!dataUrl || !dataUrl.startsWith("data:image")) { | ||||
|                 log.info("Image could not be recognized as data URL:", dataUrl.substr(0, Math.min(100, dataUrl.length))); | ||||
|                 continue; | ||||
|             } | ||||
|   | ||||
| @@ -5,10 +5,31 @@ const sql = require('../../services/sql'); | ||||
| const dateUtils = require('../../services/date_utils'); | ||||
| const noteService = require('../../services/notes'); | ||||
| const attributeService = require('../../services/attributes'); | ||||
| const cls = require('../../services/cls'); | ||||
| const repository = require('../../services/repository'); | ||||
|  | ||||
| function getInboxNote(req) { | ||||
|     return attributeService.getNoteWithLabel('inbox') | ||||
|         || dateNoteService.getDateNote(req.params.date); | ||||
|     const hoistedNote = getHoistedNote(); | ||||
|  | ||||
|     let inbox; | ||||
|  | ||||
|     if (hoistedNote) { | ||||
|         ([inbox] = hoistedNote.getDescendantNotesWithLabel('hoistedInbox')); | ||||
|  | ||||
|         if (!inbox) { | ||||
|             ([inbox] = hoistedNote.getDescendantNotesWithLabel('inbox')); | ||||
|         } | ||||
|  | ||||
|         if (!inbox) { | ||||
|             inbox = hoistedNote; | ||||
|         } | ||||
|     } | ||||
|     else { | ||||
|         inbox = attributeService.getNoteWithLabel('inbox') | ||||
|             || dateNoteService.getDateNote(req.params.date); | ||||
|     } | ||||
|  | ||||
|     return inbox; | ||||
| } | ||||
|  | ||||
| function getDateNote(req) { | ||||
| @@ -59,24 +80,60 @@ function createSqlConsole() { | ||||
|     return note; | ||||
| } | ||||
|  | ||||
| function createSearchNote() { | ||||
|     const today = dateUtils.localNowDate(); | ||||
| function createSearchNote(req) { | ||||
|     const params = req.body; | ||||
|     const searchString = params.searchString || ""; | ||||
|     let ancestorNoteId = params.ancestorNoteId; | ||||
|  | ||||
|     const searchHome = | ||||
|         attributeService.getNoteWithLabel('searchHome') | ||||
|         || dateNoteService.getDateNote(today); | ||||
|     const hoistedNote = getHoistedNote(); | ||||
|  | ||||
|     let searchHome; | ||||
|  | ||||
|     if (hoistedNote) { | ||||
|         ([searchHome] = hoistedNote.getDescendantNotesWithLabel('hoistedSearchHome')); | ||||
|  | ||||
|         if (!searchHome) { | ||||
|             ([searchHome] = hoistedNote.getDescendantNotesWithLabel('searchHome')); | ||||
|         } | ||||
|  | ||||
|         if (!searchHome) { | ||||
|             searchHome = hoistedNote; | ||||
|         } | ||||
|  | ||||
|         if (!ancestorNoteId) { | ||||
|             ancestorNoteId = hoistedNote.noteId; | ||||
|         } | ||||
|     } | ||||
|     else { | ||||
|         const today = dateUtils.localNowDate(); | ||||
|  | ||||
|         searchHome = attributeService.getNoteWithLabel('searchHome') | ||||
|                   || dateNoteService.getDateNote(today); | ||||
|     } | ||||
|  | ||||
|     const {note} = noteService.createNewNote({ | ||||
|         parentNoteId: searchHome.noteId, | ||||
|         title: 'Search: ', | ||||
|         title: 'Search: ' + searchString, | ||||
|         content: "", | ||||
|         type: 'search', | ||||
|         mime: 'application/json' | ||||
|     }); | ||||
|  | ||||
|     note.setLabel('searchString', searchString); | ||||
|  | ||||
|     if (ancestorNoteId) { | ||||
|         note.setRelation('ancestor', ancestorNoteId); | ||||
|     } | ||||
|  | ||||
|     return note; | ||||
| } | ||||
|  | ||||
| function getHoistedNote() { | ||||
|     return cls.getHoistedNoteId() && cls.getHoistedNoteId() !== 'root' | ||||
|         ? repository.getNote(cls.getHoistedNoteId()) | ||||
|         : null; | ||||
| } | ||||
|  | ||||
| module.exports = { | ||||
|     getInboxNote, | ||||
|     getDateNote, | ||||
|   | ||||
| @@ -4,6 +4,7 @@ const noteService = require('../../services/notes'); | ||||
| const treeService = require('../../services/tree'); | ||||
| const repository = require('../../services/repository'); | ||||
| const utils = require('../../services/utils'); | ||||
| const log = require('../../services/log'); | ||||
| const TaskContext = require('../../services/task_context'); | ||||
|  | ||||
| function getNote(req) { | ||||
| @@ -85,10 +86,20 @@ function undeleteNote(req) { | ||||
|     taskContext.taskSucceeded(); | ||||
| } | ||||
|  | ||||
| function sortNotes(req) { | ||||
| function sortChildNotes(req) { | ||||
|     const noteId = req.params.noteId; | ||||
|     const {sortBy, sortDirection} = req.body; | ||||
|  | ||||
|     treeService.sortNotesAlphabetically(noteId); | ||||
|     log.info(`Sorting ${noteId} children with ${sortBy} ${sortDirection}`); | ||||
|  | ||||
|     const reverse = sortDirection === 'desc'; | ||||
|  | ||||
|     if (sortBy === 'title') { | ||||
|         treeService.sortNotesByTitle(noteId, false, reverse); | ||||
|     } | ||||
|     else { | ||||
|         treeService.sortNotes(noteId, sortBy, reverse); | ||||
|     } | ||||
| } | ||||
|  | ||||
| function protectNote(req) { | ||||
| @@ -117,7 +128,8 @@ function setNoteTypeMime(req) { | ||||
| } | ||||
|  | ||||
| function getRelationMap(req) { | ||||
|     const noteIds = req.body.noteIds; | ||||
|     const {relationMapNoteId, noteIds} = req.body; | ||||
|  | ||||
|     const resp = { | ||||
|         // noteId => title | ||||
|         noteTitles: {}, | ||||
| @@ -134,12 +146,23 @@ function getRelationMap(req) { | ||||
|  | ||||
|     const questionMarks = noteIds.map(noteId => '?').join(','); | ||||
|  | ||||
|     const relationMapNote = repository.getNote(relationMapNoteId); | ||||
|  | ||||
|     const displayRelationsVal = relationMapNote.getLabelValue('displayRelations'); | ||||
|     const displayRelations = !displayRelationsVal ? [] : displayRelationsVal | ||||
|         .split(",") | ||||
|         .map(token => token.trim()); | ||||
|  | ||||
|     console.log("displayRelations", displayRelations); | ||||
|  | ||||
|     const notes = repository.getEntities(`SELECT * FROM notes WHERE isDeleted = 0 AND noteId IN (${questionMarks})`, noteIds); | ||||
|  | ||||
|     for (const note of notes) { | ||||
|         resp.noteTitles[note.noteId] = note.title; | ||||
|  | ||||
|         resp.relations = resp.relations.concat(note.getRelations() | ||||
|             .filter(relation => !relation.isAutoLink() || displayRelations.includes(relation.name)) | ||||
|             .filter(relation => displayRelations.length === 0 || displayRelations.includes(relation.name)) | ||||
|             .filter(relation => noteIds.includes(relation.value)) | ||||
|             .map(relation => ({ | ||||
|                 attributeId: relation.attributeId, | ||||
| @@ -203,7 +226,7 @@ module.exports = { | ||||
|     deleteNote, | ||||
|     undeleteNote, | ||||
|     createNote, | ||||
|     sortNotes, | ||||
|     sortChildNotes, | ||||
|     protectNote, | ||||
|     setNoteTypeMime, | ||||
|     getRelationMap, | ||||
|   | ||||
| @@ -40,7 +40,8 @@ const ALLOWED_OPTIONS = new Set([ | ||||
|     'nativeTitleBarVisible', | ||||
|     'attributeListExpanded', | ||||
|     'promotedAttributesExpanded', | ||||
|     'similarNotesExpanded' | ||||
|     'similarNotesExpanded', | ||||
|     'headingStyle' | ||||
| ]); | ||||
|  | ||||
| function getOptions() { | ||||
|   | ||||
| @@ -135,6 +135,10 @@ function getTree(req) { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     if (!(subTreeNoteId in noteCache.notes)) { | ||||
|         return [404, `Note ${subTreeNoteId} not found in the cache`]; | ||||
|     } | ||||
|  | ||||
|     collect(noteCache.notes[subTreeNoteId]); | ||||
|  | ||||
|     return getNotesAndBranchesAndAttributes(collectedNoteIds); | ||||
|   | ||||
| @@ -19,6 +19,7 @@ function index(req, res) { | ||||
|     res.render(view, { | ||||
|         csrfToken: csrfToken, | ||||
|         theme: options.theme, | ||||
|         headingStyle: options.headingStyle, | ||||
|         mainFontSize: parseInt(options.mainFontSize), | ||||
|         treeFontSize: parseInt(options.treeFontSize), | ||||
|         detailFontSize: parseInt(options.detailFontSize), | ||||
|   | ||||
| @@ -150,7 +150,7 @@ function register(app) { | ||||
|     apiRoute(DELETE, '/api/notes/:noteId', notesApiRoute.deleteNote); | ||||
|     apiRoute(PUT, '/api/notes/:noteId/undelete', notesApiRoute.undeleteNote); | ||||
|     apiRoute(POST, '/api/notes/:parentNoteId/children', notesApiRoute.createNote); | ||||
|     apiRoute(PUT, '/api/notes/:noteId/sort', notesApiRoute.sortNotes); | ||||
|     apiRoute(PUT, '/api/notes/:noteId/sort-children', notesApiRoute.sortChildNotes); | ||||
|     apiRoute(PUT, '/api/notes/:noteId/protect/:isProtected', notesApiRoute.protectNote); | ||||
|     apiRoute(PUT, /\/api\/notes\/(.*)\/type\/(.*)\/mime\/(.*)/, notesApiRoute.setNoteTypeMime); | ||||
|     apiRoute(GET, '/api/notes/:noteId/revisions', noteRevisionsApiRoute.getNoteRevisions); | ||||
|   | ||||
| @@ -27,7 +27,6 @@ const BUILTIN_ATTRIBUTES = [ | ||||
|     { type: 'label', name: 'run', isDangerous: true }, | ||||
|     { type: 'label', name: 'customRequestHandler', isDangerous: true }, | ||||
|     { type: 'label', name: 'customResourceProvider', isDangerous: true }, | ||||
|     { type: 'label', name: 'bookZoomLevel', isDangerous: false }, | ||||
|     { type: 'label', name: 'widget', isDangerous: true }, | ||||
|     { type: 'label', name: 'noteInfoWidgetDisabled' }, | ||||
|     { type: 'label', name: 'linkMapWidgetDisabled' }, | ||||
| @@ -38,8 +37,12 @@ const BUILTIN_ATTRIBUTES = [ | ||||
|     { type: 'label', name: 'workspaceIconClass' }, | ||||
|     { type: 'label', name: 'workspaceTabBackgroundColor' }, | ||||
|     { type: 'label', name: 'searchHome' }, | ||||
|     { type: 'label', name: 'hoistedInbox' }, | ||||
|     { type: 'label', name: 'hoistedSearchHome' }, | ||||
|     { type: 'label', name: 'sqlConsoleHome' }, | ||||
|     { type: 'label', name: 'datePattern' }, | ||||
|     { type: 'label', name: 'pageSize' }, | ||||
|     { type: 'label', name: 'viewType' }, | ||||
|  | ||||
|     // relation names | ||||
|     { type: 'relation', name: 'runOnNoteCreation', isDangerous: true }, | ||||
|   | ||||
| @@ -359,7 +359,7 @@ function BackendScriptApi(currentNote, apiParams) { | ||||
|      * @method | ||||
|      * @param {string} parentNoteId - this note's child notes will be sorted | ||||
|      */ | ||||
|     this.sortNotesAlphabetically = treeService.sortNotesAlphabetically; | ||||
|     this.sortNotesByTitle = treeService.sortNotesByTitle; | ||||
|  | ||||
|     /** | ||||
|      * This method finds note by its noteId and prefix and either sets it to the given parentNoteId | ||||
|   | ||||
| @@ -1 +1 @@ | ||||
| module.exports = { buildDate:"2021-02-19T23:15:18+01:00", buildRevision: "56506d33a7fea668f9ba2683b2836a30d28cd96c" }; | ||||
| module.exports = { buildDate:"2021-03-14T22:56:27+01:00", buildRevision: "6c8d20288df302f3a415bd1bdcace98bf29d4bf6" }; | ||||
|   | ||||
| @@ -44,10 +44,14 @@ function isEntityEventsDisabled() { | ||||
|     return !!namespace.get('disableEntityEvents'); | ||||
| } | ||||
|  | ||||
| function clearEntityChanges() { | ||||
|     namespace.set('entityChanges', []); | ||||
| } | ||||
|  | ||||
| function getAndClearEntityChanges() { | ||||
|     const entityChanges = namespace.get('entityChanges') || []; | ||||
|  | ||||
|     namespace.set('entityChanges', []); | ||||
|     clearEntityChanges(); | ||||
|  | ||||
|     return entityChanges; | ||||
| } | ||||
| @@ -92,6 +96,7 @@ module.exports = { | ||||
|     disableEntityEvents, | ||||
|     isEntityEventsDisabled, | ||||
|     reset, | ||||
|     clearEntityChanges, | ||||
|     getAndClearEntityChanges, | ||||
|     addEntityChange, | ||||
|     getEntityFromCache, | ||||
|   | ||||
| @@ -30,6 +30,23 @@ function addEntityChange(entityChange, sourceId, isSynced) { | ||||
|     cls.addEntityChange(localEntityChange); | ||||
| } | ||||
|  | ||||
| function addNoteReorderingEntityChange(parentNoteId, sourceId) { | ||||
|     addEntityChange({ | ||||
|         entityName: "note_reordering", | ||||
|         entityId: parentNoteId, | ||||
|         hash: 'N/A', | ||||
|         isErased: false, | ||||
|         utcDateChanged: dateUtils.utcNowDateTime() | ||||
|     }, sourceId); | ||||
|  | ||||
|     const eventService = require('./events'); | ||||
|  | ||||
|     eventService.emit(eventService.ENTITY_CHANGED, { | ||||
|         entityName: 'note_reordering', | ||||
|         entity: sql.getMap(`SELECT branchId, notePosition FROM branches WHERE isDeleted = 0 AND parentNoteId = ?`, [parentNoteId]) | ||||
|     }); | ||||
| } | ||||
|  | ||||
| function moveEntityChangeToTop(entityName, entityId) { | ||||
|     const [hash, isSynced] = sql.getRow(`SELECT * FROM entity_changes WHERE entityName = ? AND entityId = ?`, [entityName, entityId]); | ||||
|  | ||||
| @@ -121,13 +138,7 @@ function fillAllEntityChanges() { | ||||
| } | ||||
|  | ||||
| module.exports = { | ||||
|     addNoteReorderingEntityChange: (parentNoteId, sourceId) => addEntityChange({ | ||||
|         entityName: "note_reordering", | ||||
|         entityId: parentNoteId, | ||||
|         hash: 'N/A', | ||||
|         isErased: false, | ||||
|         utcDateChanged: dateUtils.utcNowDateTime() | ||||
|     }, sourceId), | ||||
|     addNoteReorderingEntityChange, | ||||
|     moveEntityChangeToTop, | ||||
|     addEntityChange, | ||||
|     fillAllEntityChanges, | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| "use strict"; | ||||
|  | ||||
| const TurndownService = require('turndown'); | ||||
| const turndownPluginGfm = require('turndown-plugin-gfm'); | ||||
| const turndownPluginGfm = require('joplin-turndown-plugin-gfm'); | ||||
|  | ||||
| let instance = null; | ||||
|  | ||||
| @@ -16,4 +16,4 @@ function toMarkdown(content) { | ||||
|  | ||||
| module.exports = { | ||||
|     toMarkdown | ||||
| }; | ||||
| }; | ||||
|   | ||||
| @@ -31,7 +31,7 @@ eventService.subscribe(eventService.NOTE_TITLE_CHANGED, note => { | ||||
|  | ||||
|         for (const parentNote of noteFromCache.parents) { | ||||
|             if (parentNote.hasLabel("sorted")) { | ||||
|                 treeService.sortNotesAlphabetically(parentNote.noteId); | ||||
|                 treeService.sortNotesByTitle(parentNote.noteId); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| @@ -53,23 +53,19 @@ eventService.subscribe(eventService.ENTITY_CREATED, ({ entityName, entity }) => | ||||
|         if (entity.type === 'relation' && entity.name === 'template') { | ||||
|             const note = repository.getNote(entity.noteId); | ||||
|  | ||||
|             if (!note.isStringNote()) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             const content = note.getContent(); | ||||
|  | ||||
|             if (content && content.trim().length > 0) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             const templateNote = repository.getNote(entity.value); | ||||
|  | ||||
|             if (!templateNote) { | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|             if (templateNote.isStringNote()) { | ||||
|             const content = note.getContent(); | ||||
|  | ||||
|             if (["text", "code"].includes(note.type) | ||||
|                 // if the note has already content we're not going to overwrite it with template's one | ||||
|                 && (!content || content.trim().length === 0) | ||||
|                 && templateNote.isStringNote()) { | ||||
|  | ||||
|                 const templateNoteContent = templateNote.getContent(); | ||||
|  | ||||
|                 if (templateNoteContent) { | ||||
| @@ -81,17 +77,21 @@ eventService.subscribe(eventService.ENTITY_CREATED, ({ entityName, entity }) => | ||||
|                 note.save(); | ||||
|             } | ||||
|  | ||||
|             noteService.duplicateSubtreeWithoutRoot(templateNote.noteId, note.noteId); | ||||
|             // we'll copy the children notes only if there's none so far | ||||
|             // this protects against e.g. multiple assignment of template relation resulting in having multiple copies of the subtree | ||||
|             if (note.getChildNotes().length === 0 && !note.isDescendantOfNote(templateNote.noteId)) { | ||||
|                 noteService.duplicateSubtreeWithoutRoot(templateNote.noteId, note.noteId); | ||||
|             } | ||||
|         } | ||||
|         else if (entity.type === 'label' && entity.name === 'sorted') { | ||||
|             treeService.sortNotesAlphabetically(entity.noteId); | ||||
|             treeService.sortNotesByTitle(entity.noteId); | ||||
|  | ||||
|             if (entity.isInheritable) { | ||||
|                 const note = noteCache.notes[entity.noteId]; | ||||
|  | ||||
|                 if (note) { | ||||
|                     for (const noteId of note.subtreeNoteIds) { | ||||
|                         treeService.sortNotesAlphabetically(noteId); | ||||
|                         treeService.sortNotesByTitle(noteId); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|   | ||||
| @@ -463,7 +463,7 @@ async function importZip(taskContext, fileBuffer, importRootNote) { | ||||
|         if (!metaFile) { | ||||
|             // if there's no meta file then the notes are created based on the order in that tar file but that | ||||
|             // is usually quite random so we sort the notes in the way they would appear in the file manager | ||||
|             treeService.sortNotesAlphabetically(noteId, true); | ||||
|             treeService.sortNotesByTitle(noteId, true); | ||||
|         } | ||||
|  | ||||
|         taskContext.increaseProgressCount(); | ||||
|   | ||||
| @@ -180,10 +180,14 @@ class Note { | ||||
|         return !!this.ownedAttributes.find(attr => attr.type === 'label' && attr.name === 'archived' && attr.isInheritable); | ||||
|     } | ||||
|  | ||||
|     // will sort the parents so that non-archived are first and archived at the end | ||||
|     // this is done so that non-archived paths are always explored as first when searching for note path | ||||
|     // will sort the parents so that non-search & non-archived are first and archived at the end | ||||
|     // this is done so that non-search & non-archived paths are always explored as first when looking for note path | ||||
|     resortParents() { | ||||
|         this.parents.sort((a, b) => a.hasInheritableOwnedArchivedLabel ? 1 : -1); | ||||
|         this.parentBranches.sort((a, b) => | ||||
|             a.branchId.startsWith('virt-') | ||||
|             || a.parentNote.hasInheritableOwnedArchivedLabel ? 1 : -1); | ||||
|  | ||||
|         this.parents = this.parentBranches.map(branch => branch.parentNote); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|   | ||||
| @@ -120,7 +120,11 @@ eventService.subscribe([eventService.ENTITY_CHANGED, eventService.ENTITY_DELETED | ||||
|             delete noteCache.attributes[attributeId]; | ||||
|  | ||||
|             if (attr) { | ||||
|                 delete noteCache.attributeIndex[`${attr.type}-${attr.name.toLowerCase()}`]; | ||||
|                 const key = `${attr.type}-${attr.name.toLowerCase()}`; | ||||
|  | ||||
|                 if (key in noteCache.attributeIndex) { | ||||
|                     noteCache.attributeIndex[key] = noteCache.attributeIndex[key].filter(attr => attr.attributeId !== attributeId); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         else if (attributeId in noteCache.attributes) { | ||||
| @@ -149,6 +153,19 @@ eventService.subscribe([eventService.ENTITY_CHANGED, eventService.ENTITY_DELETED | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     else if (entityName === 'note_reordering') { | ||||
|         const parentNoteIds = new Set(); | ||||
|  | ||||
|         for (const branchId in entity) { | ||||
|             const branch = noteCache.branches[branchId]; | ||||
|  | ||||
|             if (branch) { | ||||
|                 branch.notePosition = entity[branchId]; | ||||
|  | ||||
|                 parentNoteIds.add(branch.parentNoteId); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| }); | ||||
|  | ||||
| eventService.subscribe(eventService.ENTER_PROTECTED_SESSION, () => { | ||||
|   | ||||
| @@ -23,7 +23,6 @@ const IGNORED_ATTR_NAMES = [ | ||||
|     "archived", | ||||
|     "hidepromotedattributes", | ||||
|     "keyboardshortcut", | ||||
|     "bookzoomlevel", | ||||
|     "noteinfowidgetdisabled", | ||||
|     "linkmapwidgetdisabled", | ||||
|     "noterevisionswidgetdisabled", | ||||
| @@ -238,7 +237,15 @@ async function findSimilarNotes(noteId) { | ||||
|         return []; | ||||
|     } | ||||
|  | ||||
|     const dateLimits = buildDateLimits(baseNote); | ||||
|     let dateLimits; | ||||
|  | ||||
|     try { | ||||
|         dateLimits = buildDateLimits(baseNote); | ||||
|     } | ||||
|     catch (e) { | ||||
|         throw new Error(`Date limits failed with ${e.message}, entity: ${JSON.stringify(baseNote.pojo)}`); | ||||
|     } | ||||
|  | ||||
|     const rewardMap = buildRewardMap(baseNote); | ||||
|     let ancestorRewardCache = {}; | ||||
|     const ancestorNoteIds = new Set(baseNote.ancestors.map(note => note.noteId)); | ||||
|   | ||||
| @@ -84,7 +84,8 @@ const defaultOptions = [ | ||||
|     { name: 'attributeListExpanded', value: 'false', isSynced: false }, | ||||
|     { name: 'promotedAttributesExpanded', value: 'true', isSynced: true }, | ||||
|     { name: 'similarNotesExpanded', value: 'true', isSynced: true }, | ||||
|     { name: 'debugModeEnabled', value: 'false', isSynced: false } | ||||
|     { name: 'debugModeEnabled', value: 'false', isSynced: false }, | ||||
|     { name: 'headingStyle', value: 'markdown', isSynced: true }, | ||||
| ]; | ||||
|  | ||||
| function initStartupOptions() { | ||||
|   | ||||
| @@ -84,7 +84,15 @@ function exec(opts) { | ||||
|                 }); | ||||
|             }); | ||||
|  | ||||
|             request.end(opts.body); | ||||
|             let payload; | ||||
|  | ||||
|             if (opts.body) { | ||||
|                 payload = typeof opts.body === 'object' | ||||
|                     ? JSON.stringify(opts.body) | ||||
|                     : opts.body; | ||||
|             } | ||||
|  | ||||
|             request.end(payload); | ||||
|         } | ||||
|         catch (e) { | ||||
|             reject(generateError(opts, e.message)); | ||||
|   | ||||
| @@ -19,6 +19,8 @@ class SearchResult { | ||||
|     computeScore(tokens) { | ||||
|         this.score = 0; | ||||
|  | ||||
|         // matches in attributes don't get extra points and thus are implicitly valued less than note path matches | ||||
|  | ||||
|         const chunks = this.notePathTitle.toLowerCase().split(" "); | ||||
|  | ||||
|         for (const chunk of chunks) { | ||||
|   | ||||
| @@ -3,6 +3,7 @@ | ||||
| const log = require('./log'); | ||||
| const Database = require('better-sqlite3'); | ||||
| const dataDir = require('./data_dir'); | ||||
| const cls = require('./cls'); | ||||
|  | ||||
| const dbConnection = new Database(dataDir.DOCUMENT_PATH); | ||||
| dbConnection.pragma('journal_mode = WAL'); | ||||
| @@ -31,7 +32,7 @@ function insert(tableName, rec, replace = false) { | ||||
|  | ||||
|     const res = execute(query, Object.values(rec)); | ||||
|  | ||||
|     return res.lastInsertRowid; | ||||
|     return res ? res.lastInsertRowid : null; | ||||
| } | ||||
|  | ||||
| function replace(tableName, rec) { | ||||
| @@ -229,13 +230,20 @@ function wrap(query, func) { | ||||
| } | ||||
|  | ||||
| function transactional(func) { | ||||
|     const ret = dbConnection.transaction(func).deferred(); | ||||
|     try { | ||||
|         const ret = dbConnection.transaction(func).deferred(); | ||||
|  | ||||
|     if (!dbConnection.inTransaction) { // i.e. transaction was really committed (and not just savepoint released) | ||||
|         require('./ws.js').sendTransactionSyncsToAllClients(); | ||||
|         if (!dbConnection.inTransaction) { // i.e. transaction was really committed (and not just savepoint released) | ||||
|             require('./ws.js').sendTransactionSyncsToAllClients(); | ||||
|         } | ||||
|  | ||||
|         return ret; | ||||
|     } | ||||
|     catch (e) { | ||||
|         cls.clearEntityChanges(); | ||||
|  | ||||
|     return ret; | ||||
|         throw e; | ||||
|     } | ||||
| } | ||||
|  | ||||
| function fillNoteIdList(noteIds, truncate = true) { | ||||
|   | ||||
| @@ -101,7 +101,7 @@ async function doLogin() { | ||||
|     }); | ||||
|  | ||||
|     if (sourceIdService.isLocalSourceId(resp.sourceId)) { | ||||
|         throw new Error(`Sync server has source ID ${resp.sourceId} which is also local. Your sync setup is probably trying to connect to itself.`); | ||||
|         throw new Error(`Sync server has source ID ${resp.sourceId} which is also local. This usually happens when the sync client is (mis)configured to sync with itself (URL points back to client) instead of the correct sync server.`); | ||||
|     } | ||||
|  | ||||
|     syncContext.sourceId = resp.sourceId; | ||||
|   | ||||
| @@ -5,6 +5,7 @@ const repository = require('./repository'); | ||||
| const Branch = require('../entities/branch'); | ||||
| const entityChangesService = require('./entity_changes.js'); | ||||
| const protectedSessionService = require('./protected_session'); | ||||
| const noteCache = require('./note_cache/note_cache'); | ||||
|  | ||||
| function getNotes(noteIds) { | ||||
|     // we return also deleted notes which have been specifically asked for | ||||
| @@ -105,7 +106,7 @@ function loadSubtreeNoteIds(parentNoteId, subtreeNoteIds) { | ||||
|     } | ||||
| } | ||||
|  | ||||
| function sortNotesAlphabetically(parentNoteId, directoriesFirst = false) { | ||||
| function sortNotesByTitle(parentNoteId, foldersFirst = false, reverse = false) { | ||||
|     sql.transactional(() => { | ||||
|         const notes = sql.getRows( | ||||
|             `SELECT branches.branchId, notes.noteId, title, isProtected,  | ||||
| @@ -119,7 +120,7 @@ function sortNotesAlphabetically(parentNoteId, directoriesFirst = false) { | ||||
|         protectedSessionService.decryptNotes(notes); | ||||
|  | ||||
|         notes.sort((a, b) => { | ||||
|             if (directoriesFirst && ((a.hasChildren && !b.hasChildren) || (!a.hasChildren && b.hasChildren))) { | ||||
|             if (foldersFirst && ((a.hasChildren && !b.hasChildren) || (!a.hasChildren && b.hasChildren))) { | ||||
|                 // exactly one note of the two is a directory so the sorting will be done based on this status | ||||
|                 return a.hasChildren ? -1 : 1; | ||||
|             } | ||||
| @@ -128,12 +129,45 @@ function sortNotesAlphabetically(parentNoteId, directoriesFirst = false) { | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         if (reverse) { | ||||
|             notes.reverse(); | ||||
|         } | ||||
|  | ||||
|         let position = 10; | ||||
|  | ||||
|         for (const note of notes) { | ||||
|             sql.execute("UPDATE branches SET notePosition = ? WHERE branchId = ?", | ||||
|                 [position, note.branchId]); | ||||
|  | ||||
|             noteCache.branches[note.branchId].notePosition = position; | ||||
|  | ||||
|             position += 10; | ||||
|         } | ||||
|  | ||||
|         entityChangesService.addNoteReorderingEntityChange(parentNoteId); | ||||
|     }); | ||||
| } | ||||
|  | ||||
| function sortNotes(parentNoteId, sortBy, reverse = false) { | ||||
|     sql.transactional(() => { | ||||
|         const notes = repository.getNote(parentNoteId).getChildNotes(); | ||||
|  | ||||
|         notes.sort((a, b) => a[sortBy] < b[sortBy] ? -1 : 1); | ||||
|  | ||||
|         if (reverse) { | ||||
|             notes.reverse(); | ||||
|         } | ||||
|  | ||||
|         let position = 10; | ||||
|  | ||||
|         for (const note of notes) { | ||||
|             const branch = note.getBranches().find(b => b.parentNoteId === parentNoteId); | ||||
|  | ||||
|             sql.execute("UPDATE branches SET notePosition = ? WHERE branchId = ?", | ||||
|                 [position, branch.branchId]); | ||||
|  | ||||
|             noteCache.branches[branch.branchId].notePosition = position; | ||||
|  | ||||
|             position += 10; | ||||
|         } | ||||
|  | ||||
| @@ -191,6 +225,7 @@ function setNoteToParent(noteId, prefix, parentNoteId) { | ||||
| module.exports = { | ||||
|     getNotes, | ||||
|     validateParentChild, | ||||
|     sortNotesAlphabetically, | ||||
|     sortNotesByTitle, | ||||
|     sortNotes, | ||||
|     setNoteToParent | ||||
| }; | ||||
|   | ||||
| @@ -246,9 +246,13 @@ function getNoteTitle(filePath, replaceUnderscoresWithSpaces, noteMeta) { | ||||
|     } | ||||
| } | ||||
|  | ||||
| function timeLimit(promise, limitMs) { | ||||
| function timeLimit(promise, limitMs, errorMessage) { | ||||
|     if (!promise || !promise.then) { // it's not actually a promise | ||||
|         return promise; | ||||
|     } | ||||
|  | ||||
|     // better stack trace if created outside of promise | ||||
|     const error = new Error('Process exceeded time limit ' + limitMs); | ||||
|     const error = new Error(errorMessage || `Process exceeded time limit ${limitMs}`); | ||||
|  | ||||
|     return new Promise((res, rej) => { | ||||
|         let resolved = false; | ||||
|   | ||||
| @@ -106,8 +106,6 @@ function sendPing(client, entityChanges = []) { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     const stats = require('./sync').stats; | ||||
|  | ||||
|     sendMessage(client, { | ||||
|         type: 'sync', | ||||
|         data: entityChanges | ||||
| @@ -118,9 +116,7 @@ function sendTransactionSyncsToAllClients() { | ||||
|     if (webSocketServer) { | ||||
|         const entityChanges = cls.getAndClearEntityChanges(); | ||||
|  | ||||
|         webSocketServer.clients.forEach(function each(client) { | ||||
|            sendPing(client, entityChanges); | ||||
|         }); | ||||
|         webSocketServer.clients.forEach(client => sendPing(client, entityChanges)); | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -5,7 +5,7 @@ | ||||
|     <link rel="shortcut icon" href="favicon.ico"> | ||||
|     <title>Trilium Notes</title> | ||||
| </head> | ||||
| <body class="desktop theme-<%= theme %>" style="--main-font-size: <%= mainFontSize %>%; --tree-font-size: <%= treeFontSize %>%; --detail-font-size: <%= detailFontSize %>%;"> | ||||
| <body class="desktop theme-<%= theme %> heading-style-<%= headingStyle %>" style="--main-font-size: <%= mainFontSize %>%; --tree-font-size: <%= treeFontSize %>%; --detail-font-size: <%= detailFontSize %>%;"> | ||||
| <noscript>Trilium requires JavaScript to be enabled.</noscript> | ||||
|  | ||||
| <script> | ||||
| @@ -39,6 +39,7 @@ | ||||
| <%- include('dialogs/move_to.ejs') %> | ||||
| <%- include('dialogs/backend_log.ejs') %> | ||||
| <%- include('dialogs/include_note.ejs') %> | ||||
| <%- include('dialogs/sort_child_notes.ejs') %> | ||||
|  | ||||
| <script type="text/javascript"> | ||||
|     window.baseApiUrl = 'api/'; | ||||
|   | ||||
| @@ -106,7 +106,7 @@ | ||||
|  | ||||
|                             <p class="card-text"> | ||||
|                                 <ul> | ||||
|                                     <li><kbd>#</kbd>, <kbd>##</kbd>, <kbd>###</kbd> etc. followed by space for headings</li> | ||||
|                                     <li><kbd>##</kbd>, <kbd>###</kbd>, <kbd>####</kbd> etc. followed by space for headings</li> | ||||
|                                     <li><kbd>*</kbd> or <kbd>-</kbd> followed by space for bullet list</li> | ||||
|                                     <li><kbd>1.</kbd> or <kbd>1)</kbd> followed by space for numbered list</li> | ||||
|                                     <li>start a line with <kbd>></kbd> followed by space for block quote</li> | ||||
|   | ||||
							
								
								
									
										60
									
								
								src/views/dialogs/sort_child_notes.ejs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								src/views/dialogs/sort_child_notes.ejs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,60 @@ | ||||
| <div id="sort-child-notes-dialog" class="modal mx-auto" tabindex="-1" role="dialog"> | ||||
|     <div class="modal-dialog modal-lg" style="max-width: 500px" role="document"> | ||||
|         <div class="modal-content"> | ||||
|             <div class="modal-header"> | ||||
|                 <h5 class="modal-title mr-auto">Sort children by ...</h5> | ||||
|  | ||||
|                 <button type="button" class="close" data-dismiss="modal" aria-label="Close" style="margin-left: 0 !important;"> | ||||
|                     <span aria-hidden="true">×</span> | ||||
|                 </button> | ||||
|             </div> | ||||
|             <form id="sort-child-notes-form"> | ||||
|                 <div class="modal-body"> | ||||
|                     <h5>Sorting criteria</h5> | ||||
|  | ||||
|                     <div class="form-check"> | ||||
|                         <input class="form-check-input" type="radio" name="sort-by" value="title" id="sort-by-title" checked> | ||||
|                         <label class="form-check-label" for="sort-by-title"> | ||||
|                             title | ||||
|                         </label> | ||||
|                     </div> | ||||
|  | ||||
|                     <div class="form-check"> | ||||
|                         <input class="form-check-input" type="radio" name="sort-by" value="dateCreated" id="sort-by-date-created"> | ||||
|                         <label class="form-check-label" for="sort-by-date-created"> | ||||
|                             date created | ||||
|                         </label> | ||||
|                     </div> | ||||
|  | ||||
|                     <div class="form-check"> | ||||
|                         <input class="form-check-input" type="radio" name="sort-by" value="dateModified" id="sort-by-date-modified"> | ||||
|                         <label class="form-check-label" for="sort-by-date-modified"> | ||||
|                             date modified | ||||
|                         </label> | ||||
|                     </div> | ||||
|  | ||||
|                     <br/> | ||||
|  | ||||
|                     <h5>Sorting direction</h5> | ||||
|  | ||||
|                     <div class="form-check"> | ||||
|                         <input class="form-check-input" type="radio" name="sort-direction" value="asc" id="sort-direction-asc" checked> | ||||
|                         <label class="form-check-label" for="sort-direction-asc"> | ||||
|                             ascending | ||||
|                         </label> | ||||
|                     </div> | ||||
|  | ||||
|                     <div class="form-check"> | ||||
|                         <input class="form-check-input" type="radio" name="sort-direction" value="desc" id="sort-direction-desc"> | ||||
|                         <label class="form-check-label" for="sort-direction-desc"> | ||||
|                             descending | ||||
|                         </label> | ||||
|                     </div> | ||||
|                 </div> | ||||
|                 <div class="modal-footer"> | ||||
|                     <button type="submit" class="btn btn-primary">Sort <kbd>enter</kbd></button> | ||||
|                 </div> | ||||
|             </form> | ||||
|         </div> | ||||
|     </div> | ||||
| </div> | ||||
| @@ -95,7 +95,7 @@ | ||||
|         } | ||||
|     </style> | ||||
| </head> | ||||
| <body class="mobile theme-<%= theme %>"> | ||||
| <body class="mobile theme-<%= theme %> heading-style-<%= headingStyle %>"> | ||||
| <noscript>Trilium requires JavaScript to be enabled.</noscript> | ||||
|  | ||||
| <div id="toast-container" class="d-flex flex-column justify-content-center align-items-center"></div> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user