diff --git a/src/becca/becca_service.js b/src/becca/becca_service.js index f2cebbaae..929be2213 100644 --- a/src/becca/becca_service.js +++ b/src/becca/becca_service.js @@ -83,8 +83,10 @@ function getNoteTitleArrayForPath(notePathArray) { throw new Error(`${notePathArray} is not an array.`); } - if (notePathArray.length === 1 && notePathArray[0] === cls.getHoistedNoteId()) { - return [getNoteTitle(cls.getHoistedNoteId())]; + const hoistedNoteId = cls.getHoistedNoteId(); + + if (notePathArray.length === 1 && notePathArray[0] === hoistedNoteId) { + return [getNoteTitle(hoistedNoteId)]; } const titles = []; @@ -92,6 +94,9 @@ function getNoteTitleArrayForPath(notePathArray) { let parentNoteId = 'root'; let hoistedNotePassed = false; + // this is a notePath from outside of hoisted subtree so full title path needs to be returned + const outsideOfHoistedSubtree = !notePathArray.includes(hoistedNoteId); + for (const noteId of notePathArray) { // start collecting path segment titles only after hoisted note if (hoistedNotePassed) { @@ -100,7 +105,7 @@ function getNoteTitleArrayForPath(notePathArray) { titles.push(title); } - if (noteId === cls.getHoistedNoteId()) { + if (!hoistedNotePassed && (noteId === hoistedNoteId || outsideOfHoistedSubtree)) { hoistedNotePassed = true; } diff --git a/src/becca/entities/branch.js b/src/becca/entities/branch.js index af740bb1d..b35e8875f 100644 --- a/src/becca/entities/branch.js +++ b/src/becca/entities/branch.js @@ -193,9 +193,15 @@ class Branch extends AbstractEntity { beforeSaving() { if (this.notePosition === undefined || this.notePosition === null) { - // TODO finding new position can be refactored into becca - const maxNotePos = sql.getValue('SELECT MAX(notePosition) FROM branches WHERE parentNoteId = ? AND isDeleted = 0', [this.parentNoteId]); - this.notePosition = maxNotePos === null ? 0 : maxNotePos + 10; + let maxNotePos = 0; + + for (const childBranch of this.parentNote.getChildBranches()) { + if (maxNotePos < childBranch.notePosition && childBranch.branchId !== 'hidden') { + maxNotePos = childBranch.notePosition; + } + } + + this.notePosition = maxNotePos + 10; } if (!this.isExpanded) { diff --git a/src/public/app/widgets/note_tree.js b/src/public/app/widgets/note_tree.js index 4d9e791b1..3313612c3 100644 --- a/src/public/app/widgets/note_tree.js +++ b/src/public/app/widgets/note_tree.js @@ -396,6 +396,10 @@ export default class NoteTreeWidget extends NoteContextAwareWidget { autoExpandMS: 600, preventLazyParents: false, dragStart: (node, data) => { + if (['root', 'hidden', 'lb_root', 'lb_availableshortcuts', 'lb_visibleshortcuts'].includes(node.data.noteId)) { + return false; + } + const notes = this.getSelectedOrActiveNodes(node).map(node => ({ noteId: node.data.noteId, branchId: node.data.branchId, @@ -417,7 +421,19 @@ export default class NoteTreeWidget extends NoteContextAwareWidget { data.dataTransfer.setData("text", JSON.stringify(notes)); return true; // allow dragging to start }, - dragEnter: (node, data) => node.data.noteType !== 'search', + dragEnter: (node, data) => { + console.log(data, node.data.noteType); + + if (node.data.noteType === 'search') { + return false; + } else if (node.data.noteId === 'lb_root') { + return false; + } else if (node.data.noteType === 'shortcut') { + return ['before', 'after']; + } else { + return true; + } + }, dragDrop: async (node, data) => { if ((data.hitMode === 'over' && node.data.noteType === 'search') || (['after', 'before'].includes(data.hitMode) diff --git a/src/routes/api/attributes.js b/src/routes/api/attributes.js index 31f544c1c..6a2269228 100644 --- a/src/routes/api/attributes.js +++ b/src/routes/api/attributes.js @@ -20,8 +20,12 @@ function updateNoteAttribute(req) { if (body.attributeId) { attribute = becca.getAttribute(body.attributeId); + if (!attribute) { + return [404, `Attribute '${body.attributeId}' does not exist.`]; + } + if (attribute.noteId !== noteId) { - return [400, `Attribute ${body.attributeId} is not owned by ${noteId}`]; + return [400, `Attribute '${body.attributeId}' is not owned by ${noteId}`]; } if (body.type !== attribute.type diff --git a/src/services/cloning.js b/src/services/cloning.js index 395cb6772..47018ea5d 100644 --- a/src/services/cloning.js +++ b/src/services/cloning.js @@ -73,10 +73,7 @@ function ensureNoteIsPresentInParent(noteId, parentNoteId, prefix) { const parentNote = becca.getNote(parentNoteId); if (parentNote.type === 'search') { - return { - success: false, - message: "Can't clone into a search note" - }; + return { success: false, message: "Can't clone into a search note" }; } const validationResult = treeService.validateParentChild(parentNoteId, noteId); @@ -122,6 +119,12 @@ function toggleNoteInParent(present, noteId, parentNoteId, prefix) { } function cloneNoteAfter(noteId, afterBranchId) { + if (['hidden', 'root'].includes(noteId)) { + return { success: false, message: 'Cloning the given note is forbidden.' }; + } else if (afterBranchId === 'hidden') { + return { success: false, message: 'Cannot clone after the hidden branch.' }; + } + const afterNote = becca.getBranch(afterBranchId); if (isNoteDeleted(noteId) || isNoteDeleted(afterNote.parentNoteId)) { diff --git a/src/services/hoisted_note.js b/src/services/hoisted_note.js new file mode 100644 index 000000000..9b4d2f023 --- /dev/null +++ b/src/services/hoisted_note.js @@ -0,0 +1,27 @@ +const cls = require("./cls"); +const becca = require("../becca/becca"); + +function getHoistedNoteId() { + return cls.getHoistedNoteId(); +} + +function isHoistedInHiddenSubtree() { + const hoistedNoteId = getHoistedNoteId(); + + if (hoistedNoteId === 'root') { + return false; + } + + const hoistedNote = becca.getNote(hoistedNoteId); + + if (!hoistedNote) { + throw new Error(`Cannot find hoisted note ${hoistedNoteId}`); + } + + return hoistedNote.hasAncestor('hidden'); +} + +module.exports = { + getHoistedNoteId, + isHoistedInHiddenSubtree +}; diff --git a/src/services/search/search_context.js b/src/services/search/search_context.js index 569f773b8..c71addb17 100644 --- a/src/services/search/search_context.js +++ b/src/services/search/search_context.js @@ -1,6 +1,6 @@ "use strict"; -const cls = require('../cls'); +const hoistedNoteService = require("../hoisted_note"); class SearchContext { constructor(params = {}) { @@ -9,8 +9,10 @@ class SearchContext { this.ignoreHoistedNote = !!params.ignoreHoistedNote; this.ancestorNoteId = params.ancestorNoteId; - if (!this.ancestorNoteId && !this.ignoreHoistedNote) { - this.ancestorNoteId = cls.getHoistedNoteId(); + if (!this.ancestorNoteId && !this.ignoreHoistedNote && !hoistedNoteService.isHoistedInHiddenSubtree()) { + // hoisting in hidden subtree should not limit autocomplete + // since we want to link (create relations) to the normal non-hidden notes + this.ancestorNoteId = hoistedNoteService.getHoistedNoteId(); } this.ancestorDepth = params.ancestorDepth; diff --git a/src/services/special_notes.js b/src/services/special_notes.js index 2aacc32dc..3aeb0cec8 100644 --- a/src/services/special_notes.js +++ b/src/services/special_notes.js @@ -53,6 +53,16 @@ function getHiddenRoot() { hidden.addLabel("docName", "hidden"); } + const MAX_POS = 999_999_999; + + const branch = hidden.getBranches()[0]; + if (branch.notePosition !== MAX_POS) { + // we want to keep the hidden subtree always last, otherwise there will be problems with e.g. keyboard navigation + // over tree when it's in the middle + branch.notePosition = MAX_POS; + branch.save(); + } + return hidden; } diff --git a/src/services/tree.js b/src/services/tree.js index b231c6038..aa9983cbc 100644 --- a/src/services/tree.js +++ b/src/services/tree.js @@ -30,13 +30,13 @@ function getNotes(noteIds) { } function validateParentChild(parentNoteId, childNoteId, branchId = null) { - if (childNoteId === 'root') { - return { success: false, message: 'Cannot move root note.'}; + if (['root', 'hidden'].includes(childNoteId)) { + return { success: false, message: `Cannot change this note's location.`}; } if (parentNoteId === 'none') { // this shouldn't happen - return { success: false, message: 'Cannot move anything into root parent.' }; + return { success: false, message: `Cannot move anything into 'none' parent.` }; } const existing = getExistingBranch(parentNoteId, childNoteId); @@ -58,13 +58,10 @@ function validateParentChild(parentNoteId, childNoteId, branchId = null) { }; } - const parentNoteIsShortcut = becca.getNote(parentNoteId).type === 'shortcut'; - const childNoteIsShortcut = becca.getNote(childNoteId).type === 'shortcut'; - - if (parentNoteIsShortcut !== childNoteIsShortcut) { + if (becca.getNote(parentNoteId).type === 'shortcut') { return { success: false, - message: 'Moving/cloning is not possible between shortcuts / normal notes.' + message: 'Shortcut note cannot have any children.' }; } @@ -189,6 +186,10 @@ function sortNotes(parentNoteId, customSortBy = 'title', reverse = false, folder for (const note of notes) { const branch = note.getParentBranches().find(b => b.parentNoteId === parentNoteId); + if (branch.branchId === 'hidden') { + position = 999_999_999; + } + sql.execute("UPDATE branches SET notePosition = ? WHERE branchId = ?", [position, branch.branchId]);