refactoring of note changes / cloning

This commit is contained in:
azivner 2018-01-13 18:02:41 -05:00
parent 4f649c2e21
commit 16eb156033
16 changed files with 349 additions and 315 deletions

View file

@ -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

View file

@ -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);
}

View 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
};
})();

View file

@ -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
}

View file

@ -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");
}

View file

@ -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");
}

View file

@ -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) {

View file

@ -3,7 +3,7 @@
const treeChanges = (function() {
async function moveBeforeNode(nodesToMove, beforeNode) {
for (const nodeToMove of nodesToMove) {
const resp = await server.put('notes/' + nodeToMove.data.note_tree_id + '/move-before/' + beforeNode.data.note_tree_id);
const resp = await server.put('tree/' + nodeToMove.data.note_tree_id + '/move-before/' + beforeNode.data.note_tree_id);
if (!resp.success) {
alert(resp.message);
@ -16,7 +16,7 @@ const treeChanges = (function() {
async function moveAfterNode(nodesToMove, afterNode) {
for (const nodeToMove of nodesToMove) {
const resp = await server.put('notes/' + nodeToMove.data.note_tree_id + '/move-after/' + afterNode.data.note_tree_id);
const resp = await server.put('tree/' + nodeToMove.data.note_tree_id + '/move-after/' + afterNode.data.note_tree_id);
if (!resp.success) {
alert(resp.message);
@ -27,21 +27,9 @@ const treeChanges = (function() {
}
}
// beware that first arg is noteId and second is noteTreeId!
async function cloneNoteAfter(noteId, afterNoteTreeId) {
const resp = await server.put('notes/' + noteId + '/clone-after/' + afterNoteTreeId);
if (!resp.success) {
alert(resp.message);
return;
}
await noteTree.reload();
}
async function moveToNode(nodesToMove, toNode) {
for (const nodeToMove of nodesToMove) {
const resp = await server.put('notes/' + nodeToMove.data.note_tree_id + '/move-to/' + toNode.data.note_id);
const resp = await server.put('tree/' + nodeToMove.data.note_tree_id + '/move-to/' + toNode.data.note_id);
if (!resp.success) {
alert(resp.message);
@ -65,25 +53,12 @@ const treeChanges = (function() {
}
}
async function cloneNoteTo(childNoteId, parentNoteId, prefix) {
const resp = await server.put('notes/' + childNoteId + '/clone-to/' + parentNoteId, {
prefix: prefix
});
if (!resp.success) {
alert(resp.message);
return;
}
await noteTree.reload();
}
async function deleteNode(node) {
if (!confirm('Are you sure you want to delete note "' + node.title + '" and all its sub-notes?')) {
return;
}
await server.remove('notes/' + node.data.note_tree_id);
await server.remove('tree/' + node.data.note_tree_id);
if (!isTopLevelNode(node) && node.getParent().getChildren().length <= 1) {
node.getParent().folder = false;
@ -119,7 +94,7 @@ const treeChanges = (function() {
return;
}
const resp = await server.put('notes/' + node.data.note_tree_id + '/move-after/' + node.getParent().data.note_tree_id);
const resp = await server.put('tree/' + node.data.note_tree_id + '/move-after/' + node.getParent().data.note_tree_id);
if (!resp.success) {
alert(resp.message);
@ -153,8 +128,6 @@ const treeChanges = (function() {
moveAfterNode,
moveToNode,
deleteNode,
moveNodeUpInHierarchy,
cloneNoteAfter,
cloneNoteTo
moveNodeUpInHierarchy
};
})();

84
routes/api/cloning.js Normal file
View 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;

View file

@ -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 + '%';

View file

@ -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
View 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;

View file

@ -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);

View file

@ -1,3 +1,5 @@
"use strict";
const sql = require('./sql');
const utils = require('./utils');
const sync_table = require('./sync_table');

84
services/tree.js Normal file
View 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
};

View file

@ -418,6 +418,7 @@
<!-- Tree scripts -->
<script src="javascripts/note_tree.js"></script>
<script src="javascripts/tree_changes.js"></script>
<script src="javascripts/cloning.js"></script>
<script src="javascripts/tree_utils.js"></script>
<script src="javascripts/drag_and_drop.js"></script>
<script src="javascripts/context_menu.js"></script>