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 | ## 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 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 | ## Supported platforms | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,3 +1,5 @@ | |||||||
|  | "use strict"; | ||||||
|  |  | ||||||
| const sql = require('../services/sql'); | const sql = require('../services/sql'); | ||||||
| const notes = require('../services/notes'); | const notes = require('../services/notes'); | ||||||
| const axios = require('axios'); | const axios = require('axios'); | ||||||
| @@ -179,7 +181,7 @@ sql.dbReady.then(async () => { | |||||||
|     let importedComments = 0; |     let importedComments = 0; | ||||||
|  |  | ||||||
|     for (const account of redditAccounts) { |     for (const account of redditAccounts) { | ||||||
|         log.info("Importing account " + account); |         log.info("Reddit: Importing account " + account); | ||||||
|  |  | ||||||
|         importedComments += await importReddit(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') { |         else if (clipboardMode === 'copy') { | ||||||
|             for (const noteId of clipboardIds) { |             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 |             // 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') { |         else if (clipboardMode === 'copy') { | ||||||
|             for (const noteId of clipboardIds) { |             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 |             // 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') { |             else if (linkType === 'selected-to-current') { | ||||||
|                 const prefix = clonePrefixEl.val(); |                 const prefix = clonePrefixEl.val(); | ||||||
|  |  | ||||||
|                 treeChanges.cloneNoteTo(noteId, noteEditor.getCurrentNoteId(), prefix); |                 cloning.cloneNoteTo(noteId, noteEditor.getCurrentNoteId(), prefix); | ||||||
|  |  | ||||||
|                 dialogEl.dialog("close"); |                 dialogEl.dialog("close"); | ||||||
|             } |             } | ||||||
|             else if (linkType === 'current-to-selected') { |             else if (linkType === 'current-to-selected') { | ||||||
|                 const prefix = clonePrefixEl.val(); |                 const prefix = clonePrefixEl.val(); | ||||||
|  |  | ||||||
|                 treeChanges.cloneNoteTo(noteEditor.getCurrentNoteId(), noteId, prefix); |                 cloning.cloneNoteTo(noteEditor.getCurrentNoteId(), noteId, prefix); | ||||||
|  |  | ||||||
|                 dialogEl.dialog("close"); |                 dialogEl.dialog("close"); | ||||||
|             } |             } | ||||||
|   | |||||||
| @@ -86,13 +86,13 @@ const recentNotes = (function() { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     async function addCurrentAsChild() { |     async function addCurrentAsChild() { | ||||||
|         await treeChanges.cloneNoteTo(noteEditor.getCurrentNoteId(), getSelectedNoteId()); |         await cloning.cloneNoteTo(noteEditor.getCurrentNoteId(), getSelectedNoteId()); | ||||||
|  |  | ||||||
|         dialogEl.dialog("close"); |         dialogEl.dialog("close"); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     async function addRecentAsChild() { |     async function addRecentAsChild() { | ||||||
|         await treeChanges.cloneNoteTo(getSelectedNoteId(), noteEditor.getCurrentNoteId()); |         await cloning.cloneNoteTo(getSelectedNoteId(), noteEditor.getCurrentNoteId()); | ||||||
|  |  | ||||||
|         dialogEl.dialog("close"); |         dialogEl.dialog("close"); | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -368,7 +368,7 @@ const noteTree = (function() { | |||||||
|  |  | ||||||
|         const expandedNum = isExpanded ? 1 : 0; |         const expandedNum = isExpanded ? 1 : 0; | ||||||
|  |  | ||||||
|         await server.put('notes/' + noteTreeId + '/expanded/' + expandedNum); |         await server.put('tree/' + noteTreeId + '/expanded/' + expandedNum); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     function setCurrentNotePathToHash(node) { |     function setCurrentNotePathToHash(node) { | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ | |||||||
| const treeChanges = (function() { | const treeChanges = (function() { | ||||||
|     async function moveBeforeNode(nodesToMove, beforeNode) { |     async function moveBeforeNode(nodesToMove, beforeNode) { | ||||||
|         for (const nodeToMove of nodesToMove) { |         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) { |             if (!resp.success) { | ||||||
|                 alert(resp.message); |                 alert(resp.message); | ||||||
| @@ -16,7 +16,7 @@ const treeChanges = (function() { | |||||||
|  |  | ||||||
|     async function moveAfterNode(nodesToMove, afterNode) { |     async function moveAfterNode(nodesToMove, afterNode) { | ||||||
|         for (const nodeToMove of nodesToMove) { |         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) { |             if (!resp.success) { | ||||||
|                 alert(resp.message); |                 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) { |     async function moveToNode(nodesToMove, toNode) { | ||||||
|         for (const nodeToMove of nodesToMove) { |         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) { |             if (!resp.success) { | ||||||
|                 alert(resp.message); |                 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) { |     async function deleteNode(node) { | ||||||
|         if (!confirm('Are you sure you want to delete note "' + node.title + '" and all its sub-notes?')) { |         if (!confirm('Are you sure you want to delete note "' + node.title + '" and all its sub-notes?')) { | ||||||
|             return; |             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) { |         if (!isTopLevelNode(node) && node.getParent().getChildren().length <= 1) { | ||||||
|             node.getParent().folder = false; |             node.getParent().folder = false; | ||||||
| @@ -119,7 +94,7 @@ const treeChanges = (function() { | |||||||
|             return; |             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) { |         if (!resp.success) { | ||||||
|             alert(resp.message); |             alert(resp.message); | ||||||
| @@ -153,8 +128,6 @@ const treeChanges = (function() { | |||||||
|         moveAfterNode, |         moveAfterNode, | ||||||
|         moveToNode, |         moveToNode, | ||||||
|         deleteNode, |         deleteNode, | ||||||
|         moveNodeUpInHierarchy, |         moveNodeUpInHierarchy | ||||||
|         cloneNoteAfter, |  | ||||||
|         cloneNoteTo |  | ||||||
|     }; |     }; | ||||||
| })(); | })(); | ||||||
							
								
								
									
										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({}); |     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) => { | router.get('/', auth.checkApiAuth, wrap(async (req, res, next) => { | ||||||
|     const search = '%' + req.query.search + '%'; |     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 | // API routes | ||||||
| const treeApiRoute = require('./api/tree'); | const treeApiRoute = require('./api/tree'); | ||||||
| const notesApiRoute = require('./api/notes'); | 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 noteHistoryApiRoute = require('./api/note_history'); | ||||||
| const recentChangesApiRoute = require('./api/recent_changes'); | const recentChangesApiRoute = require('./api/recent_changes'); | ||||||
| const settingsApiRoute = require('./api/settings'); | const settingsApiRoute = require('./api/settings'); | ||||||
| @@ -36,7 +37,8 @@ function register(app) { | |||||||
|  |  | ||||||
|     app.use('/api/tree', treeApiRoute); |     app.use('/api/tree', treeApiRoute); | ||||||
|     app.use('/api/notes', notesApiRoute); |     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', attributesRoute); | ||||||
|     app.use('/api/notes-history', noteHistoryApiRoute); |     app.use('/api/notes-history', noteHistoryApiRoute); | ||||||
|     app.use('/api/recent-changes', recentChangesApiRoute); |     app.use('/api/recent-changes', recentChangesApiRoute); | ||||||
|   | |||||||
| @@ -1,3 +1,5 @@ | |||||||
|  | "use strict"; | ||||||
|  |  | ||||||
| const sql = require('./sql'); | const sql = require('./sql'); | ||||||
| const utils = require('./utils'); | const utils = require('./utils'); | ||||||
| const sync_table = require('./sync_table'); | 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 --> |     <!-- Tree scripts --> | ||||||
|     <script src="javascripts/note_tree.js"></script> |     <script src="javascripts/note_tree.js"></script> | ||||||
|     <script src="javascripts/tree_changes.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/tree_utils.js"></script> | ||||||
|     <script src="javascripts/drag_and_drop.js"></script> |     <script src="javascripts/drag_and_drop.js"></script> | ||||||
|     <script src="javascripts/context_menu.js"></script> |     <script src="javascripts/context_menu.js"></script> | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user