mirror of
				https://github.com/TriliumNext/Notes.git
				synced 2025-10-31 21:11:30 +08:00 
			
		
		
		
	better conflict detection
This commit is contained in:
		
							parent
							
								
									3585982758
								
							
						
					
					
						commit
						ad7fa5e096
					
				| @ -11,7 +11,7 @@ const contextMenu = (function() { | |||||||
|             for (const nodeKey of clipboardIds) { |             for (const nodeKey of clipboardIds) { | ||||||
|                 const subjectNode = treeUtils.getNodeByKey(nodeKey); |                 const subjectNode = treeUtils.getNodeByKey(nodeKey); | ||||||
| 
 | 
 | ||||||
|                 treeChanges.moveAfterNode(subjectNode, node); |                 treeChanges.moveAfterNode([subjectNode], node); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             clipboardIds = []; |             clipboardIds = []; | ||||||
| @ -37,7 +37,7 @@ const contextMenu = (function() { | |||||||
|             for (const nodeKey of clipboardIds) { |             for (const nodeKey of clipboardIds) { | ||||||
|                 const subjectNode = treeUtils.getNodeByKey(nodeKey); |                 const subjectNode = treeUtils.getNodeByKey(nodeKey); | ||||||
| 
 | 
 | ||||||
|                 treeChanges.moveToNode(subjectNode, node); |                 treeChanges.moveToNode([subjectNode], node); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             clipboardIds = []; |             clipboardIds = []; | ||||||
| @ -47,7 +47,6 @@ const contextMenu = (function() { | |||||||
|             for (const noteId of clipboardIds) { |             for (const noteId of clipboardIds) { | ||||||
|                 treeChanges.cloneNoteTo(noteId, node.data.note_id); |                 treeChanges.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
 | ||||||
|         } |         } | ||||||
|         else if (clipboardIds.length === 0) { |         else if (clipboardIds.length === 0) { | ||||||
|  | |||||||
| @ -478,22 +478,16 @@ const noteTree = (function() { | |||||||
|             "ctrl+c": () => { |             "ctrl+c": () => { | ||||||
|                 contextMenu.copy(getSelectedNodes()); |                 contextMenu.copy(getSelectedNodes()); | ||||||
| 
 | 
 | ||||||
|                 showMessage("Note(s) copied into clipboard."); |  | ||||||
| 
 |  | ||||||
|                 return false; |                 return false; | ||||||
|             }, |             }, | ||||||
|             "ctrl+x": () => { |             "ctrl+x": () => { | ||||||
|                 contextMenu.cut(getSelectedNodes()); |                 contextMenu.cut(getSelectedNodes()); | ||||||
| 
 | 
 | ||||||
|                 showMessage("Note(s) cut into clipboard."); |  | ||||||
| 
 |  | ||||||
|                 return false; |                 return false; | ||||||
|             }, |             }, | ||||||
|             "ctrl+v": node => { |             "ctrl+v": node => { | ||||||
|                 contextMenu.pasteInto(node); |                 contextMenu.pasteInto(node); | ||||||
| 
 | 
 | ||||||
|                 showMessage("Note(s) pasted from clipboard into current note."); |  | ||||||
| 
 |  | ||||||
|                 return false; |                 return false; | ||||||
|             }, |             }, | ||||||
|             "ctrl+return": node => { |             "ctrl+return": node => { | ||||||
|  | |||||||
| @ -3,7 +3,12 @@ | |||||||
| 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) { | ||||||
|             await server.put('notes/' + nodeToMove.data.note_tree_id + '/move-before/' + beforeNode.data.note_tree_id); |             const resp = await server.put('notes/' + nodeToMove.data.note_tree_id + '/move-before/' + beforeNode.data.note_tree_id); | ||||||
|  | 
 | ||||||
|  |             if (!resp.success) { | ||||||
|  |                 alert(resp.message); | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
| 
 | 
 | ||||||
|             changeNode(nodeToMove, node => node.moveTo(beforeNode, 'before')); |             changeNode(nodeToMove, node => node.moveTo(beforeNode, 'before')); | ||||||
|         } |         } | ||||||
| @ -11,7 +16,12 @@ const treeChanges = (function() { | |||||||
| 
 | 
 | ||||||
|     async function moveAfterNode(nodesToMove, afterNode) { |     async function moveAfterNode(nodesToMove, afterNode) { | ||||||
|         for (const nodeToMove of nodesToMove) { |         for (const nodeToMove of nodesToMove) { | ||||||
|             await server.put('notes/' + nodeToMove.data.note_tree_id + '/move-after/' + afterNode.data.note_tree_id); |             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')); |             changeNode(nodeToMove, node => node.moveTo(afterNode, 'after')); | ||||||
|         } |         } | ||||||
| @ -31,7 +41,12 @@ const treeChanges = (function() { | |||||||
| 
 | 
 | ||||||
|     async function moveToNode(nodesToMove, toNode) { |     async function moveToNode(nodesToMove, toNode) { | ||||||
|         for (const nodeToMove of nodesToMove) { |         for (const nodeToMove of nodesToMove) { | ||||||
|             await server.put('notes/' + nodeToMove.data.note_tree_id + '/move-to/' + toNode.data.note_id); |             const resp = await server.put('notes/' + nodeToMove.data.note_tree_id + '/move-to/' + toNode.data.note_id); | ||||||
|  | 
 | ||||||
|  |             if (!resp.success) { | ||||||
|  |                 alert(resp.message); | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
| 
 | 
 | ||||||
|             changeNode(nodeToMove, node => { |             changeNode(nodeToMove, node => { | ||||||
|                 // first expand which will force lazy load and only then move the node
 |                 // first expand which will force lazy load and only then move the node
 | ||||||
| @ -100,7 +115,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; | ||||||
|  | |||||||
| @ -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,29 +209,51 @@ 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; | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         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) => { | ||||||
|  | |||||||
| @ -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> | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 azivner
						azivner