diff --git a/src/public/javascripts/entities/note_short.js b/src/public/javascripts/entities/note_short.js index 55cb896e9..ac9346cec 100644 --- a/src/public/javascripts/entities/note_short.js +++ b/src/public/javascripts/entities/note_short.js @@ -16,13 +16,18 @@ class NoteShort { async getBranches() { const branches = []; - for (const parent of this.treeCache.parents[this.noteId]) { - branches.push(await this.treeCache.getBranchByChildParent(this.noteId, parent.noteId)); + for (const parentNoteId of this.treeCache.parents[this.noteId]) { + branches.push(await this.treeCache.getBranchByChildParent(this.noteId, parentNoteId)); } return branches; } + hasChildren() { + return this.treeCache.children[this.noteId] + && this.treeCache.children[this.noteId].length > 0; + } + async getChildBranches() { if (!this.treeCache.children[this.noteId]) { return []; @@ -30,19 +35,33 @@ class NoteShort { const branches = []; - for (const child of this.treeCache.children[this.noteId]) { - branches.push(await this.treeCache.getBranchByChildParent(child.noteId, this.noteId)); + for (const childNoteId of this.treeCache.children[this.noteId]) { + branches.push(await this.treeCache.getBranchByChildParent(childNoteId, this.noteId)); } return branches; } + async __getNotes(noteIds) { + if (!noteIds) { + return []; + } + + const notes = []; + + for (const noteId of noteIds) { + notes.push(await this.treeCache.getNote(noteId)); + } + + return notes; + } + async getParentNotes() { - return this.treeCache.parents[this.noteId] || []; + return this.__getNotes(this.treeCache.parents[this.noteId]); } async getChildNotes() { - return this.treeCache.children[this.noteId] || []; + return this.__getNotes(this.treeCache.children[this.noteId]); } get toString() { diff --git a/src/public/javascripts/services/tree.js b/src/public/javascripts/services/tree.js index 2c4680754..a89e9c6df 100644 --- a/src/public/javascripts/services/tree.js +++ b/src/public/javascripts/services/tree.js @@ -285,14 +285,14 @@ async function treeInitialized() { } } -function initFancyTree(branch) { - utils.assertArguments(branch); +function initFancyTree(tree) { + utils.assertArguments(tree); $tree.fancytree({ autoScroll: true, keyboard: false, // we takover keyboard handling in the hotkeys plugin extensions: ["hotkeys", "filter", "dnd", "clones"], - source: branch, + source: tree, scrollParent: $tree, click: (event, data) => { const targetType = data.targetType; @@ -375,7 +375,7 @@ async function loadTree() { startNotePath = getNotePathFromAddress(); } - return await treeBuilder.prepareTree(resp.notes, resp.branches); + return await treeBuilder.prepareTree(resp.notes, resp.branches, resp.parentToChildren); } function collapseTree(node = null) { diff --git a/src/public/javascripts/services/tree_builder.js b/src/public/javascripts/services/tree_builder.js index 9f85ee4bb..2631cd68d 100644 --- a/src/public/javascripts/services/tree_builder.js +++ b/src/public/javascripts/services/tree_builder.js @@ -5,10 +5,10 @@ import server from "./server.js"; import treeCache from "./tree_cache.js"; import messagingService from "./messaging.js"; -async function prepareTree(noteRows, branchRows) { - utils.assertArguments(noteRows); +async function prepareTree(noteRows, branchRows, parentToChildren) { + utils.assertArguments(noteRows, branchRows, parentToChildren); - treeCache.load(noteRows, branchRows); + treeCache.load(noteRows, branchRows, parentToChildren); return await prepareRealBranch(await treeCache.getNote('root')); } @@ -49,9 +49,7 @@ async function prepareRealBranch(parentNote) { expanded: note.type !== 'search' && branch.isExpanded }; - const hasChildren = (await note.getChildNotes()).length > 0; - - if (hasChildren || note.type === 'search') { + if (note.hasChildren() || note.type === 'search') { node.folder = true; if (node.expanded && note.type !== 'search') { diff --git a/src/public/javascripts/services/tree_cache.js b/src/public/javascripts/services/tree_cache.js index e2552bd00..d3f9aca48 100644 --- a/src/public/javascripts/services/tree_cache.js +++ b/src/public/javascripts/services/tree_cache.js @@ -2,45 +2,82 @@ import utils from "./utils.js"; import Branch from "../entities/branch.js"; import NoteShort from "../entities/note_short.js"; import infoService from "./info.js"; +import server from "./server.js"; class TreeCache { - load(noteRows, branchRows) { - this.parents = []; - this.children = []; + load(noteRows, branchRows, parentToChildren) { + this.parents = {}; + this.children = {}; this.childParentToBranch = {}; /** @type {Object.} */ this.notes = {}; + + /** @type {Object.} */ + this.branches = {}; + + this.addResp(noteRows, branchRows, parentToChildren); + } + + addResp(noteRows, branchRows, parentToChildren) { for (const noteRow of noteRows) { const note = new NoteShort(this, noteRow); this.notes[note.noteId] = note; } - /** @type {Object.} */ - this.branches = {}; for (const branchRow of branchRows) { const branch = new Branch(this, branchRow); this.addBranch(branch); } + + for (const relation of parentToChildren) { + this.addBranchRelationship(relation.branchId, relation.childNoteId, relation.parentNoteId); + } } /** @return NoteShort */ async getNote(noteId) { + if (this.notes[noteId] === undefined) { + const resp = await server.post('tree/load', { + noteIds: [noteId] + }); + + this.addResp(resp.notes, resp.branches, resp.parentToChildren); + } + + if (!this.notes[noteId]) { + throw new Error(`Can't find note ${noteId}`); + } + return this.notes[noteId]; } addBranch(branch) { this.branches[branch.branchId] = branch; - this.parents[branch.noteId] = this.parents[branch.noteId] || []; - this.parents[branch.noteId].push(this.notes[branch.parentNoteId]); + this.addBranchRelationship(branch.branchId, branch.noteId, branch.parentNoteId); + } - this.children[branch.parentNoteId] = this.children[branch.parentNoteId] || []; - this.children[branch.parentNoteId].push(this.notes[branch.noteId]); + addBranchRelationship(branchId, childNoteId, parentNoteId) { + this.addParentChildRelationship(parentNoteId, childNoteId); - this.childParentToBranch[branch.noteId + '-' + branch.parentNoteId] = branch; + this.childParentToBranch[childNoteId + '-' + parentNoteId] = branchId; + } + + addParentChildRelationship(parentNoteId, childNoteId) { + this.parents[childNoteId] = this.parents[childNoteId] || []; + + if (!this.parents[childNoteId].includes(parentNoteId)) { + this.parents[childNoteId].push(parentNoteId); + } + + this.children[parentNoteId] = this.children[parentNoteId] || []; + + if (!this.children[parentNoteId].includes(childNoteId)) { + this.children[parentNoteId].push(childNoteId); + } } add(note, branch) { @@ -51,19 +88,34 @@ class TreeCache { /** @return Branch */ async getBranch(branchId) { + if (this.branches[branchId] === undefined) { + const resp = await server.post('tree/load', { + branchIds: [branchId] + }); + + this.addResp(resp.notes, resp.branches, resp.parentToChildren); + } + + if (!this.branches[branchId]) { + throw new Error(`Can't find branch ${branchId}`); + } + return this.branches[branchId]; } /** @return Branch */ async getBranchByChildParent(childNoteId, parentNoteId) { - const key = (childNoteId + '-' + parentNoteId); - const branch = this.childParentToBranch[key]; + // this will make sure the note and its relationships are loaded + await this.getNote(parentNoteId); - if (!branch) { + const key = (childNoteId + '-' + parentNoteId); + const branchId = this.childParentToBranch[key]; + + if (!branchId) { infoService.throwError("Cannot find branch for child-parent=" + key); } - return branch; + return await this.getBranch(branchId); } /* Move note from one parent to another. */ @@ -78,33 +130,14 @@ class TreeCache { delete treeCache.childParentToBranch[childNoteId + '-' + oldParentNoteId]; // this is correct because we know that oldParentId isn't same as newParentId // remove old associations - treeCache.parents[childNoteId] = treeCache.parents[childNoteId].filter(p => p.noteId !== oldParentNoteId); - treeCache.children[oldParentNoteId] = treeCache.children[oldParentNoteId].filter(ch => ch.noteId !== childNoteId); + treeCache.parents[childNoteId] = treeCache.parents[childNoteId].filter(p => p !== oldParentNoteId); + treeCache.children[oldParentNoteId] = treeCache.children[oldParentNoteId].filter(ch => ch !== childNoteId); // add new associations - treeCache.parents[childNoteId].push(await treeCache.getNote(newParentNoteId)); + treeCache.parents[childNoteId].push(newParentNoteId); treeCache.children[newParentNoteId] = treeCache.children[newParentNoteId] || []; // this might be first child - treeCache.children[newParentNoteId].push(await treeCache.getNote(childNoteId)); - } - - removeParentChildRelation(parentNoteId, childNoteId) { - utils.assertArguments(parentNoteId, childNoteId); - - treeCache.parents[childNoteId] = treeCache.parents[childNoteId].filter(p => p.noteId !== parentNoteId); - treeCache.children[parentNoteId] = treeCache.children[parentNoteId].filter(ch => ch.noteId !== childNoteId); - - delete treeCache.childParentToBranch[childNoteId + '-' + parentNoteId]; - } - - async setParentChildRelation(branchId, parentNoteId, childNoteId) { - treeCache.parents[childNoteId] = treeCache.parents[childNoteId] || []; - treeCache.parents[childNoteId].push(await treeCache.getNote(parentNoteId)); - - treeCache.children[parentNoteId] = treeCache.children[parentNoteId] || []; - treeCache.children[parentNoteId].push(await treeCache.getNote(childNoteId)); - - treeCache.childParentToBranch[childNoteId + '-' + parentNoteId] = await treeCache.getBranch(branchId); + treeCache.children[newParentNoteId].push(childNoteId); } } diff --git a/src/routes/api/tree.js b/src/routes/api/tree.js index 7322e0b40..0aa95dd5c 100644 --- a/src/routes/api/tree.js +++ b/src/routes/api/tree.js @@ -3,7 +3,26 @@ const sql = require('../../services/sql'); const optionService = require('../../services/options'); const protectedSessionService = require('../../services/protected_session'); -const utils = require('../../services/utils'); + +async function getNotes(noteIds) { + const questionMarks = noteIds.map(() => "?").join(","); + + const notes = await sql.getRows(` + SELECT noteId, title, isProtected, type, mime + FROM notes WHERE isDeleted = 0 AND noteId IN (${questionMarks})`, noteIds); + + protectedSessionService.decryptNotes(notes); + + notes.forEach(note => note.isProtected = !!note.isProtected); + return notes; +} + +async function getParentToChildren(noteIds) { + const questionMarks = noteIds.map(() => "?").join(","); + + return await sql.getRows(`SELECT branchId, noteId AS 'childNoteId', parentNoteId FROM branches WHERE isDeleted = 0 + AND parentNoteId IN (${questionMarks})`, noteIds); +} async function getTree() { const branches = await sql.getRows(` @@ -18,34 +37,43 @@ async function getTree() { SELECT branches.* FROM tree JOIN branches USING(branchId);`); const noteIds = branches.map(b => b.noteId); - const questionMarks = branches.map(() => "?").join(","); - const notes = await sql.getRows(` - SELECT noteId, title, isProtected, type, mime - FROM notes WHERE isDeleted = 0 AND noteId IN (${questionMarks})`, noteIds); + const notes = await getNotes(noteIds); - protectedSessionService.decryptNotes(notes); - - notes.forEach(note => note.isProtected = !!note.isProtected); - - const relationships = await sql.getRows(`SELECT noteId, parentNoteId FROM branches WHERE isDeleted = 0 - AND parentNoteId IN (${questionMarks})`, noteIds); - - const parentToChild = {}; - - for (const rel of relationships) { - parentToChild[rel.parentNoteId] = parentToChild[rel.parentNoteId] || []; - parentToChild[rel.parentNoteId].push(rel.noteId); - } + const parentToChildren = await getParentToChildren(noteIds); return { startNotePath: await optionService.getOption('startNotePath'), - branches: branches, - notes: notes, - parentToChild + branches, + notes, + parentToChildren + }; +} + +async function load(req) { + let noteIds = req.body.noteIds; + const branchIds = req.body.branchIds; + + if (branchIds && branchIds.length > 0) { + noteIds = await sql.getColumn(`SELECT noteId FROM branches WHERE isDeleted = 0 AND branchId IN(${branchIds.map(() => "?").join(",")})`, branchIds); + } + + const questionMarks = noteIds.map(() => "?").join(","); + + const branches = await sql.getRows(`SELECT * FROM branches WHERE isDeleted = 0 AND noteId IN (${questionMarks})`, noteIds); + + const notes = await getNotes(noteIds); + + const parentToChildren = await getParentToChildren(noteIds); + + return { + branches, + notes, + parentToChildren }; } module.exports = { - getTree + getTree, + load }; diff --git a/src/routes/routes.js b/src/routes/routes.js index db956aab7..568e9fad4 100644 --- a/src/routes/routes.js +++ b/src/routes/routes.js @@ -99,6 +99,7 @@ function register(app) { route(GET, '/setup', [auth.checkAppNotInitialized], setupRoute.setupPage); apiRoute(GET, '/api/tree', treeApiRoute.getTree); + apiRoute(POST, '/api/tree/load', treeApiRoute.load); apiRoute(PUT, '/api/branches/:branchId/set-prefix', branchesApiRoute.setPrefix); apiRoute(PUT, '/api/branches/:branchId/move-to/:parentNoteId', branchesApiRoute.moveBranchToParent);