diff --git a/README.md b/README.md
index a645f72a3..98d450f41 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/plugins/reddit.js b/plugins/reddit.js
index cb68b2612..d90073f7f 100644
--- a/plugins/reddit.js
+++ b/plugins/reddit.js
@@ -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);
}
diff --git a/public/javascripts/cloning.js b/public/javascripts/cloning.js
new file mode 100644
index 000000000..6be426b7d
--- /dev/null
+++ b/public/javascripts/cloning.js
@@ -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
+ };
+})();
\ No newline at end of file
diff --git a/public/javascripts/context_menu.js b/public/javascripts/context_menu.js
index 0f1864c60..981bd8fa3 100644
--- a/public/javascripts/context_menu.js
+++ b/public/javascripts/context_menu.js
@@ -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
}
diff --git a/public/javascripts/dialogs/add_link.js b/public/javascripts/dialogs/add_link.js
index 67a960e27..15b3d39ab 100644
--- a/public/javascripts/dialogs/add_link.js
+++ b/public/javascripts/dialogs/add_link.js
@@ -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");
}
diff --git a/public/javascripts/dialogs/recent_notes.js b/public/javascripts/dialogs/recent_notes.js
index 397be4e69..db174065b 100644
--- a/public/javascripts/dialogs/recent_notes.js
+++ b/public/javascripts/dialogs/recent_notes.js
@@ -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");
}
diff --git a/public/javascripts/note_tree.js b/public/javascripts/note_tree.js
index 0a80a4caf..834b6895c 100644
--- a/public/javascripts/note_tree.js
+++ b/public/javascripts/note_tree.js
@@ -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) {
diff --git a/public/javascripts/tree_changes.js b/public/javascripts/tree_changes.js
index 0378ece5a..3a09f9810 100644
--- a/public/javascripts/tree_changes.js
+++ b/public/javascripts/tree_changes.js
@@ -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
};
})();
\ No newline at end of file
diff --git a/routes/api/cloning.js b/routes/api/cloning.js
new file mode 100644
index 000000000..2ada25e62
--- /dev/null
+++ b/routes/api/cloning.js
@@ -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;
\ No newline at end of file
diff --git a/routes/api/notes.js b/routes/api/notes.js
index 93dec96a7..1a2b83d36 100644
--- a/routes/api/notes.js
+++ b/routes/api/notes.js
@@ -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 + '%';
diff --git a/routes/api/notes_move.js b/routes/api/notes_move.js
deleted file mode 100644
index b75b17ab7..000000000
--- a/routes/api/notes_move.js
+++ /dev/null
@@ -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;
\ No newline at end of file
diff --git a/routes/api/tree_changes.js b/routes/api/tree_changes.js
new file mode 100644
index 000000000..6dd603e71
--- /dev/null
+++ b/routes/api/tree_changes.js
@@ -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;
\ No newline at end of file
diff --git a/routes/routes.js b/routes/routes.js
index 4e5bb4763..bcc2abe7a 100644
--- a/routes/routes.js
+++ b/routes/routes.js
@@ -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);
diff --git a/services/attributes.js b/services/attributes.js
index ede044a37..e2e68f04f 100644
--- a/services/attributes.js
+++ b/services/attributes.js
@@ -1,3 +1,5 @@
+"use strict";
+
const sql = require('./sql');
const utils = require('./utils');
const sync_table = require('./sync_table');
diff --git a/services/tree.js b/services/tree.js
new file mode 100644
index 000000000..a8152b7a3
--- /dev/null
+++ b/services/tree.js
@@ -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
+};
\ No newline at end of file
diff --git a/views/index.ejs b/views/index.ejs
index 2f5ba0e2e..1e4ad69ca 100644
--- a/views/index.ejs
+++ b/views/index.ejs
@@ -418,6 +418,7 @@
+