mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 02:16:05 +01:00 
			
		
		
		
	Compare commits
	
		
			25 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 361d8a4216 | ||
|  | ae6e222c50 | ||
|  | 37995f1ce5 | ||
|  | ad7fa5e096 | ||
|  | 3585982758 | ||
|  | c776f298f2 | ||
|  | f07c427da1 | ||
|  | e560072f8b | ||
|  | 3f976a3821 | ||
|  | 274bb32696 | ||
|  | 99b163a042 | ||
|  | fdcc833f6d | ||
|  | a8e45019e4 | ||
|  | 7f7028873c | ||
|  | 2d2d76a715 | ||
|  | 69cbfaae17 | ||
|  | aebce8f12b | ||
|  | 045ca1f0bf | ||
|  | bf2db6eac7 | ||
|  | cf84114f91 | ||
|  | 6426157bb3 | ||
|  | 332fc16852 | ||
|  | da2cd57428 | ||
|  | de9bab1181 | ||
|  | 136375cf11 | 
| @@ -1,7 +1,7 @@ | |||||||
| { | { | ||||||
|   "name": "trilium", |   "name": "trilium", | ||||||
|   "description": "Trilium Notes", |   "description": "Trilium Notes", | ||||||
|   "version": "0.1.1", |   "version": "0.2.0", | ||||||
|   "scripts": { |   "scripts": { | ||||||
|     "start": "node ./bin/www", |     "start": "node ./bin/www", | ||||||
|     "test-electron": "xo", |     "test-electron": "xo", | ||||||
|   | |||||||
| @@ -3,54 +3,72 @@ | |||||||
| const contextMenu = (function() { | const contextMenu = (function() { | ||||||
|     const treeEl = $("#tree"); |     const treeEl = $("#tree"); | ||||||
|  |  | ||||||
|     let clipboardId = null; |     let clipboardIds = []; | ||||||
|     let clipboardMode = null; |     let clipboardMode = null; | ||||||
|  |  | ||||||
|     function pasteAfter(node) { |     function pasteAfter(node) { | ||||||
|         if (clipboardMode === 'cut') { |         if (clipboardMode === 'cut') { | ||||||
|             const subjectNode = treeUtils.getNodeByKey(clipboardId); |             for (const nodeKey of clipboardIds) { | ||||||
|  |                 const subjectNode = treeUtils.getNodeByKey(nodeKey); | ||||||
|  |  | ||||||
|             treeChanges.moveAfterNode(subjectNode, node); |                 treeChanges.moveAfterNode([subjectNode], node); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             clipboardIds = []; | ||||||
|  |             clipboardMode = null; | ||||||
|         } |         } | ||||||
|         else if (clipboardMode === 'copy') { |         else if (clipboardMode === 'copy') { | ||||||
|             treeChanges.cloneNoteAfter(clipboardId, node.data.note_tree_id); |             for (const noteId of clipboardIds) { | ||||||
|  |                 treeChanges.cloneNoteAfter(noteId, node.data.note_tree_id); | ||||||
|             } |             } | ||||||
|         else if (clipboardId === null) { |  | ||||||
|  |             // copy will keep clipboardIds and clipboardMode so it's possible to paste into multiple places | ||||||
|  |         } | ||||||
|  |         else if (clipboardIds.length === 0) { | ||||||
|             // just do nothing |             // just do nothing | ||||||
|         } |         } | ||||||
|         else { |         else { | ||||||
|             throwError("Unrecognized clipboard mode=" + clipboardMode); |             throwError("Unrecognized clipboard mode=" + clipboardMode); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         clipboardId = null; |  | ||||||
|         clipboardMode = null; |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     function pasteInto(node) { |     function pasteInto(node) { | ||||||
|         if (clipboardMode === 'cut') { |         if (clipboardMode === 'cut') { | ||||||
|             const subjectNode = treeUtils.getNodeByKey(clipboardId); |             for (const nodeKey of clipboardIds) { | ||||||
|  |                 const subjectNode = treeUtils.getNodeByKey(nodeKey); | ||||||
|  |  | ||||||
|             treeChanges.moveToNode(subjectNode, node); |                 treeChanges.moveToNode([subjectNode], node); | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             clipboardIds = []; | ||||||
|  |             clipboardMode = null; | ||||||
|         } |         } | ||||||
|         else if (clipboardMode === 'copy') { |         else if (clipboardMode === 'copy') { | ||||||
|             treeChanges.cloneNoteTo(clipboardId, node.data.note_id); |             for (const noteId of clipboardIds) { | ||||||
|  |                 treeChanges.cloneNoteTo(noteId, node.data.note_id); | ||||||
|  |             } | ||||||
|  |             // copy will keep clipboardIds and clipboardMode so it's possible to paste into multiple places | ||||||
|  |         } | ||||||
|  |         else if (clipboardIds.length === 0) { | ||||||
|  |             // just do nothing | ||||||
|         } |         } | ||||||
|         else { |         else { | ||||||
|             throwError("Unrecognized clipboard mode=" + mode); |             throwError("Unrecognized clipboard mode=" + mode); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         clipboardId = null; |  | ||||||
|         clipboardMode = null; |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     function copy(node) { |     function copy(nodes) { | ||||||
|         clipboardId = node.data.note_id; |         clipboardIds = nodes.map(node => node.data.note_id); | ||||||
|         clipboardMode = 'copy'; |         clipboardMode = 'copy'; | ||||||
|  |  | ||||||
|  |         showMessage("Note(s) have been copied into clipboard."); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     function cut(node) { |     function cut(nodes) { | ||||||
|         clipboardId = node.key; |         clipboardIds = nodes.map(node => node.key); | ||||||
|         clipboardMode = 'cut'; |         clipboardMode = 'cut'; | ||||||
|  |  | ||||||
|  |         showMessage("Note(s) have been cut into clipboard."); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const contextMenuSettings = { |     const contextMenuSettings = { | ||||||
| @@ -71,13 +89,14 @@ const contextMenu = (function() { | |||||||
|             {title: "Paste into <kbd>Ctrl+V</kbd>", cmd: "pasteInto", uiIcon: "ui-icon-clipboard"}, |             {title: "Paste into <kbd>Ctrl+V</kbd>", cmd: "pasteInto", uiIcon: "ui-icon-clipboard"}, | ||||||
|             {title: "Paste after", cmd: "pasteAfter", uiIcon: "ui-icon-clipboard"}, |             {title: "Paste after", cmd: "pasteAfter", uiIcon: "ui-icon-clipboard"}, | ||||||
|             {title: "----"}, |             {title: "----"}, | ||||||
|             {title: "Collapse sub-tree <kbd>Alt+-</kbd>", cmd: "collapse-sub-tree", uiIcon: "ui-icon-minus"} |             {title: "Collapse sub-tree <kbd>Alt+-</kbd>", cmd: "collapse-sub-tree", uiIcon: "ui-icon-minus"}, | ||||||
|  |             {title: "Force note sync", cmd: "force-note-sync", uiIcon: "ui-icon-refresh"} | ||||||
|         ], |         ], | ||||||
|         beforeOpen: (event, ui) => { |         beforeOpen: (event, ui) => { | ||||||
|             const node = $.ui.fancytree.getNode(ui.target); |             const node = $.ui.fancytree.getNode(ui.target); | ||||||
|             // Modify menu entries depending on node status |             // Modify menu entries depending on node status | ||||||
|             treeEl.contextmenu("enableEntry", "pasteAfter", clipboardId !== null); |             treeEl.contextmenu("enableEntry", "pasteAfter", clipboardIds.length > 0); | ||||||
|             treeEl.contextmenu("enableEntry", "pasteInto", clipboardId !== null); |             treeEl.contextmenu("enableEntry", "pasteInto", clipboardIds.length > 0); | ||||||
|  |  | ||||||
|             // Activate node on right-click |             // Activate node on right-click | ||||||
|             node.setActive(); |             node.setActive(); | ||||||
| @@ -108,10 +127,10 @@ const contextMenu = (function() { | |||||||
|                 protected_session.protectSubTree(node.data.note_id, false); |                 protected_session.protectSubTree(node.data.note_id, false); | ||||||
|             } |             } | ||||||
|             else if (ui.cmd === "copy") { |             else if (ui.cmd === "copy") { | ||||||
|                 copy(node); |                 copy(noteTree.getSelectedNodes()); | ||||||
|             } |             } | ||||||
|             else if (ui.cmd === "cut") { |             else if (ui.cmd === "cut") { | ||||||
|                 cut(node); |                 cut(noteTree.getSelectedNodes()); | ||||||
|             } |             } | ||||||
|             else if (ui.cmd === "pasteAfter") { |             else if (ui.cmd === "pasteAfter") { | ||||||
|                 pasteAfter(node); |                 pasteAfter(node); | ||||||
| @@ -125,6 +144,9 @@ const contextMenu = (function() { | |||||||
|             else if (ui.cmd === "collapse-sub-tree") { |             else if (ui.cmd === "collapse-sub-tree") { | ||||||
|                 noteTree.collapseTree(node); |                 noteTree.collapseTree(node); | ||||||
|             } |             } | ||||||
|  |             else if (ui.cmd === "force-note-sync") { | ||||||
|  |                 forceNoteSync(node.data.note_id); | ||||||
|  |             } | ||||||
|             else { |             else { | ||||||
|                 messaging.logError("Unknown command: " + ui.cmd); |                 messaging.logError("Unknown command: " + ui.cmd); | ||||||
|             } |             } | ||||||
|   | |||||||
| @@ -46,14 +46,19 @@ const dragAndDropSetup = { | |||||||
|         // This function MUST be defined to enable dropping of items on the tree. |         // This function MUST be defined to enable dropping of items on the tree. | ||||||
|         // data.hitMode is 'before', 'after', or 'over'. |         // data.hitMode is 'before', 'after', or 'over'. | ||||||
|  |  | ||||||
|  |         const nodeToMove = data.otherNode; | ||||||
|  |         nodeToMove.setSelected(true); | ||||||
|  |  | ||||||
|  |         const selectedNodes = noteTree.getSelectedNodes(); | ||||||
|  |  | ||||||
|         if (data.hitMode === "before") { |         if (data.hitMode === "before") { | ||||||
|             treeChanges.moveBeforeNode(data.otherNode, node); |             treeChanges.moveBeforeNode(selectedNodes, node); | ||||||
|         } |         } | ||||||
|         else if (data.hitMode === "after") { |         else if (data.hitMode === "after") { | ||||||
|             treeChanges.moveAfterNode(data.otherNode, node); |             treeChanges.moveAfterNode(selectedNodes, node); | ||||||
|         } |         } | ||||||
|         else if (data.hitMode === "over") { |         else if (data.hitMode === "over") { | ||||||
|             treeChanges.moveToNode(data.otherNode, node); |             treeChanges.moveToNode(selectedNodes, node); | ||||||
|         } |         } | ||||||
|         else { |         else { | ||||||
|             throw new Exception("Unknown hitMode=" + data.hitMode); |             throw new Exception("Unknown hitMode=" + data.hitMode); | ||||||
|   | |||||||
| @@ -57,6 +57,42 @@ $(document).bind('keydown', 'ctrl+f', () => { | |||||||
|     } |     } | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | $(document).bind('keydown', "ctrl+shift+left", () => { | ||||||
|  |     const node = noteTree.getCurrentNode(); | ||||||
|  |     node.navigate($.ui.keyCode.LEFT, true); | ||||||
|  |  | ||||||
|  |     $("#note-detail").focus(); | ||||||
|  |  | ||||||
|  |     return false; | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | $(document).bind('keydown', "ctrl+shift+right", () => { | ||||||
|  |     const node = noteTree.getCurrentNode(); | ||||||
|  |     node.navigate($.ui.keyCode.RIGHT, true); | ||||||
|  |  | ||||||
|  |     $("#note-detail").focus(); | ||||||
|  |  | ||||||
|  |     return false; | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | $(document).bind('keydown', "ctrl+shift+up", () => { | ||||||
|  |     const node = noteTree.getCurrentNode(); | ||||||
|  |     node.navigate($.ui.keyCode.UP, true); | ||||||
|  |  | ||||||
|  |     $("#note-detail").focus(); | ||||||
|  |  | ||||||
|  |     return false; | ||||||
|  | }); | ||||||
|  |  | ||||||
|  | $(document).bind('keydown', "ctrl+shift+down", () => { | ||||||
|  |     const node = noteTree.getCurrentNode(); | ||||||
|  |     node.navigate($.ui.keyCode.DOWN, true); | ||||||
|  |  | ||||||
|  |     $("#note-detail").focus(); | ||||||
|  |  | ||||||
|  |     return false; | ||||||
|  | }); | ||||||
|  |  | ||||||
| $(window).on('beforeunload', () => { | $(window).on('beforeunload', () => { | ||||||
|     // this makes sure that when user e.g. reloads the page or navigates away from the page, the note's content is saved |     // this makes sure that when user e.g. reloads the page or navigates away from the page, the note's content is saved | ||||||
|     // this sends the request asynchronously and doesn't wait for result |     // this sends the request asynchronously and doesn't wait for result | ||||||
|   | |||||||
| @@ -128,6 +128,9 @@ const noteEditor = (function() { | |||||||
|         setNoteBackgroundIfProtected(currentNote); |         setNoteBackgroundIfProtected(currentNote); | ||||||
|         noteTree.setNoteTreeBackgroundBasedOnProtectedStatus(noteId); |         noteTree.setNoteTreeBackgroundBasedOnProtectedStatus(noteId); | ||||||
|  |  | ||||||
|  |         // after loading new note make sure editor is scrolled to the top | ||||||
|  |         noteDetailWrapperEl.scrollTop(0); | ||||||
|  |  | ||||||
|         showAppIfHidden(); |         showAppIfHidden(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -4,6 +4,7 @@ const noteTree = (function() { | |||||||
|     const treeEl = $("#tree"); |     const treeEl = $("#tree"); | ||||||
|     const parentListEl = $("#parent-list"); |     const parentListEl = $("#parent-list"); | ||||||
|     const parentListListEl = $("#parent-list-list"); |     const parentListListEl = $("#parent-list-list"); | ||||||
|  |     const noteDetailEl = $("#note-detail"); | ||||||
|  |  | ||||||
|     let startNotePath = null; |     let startNotePath = null; | ||||||
|     let notesTreeMap = {}; |     let notesTreeMap = {}; | ||||||
| @@ -59,23 +60,6 @@ const noteTree = (function() { | |||||||
|         return treeUtils.getNotePath(node); |         return treeUtils.getNotePath(node); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     function getCurrentNoteId() { |  | ||||||
|         const node = getCurrentNode(); |  | ||||||
|  |  | ||||||
|         return node ? node.data.note_id : null; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     function getCurrentClones() { |  | ||||||
|         const noteId = getCurrentNoteId(); |  | ||||||
|  |  | ||||||
|         if (noteId) { |  | ||||||
|             return getNodesByNoteId(noteId); |  | ||||||
|         } |  | ||||||
|         else { |  | ||||||
|             return []; |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     function getNodesByNoteTreeId(noteTreeId) { |     function getNodesByNoteTreeId(noteTreeId) { | ||||||
|         assertArguments(noteTreeId); |         assertArguments(noteTreeId); | ||||||
|  |  | ||||||
| @@ -185,13 +169,15 @@ const noteTree = (function() { | |||||||
|             const noteTreeId = getNoteTreeId(parentNoteId, noteId); |             const noteTreeId = getNoteTreeId(parentNoteId, noteId); | ||||||
|             const noteTree = notesTreeMap[noteTreeId]; |             const noteTree = notesTreeMap[noteTreeId]; | ||||||
|  |  | ||||||
|  |             const title = (noteTree.prefix ? (noteTree.prefix + " - ") : "") + noteIdToTitle[noteTree.note_id]; | ||||||
|  |  | ||||||
|             const node = { |             const node = { | ||||||
|                 note_id: noteTree.note_id, |                 note_id: noteTree.note_id, | ||||||
|                 parent_note_id: noteTree.parent_note_id, |                 parent_note_id: noteTree.parent_note_id, | ||||||
|                 note_tree_id: noteTree.note_tree_id, |                 note_tree_id: noteTree.note_tree_id, | ||||||
|                 is_protected: noteTree.is_protected, |                 is_protected: noteTree.is_protected, | ||||||
|                 prefix: noteTree.prefix, |                 prefix: noteTree.prefix, | ||||||
|                 title: (noteTree.prefix ? (noteTree.prefix + " - ") : "") + noteIdToTitle[noteTree.note_id], |                 title: escapeHtml(title), | ||||||
|                 extraClasses: getExtraClasses(noteTree), |                 extraClasses: getExtraClasses(noteTree), | ||||||
|                 refKey: noteTree.note_id, |                 refKey: noteTree.note_id, | ||||||
|                 expanded: noteTree.is_expanded |                 expanded: noteTree.is_expanded | ||||||
| @@ -223,8 +209,6 @@ const noteTree = (function() { | |||||||
|  |  | ||||||
|         let parentNoteId = 'root'; |         let parentNoteId = 'root'; | ||||||
|  |  | ||||||
|         //console.log(now(), "Run path: ", runPath); |  | ||||||
|  |  | ||||||
|         for (const childNoteId of runPath) { |         for (const childNoteId of runPath) { | ||||||
|             const node = getNodesByNoteId(childNoteId).find(node => node.data.parent_note_id === parentNoteId); |             const node = getNodesByNoteId(childNoteId).find(node => node.data.parent_note_id === parentNoteId); | ||||||
|  |  | ||||||
| @@ -237,6 +221,8 @@ const noteTree = (function() { | |||||||
|  |  | ||||||
|             parentNoteId = childNoteId; |             parentNoteId = childNoteId; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         clearSelectedNodes(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     /** |     /** | ||||||
| @@ -396,6 +382,22 @@ const noteTree = (function() { | |||||||
|         recentNotes.addRecentNote(currentNoteTreeId, currentNotePath); |         recentNotes.addRecentNote(currentNoteTreeId, currentNotePath); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     function getSelectedNodes() { | ||||||
|  |         return getTree().getSelectedNodes(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     function clearSelectedNodes() { | ||||||
|  |         for (const selectedNode of getSelectedNodes()) { | ||||||
|  |             selectedNode.setSelected(false); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         const currentNode = getCurrentNode(); | ||||||
|  |  | ||||||
|  |         if (currentNode) { | ||||||
|  |             currentNode.setSelected(true); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|     function initFancyTree(noteTree) { |     function initFancyTree(noteTree) { | ||||||
|         assertArguments(noteTree); |         assertArguments(noteTree); | ||||||
|  |  | ||||||
| @@ -403,28 +405,62 @@ const noteTree = (function() { | |||||||
|             "del": node => { |             "del": node => { | ||||||
|                 treeChanges.deleteNode(node); |                 treeChanges.deleteNode(node); | ||||||
|             }, |             }, | ||||||
|             "shift+up": node => { |             "ctrl+up": node => { | ||||||
|                 const beforeNode = node.getPrevSibling(); |                 const beforeNode = node.getPrevSibling(); | ||||||
|  |  | ||||||
|                 if (beforeNode !== null) { |                 if (beforeNode !== null) { | ||||||
|                     treeChanges.moveBeforeNode(node, beforeNode); |                     treeChanges.moveBeforeNode([node], beforeNode); | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|  |                 return false; | ||||||
|             }, |             }, | ||||||
|             "shift+down": node => { |             "ctrl+down": node => { | ||||||
|                 let afterNode = node.getNextSibling(); |                 let afterNode = node.getNextSibling(); | ||||||
|                 if (afterNode !== null) { |                 if (afterNode !== null) { | ||||||
|                     treeChanges.moveAfterNode(node, afterNode); |                     treeChanges.moveAfterNode([node], afterNode); | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|  |                 return false; | ||||||
|             }, |             }, | ||||||
|             "shift+left": node => { |             "ctrl+left": node => { | ||||||
|                 treeChanges.moveNodeUpInHierarchy(node); |                 treeChanges.moveNodeUpInHierarchy(node); | ||||||
|  |  | ||||||
|  |                 return false; | ||||||
|             }, |             }, | ||||||
|             "shift+right": node => { |             "ctrl+right": node => { | ||||||
|                 let toNode = node.getPrevSibling(); |                 let toNode = node.getPrevSibling(); | ||||||
|  |  | ||||||
|                 if (toNode !== null) { |                 if (toNode !== null) { | ||||||
|                     treeChanges.moveToNode(node, toNode); |                     treeChanges.moveToNode([node], toNode); | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|  |                 return false; | ||||||
|  |             }, | ||||||
|  |             "shift+up": node => { | ||||||
|  |                 node.navigate($.ui.keyCode.UP, true).then(() => { | ||||||
|  |                     const currentNode = getCurrentNode(); | ||||||
|  |  | ||||||
|  |                     if (currentNode.isSelected()) { | ||||||
|  |                         node.setSelected(false); | ||||||
|  |                     } | ||||||
|  |  | ||||||
|  |                     currentNode.setSelected(true); | ||||||
|  |                 }); | ||||||
|  |  | ||||||
|  |                 return false; | ||||||
|  |             }, | ||||||
|  |             "shift+down": node => { | ||||||
|  |                 node.navigate($.ui.keyCode.DOWN, true).then(() => { | ||||||
|  |                     const currentNode = getCurrentNode(); | ||||||
|  |  | ||||||
|  |                     if (currentNode.isSelected()) { | ||||||
|  |                         node.setSelected(false); | ||||||
|  |                     } | ||||||
|  |  | ||||||
|  |                     currentNode.setSelected(true); | ||||||
|  |                 }); | ||||||
|  |  | ||||||
|  |                 return false; | ||||||
|             }, |             }, | ||||||
|             "f2": node => { |             "f2": node => { | ||||||
|                 editTreePrefix.showDialog(node); |                 editTreePrefix.showDialog(node); | ||||||
| @@ -432,26 +468,54 @@ const noteTree = (function() { | |||||||
|             "alt+-": node => { |             "alt+-": node => { | ||||||
|                 collapseTree(node); |                 collapseTree(node); | ||||||
|             }, |             }, | ||||||
|  |             "ctrl+a": node => { | ||||||
|  |                 for (const child of node.getParent().getChildren()) { | ||||||
|  |                     child.setSelected(true); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 return false; | ||||||
|  |             }, | ||||||
|  |             "ctrl+c": () => { | ||||||
|  |                 contextMenu.copy(getSelectedNodes()); | ||||||
|  |  | ||||||
|  |                 return false; | ||||||
|  |             }, | ||||||
|  |             "ctrl+x": () => { | ||||||
|  |                 contextMenu.cut(getSelectedNodes()); | ||||||
|  |  | ||||||
|  |                 return false; | ||||||
|  |             }, | ||||||
|  |             "ctrl+v": node => { | ||||||
|  |                 contextMenu.pasteInto(node); | ||||||
|  |  | ||||||
|  |                 return false; | ||||||
|  |             }, | ||||||
|  |             "ctrl+return": node => { | ||||||
|  |                 noteDetailEl.focus(); | ||||||
|  |             }, | ||||||
|  |             "return": node => { | ||||||
|  |                 noteDetailEl.focus(); | ||||||
|  |             }, | ||||||
|             // code below shouldn't be necessary normally, however there's some problem with interaction with context menu plugin |             // code below shouldn't be necessary normally, however there's some problem with interaction with context menu plugin | ||||||
|             // after opening context menu, standard shortcuts don't work, but they are detected here |             // after opening context menu, standard shortcuts don't work, but they are detected here | ||||||
|             // so we essentially takeover the standard handling with our implementation. |             // so we essentially takeover the standard handling with our implementation. | ||||||
|             "left": node => { |             "left": node => { | ||||||
|                 node.navigate($.ui.keyCode.LEFT, true); |                 node.navigate($.ui.keyCode.LEFT, true).then(() => clearSelectedNodes()); | ||||||
|  |  | ||||||
|                 return false; |                 return false; | ||||||
|             }, |             }, | ||||||
|             "right": node => { |             "right": node => { | ||||||
|                 node.navigate($.ui.keyCode.RIGHT, true); |                 node.navigate($.ui.keyCode.RIGHT, true).then(() => clearSelectedNodes()); | ||||||
|  |  | ||||||
|                 return false; |                 return false; | ||||||
|             }, |             }, | ||||||
|             "up": node => { |             "up": node => { | ||||||
|                 node.navigate($.ui.keyCode.UP, true); |                 node.navigate($.ui.keyCode.UP, true).then(() => clearSelectedNodes()); | ||||||
|  |  | ||||||
|                 return false; |                 return false; | ||||||
|             }, |             }, | ||||||
|             "down": node => { |             "down": node => { | ||||||
|                 node.navigate($.ui.keyCode.DOWN, true); |                 node.navigate($.ui.keyCode.DOWN, true).then(() => clearSelectedNodes()); | ||||||
|  |  | ||||||
|                 return false; |                 return false; | ||||||
|             } |             } | ||||||
| @@ -463,6 +527,22 @@ const noteTree = (function() { | |||||||
|             extensions: ["hotkeys", "filter", "dnd", "clones"], |             extensions: ["hotkeys", "filter", "dnd", "clones"], | ||||||
|             source: noteTree, |             source: noteTree, | ||||||
|             scrollParent: $("#tree"), |             scrollParent: $("#tree"), | ||||||
|  |             click: (event, data) => { | ||||||
|  |                 const targetType = data.targetType; | ||||||
|  |                 const node = data.node; | ||||||
|  |  | ||||||
|  |                 if (targetType === 'title' || targetType === 'icon') { | ||||||
|  |                     node.setActive(); | ||||||
|  |  | ||||||
|  |                     if (!event.ctrlKey) { | ||||||
|  |                         clearSelectedNodes(); | ||||||
|  |                     } | ||||||
|  |  | ||||||
|  |                     node.setSelected(true); | ||||||
|  |  | ||||||
|  |                     return false; | ||||||
|  |                 } | ||||||
|  |             }, | ||||||
|             activate: (event, data) => { |             activate: (event, data) => { | ||||||
|                 const node = data.node.data; |                 const node = data.node.data; | ||||||
|  |  | ||||||
| @@ -513,57 +593,6 @@ const noteTree = (function() { | |||||||
|                 mode: "hide"       // Grayout unmatched nodes (pass "hide" to remove unmatched node instead) |                 mode: "hide"       // Grayout unmatched nodes (pass "hide" to remove unmatched node instead) | ||||||
|             }, |             }, | ||||||
|             dnd: dragAndDropSetup, |             dnd: dragAndDropSetup, | ||||||
|             keydown: (event, data) => { |  | ||||||
|                 const node = data.node; |  | ||||||
|                 // Eat keyboard events, when a menu is open |  | ||||||
|                 if ($(".contextMenu:visible").length > 0) |  | ||||||
|                     return false; |  | ||||||
|  |  | ||||||
|                 switch (event.which) { |  | ||||||
|                     // Open context menu on [Space] key (simulate right click) |  | ||||||
|                     case 32: // [Space] |  | ||||||
|                         $(node.span).trigger("mousedown", { |  | ||||||
|                             preventDefault: true, |  | ||||||
|                             button: 2 |  | ||||||
|                         }) |  | ||||||
|                             .trigger("mouseup", { |  | ||||||
|                                 preventDefault: true, |  | ||||||
|                                 pageX: node.span.offsetLeft, |  | ||||||
|                                 pageY: node.span.offsetTop, |  | ||||||
|                                 button: 2 |  | ||||||
|                             }); |  | ||||||
|                         return false; |  | ||||||
|  |  | ||||||
|                     // Handle Ctrl+C, +X and +V |  | ||||||
|                     case 67: |  | ||||||
|                         if (event.ctrlKey) { // Ctrl+C |  | ||||||
|                             contextMenu.copy(node); |  | ||||||
|  |  | ||||||
|                             showMessage("Note copied into clipboard."); |  | ||||||
|  |  | ||||||
|                             return false; |  | ||||||
|                         } |  | ||||||
|                         break; |  | ||||||
|                     case 88: |  | ||||||
|                         if (event.ctrlKey) { // Ctrl+X |  | ||||||
|                             contextMenu.cut(node); |  | ||||||
|  |  | ||||||
|                             showMessage("Note cut into clipboard."); |  | ||||||
|  |  | ||||||
|                             return false; |  | ||||||
|                         } |  | ||||||
|                         break; |  | ||||||
|                     case 86: |  | ||||||
|                         if (event.ctrlKey) { // Ctrl+V |  | ||||||
|                             contextMenu.pasteInto(node); |  | ||||||
|  |  | ||||||
|                             showMessage("Note pasted from clipboard into current note."); |  | ||||||
|  |  | ||||||
|                             return false; |  | ||||||
|                         } |  | ||||||
|                         break; |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|             lazyLoad: function(event, data){ |             lazyLoad: function(event, data){ | ||||||
|                 const node = data.node.data; |                 const node = data.node.data; | ||||||
|  |  | ||||||
| @@ -772,7 +801,11 @@ const noteTree = (function() { | |||||||
|     $(window).bind('hashchange', function() { |     $(window).bind('hashchange', function() { | ||||||
|         const notePath = getNotePathFromAddress(); |         const notePath = getNotePathFromAddress(); | ||||||
|  |  | ||||||
|  |         if (getCurrentNotePath() !== notePath) { | ||||||
|  |             console.log("Switching to " + notePath + " because of hash change"); | ||||||
|  |  | ||||||
|             activateNode(notePath); |             activateNode(notePath); | ||||||
|  |         } | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     if (isElectron()) { |     if (isElectron()) { | ||||||
| @@ -807,6 +840,7 @@ const noteTree = (function() { | |||||||
|         setPrefix, |         setPrefix, | ||||||
|         getNotePathTitle, |         getNotePathTitle, | ||||||
|         removeParentChildRelation, |         removeParentChildRelation, | ||||||
|         setParentChildRelation |         setParentChildRelation, | ||||||
|  |         getSelectedNodes | ||||||
|     }; |     }; | ||||||
| })(); | })(); | ||||||
| @@ -14,3 +14,9 @@ async function syncNow() { | |||||||
|         showError("Sync failed: " + result.message); |         showError("Sync failed: " + result.message); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | async function forceNoteSync(noteId) { | ||||||
|  |     const result = await server.post('sync/force-note-sync/' + noteId); | ||||||
|  |  | ||||||
|  |     showMessage("Note added to sync queue."); | ||||||
|  | } | ||||||
| @@ -1,16 +1,30 @@ | |||||||
| "use strict"; | "use strict"; | ||||||
|  |  | ||||||
| const treeChanges = (function() { | const treeChanges = (function() { | ||||||
|     async function moveBeforeNode(node, beforeNode) { |     async function moveBeforeNode(nodesToMove, beforeNode) { | ||||||
|         await server.put('notes/' + node.data.note_tree_id + '/move-before/' + beforeNode.data.note_tree_id); |         for (const nodeToMove of nodesToMove) { | ||||||
|  |             const resp = await server.put('notes/' + nodeToMove.data.note_tree_id + '/move-before/' + beforeNode.data.note_tree_id); | ||||||
|  |  | ||||||
|         changeNode(node, node => node.moveTo(beforeNode, 'before')); |             if (!resp.success) { | ||||||
|  |                 alert(resp.message); | ||||||
|  |                 return; | ||||||
|             } |             } | ||||||
|  |  | ||||||
|     async function moveAfterNode(node, afterNode) { |             changeNode(nodeToMove, node => node.moveTo(beforeNode, 'before')); | ||||||
|         await server.put('notes/' + node.data.note_tree_id + '/move-after/' + afterNode.data.note_tree_id); |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|         changeNode(node, node => node.moveTo(afterNode, 'after')); |     async function moveAfterNode(nodesToMove, afterNode) { | ||||||
|  |         for (const nodeToMove of nodesToMove) { | ||||||
|  |             const resp = await server.put('notes/' + nodeToMove.data.note_tree_id + '/move-after/' + afterNode.data.note_tree_id); | ||||||
|  |  | ||||||
|  |             if (!resp.success) { | ||||||
|  |                 alert(resp.message); | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             changeNode(nodeToMove, node => node.moveTo(afterNode, 'after')); | ||||||
|  |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // beware that first arg is noteId and second is noteTreeId! |     // beware that first arg is noteId and second is noteTreeId! | ||||||
| @@ -25,18 +39,31 @@ const treeChanges = (function() { | |||||||
|         await noteTree.reload(); |         await noteTree.reload(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     async function moveToNode(node, toNode) { |     async function moveToNode(nodesToMove, toNode) { | ||||||
|         await server.put('notes/' + node.data.note_tree_id + '/move-to/' + toNode.data.note_id); |         for (const nodeToMove of nodesToMove) { | ||||||
|  |             const resp = await server.put('notes/' + nodeToMove.data.note_tree_id + '/move-to/' + toNode.data.note_id); | ||||||
|  |  | ||||||
|         changeNode(node, node => { |             if (!resp.success) { | ||||||
|             node.moveTo(toNode); |                 alert(resp.message); | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             changeNode(nodeToMove, node => { | ||||||
|  |                 // first expand which will force lazy load and only then move the node | ||||||
|  |                 // if this is not expanded before moving, then lazy load won't happen because it already contains node | ||||||
|  |                 // this doesn't work if this isn't a folder yet, that's why we expand second time below | ||||||
|                 toNode.setExpanded(true); |                 toNode.setExpanded(true); | ||||||
|  |  | ||||||
|  |                 node.moveTo(toNode); | ||||||
|  |  | ||||||
|                 toNode.folder = true; |                 toNode.folder = true; | ||||||
|                 toNode.renderTitle(); |                 toNode.renderTitle(); | ||||||
|  |  | ||||||
|  |                 // this expands the note in case it become the folder only after the move | ||||||
|  |                 toNode.setExpanded(true); | ||||||
|             }); |             }); | ||||||
|         } |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|     async function cloneNoteTo(childNoteId, parentNoteId, prefix) { |     async function cloneNoteTo(childNoteId, parentNoteId, prefix) { | ||||||
|         const resp = await server.put('notes/' + childNoteId + '/clone-to/' + parentNoteId, { |         const resp = await server.put('notes/' + childNoteId + '/clone-to/' + parentNoteId, { | ||||||
| @@ -92,7 +119,12 @@ const treeChanges = (function() { | |||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         await server.put('notes/' + node.data.note_tree_id + '/move-after/' + node.getParent().data.note_tree_id); |         const resp = await server.put('notes/' + node.data.note_tree_id + '/move-after/' + node.getParent().data.note_tree_id); | ||||||
|  |  | ||||||
|  |         if (!resp.success) { | ||||||
|  |             alert(resp.message); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|         if (!isTopLevelNode(node) && node.getParent().getChildren().length <= 1) { |         if (!isTopLevelNode(node) && node.getParent().getChildren().length <= 1) { | ||||||
|             node.getParent().folder = false; |             node.getParent().folder = false; | ||||||
|   | |||||||
| @@ -37,7 +37,7 @@ const treeUtils = (function() { | |||||||
|  |  | ||||||
|         const title = (prefix ? (prefix + " - ") : "") + noteTitle; |         const title = (prefix ? (prefix + " - ") : "") + noteTitle; | ||||||
|  |  | ||||||
|         node.setTitle(title); |         node.setTitle(escapeHtml(title)); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     return { |     return { | ||||||
|   | |||||||
| @@ -94,3 +94,7 @@ function isTopLevelNode(node) { | |||||||
| function isRootNode(node) { | function isRootNode(node) { | ||||||
|     return node.key === "root_1"; |     return node.key === "root_1"; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | function escapeHtml(str) { | ||||||
|  |     return $('<div/>').text(str).html(); | ||||||
|  | } | ||||||
| @@ -74,6 +74,12 @@ span.fancytree-node.fancytree-active-clone:not(.fancytree-active) .fancytree-tit | |||||||
|     font-weight: bold; |     font-weight: bold; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | /* By default not focused active tree item is not easily visible, this makes it more visible */ | ||||||
|  | span.fancytree-active:not(.fancytree-focused) .fancytree-title { | ||||||
|  |     background-color: #ddd !important; | ||||||
|  |     border-color: #555 !important; | ||||||
|  | } | ||||||
|  |  | ||||||
| .ui-autocomplete { | .ui-autocomplete { | ||||||
|     max-height: 300px; |     max-height: 300px; | ||||||
|     overflow-y: auto; |     overflow-y: auto; | ||||||
| @@ -169,7 +175,6 @@ div.ui-tooltip { | |||||||
|  |  | ||||||
| /* Allow to use <kbd> elements inside the title to define shortcut hints. */ | /* Allow to use <kbd> elements inside the title to define shortcut hints. */ | ||||||
| .ui-menu kbd, button kbd { | .ui-menu kbd, button kbd { | ||||||
|     float: right; |  | ||||||
|     color: black; |     color: black; | ||||||
|     border: none; |     border: none; | ||||||
|     background-color: transparent; |     background-color: transparent; | ||||||
| @@ -178,6 +183,7 @@ div.ui-tooltip { | |||||||
|  |  | ||||||
| .ui-menu kbd { | .ui-menu kbd { | ||||||
|     margin-left: 30px; |     margin-left: 30px; | ||||||
|  |     float: right; | ||||||
| } | } | ||||||
|  |  | ||||||
| #note-id-display { | #note-id-display { | ||||||
|   | |||||||
| @@ -12,6 +12,15 @@ router.put('/:noteTreeId/move-to/:parentNoteId', auth.checkApiAuth, async (req, | |||||||
|     const parentNoteId = req.params.parentNoteId; |     const parentNoteId = req.params.parentNoteId; | ||||||
|     const sourceId = req.headers.source_id; |     const sourceId = req.headers.source_id; | ||||||
|  |  | ||||||
|  |     const noteToMove = await sql.getFirst("SELECT * FROM notes_tree WHERE note_tree_id = ?", [noteTreeId]); | ||||||
|  |  | ||||||
|  |     if (!await checkTreeCycle(parentNoteId, noteToMove.note_id)) { | ||||||
|  |         return res.send({ | ||||||
|  |             success: false, | ||||||
|  |             message: 'Moving note here would create cycle.' | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     const maxNotePos = await sql.getFirstValue('SELECT MAX(note_position) FROM notes_tree WHERE parent_note_id = ? AND is_deleted = 0', [parentNoteId]); |     const maxNotePos = await sql.getFirstValue('SELECT MAX(note_position) FROM notes_tree WHERE parent_note_id = ? AND is_deleted = 0', [parentNoteId]); | ||||||
|     const newNotePos = maxNotePos === null ? 0 : maxNotePos + 1; |     const newNotePos = maxNotePos === null ? 0 : maxNotePos + 1; | ||||||
|  |  | ||||||
| @@ -24,7 +33,7 @@ router.put('/:noteTreeId/move-to/:parentNoteId', auth.checkApiAuth, async (req, | |||||||
|         await sync_table.addNoteTreeSync(noteTreeId, sourceId); |         await sync_table.addNoteTreeSync(noteTreeId, sourceId); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     res.send({}); |     res.send({ success: true }); | ||||||
| }); | }); | ||||||
|  |  | ||||||
| router.put('/:noteTreeId/move-before/:beforeNoteTreeId', auth.checkApiAuth, async (req, res, next) => { | router.put('/:noteTreeId/move-before/:beforeNoteTreeId', auth.checkApiAuth, async (req, res, next) => { | ||||||
| @@ -32,8 +41,16 @@ router.put('/:noteTreeId/move-before/:beforeNoteTreeId', auth.checkApiAuth, asyn | |||||||
|     const beforeNoteTreeId = req.params.beforeNoteTreeId; |     const beforeNoteTreeId = req.params.beforeNoteTreeId; | ||||||
|     const sourceId = req.headers.source_id; |     const sourceId = req.headers.source_id; | ||||||
|  |  | ||||||
|  |     const noteToMove = await sql.getFirst("SELECT * FROM notes_tree WHERE note_tree_id = ?", [noteTreeId]); | ||||||
|     const beforeNote = await sql.getFirst("SELECT * FROM notes_tree WHERE note_tree_id = ?", [beforeNoteTreeId]); |     const beforeNote = await sql.getFirst("SELECT * FROM notes_tree WHERE note_tree_id = ?", [beforeNoteTreeId]); | ||||||
|  |  | ||||||
|  |     if (!await checkTreeCycle(beforeNote.parent_note_id, noteToMove.note_id)) { | ||||||
|  |         return res.send({ | ||||||
|  |             success: false, | ||||||
|  |             message: 'Moving note here would create cycle.' | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     if (beforeNote) { |     if (beforeNote) { | ||||||
|         await sql.doInTransaction(async () => { |         await sql.doInTransaction(async () => { | ||||||
|             // we don't change date_modified so other changes are prioritized in case of conflict |             // we don't change date_modified so other changes are prioritized in case of conflict | ||||||
| @@ -51,7 +68,7 @@ router.put('/:noteTreeId/move-before/:beforeNoteTreeId', auth.checkApiAuth, asyn | |||||||
|             await sync_table.addNoteTreeSync(noteTreeId, sourceId); |             await sync_table.addNoteTreeSync(noteTreeId, sourceId); | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|         res.send({}); |         res.send({ success: true }); | ||||||
|     } |     } | ||||||
|     else { |     else { | ||||||
|         res.status(500).send("Before note " + beforeNoteTreeId + " doesn't exist."); |         res.status(500).send("Before note " + beforeNoteTreeId + " doesn't exist."); | ||||||
| @@ -63,8 +80,16 @@ router.put('/:noteTreeId/move-after/:afterNoteTreeId', auth.checkApiAuth, async | |||||||
|     const afterNoteTreeId = req.params.afterNoteTreeId; |     const afterNoteTreeId = req.params.afterNoteTreeId; | ||||||
|     const sourceId = req.headers.source_id; |     const sourceId = req.headers.source_id; | ||||||
|  |  | ||||||
|  |     const noteToMove = await sql.getFirst("SELECT * FROM notes_tree WHERE note_tree_id = ?", [noteTreeId]); | ||||||
|     const afterNote = await sql.getFirst("SELECT * FROM notes_tree WHERE note_tree_id = ?", [afterNoteTreeId]); |     const afterNote = await sql.getFirst("SELECT * FROM notes_tree WHERE note_tree_id = ?", [afterNoteTreeId]); | ||||||
|  |  | ||||||
|  |     if (!await checkTreeCycle(afterNote.parent_note_id, noteToMove.note_id)) { | ||||||
|  |         return res.send({ | ||||||
|  |             success: false, | ||||||
|  |             message: 'Moving note here would create cycle.' | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     if (afterNote) { |     if (afterNote) { | ||||||
|         await sql.doInTransaction(async () => { |         await sql.doInTransaction(async () => { | ||||||
|             // we don't change date_modified so other changes are prioritized in case of conflict |             // we don't change date_modified so other changes are prioritized in case of conflict | ||||||
| @@ -80,7 +105,7 @@ router.put('/:noteTreeId/move-after/:afterNoteTreeId', auth.checkApiAuth, async | |||||||
|             await sync_table.addNoteTreeSync(noteTreeId, sourceId); |             await sync_table.addNoteTreeSync(noteTreeId, sourceId); | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|         res.send({}); |         res.send({ success: true }); | ||||||
|     } |     } | ||||||
|     else { |     else { | ||||||
|         res.status(500).send("After note " + afterNoteTreeId + " doesn't exist."); |         res.status(500).send("After note " + afterNoteTreeId + " doesn't exist."); | ||||||
| @@ -102,7 +127,7 @@ router.put('/:childNoteId/clone-to/:parentNoteId', auth.checkApiAuth, async (req | |||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     if (!await checkCycle(parentNoteId, childNoteId)) { |     if (!await checkTreeCycle(parentNoteId, childNoteId)) { | ||||||
|         return res.send({ |         return res.send({ | ||||||
|             success: false, |             success: false, | ||||||
|             message: 'Cloning note here would create cycle.' |             message: 'Cloning note here would create cycle.' | ||||||
| @@ -131,9 +156,7 @@ router.put('/:childNoteId/clone-to/:parentNoteId', auth.checkApiAuth, async (req | |||||||
|         await sql.execute("UPDATE notes_tree SET is_expanded = 1 WHERE note_id = ?", [parentNoteId]); |         await sql.execute("UPDATE notes_tree SET is_expanded = 1 WHERE note_id = ?", [parentNoteId]); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     res.send({ |     res.send({ success: true }); | ||||||
|         success: true |  | ||||||
|     }); |  | ||||||
| }); | }); | ||||||
|  |  | ||||||
| router.put('/:noteId/clone-after/:afterNoteTreeId', auth.checkApiAuth, async (req, res, next) => { | router.put('/:noteId/clone-after/:afterNoteTreeId', auth.checkApiAuth, async (req, res, next) => { | ||||||
| @@ -147,7 +170,7 @@ router.put('/:noteId/clone-after/:afterNoteTreeId', auth.checkApiAuth, async (re | |||||||
|         return res.status(500).send("After note " + afterNoteTreeId + " doesn't exist."); |         return res.status(500).send("After note " + afterNoteTreeId + " doesn't exist."); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     if (!await checkCycle(afterNote.parent_note_id, noteId)) { |     if (!await checkTreeCycle(afterNote.parent_note_id, noteId)) { | ||||||
|         return res.send({ |         return res.send({ | ||||||
|             success: false, |             success: false, | ||||||
|             message: 'Cloning note here would create cycle.' |             message: 'Cloning note here would create cycle.' | ||||||
| @@ -186,24 +209,43 @@ router.put('/:noteId/clone-after/:afterNoteTreeId', auth.checkApiAuth, async (re | |||||||
|         await sync_table.addNoteTreeSync(noteTree.note_tree_id, sourceId); |         await sync_table.addNoteTreeSync(noteTree.note_tree_id, sourceId); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     res.send({ |     res.send({ success: true }); | ||||||
|         success: true |  | ||||||
|     }); |  | ||||||
| }); | }); | ||||||
|  |  | ||||||
| async function checkCycle(parentNoteId, childNoteId) { | async function loadSubTreeNoteIds(parentNoteId, subTreeNoteIds) { | ||||||
|  |     subTreeNoteIds.push(parentNoteId); | ||||||
|  |  | ||||||
|  |     const children = await sql.getFirstColumn("SELECT note_id FROM notes_tree WHERE parent_note_id = ?", [parentNoteId]); | ||||||
|  |  | ||||||
|  |     for (const childNoteId of children) { | ||||||
|  |         await loadSubTreeNoteIds(childNoteId, subTreeNoteIds); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Tree cycle can be created when cloning or when moving existing clone. This method should detect both cases. | ||||||
|  |  */ | ||||||
|  | async function checkTreeCycle(parentNoteId, childNoteId) { | ||||||
|  |     const subTreeNoteIds = []; | ||||||
|  |  | ||||||
|  |     // we'll load the whole sub tree - because the cycle can start in one of the notes in the sub tree | ||||||
|  |     await loadSubTreeNoteIds(childNoteId, subTreeNoteIds); | ||||||
|  |  | ||||||
|  |     async function checkTreeCycleInner(parentNoteId) { | ||||||
|         if (parentNoteId === 'root') { |         if (parentNoteId === 'root') { | ||||||
|             return true; |             return true; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|     if (parentNoteId === childNoteId) { |         if (subTreeNoteIds.includes(parentNoteId)) { | ||||||
|  |             // while towards the root of the tree we encountered noteId which is already present in the subtree | ||||||
|  |             // joining parentNoteId with childNoteId would then clearly create a cycle | ||||||
|             return false; |             return false; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         const parentNoteIds = await sql.getFirstColumn("SELECT DISTINCT parent_note_id FROM notes_tree WHERE note_id = ?", [parentNoteId]); |         const parentNoteIds = await sql.getFirstColumn("SELECT DISTINCT parent_note_id FROM notes_tree WHERE note_id = ?", [parentNoteId]); | ||||||
|  |  | ||||||
|         for (const pid of parentNoteIds) { |         for (const pid of parentNoteIds) { | ||||||
|         if (!await checkCycle(pid, childNoteId)) { |             if (!await checkTreeCycleInner(pid)) { | ||||||
|                 return false; |                 return false; | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| @@ -211,6 +253,9 @@ async function checkCycle(parentNoteId, childNoteId) { | |||||||
|         return true; |         return true; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     return await checkTreeCycleInner(parentNoteId); | ||||||
|  | } | ||||||
|  |  | ||||||
| router.put('/:noteTreeId/expanded/:expanded', auth.checkApiAuth, async (req, res, next) => { | router.put('/:noteTreeId/expanded/:expanded', auth.checkApiAuth, async (req, res, next) => { | ||||||
|     const noteTreeId = req.params.noteTreeId; |     const noteTreeId = req.params.noteTreeId; | ||||||
|     const expanded = req.params.expanded; |     const expanded = req.params.expanded; | ||||||
|   | |||||||
| @@ -46,6 +46,30 @@ router.post('/force-full-sync', auth.checkApiAuth, async (req, res, next) => { | |||||||
|     res.send({}); |     res.send({}); | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | router.post('/force-note-sync/:noteId', auth.checkApiAuth, async (req, res, next) => { | ||||||
|  |     const noteId = req.params.noteId; | ||||||
|  |  | ||||||
|  |     await sql.doInTransaction(async () => { | ||||||
|  |         await sync_table.addNoteSync(noteId); | ||||||
|  |  | ||||||
|  |         for (const noteTreeId of await sql.getFirstColumn("SELECT note_tree_id FROM notes_tree WHERE is_deleted = 0 AND note_id = ?", [noteId])) { | ||||||
|  |             await sync_table.addNoteTreeSync(noteTreeId); | ||||||
|  |             await sync_table.addRecentNoteSync(noteTreeId); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         for (const noteHistoryId of await sql.getFirstColumn("SELECT note_history_id FROM notes_history WHERE note_id = ?", [noteId])) { | ||||||
|  |             await sync_table.addNoteHistorySync(noteHistoryId); | ||||||
|  |         } | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     log.info("Forcing note sync for " + noteId); | ||||||
|  |  | ||||||
|  |     // not awaiting for the job to finish (will probably take a long time) | ||||||
|  |     sync.sync(); | ||||||
|  |  | ||||||
|  |     res.send({}); | ||||||
|  | }); | ||||||
|  |  | ||||||
| router.get('/changed', auth.checkApiAuth, async (req, res, next) => { | router.get('/changed', auth.checkApiAuth, async (req, res, next) => { | ||||||
|     const lastSyncId = parseInt(req.query.lastSyncId); |     const lastSyncId = parseInt(req.query.lastSyncId); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -3,12 +3,9 @@ | |||||||
| const migration = require('./migration'); | const migration = require('./migration'); | ||||||
| const sql = require('./sql'); | const sql = require('./sql'); | ||||||
| const utils = require('./utils'); | const utils = require('./utils'); | ||||||
| const options = require('./options'); |  | ||||||
|  |  | ||||||
| async function checkAuth(req, res, next) { | async function checkAuth(req, res, next) { | ||||||
|     const username = await options.getOption('username'); |     if (!await sql.isUserInitialized()) { | ||||||
|  |  | ||||||
|     if (!username) { |  | ||||||
|         res.redirect("setup"); |         res.redirect("setup"); | ||||||
|     } |     } | ||||||
|     else if (!req.session.loggedIn && !utils.isElectron()) { |     else if (!req.session.loggedIn && !utils.isElectron()) { | ||||||
| @@ -53,9 +50,7 @@ async function checkApiAuthForMigrationPage(req, res, next) { | |||||||
| } | } | ||||||
|  |  | ||||||
| async function checkAppNotInitialized(req, res, next) { | async function checkAppNotInitialized(req, res, next) { | ||||||
|     const username = await options.getOption('username'); |     if (await sql.isUserInitialized()) { | ||||||
|  |  | ||||||
|     if (username) { |  | ||||||
|         res.status(400).send("App already initialized."); |         res.status(400).send("App already initialized."); | ||||||
|     } |     } | ||||||
|     else { |     else { | ||||||
|   | |||||||
| @@ -1 +1 @@ | |||||||
| module.exports = { build_date:"2017-12-27T17:41:07-05:00", build_revision: "6405d6e06658188f14f29b0a2e1891e5287000f5" }; | module.exports = { build_date:"2018-01-01T23:29:34-05:00", build_revision: "ae6e222c506c170ecd24d758328e0678f158bb47" }; | ||||||
|   | |||||||
| @@ -17,6 +17,46 @@ async function runCheck(query, errorText, errorList) { | |||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | async function checkTreeCycles(errorList) { | ||||||
|  |     const childToParents = {}; | ||||||
|  |     const rows = await sql.getAll("SELECT note_id, parent_note_id FROM notes_tree"); | ||||||
|  |  | ||||||
|  |     for (const row of rows) { | ||||||
|  |         const childNoteId = row.note_id; | ||||||
|  |         const parentNoteId = row.parent_note_id; | ||||||
|  |  | ||||||
|  |         if (!childToParents[childNoteId]) { | ||||||
|  |             childToParents[childNoteId] = []; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         childToParents[childNoteId].push(parentNoteId); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     function checkTreeCycle(noteId, path, errorList) { | ||||||
|  |         if (noteId === 'root') { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         for (const parentNoteId of childToParents[noteId]) { | ||||||
|  |             if (path.includes(parentNoteId)) { | ||||||
|  |                 errorList.push(`Tree cycle detected at parent-child relationship: ${parentNoteId} - ${noteId}, whole path: ${path}`); | ||||||
|  |             } | ||||||
|  |             else { | ||||||
|  |                 const newPath = path.slice(); | ||||||
|  |                 newPath.push(noteId); | ||||||
|  |  | ||||||
|  |                 checkTreeCycle(parentNoteId, newPath, errorList); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const noteIds = Object.keys(childToParents); | ||||||
|  |  | ||||||
|  |     for (const noteId of noteIds) { | ||||||
|  |         checkTreeCycle(noteId, [], errorList); | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
| async function runSyncRowChecks(table, key, errorList) { | async function runSyncRowChecks(table, key, errorList) { | ||||||
|     await runCheck(` |     await runCheck(` | ||||||
|         SELECT  |         SELECT  | ||||||
| @@ -43,6 +83,8 @@ async function runSyncRowChecks(table, key, errorList) { | |||||||
| async function runChecks() { | async function runChecks() { | ||||||
|     const errorList = []; |     const errorList = []; | ||||||
|  |  | ||||||
|  |     const startTime = new Date(); | ||||||
|  |  | ||||||
|     await runCheck(` |     await runCheck(` | ||||||
|           SELECT  |           SELECT  | ||||||
|             note_id  |             note_id  | ||||||
| @@ -124,11 +166,21 @@ async function runChecks() { | |||||||
|     await runSyncRowChecks("notes_tree", "note_tree_id", errorList); |     await runSyncRowChecks("notes_tree", "note_tree_id", errorList); | ||||||
|     await runSyncRowChecks("recent_notes", "note_tree_id", errorList); |     await runSyncRowChecks("recent_notes", "note_tree_id", errorList); | ||||||
|  |  | ||||||
|  |     if (errorList.length === 0) { | ||||||
|  |         // we run this only if basic checks passed since this assumes basic data consistency | ||||||
|  |  | ||||||
|  |         await checkTreeCycles(errorList); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const elapsedTimeMs = new Date().getTime() - startTime.getTime(); | ||||||
|  |  | ||||||
|     if (errorList.length > 0) { |     if (errorList.length > 0) { | ||||||
|  |         log.info(`Consistency checks failed (took ${elapsedTimeMs}ms) with these errors: ` + JSON.stringify(errorList)); | ||||||
|  |  | ||||||
|         messaging.sendMessageToAllClients({type: 'consistency-checks-failed'}); |         messaging.sendMessageToAllClients({type: 'consistency-checks-failed'}); | ||||||
|     } |     } | ||||||
|     else { |     else { | ||||||
|         log.info("All consistency checks passed."); |         log.info(`All consistency checks passed (took ${elapsedTimeMs}ms)`); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,6 +1,7 @@ | |||||||
| const sql = require('./sql'); | const sql = require('./sql'); | ||||||
| const utils = require('./utils'); | const utils = require('./utils'); | ||||||
| const options = require('./options'); | const options = require('./options'); | ||||||
|  | const log = require('./log'); | ||||||
|  |  | ||||||
| function getHash(rows) { | function getHash(rows) { | ||||||
|     let hash = ''; |     let hash = ''; | ||||||
| @@ -13,9 +14,11 @@ function getHash(rows) { | |||||||
| } | } | ||||||
|  |  | ||||||
| async function getHashes() { | async function getHashes() { | ||||||
|  |     const startTime = new Date(); | ||||||
|  |  | ||||||
|     const optionsQuestionMarks = Array(options.SYNCED_OPTIONS.length).fill('?').join(','); |     const optionsQuestionMarks = Array(options.SYNCED_OPTIONS.length).fill('?').join(','); | ||||||
|  |  | ||||||
|     return { |     const hashes = { | ||||||
|         notes: getHash(await sql.getAll(`SELECT |         notes: getHash(await sql.getAll(`SELECT | ||||||
|                                                   note_id, |                                                   note_id, | ||||||
|                                                   note_title, |                                                   note_title, | ||||||
| @@ -62,6 +65,12 @@ async function getHashes() { | |||||||
|                                                   WHERE opt_name IN (${optionsQuestionMarks})  |                                                   WHERE opt_name IN (${optionsQuestionMarks})  | ||||||
|                                                   ORDER BY opt_name`, options.SYNCED_OPTIONS)) |                                                   ORDER BY opt_name`, options.SYNCED_OPTIONS)) | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|  |     const elapseTimeMs = new Date().getTime() - startTime.getTime(); | ||||||
|  |  | ||||||
|  |     log.info(`Content hash computation took ${elapseTimeMs}ms`); | ||||||
|  |  | ||||||
|  |     return hashes; | ||||||
| } | } | ||||||
|  |  | ||||||
| module.exports = { | module.exports = { | ||||||
|   | |||||||
| @@ -1,6 +1,7 @@ | |||||||
| "use strict"; | "use strict"; | ||||||
|  |  | ||||||
| const crypto = require('crypto'); | const crypto = require('crypto'); | ||||||
|  | const log = require('./log'); | ||||||
|  |  | ||||||
| function arraysIdentical(a, b) { | function arraysIdentical(a, b) { | ||||||
|     let i = a.length; |     let i = a.length; | ||||||
| @@ -72,7 +73,15 @@ function decrypt(key, iv, cipherText) { | |||||||
| function decryptString(dataKey, iv, cipherText) { | function decryptString(dataKey, iv, cipherText) { | ||||||
|     const buffer = decrypt(dataKey, iv, cipherText); |     const buffer = decrypt(dataKey, iv, cipherText); | ||||||
|  |  | ||||||
|     return buffer.toString('utf-8'); |     const str = buffer.toString('utf-8'); | ||||||
|  |  | ||||||
|  |     if (str === 'false') { | ||||||
|  |         log.error("Could not decrypt string. Buffer: " + buffer); | ||||||
|  |  | ||||||
|  |         throw new Error("Could not decrypt string."); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return str; | ||||||
| } | } | ||||||
|  |  | ||||||
| function noteTitleIv(iv) { | function noteTitleIv(iv) { | ||||||
|   | |||||||
| @@ -54,6 +54,8 @@ async function sendMessage(client, message) { | |||||||
| async function sendMessageToAllClients(message) { | async function sendMessageToAllClients(message) { | ||||||
|     const jsonStr = JSON.stringify(message); |     const jsonStr = JSON.stringify(message); | ||||||
|  |  | ||||||
|  |     log.info("Sending message to all clients: " + jsonStr); | ||||||
|  |  | ||||||
|     webSocketServer.clients.forEach(function each(client) { |     webSocketServer.clients.forEach(function each(client) { | ||||||
|         if (client.readyState === WebSocket.OPEN) { |         if (client.readyState === WebSocket.OPEN) { | ||||||
|             client.send(jsonStr); |             client.send(jsonStr); | ||||||
|   | |||||||
| @@ -49,9 +49,7 @@ const dbReady = new Promise((resolve, reject) => { | |||||||
|             // the database |             // the database | ||||||
|         } |         } | ||||||
|         else { |         else { | ||||||
|             const username = await getFirstValue("SELECT opt_value FROM options WHERE opt_name = 'username'"); |             if (!await isUserInitialized()) { | ||||||
|  |  | ||||||
|             if (!username) { |  | ||||||
|                 log.info("Login/password not initialized. DB not ready."); |                 log.info("Login/password not initialized. DB not ready."); | ||||||
|  |  | ||||||
|                 return; |                 return; | ||||||
| @@ -235,8 +233,15 @@ async function isDbUpToDate() { | |||||||
|     return upToDate; |     return upToDate; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | async function isUserInitialized() { | ||||||
|  |     const username = await getFirstValue("SELECT opt_value FROM options WHERE opt_name = 'username'"); | ||||||
|  |  | ||||||
|  |     return !!username; | ||||||
|  | } | ||||||
|  |  | ||||||
| module.exports = { | module.exports = { | ||||||
|     dbReady, |     dbReady, | ||||||
|  |     isUserInitialized, | ||||||
|     insert, |     insert, | ||||||
|     replace, |     replace, | ||||||
|     getFirstValue, |     getFirstValue, | ||||||
|   | |||||||
| @@ -24,8 +24,8 @@ async function addOptionsSync(optName, sourceId) { | |||||||
|     await addEntitySync("options", optName, sourceId); |     await addEntitySync("options", optName, sourceId); | ||||||
| } | } | ||||||
|  |  | ||||||
| async function addRecentNoteSync(notePath, sourceId) { | async function addRecentNoteSync(noteTreeId, sourceId) { | ||||||
|     await addEntitySync("recent_notes", notePath, sourceId); |     await addEntitySync("recent_notes", noteTreeId, sourceId); | ||||||
| } | } | ||||||
|  |  | ||||||
| async function addEntitySync(entityName, entityId, sourceId) { | async function addEntitySync(entityName, entityId, sourceId) { | ||||||
|   | |||||||
| @@ -40,7 +40,9 @@ async function updateNoteHistory(entity, sourceId) { | |||||||
|     const orig = await sql.getFirstOrNull("SELECT * FROM notes_history WHERE note_history_id = ?", [entity.note_history_id]); |     const orig = await sql.getFirstOrNull("SELECT * FROM notes_history WHERE note_history_id = ?", [entity.note_history_id]); | ||||||
|  |  | ||||||
|     await sql.doInTransaction(async () => { |     await sql.doInTransaction(async () => { | ||||||
|         if (orig === null || orig.date_modified_to < entity.date_modified_to) { |         // we update note history even if date modified to is the same because the only thing which might have changed | ||||||
|  |         // is the protected status (and correnspondingly note_title and note_text) which doesn't affect the date_modified_to | ||||||
|  |         if (orig === null || orig.date_modified_to <= entity.date_modified_to) { | ||||||
|             await sql.replace('notes_history', entity); |             await sql.replace('notes_history', entity); | ||||||
|  |  | ||||||
|             await sync_table.addNoteHistorySync(entity.note_history_id, sourceId); |             await sync_table.addNoteHistorySync(entity.note_history_id, sourceId); | ||||||
|   | |||||||
| @@ -48,6 +48,5 @@ | |||||||
|     <script>if (typeof module === 'object') {window.module = module; module = undefined;}</script> |     <script>if (typeof module === 'object') {window.module = module; module = undefined;}</script> | ||||||
|  |  | ||||||
|     <link href="libraries/bootstrap/css/bootstrap.css" rel="stylesheet"> |     <link href="libraries/bootstrap/css/bootstrap.css" rel="stylesheet"> | ||||||
|     <script src="libraries/bootstrap/js/bootstrap.js"></script> |  | ||||||
|   </body> |   </body> | ||||||
| </html> | </html> | ||||||
		Reference in New Issue
	
	Block a user