mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 02:16:05 +01:00 
			
		
		
		
	refactoring of note changes / cloning
This commit is contained in:
		| @@ -17,7 +17,7 @@ Trilium Notes is a hierarchical note taking application. Picture tells a thousan | ||||
| ## Builds | ||||
|  | ||||
| * If you want to install Trilium on server, follow [this page](https://github.com/zadam/trilium/wiki/Installation-as-webapp) | ||||
| * If you want to use Trilium on the desktop, download binary release for your platfor from [latest release](https://github.com/zadam/trilium/releases/latest), unzip the package and run ```trilium``` executable. | ||||
| * If you want to use Trilium on the desktop, download binary release for your platform from [latest release](https://github.com/zadam/trilium/releases/latest), unzip the package and run ```trilium``` executable. | ||||
|  | ||||
| ## Supported platforms | ||||
|  | ||||
|   | ||||
| @@ -1,3 +1,5 @@ | ||||
| "use strict"; | ||||
|  | ||||
| const sql = require('../services/sql'); | ||||
| const notes = require('../services/notes'); | ||||
| const axios = require('axios'); | ||||
| @@ -179,7 +181,7 @@ sql.dbReady.then(async () => { | ||||
|     let importedComments = 0; | ||||
|  | ||||
|     for (const account of redditAccounts) { | ||||
|         log.info("Importing account " + account); | ||||
|         log.info("Reddit: Importing account " + account); | ||||
|  | ||||
|         importedComments += await importReddit(account); | ||||
|     } | ||||
|   | ||||
							
								
								
									
										33
									
								
								public/javascripts/cloning.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								public/javascripts/cloning.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | ||||
| "use strict"; | ||||
|  | ||||
| const cloning = (function() { | ||||
|     async function cloneNoteTo(childNoteId, parentNoteId, prefix) { | ||||
|         const resp = await server.put('notes/' + childNoteId + '/clone-to/' + parentNoteId, { | ||||
|             prefix: prefix | ||||
|         }); | ||||
|  | ||||
|         if (!resp.success) { | ||||
|             alert(resp.message); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         await noteTree.reload(); | ||||
|     } | ||||
|  | ||||
|     // beware that first arg is noteId and second is noteTreeId! | ||||
|     async function cloneNoteAfter(noteId, afterNoteTreeId) { | ||||
|         const resp = await server.put('notes/' + noteId + '/clone-after/' + afterNoteTreeId); | ||||
|  | ||||
|         if (!resp.success) { | ||||
|             alert(resp.message); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         await noteTree.reload(); | ||||
|     } | ||||
|  | ||||
|     return { | ||||
|         cloneNoteAfter, | ||||
|         cloneNoteTo | ||||
|     }; | ||||
| })(); | ||||
| @@ -19,7 +19,7 @@ const contextMenu = (function() { | ||||
|         } | ||||
|         else if (clipboardMode === 'copy') { | ||||
|             for (const noteId of clipboardIds) { | ||||
|                 treeChanges.cloneNoteAfter(noteId, node.data.note_tree_id); | ||||
|                 cloning.cloneNoteAfter(noteId, node.data.note_tree_id); | ||||
|             } | ||||
|  | ||||
|             // copy will keep clipboardIds and clipboardMode so it's possible to paste into multiple places | ||||
| @@ -45,7 +45,7 @@ const contextMenu = (function() { | ||||
|         } | ||||
|         else if (clipboardMode === 'copy') { | ||||
|             for (const noteId of clipboardIds) { | ||||
|                 treeChanges.cloneNoteTo(noteId, node.data.note_id); | ||||
|                 cloning.cloneNoteTo(noteId, node.data.note_id); | ||||
|             } | ||||
|             // copy will keep clipboardIds and clipboardMode so it's possible to paste into multiple places | ||||
|         } | ||||
|   | ||||
| @@ -78,14 +78,14 @@ const addLink = (function() { | ||||
|             else if (linkType === 'selected-to-current') { | ||||
|                 const prefix = clonePrefixEl.val(); | ||||
|  | ||||
|                 treeChanges.cloneNoteTo(noteId, noteEditor.getCurrentNoteId(), prefix); | ||||
|                 cloning.cloneNoteTo(noteId, noteEditor.getCurrentNoteId(), prefix); | ||||
|  | ||||
|                 dialogEl.dialog("close"); | ||||
|             } | ||||
|             else if (linkType === 'current-to-selected') { | ||||
|                 const prefix = clonePrefixEl.val(); | ||||
|  | ||||
|                 treeChanges.cloneNoteTo(noteEditor.getCurrentNoteId(), noteId, prefix); | ||||
|                 cloning.cloneNoteTo(noteEditor.getCurrentNoteId(), noteId, prefix); | ||||
|  | ||||
|                 dialogEl.dialog("close"); | ||||
|             } | ||||
|   | ||||
| @@ -86,13 +86,13 @@ const recentNotes = (function() { | ||||
|     } | ||||
|  | ||||
|     async function addCurrentAsChild() { | ||||
|         await treeChanges.cloneNoteTo(noteEditor.getCurrentNoteId(), getSelectedNoteId()); | ||||
|         await cloning.cloneNoteTo(noteEditor.getCurrentNoteId(), getSelectedNoteId()); | ||||
|  | ||||
|         dialogEl.dialog("close"); | ||||
|     } | ||||
|  | ||||
|     async function addRecentAsChild() { | ||||
|         await treeChanges.cloneNoteTo(getSelectedNoteId(), noteEditor.getCurrentNoteId()); | ||||
|         await cloning.cloneNoteTo(getSelectedNoteId(), noteEditor.getCurrentNoteId()); | ||||
|  | ||||
|         dialogEl.dialog("close"); | ||||
|     } | ||||
|   | ||||
| @@ -368,7 +368,7 @@ const noteTree = (function() { | ||||
|  | ||||
|         const expandedNum = isExpanded ? 1 : 0; | ||||
|  | ||||
|         await server.put('notes/' + noteTreeId + '/expanded/' + expandedNum); | ||||
|         await server.put('tree/' + noteTreeId + '/expanded/' + expandedNum); | ||||
|     } | ||||
|  | ||||
|     function setCurrentNotePathToHash(node) { | ||||
|   | ||||
| @@ -3,7 +3,7 @@ | ||||
| const treeChanges = (function() { | ||||
|     async function moveBeforeNode(nodesToMove, beforeNode) { | ||||
|         for (const nodeToMove of nodesToMove) { | ||||
|             const resp = await server.put('notes/' + nodeToMove.data.note_tree_id + '/move-before/' + beforeNode.data.note_tree_id); | ||||
|             const resp = await server.put('tree/' + nodeToMove.data.note_tree_id + '/move-before/' + beforeNode.data.note_tree_id); | ||||
|  | ||||
|             if (!resp.success) { | ||||
|                 alert(resp.message); | ||||
| @@ -16,7 +16,7 @@ const treeChanges = (function() { | ||||
|  | ||||
|     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); | ||||
|             const resp = await server.put('tree/' + nodeToMove.data.note_tree_id + '/move-after/' + afterNode.data.note_tree_id); | ||||
|  | ||||
|             if (!resp.success) { | ||||
|                 alert(resp.message); | ||||
| @@ -27,21 +27,9 @@ const treeChanges = (function() { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // beware that first arg is noteId and second is noteTreeId! | ||||
|     async function cloneNoteAfter(noteId, afterNoteTreeId) { | ||||
|         const resp = await server.put('notes/' + noteId + '/clone-after/' + afterNoteTreeId); | ||||
|  | ||||
|         if (!resp.success) { | ||||
|             alert(resp.message); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         await noteTree.reload(); | ||||
|     } | ||||
|  | ||||
|     async function moveToNode(nodesToMove, toNode) { | ||||
|         for (const nodeToMove of nodesToMove) { | ||||
|             const resp = await server.put('notes/' + nodeToMove.data.note_tree_id + '/move-to/' + toNode.data.note_id); | ||||
|             const resp = await server.put('tree/' + nodeToMove.data.note_tree_id + '/move-to/' + toNode.data.note_id); | ||||
|  | ||||
|             if (!resp.success) { | ||||
|                 alert(resp.message); | ||||
| @@ -65,25 +53,12 @@ const treeChanges = (function() { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     async function cloneNoteTo(childNoteId, parentNoteId, prefix) { | ||||
|         const resp = await server.put('notes/' + childNoteId + '/clone-to/' + parentNoteId, { | ||||
|             prefix: prefix | ||||
|         }); | ||||
|  | ||||
|         if (!resp.success) { | ||||
|             alert(resp.message); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         await noteTree.reload(); | ||||
|     } | ||||
|  | ||||
|     async function deleteNode(node) { | ||||
|         if (!confirm('Are you sure you want to delete note "' + node.title + '" and all its sub-notes?')) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         await server.remove('notes/' + node.data.note_tree_id); | ||||
|         await server.remove('tree/' + node.data.note_tree_id); | ||||
|  | ||||
|         if (!isTopLevelNode(node) && node.getParent().getChildren().length <= 1) { | ||||
|             node.getParent().folder = false; | ||||
| @@ -119,7 +94,7 @@ const treeChanges = (function() { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         const resp = await server.put('notes/' + node.data.note_tree_id + '/move-after/' + node.getParent().data.note_tree_id); | ||||
|         const resp = await server.put('tree/' + node.data.note_tree_id + '/move-after/' + node.getParent().data.note_tree_id); | ||||
|  | ||||
|         if (!resp.success) { | ||||
|             alert(resp.message); | ||||
| @@ -153,8 +128,6 @@ const treeChanges = (function() { | ||||
|         moveAfterNode, | ||||
|         moveToNode, | ||||
|         deleteNode, | ||||
|         moveNodeUpInHierarchy, | ||||
|         cloneNoteAfter, | ||||
|         cloneNoteTo | ||||
|         moveNodeUpInHierarchy | ||||
|     }; | ||||
| })(); | ||||
							
								
								
									
										84
									
								
								routes/api/cloning.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								routes/api/cloning.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,84 @@ | ||||
| "use strict"; | ||||
|  | ||||
| const express = require('express'); | ||||
| const router = express.Router(); | ||||
| const sql = require('../../services/sql'); | ||||
| const auth = require('../../services/auth'); | ||||
| const utils = require('../../services/utils'); | ||||
| const sync_table = require('../../services/sync_table'); | ||||
| const wrap = require('express-promise-wrap').wrap; | ||||
| const tree = require('../../services/tree'); | ||||
|  | ||||
| router.put('/:childNoteId/clone-to/:parentNoteId', auth.checkApiAuth, wrap(async (req, res, next) => { | ||||
|     const parentNoteId = req.params.parentNoteId; | ||||
|     const childNoteId = req.params.childNoteId; | ||||
|     const prefix = req.body.prefix; | ||||
|     const sourceId = req.headers.source_id; | ||||
|  | ||||
|     if (!await tree.validateParentChild(res, parentNoteId, childNoteId)) { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     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; | ||||
|  | ||||
|     await sql.doInTransaction(async () => { | ||||
|         const noteTree = { | ||||
|             note_tree_id: utils.newNoteTreeId(), | ||||
|             note_id: childNoteId, | ||||
|             parent_note_id: parentNoteId, | ||||
|             prefix: prefix, | ||||
|             note_position: newNotePos, | ||||
|             is_expanded: 0, | ||||
|             date_modified: utils.nowDate(), | ||||
|             is_deleted: 0 | ||||
|         }; | ||||
|  | ||||
|         await sql.replace("notes_tree", noteTree); | ||||
|  | ||||
|         await sync_table.addNoteTreeSync(noteTree.note_tree_id, sourceId); | ||||
|  | ||||
|         await sql.execute("UPDATE notes_tree SET is_expanded = 1 WHERE note_id = ?", [parentNoteId]); | ||||
|     }); | ||||
|  | ||||
|     res.send({ success: true }); | ||||
| })); | ||||
|  | ||||
| router.put('/:noteId/clone-after/:afterNoteTreeId', auth.checkApiAuth, wrap(async (req, res, next) => { | ||||
|     const noteId = req.params.noteId; | ||||
|     const afterNoteTreeId = req.params.afterNoteTreeId; | ||||
|     const sourceId = req.headers.source_id; | ||||
|  | ||||
|     const afterNote = await tree.getNoteTree(afterNoteTreeId); | ||||
|  | ||||
|     if (!await tree.validateParentChild(res, afterNote.parent_note_id, noteId)) { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     await sql.doInTransaction(async () => { | ||||
|         // we don't change date_modified so other changes are prioritized in case of conflict | ||||
|         // also we would have to sync all those modified note trees otherwise hash checks would fail | ||||
|         await sql.execute("UPDATE notes_tree SET note_position = note_position + 1 WHERE parent_note_id = ? AND note_position > ? AND is_deleted = 0", | ||||
|             [afterNote.parent_note_id, afterNote.note_position]); | ||||
|  | ||||
|         await sync_table.addNoteReorderingSync(afterNote.parent_note_id, sourceId); | ||||
|  | ||||
|         const noteTree = { | ||||
|             note_tree_id: utils.newNoteTreeId(), | ||||
|             note_id: noteId, | ||||
|             parent_note_id: afterNote.parent_note_id, | ||||
|             note_position: afterNote.note_position + 1, | ||||
|             is_expanded: 0, | ||||
|             date_modified: utils.nowDate(), | ||||
|             is_deleted: 0 | ||||
|         }; | ||||
|  | ||||
|         await sql.replace("notes_tree", noteTree); | ||||
|  | ||||
|         await sync_table.addNoteTreeSync(noteTree.note_tree_id, sourceId); | ||||
|     }); | ||||
|  | ||||
|     res.send({ success: true }); | ||||
| })); | ||||
|  | ||||
| module.exports = router; | ||||
| @@ -58,14 +58,6 @@ router.put('/:noteId', auth.checkApiAuth, wrap(async (req, res, next) => { | ||||
|     res.send({}); | ||||
| })); | ||||
|  | ||||
| router.delete('/:noteTreeId', auth.checkApiAuth, wrap(async (req, res, next) => { | ||||
|     await sql.doInTransaction(async () => { | ||||
|         await notes.deleteNote(req.params.noteTreeId, req.headers.source_id); | ||||
|     }); | ||||
|  | ||||
|     res.send({}); | ||||
| })); | ||||
|  | ||||
| router.get('/', auth.checkApiAuth, wrap(async (req, res, next) => { | ||||
|     const search = '%' + req.query.search + '%'; | ||||
|  | ||||
|   | ||||
| @@ -1,263 +0,0 @@ | ||||
| "use strict"; | ||||
|  | ||||
| const express = require('express'); | ||||
| const router = express.Router(); | ||||
| const sql = require('../../services/sql'); | ||||
| const auth = require('../../services/auth'); | ||||
| const utils = require('../../services/utils'); | ||||
| const sync_table = require('../../services/sync_table'); | ||||
| const wrap = require('express-promise-wrap').wrap; | ||||
|  | ||||
| /** | ||||
|  * Code in this file deals with moving and cloning note tree rows. Relationship between note and parent note is unique | ||||
|  * for not deleted note trees. There may be multiple deleted note-parent note relationships. | ||||
|  */ | ||||
|  | ||||
| router.put('/:noteTreeId/move-to/:parentNoteId', auth.checkApiAuth, wrap(async (req, res, next) => { | ||||
|     const noteTreeId = req.params.noteTreeId; | ||||
|     const parentNoteId = req.params.parentNoteId; | ||||
|     const sourceId = req.headers.source_id; | ||||
|  | ||||
|     const noteToMove = await getNoteTree(noteTreeId); | ||||
|  | ||||
|     if (!await validateParentChild(res, parentNoteId, noteToMove.note_id, noteTreeId)) { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     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 now = utils.nowDate(); | ||||
|  | ||||
|     await sql.doInTransaction(async () => { | ||||
|         await sql.execute("UPDATE notes_tree SET parent_note_id = ?, note_position = ?, date_modified = ? WHERE note_tree_id = ?", | ||||
|             [parentNoteId, newNotePos, now, noteTreeId]); | ||||
|  | ||||
|         await sync_table.addNoteTreeSync(noteTreeId, sourceId); | ||||
|     }); | ||||
|  | ||||
|     res.send({ success: true }); | ||||
| })); | ||||
|  | ||||
| router.put('/:noteTreeId/move-before/:beforeNoteTreeId', auth.checkApiAuth, wrap(async (req, res, next) => { | ||||
|     const noteTreeId = req.params.noteTreeId; | ||||
|     const beforeNoteTreeId = req.params.beforeNoteTreeId; | ||||
|     const sourceId = req.headers.source_id; | ||||
|  | ||||
|     const noteToMove = await getNoteTree(noteTreeId); | ||||
|     const beforeNote = await getNoteTree(beforeNoteTreeId); | ||||
|  | ||||
|     if (!await validateParentChild(res, beforeNote.parent_note_id, noteToMove.note_id, noteTreeId)) { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     await sql.doInTransaction(async () => { | ||||
|         // we don't change date_modified so other changes are prioritized in case of conflict | ||||
|         // also we would have to sync all those modified note trees otherwise hash checks would fail | ||||
|         await sql.execute("UPDATE notes_tree SET note_position = note_position + 1 WHERE parent_note_id = ? AND note_position >= ? AND is_deleted = 0", | ||||
|             [beforeNote.parent_note_id, beforeNote.note_position]); | ||||
|  | ||||
|         await sync_table.addNoteReorderingSync(beforeNote.parent_note_id, sourceId); | ||||
|  | ||||
|         const now = utils.nowDate(); | ||||
|  | ||||
|         await sql.execute("UPDATE notes_tree SET parent_note_id = ?, note_position = ?, date_modified = ? WHERE note_tree_id = ?", | ||||
|             [beforeNote.parent_note_id, beforeNote.note_position, now, noteTreeId]); | ||||
|  | ||||
|         await sync_table.addNoteTreeSync(noteTreeId, sourceId); | ||||
|     }); | ||||
|  | ||||
|     res.send({ success: true }); | ||||
| })); | ||||
|  | ||||
| router.put('/:noteTreeId/move-after/:afterNoteTreeId', auth.checkApiAuth, wrap(async (req, res, next) => { | ||||
|     const noteTreeId = req.params.noteTreeId; | ||||
|     const afterNoteTreeId = req.params.afterNoteTreeId; | ||||
|     const sourceId = req.headers.source_id; | ||||
|  | ||||
|     const noteToMove = await getNoteTree(noteTreeId); | ||||
|     const afterNote = await getNoteTree(afterNoteTreeId); | ||||
|  | ||||
|     if (!await validateParentChild(res, afterNote.parent_note_id, noteToMove.note_id, noteTreeId)) { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     await sql.doInTransaction(async () => { | ||||
|         // we don't change date_modified so other changes are prioritized in case of conflict | ||||
|         // also we would have to sync all those modified note trees otherwise hash checks would fail | ||||
|         await sql.execute("UPDATE notes_tree SET note_position = note_position + 1 WHERE parent_note_id = ? AND note_position > ? AND is_deleted = 0", | ||||
|             [afterNote.parent_note_id, afterNote.note_position]); | ||||
|  | ||||
|         await sync_table.addNoteReorderingSync(afterNote.parent_note_id, sourceId); | ||||
|  | ||||
|         await sql.execute("UPDATE notes_tree SET parent_note_id = ?, note_position = ?, date_modified = ? WHERE note_tree_id = ?", | ||||
|             [afterNote.parent_note_id, afterNote.note_position + 1, utils.nowDate(), noteTreeId]); | ||||
|  | ||||
|         await sync_table.addNoteTreeSync(noteTreeId, sourceId); | ||||
|     }); | ||||
|  | ||||
|     res.send({ success: true }); | ||||
| })); | ||||
|  | ||||
| router.put('/:childNoteId/clone-to/:parentNoteId', auth.checkApiAuth, wrap(async (req, res, next) => { | ||||
|     const parentNoteId = req.params.parentNoteId; | ||||
|     const childNoteId = req.params.childNoteId; | ||||
|     const prefix = req.body.prefix; | ||||
|     const sourceId = req.headers.source_id; | ||||
|  | ||||
|     if (!await validateParentChild(res, parentNoteId, childNoteId)) { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     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; | ||||
|  | ||||
|     await sql.doInTransaction(async () => { | ||||
|         const noteTree = { | ||||
|             note_tree_id: utils.newNoteTreeId(), | ||||
|             note_id: childNoteId, | ||||
|             parent_note_id: parentNoteId, | ||||
|             prefix: prefix, | ||||
|             note_position: newNotePos, | ||||
|             is_expanded: 0, | ||||
|             date_modified: utils.nowDate(), | ||||
|             is_deleted: 0 | ||||
|         }; | ||||
|  | ||||
|         await sql.replace("notes_tree", noteTree); | ||||
|  | ||||
|         await sync_table.addNoteTreeSync(noteTree.note_tree_id, sourceId); | ||||
|  | ||||
|         await sql.execute("UPDATE notes_tree SET is_expanded = 1 WHERE note_id = ?", [parentNoteId]); | ||||
|     }); | ||||
|  | ||||
|     res.send({ success: true }); | ||||
| })); | ||||
|  | ||||
| router.put('/:noteId/clone-after/:afterNoteTreeId', auth.checkApiAuth, wrap(async (req, res, next) => { | ||||
|     const noteId = req.params.noteId; | ||||
|     const afterNoteTreeId = req.params.afterNoteTreeId; | ||||
|     const sourceId = req.headers.source_id; | ||||
|  | ||||
|     const afterNote = await getNoteTree(afterNoteTreeId); | ||||
|  | ||||
|     if (!await validateParentChild(res, afterNote.parent_note_id, noteId)) { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     await sql.doInTransaction(async () => { | ||||
|         // we don't change date_modified so other changes are prioritized in case of conflict | ||||
|         // also we would have to sync all those modified note trees otherwise hash checks would fail | ||||
|         await sql.execute("UPDATE notes_tree SET note_position = note_position + 1 WHERE parent_note_id = ? AND note_position > ? AND is_deleted = 0", | ||||
|             [afterNote.parent_note_id, afterNote.note_position]); | ||||
|  | ||||
|         await sync_table.addNoteReorderingSync(afterNote.parent_note_id, sourceId); | ||||
|  | ||||
|         const noteTree = { | ||||
|             note_tree_id: utils.newNoteTreeId(), | ||||
|             note_id: noteId, | ||||
|             parent_note_id: afterNote.parent_note_id, | ||||
|             note_position: afterNote.note_position + 1, | ||||
|             is_expanded: 0, | ||||
|             date_modified: utils.nowDate(), | ||||
|             is_deleted: 0 | ||||
|         }; | ||||
|  | ||||
|         await sql.replace("notes_tree", noteTree); | ||||
|  | ||||
|         await sync_table.addNoteTreeSync(noteTree.note_tree_id, sourceId); | ||||
|     }); | ||||
|  | ||||
|     res.send({ success: true }); | ||||
| })); | ||||
|  | ||||
| async function loadSubTreeNoteIds(parentNoteId, subTreeNoteIds) { | ||||
|     subTreeNoteIds.push(parentNoteId); | ||||
|  | ||||
|     const children = await sql.getFirstColumn("SELECT note_id FROM notes_tree WHERE parent_note_id = ? AND is_deleted = 0", [parentNoteId]); | ||||
|  | ||||
|     for (const childNoteId of children) { | ||||
|         await loadSubTreeNoteIds(childNoteId, subTreeNoteIds); | ||||
|     } | ||||
| } | ||||
|  | ||||
| async function getNoteTree(noteTreeId) { | ||||
|     return sql.getFirst("SELECT * FROM notes_tree WHERE note_tree_id = ?", [noteTreeId]); | ||||
| } | ||||
|  | ||||
| async function validateParentChild(res, parentNoteId, childNoteId, noteTreeId = null) { | ||||
|     const existing = await getExistingNoteTree(parentNoteId, childNoteId); | ||||
|  | ||||
|     if (existing && (noteTreeId === null || existing.note_tree_id !== noteTreeId)) { | ||||
|         res.send({ | ||||
|             success: false, | ||||
|             message: 'This note already exists in the target.' | ||||
|         }); | ||||
|  | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|     if (!await checkTreeCycle(parentNoteId, childNoteId)) { | ||||
|         res.send({ | ||||
|             success: false, | ||||
|             message: 'Moving note here would create cycle.' | ||||
|         }); | ||||
|  | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|     return true; | ||||
| } | ||||
|  | ||||
| async function getExistingNoteTree(parentNoteId, childNoteId) { | ||||
|     return await sql.getFirst('SELECT * FROM notes_tree WHERE note_id = ? AND parent_note_id = ? AND is_deleted = 0', [childNoteId, parentNoteId]); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * 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') { | ||||
|             return true; | ||||
|         } | ||||
|  | ||||
|         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; | ||||
|         } | ||||
|  | ||||
|         const parentNoteIds = await sql.getFirstColumn("SELECT DISTINCT parent_note_id FROM notes_tree WHERE note_id = ? AND is_deleted = 0", [parentNoteId]); | ||||
|  | ||||
|         for (const pid of parentNoteIds) { | ||||
|             if (!await checkTreeCycleInner(pid)) { | ||||
|                 return false; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     return await checkTreeCycleInner(parentNoteId); | ||||
| } | ||||
|  | ||||
| router.put('/:noteTreeId/expanded/:expanded', auth.checkApiAuth, wrap(async (req, res, next) => { | ||||
|     const noteTreeId = req.params.noteTreeId; | ||||
|     const expanded = req.params.expanded; | ||||
|  | ||||
|     await sql.doInTransaction(async () => { | ||||
|         await sql.execute("UPDATE notes_tree SET is_expanded = ? WHERE note_tree_id = ?", [expanded, noteTreeId]); | ||||
|  | ||||
|         // we don't sync expanded attribute | ||||
|     }); | ||||
|  | ||||
|     res.send({}); | ||||
| })); | ||||
|  | ||||
| module.exports = router; | ||||
							
								
								
									
										124
									
								
								routes/api/tree_changes.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								routes/api/tree_changes.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,124 @@ | ||||
| "use strict"; | ||||
|  | ||||
| const express = require('express'); | ||||
| const router = express.Router(); | ||||
| const sql = require('../../services/sql'); | ||||
| const auth = require('../../services/auth'); | ||||
| const utils = require('../../services/utils'); | ||||
| const sync_table = require('../../services/sync_table'); | ||||
| const tree = require('../../services/tree'); | ||||
| const wrap = require('express-promise-wrap').wrap; | ||||
|  | ||||
| /** | ||||
|  * Code in this file deals with moving and cloning note tree rows. Relationship between note and parent note is unique | ||||
|  * for not deleted note trees. There may be multiple deleted note-parent note relationships. | ||||
|  */ | ||||
|  | ||||
| router.put('/:noteTreeId/move-to/:parentNoteId', auth.checkApiAuth, wrap(async (req, res, next) => { | ||||
|     const noteTreeId = req.params.noteTreeId; | ||||
|     const parentNoteId = req.params.parentNoteId; | ||||
|     const sourceId = req.headers.source_id; | ||||
|  | ||||
|     const noteToMove = await tree.getNoteTree(noteTreeId); | ||||
|  | ||||
|     if (!await tree.validateParentChild(res, parentNoteId, noteToMove.note_id, noteTreeId)) { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     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 now = utils.nowDate(); | ||||
|  | ||||
|     await sql.doInTransaction(async () => { | ||||
|         await sql.execute("UPDATE notes_tree SET parent_note_id = ?, note_position = ?, date_modified = ? WHERE note_tree_id = ?", | ||||
|             [parentNoteId, newNotePos, now, noteTreeId]); | ||||
|  | ||||
|         await sync_table.addNoteTreeSync(noteTreeId, sourceId); | ||||
|     }); | ||||
|  | ||||
|     res.send({ success: true }); | ||||
| })); | ||||
|  | ||||
| router.put('/:noteTreeId/move-before/:beforeNoteTreeId', auth.checkApiAuth, wrap(async (req, res, next) => { | ||||
|     const noteTreeId = req.params.noteTreeId; | ||||
|     const beforeNoteTreeId = req.params.beforeNoteTreeId; | ||||
|     const sourceId = req.headers.source_id; | ||||
|  | ||||
|     const noteToMove = await tree.getNoteTree(noteTreeId); | ||||
|     const beforeNote = await tree.getNoteTree(beforeNoteTreeId); | ||||
|  | ||||
|     if (!await tree.validateParentChild(res, beforeNote.parent_note_id, noteToMove.note_id, noteTreeId)) { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     await sql.doInTransaction(async () => { | ||||
|         // we don't change date_modified so other changes are prioritized in case of conflict | ||||
|         // also we would have to sync all those modified note trees otherwise hash checks would fail | ||||
|         await sql.execute("UPDATE notes_tree SET note_position = note_position + 1 WHERE parent_note_id = ? AND note_position >= ? AND is_deleted = 0", | ||||
|             [beforeNote.parent_note_id, beforeNote.note_position]); | ||||
|  | ||||
|         await sync_table.addNoteReorderingSync(beforeNote.parent_note_id, sourceId); | ||||
|  | ||||
|         const now = utils.nowDate(); | ||||
|  | ||||
|         await sql.execute("UPDATE notes_tree SET parent_note_id = ?, note_position = ?, date_modified = ? WHERE note_tree_id = ?", | ||||
|             [beforeNote.parent_note_id, beforeNote.note_position, now, noteTreeId]); | ||||
|  | ||||
|         await sync_table.addNoteTreeSync(noteTreeId, sourceId); | ||||
|     }); | ||||
|  | ||||
|     res.send({ success: true }); | ||||
| })); | ||||
|  | ||||
| router.put('/:noteTreeId/move-after/:afterNoteTreeId', auth.checkApiAuth, wrap(async (req, res, next) => { | ||||
|     const noteTreeId = req.params.noteTreeId; | ||||
|     const afterNoteTreeId = req.params.afterNoteTreeId; | ||||
|     const sourceId = req.headers.source_id; | ||||
|  | ||||
|     const noteToMove = await tree.getNoteTree(noteTreeId); | ||||
|     const afterNote = await tree.getNoteTree(afterNoteTreeId); | ||||
|  | ||||
|     if (!await tree.validateParentChild(res, afterNote.parent_note_id, noteToMove.note_id, noteTreeId)) { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     await sql.doInTransaction(async () => { | ||||
|         // we don't change date_modified so other changes are prioritized in case of conflict | ||||
|         // also we would have to sync all those modified note trees otherwise hash checks would fail | ||||
|         await sql.execute("UPDATE notes_tree SET note_position = note_position + 1 WHERE parent_note_id = ? AND note_position > ? AND is_deleted = 0", | ||||
|             [afterNote.parent_note_id, afterNote.note_position]); | ||||
|  | ||||
|         await sync_table.addNoteReorderingSync(afterNote.parent_note_id, sourceId); | ||||
|  | ||||
|         await sql.execute("UPDATE notes_tree SET parent_note_id = ?, note_position = ?, date_modified = ? WHERE note_tree_id = ?", | ||||
|             [afterNote.parent_note_id, afterNote.note_position + 1, utils.nowDate(), noteTreeId]); | ||||
|  | ||||
|         await sync_table.addNoteTreeSync(noteTreeId, sourceId); | ||||
|     }); | ||||
|  | ||||
|     res.send({ success: true }); | ||||
| })); | ||||
|  | ||||
| router.put('/:noteTreeId/expanded/:expanded', auth.checkApiAuth, wrap(async (req, res, next) => { | ||||
|     const noteTreeId = req.params.noteTreeId; | ||||
|     const expanded = req.params.expanded; | ||||
|  | ||||
|     await sql.doInTransaction(async () => { | ||||
|         await sql.execute("UPDATE notes_tree SET is_expanded = ? WHERE note_tree_id = ?", [expanded, noteTreeId]); | ||||
|  | ||||
|         // we don't sync expanded attribute | ||||
|     }); | ||||
|  | ||||
|     res.send({}); | ||||
| })); | ||||
|  | ||||
| router.delete('/:noteTreeId', auth.checkApiAuth, wrap(async (req, res, next) => { | ||||
|     await sql.doInTransaction(async () => { | ||||
|         await notes.deleteNote(req.params.noteTreeId, req.headers.source_id); | ||||
|     }); | ||||
|  | ||||
|     res.send({}); | ||||
| })); | ||||
|  | ||||
| module.exports = router; | ||||
| @@ -7,7 +7,8 @@ const setupRoute = require('./setup'); | ||||
| // API routes | ||||
| const treeApiRoute = require('./api/tree'); | ||||
| const notesApiRoute = require('./api/notes'); | ||||
| const notesMoveApiRoute = require('./api/notes_move'); | ||||
| const treeChangesApiRoute = require('./api/tree_changes'); | ||||
| const cloningApiRoute = require('./api/cloning'); | ||||
| const noteHistoryApiRoute = require('./api/note_history'); | ||||
| const recentChangesApiRoute = require('./api/recent_changes'); | ||||
| const settingsApiRoute = require('./api/settings'); | ||||
| @@ -36,7 +37,8 @@ function register(app) { | ||||
|  | ||||
|     app.use('/api/tree', treeApiRoute); | ||||
|     app.use('/api/notes', notesApiRoute); | ||||
|     app.use('/api/notes', notesMoveApiRoute); | ||||
|     app.use('/api/tree', treeChangesApiRoute); | ||||
|     app.use('/api/notes', cloningApiRoute); | ||||
|     app.use('/api/notes', attributesRoute); | ||||
|     app.use('/api/notes-history', noteHistoryApiRoute); | ||||
|     app.use('/api/recent-changes', recentChangesApiRoute); | ||||
|   | ||||
| @@ -1,3 +1,5 @@ | ||||
| "use strict"; | ||||
|  | ||||
| const sql = require('./sql'); | ||||
| const utils = require('./utils'); | ||||
| const sync_table = require('./sync_table'); | ||||
|   | ||||
							
								
								
									
										84
									
								
								services/tree.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								services/tree.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,84 @@ | ||||
| "use strict"; | ||||
|  | ||||
| const sql = require('./sql'); | ||||
|  | ||||
| async function validateParentChild(res, parentNoteId, childNoteId, noteTreeId = null) { | ||||
|     const existing = await getExistingNoteTree(parentNoteId, childNoteId); | ||||
|  | ||||
|     if (existing && (noteTreeId === null || existing.note_tree_id !== noteTreeId)) { | ||||
|         res.send({ | ||||
|             success: false, | ||||
|             message: 'This note already exists in the target.' | ||||
|         }); | ||||
|  | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|     if (!await checkTreeCycle(parentNoteId, childNoteId)) { | ||||
|         res.send({ | ||||
|             success: false, | ||||
|             message: 'Moving note here would create cycle.' | ||||
|         }); | ||||
|  | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|     return true; | ||||
| } | ||||
|  | ||||
| async function getExistingNoteTree(parentNoteId, childNoteId) { | ||||
|     return await sql.getFirst('SELECT * FROM notes_tree WHERE note_id = ? AND parent_note_id = ? AND is_deleted = 0', [childNoteId, parentNoteId]); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * 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') { | ||||
|             return true; | ||||
|         } | ||||
|  | ||||
|         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; | ||||
|         } | ||||
|  | ||||
|         const parentNoteIds = await sql.getFirstColumn("SELECT DISTINCT parent_note_id FROM notes_tree WHERE note_id = ? AND is_deleted = 0", [parentNoteId]); | ||||
|  | ||||
|         for (const pid of parentNoteIds) { | ||||
|             if (!await checkTreeCycleInner(pid)) { | ||||
|                 return false; | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     return await checkTreeCycleInner(parentNoteId); | ||||
| } | ||||
|  | ||||
| async function getNoteTree(noteTreeId) { | ||||
|     return sql.getFirst("SELECT * FROM notes_tree WHERE note_tree_id = ?", [noteTreeId]); | ||||
| } | ||||
|  | ||||
| async function loadSubTreeNoteIds(parentNoteId, subTreeNoteIds) { | ||||
|     subTreeNoteIds.push(parentNoteId); | ||||
|  | ||||
|     const children = await sql.getFirstColumn("SELECT note_id FROM notes_tree WHERE parent_note_id = ? AND is_deleted = 0", [parentNoteId]); | ||||
|  | ||||
|     for (const childNoteId of children) { | ||||
|         await loadSubTreeNoteIds(childNoteId, subTreeNoteIds); | ||||
|     } | ||||
| } | ||||
|  | ||||
| module.exports = { | ||||
|     validateParentChild, | ||||
|     getNoteTree | ||||
| }; | ||||
| @@ -418,6 +418,7 @@ | ||||
|     <!-- Tree scripts --> | ||||
|     <script src="javascripts/note_tree.js"></script> | ||||
|     <script src="javascripts/tree_changes.js"></script> | ||||
|     <script src="javascripts/cloning.js"></script> | ||||
|     <script src="javascripts/tree_utils.js"></script> | ||||
|     <script src="javascripts/drag_and_drop.js"></script> | ||||
|     <script src="javascripts/context_menu.js"></script> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user