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