mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 10:26:08 +01:00 
			
		
		
		
	Compare commits
	
		
			60 Commits
		
	
	
		
			v0.46.1-be
			...
			v0.46.4-be
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 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. | ||||
|   | ||||
							
								
								
									
										945
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										945
									
								
								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.4-beta", | ||||
|   "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); | ||||
|   | ||||
| @@ -29,7 +29,17 @@ const TPL = ` | ||||
|         </div> | ||||
|     </div> | ||||
|      | ||||
|     <p>Zooming can be controlled with CTRL-+ and CTRL-= shortcuts as well.</p> | ||||
|     <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> | ||||
|  | ||||
|     <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); | ||||
|   | ||||
							
								
								
									
										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() { | ||||
|   | ||||
| @@ -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(); | ||||
|  | ||||
|   | ||||
| @@ -182,8 +182,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,9 +15,25 @@ export default class SpacedUpdate { | ||||
|  | ||||
|     async updateNowIfNecessary() { | ||||
|         if (this.changed) { | ||||
|             this.changed = false; | ||||
|             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() { | ||||
|   | ||||
| @@ -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; | ||||
|  | ||||
|     while (cur.noteId !== 'root') { | ||||
|         path.push(cur.noteId); | ||||
|  | ||||
|         const parents = cur.getParentNotes().filter(note => note.type !== 'search'); | ||||
|  | ||||
|         if (!parents.length) { | ||||
|             logError(`Can't find parents for note ${cur.noteId}`); | ||||
|             return; | ||||
|     return notePaths[0].notePath; | ||||
| } | ||||
|  | ||||
|         cur = parents[0]; | ||||
|     } | ||||
| function getSomeNotePath(note, hoistedNotePath = 'root') { | ||||
|     const notePath = getSomeNotePathSegments(note, hoistedNotePath); | ||||
|  | ||||
|     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(); | ||||
|   | ||||
| @@ -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.$editedNotes.empty().append($list); | ||||
|             this.$list.append($item); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -91,7 +91,13 @@ export default class Component { | ||||
|             console.log(`Call to ${fun.name} in ${this.componentId} took ${took}ms`); | ||||
|         } | ||||
|  | ||||
|         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', 'bookZoomLevel', '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,22 +1114,31 @@ 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) { | ||||
|                 await newActiveNode.setFocus(true); | ||||
|                         if (newActiveNode && activeNodeFocused) {console.log("FOCUSING!!!"); | ||||
|                             newActiveNode.setFocus(true); | ||||
|                         } | ||||
|                     }); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
| @@ -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', () => { | ||||
|             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")); | ||||
|             utils.bindElShortcut($link, 'return', () => { | ||||
|                 $link.find('a').trigger({ | ||||
|                     type: 'click', | ||||
|                     which: 1 // left click | ||||
|             $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', () => { | ||||
|                 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.")) | ||||
|   | ||||
| @@ -5,6 +5,8 @@ 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') | ||||
| @@ -59,21 +61,54 @@ function createSqlConsole() { | ||||
|     return note; | ||||
| } | ||||
|  | ||||
| function createSearchNote() { | ||||
| function createSearchNote(req) { | ||||
|     const params = req.body; | ||||
|     const searchString = params.searchString || ""; | ||||
|     let ancestorNoteId = params.ancestorNoteId; | ||||
|  | ||||
|     const hoistedNote = cls.getHoistedNoteId() && cls.getHoistedNoteId() !== 'root' | ||||
|         ? repository.getNote(cls.getHoistedNoteId()) | ||||
|         : null; | ||||
|  | ||||
|     let searchHome; | ||||
|  | ||||
|     if (hoistedNote) { | ||||
|         ([searchHome] = hoistedNote.getDescendantNotesWithLabel('hoistedSearchHome')); | ||||
|     } | ||||
|  | ||||
|     if (!searchHome) { | ||||
|         const today = dateUtils.localNowDate(); | ||||
|  | ||||
|     const searchHome = | ||||
|         attributeService.getNoteWithLabel('searchHome') | ||||
|         searchHome = attributeService.getNoteWithLabel('searchHome') | ||||
|                   || dateNoteService.getDateNote(today); | ||||
|     } | ||||
|  | ||||
|     if (hoistedNote) { | ||||
|  | ||||
|         if (!hoistedNote.getDescendantNoteIds().includes(searchHome.noteId)) { | ||||
|             // otherwise the note would be saved outside of the hoisted context which is weird | ||||
|             searchHome = hoistedNote; | ||||
|         } | ||||
|  | ||||
|         if (!ancestorNoteId) { | ||||
|             ancestorNoteId = hoistedNote.noteId; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     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; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -38,6 +38,7 @@ const BUILTIN_ATTRIBUTES = [ | ||||
|     { type: 'label', name: 'workspaceIconClass' }, | ||||
|     { type: 'label', name: 'workspaceTabBackgroundColor' }, | ||||
|     { type: 'label', name: 'searchHome' }, | ||||
|     { type: 'label', name: 'hoistedSearchHome' }, | ||||
|     { type: 'label', name: 'sqlConsoleHome' }, | ||||
|     { type: 'label', name: 'datePattern' }, | ||||
|  | ||||
|   | ||||
| @@ -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-10T23:35:12+01:00", buildRevision: "6f901e6852c33ba0dae6c70efb9f65e5b0028995" }; | ||||
|   | ||||
| @@ -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; | ||||
|  | ||||
|   | ||||
| @@ -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(); | ||||
|             } | ||||
|  | ||||
|             // 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, () => { | ||||
|   | ||||
| @@ -238,7 +238,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() { | ||||
|   | ||||
| @@ -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) { | ||||
|   | ||||
| @@ -31,7 +31,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) { | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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