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