"use strict"; import sql from "../../services/sql.js"; import utils from "../../services/utils.js"; import entityChangesService from "../../services/entity_changes.js"; import treeService from "../../services/tree.js"; import eraseService from "../../services/erase.js"; import becca from "../../becca/becca.js"; import TaskContext from "../../services/task_context.js"; import branchService from "../../services/branches.js"; import log from "../../services/log.js"; import ValidationError from "../../errors/validation_error.js"; import eventService from "../../services/events.js"; import type { Request } from "express"; /** * Code in this file deals with moving and cloning branches. The relationship between note and parent note is unique * for not deleted branches. There may be multiple deleted note-parent note relationships. */ function moveBranchToParent(req: Request) { const { branchId, parentBranchId } = req.params; const branchToMove = becca.getBranch(branchId); const targetParentBranch = becca.getBranch(parentBranchId); if (!branchToMove || !targetParentBranch) { throw new ValidationError(`One or both branches '${branchId}', '${parentBranchId}' have not been found`); } return branchService.moveBranchToBranch(branchToMove, targetParentBranch, branchId); } function moveBranchBeforeNote(req: Request) { const { branchId, beforeBranchId } = req.params; const branchToMove = becca.getBranchOrThrow(branchId); const beforeBranch = becca.getBranchOrThrow(beforeBranchId); const validationResult = treeService.validateParentChild(beforeBranch.parentNoteId, branchToMove.noteId, branchId); if (!validationResult.success) { return [200, validationResult]; } const originalBeforeNotePosition = beforeBranch.notePosition; // we don't change utcDateModified, so other changes are prioritized in case of conflict // also we would have to sync all those modified branches otherwise hash checks would fail sql.execute("UPDATE branches SET notePosition = notePosition + 10 WHERE parentNoteId = ? AND notePosition >= ? AND isDeleted = 0", [beforeBranch.parentNoteId, originalBeforeNotePosition]); // also need to update becca positions const parentNote = becca.getNoteOrThrow(beforeBranch.parentNoteId); for (const childBranch of parentNote.getChildBranches()) { if (childBranch.notePosition >= originalBeforeNotePosition) { childBranch.notePosition += 10; } } if (branchToMove.parentNoteId === beforeBranch.parentNoteId) { branchToMove.notePosition = originalBeforeNotePosition; branchToMove.save(); } else { const newBranch = branchToMove.createClone(beforeBranch.parentNoteId, originalBeforeNotePosition); newBranch.save(); branchToMove.markAsDeleted(); } treeService.sortNotesIfNeeded(parentNote.noteId); // if sorting is not needed, then still the ordering might have changed above manually entityChangesService.putNoteReorderingEntityChange(parentNote.noteId); log.info(`Moved note ${branchToMove.noteId}, branch ${branchId} before note ${beforeBranch.noteId}, branch ${beforeBranchId}`); return { success: true }; } function moveBranchAfterNote(req: Request) { const { branchId, afterBranchId } = req.params; const branchToMove = becca.getBranchOrThrow(branchId); const afterNote = becca.getBranchOrThrow(afterBranchId); const validationResult = treeService.validateParentChild(afterNote.parentNoteId, branchToMove.noteId, branchId); if (!validationResult.success) { return [200, validationResult]; } const originalAfterNotePosition = afterNote.notePosition; // we don't change utcDateModified, so other changes are prioritized in case of conflict // also we would have to sync all those modified branches otherwise hash checks would fail sql.execute("UPDATE branches SET notePosition = notePosition + 10 WHERE parentNoteId = ? AND notePosition > ? AND isDeleted = 0", [afterNote.parentNoteId, originalAfterNotePosition]); // also need to update becca positions const parentNote = becca.getNoteOrThrow(afterNote.parentNoteId); for (const childBranch of parentNote.getChildBranches()) { if (childBranch.notePosition > originalAfterNotePosition) { childBranch.notePosition += 10; } } const movedNotePosition = originalAfterNotePosition + 10; if (branchToMove.parentNoteId === afterNote.parentNoteId) { branchToMove.notePosition = movedNotePosition; branchToMove.save(); } else { const newBranch = branchToMove.createClone(afterNote.parentNoteId, movedNotePosition); newBranch.save(); branchToMove.markAsDeleted(); } treeService.sortNotesIfNeeded(parentNote.noteId); // if sorting is not needed, then still the ordering might have changed above manually entityChangesService.putNoteReorderingEntityChange(parentNote.noteId); log.info(`Moved note ${branchToMove.noteId}, branch ${branchId} after note ${afterNote.noteId}, branch ${afterBranchId}`); return { success: true }; } function setExpanded(req: Request) { const { branchId } = req.params; const expanded = parseInt(req.params.expanded); if (branchId !== "none_root") { sql.execute("UPDATE branches SET isExpanded = ? WHERE branchId = ?", [expanded, branchId]); // we don't sync expanded label // also this does not trigger updates to the frontend, this would trigger too many reloads const branch = becca.branches[branchId]; if (branch) { branch.isExpanded = !!expanded; } eventService.emit(eventService.ENTITY_CHANGED, { entityName: "branches", entity: branch }); } } function setExpandedForSubtree(req: Request) { const { branchId } = req.params; const expanded = parseInt(req.params.expanded); let branchIds = sql.getColumn( ` WITH RECURSIVE tree(branchId, noteId) AS ( SELECT branchId, noteId FROM branches WHERE branchId = ? UNION SELECT branches.branchId, branches.noteId FROM branches JOIN tree ON branches.parentNoteId = tree.noteId WHERE branches.isDeleted = 0 ) SELECT branchId FROM tree`, [branchId] ); // root is always expanded branchIds = branchIds.filter((branchId) => branchId !== "none_root"); sql.executeMany(`UPDATE branches SET isExpanded = ${expanded} WHERE branchId IN (???)`, branchIds); for (const branchId of branchIds) { const branch = becca.branches[branchId]; if (branch) { branch.isExpanded = !!expanded; } } return { branchIds }; } /** * @swagger * /api/branches/{branchId}: * delete: * summary: Delete branch (note clone) * operationId: branches-delete * parameters: * - name: branchId * in: path * required: true * schema: * $ref: "#/components/schemas/BranchId" * - name: taskId * in: query * required: true * schema: * type: string * description: Task group identifier * - name: eraseNotes * in: query * schema: * type: boolean * required: false * description: Whether to erase the note immediately * - name: last * in: query * schema: * type: boolean * required: true * description: Whether this is the last request of this task group * responses: * '200': * description: Branch successfully deleted * content: * application/json: * schema: * type: object * properties: * noteDeleted: * type: boolean * description: Whether the last note clone was deleted * security: * - session: [] * tags: ["data"] */ function deleteBranch(req: Request) { const last = req.query.last === "true"; const eraseNotes = req.query.eraseNotes === "true"; const branch = becca.getBranchOrThrow(req.params.branchId); const taskContext = TaskContext.getInstance(req.query.taskId as string, "deleteNotes"); const deleteId = utils.randomString(10); let noteDeleted; if (eraseNotes) { // erase automatically means deleting all clones + note itself branch.getNote().deleteNote(deleteId, taskContext); eraseService.eraseNotesWithDeleteId(deleteId); noteDeleted = true; } else { noteDeleted = branch.deleteBranch(deleteId, taskContext); } if (last) { taskContext.taskSucceeded(); } return { noteDeleted: noteDeleted }; } function setPrefix(req: Request) { const branchId = req.params.branchId; //TriliumNextTODO: req.body arrives as string, so req.body.prefix will be undefined – did the code below ever even work? const prefix = utils.isEmptyOrWhitespace(req.body.prefix) ? null : req.body.prefix; const branch = becca.getBranchOrThrow(branchId); branch.prefix = prefix; branch.save(); } export default { moveBranchToParent, moveBranchBeforeNote, moveBranchAfterNote, setExpanded, setExpandedForSubtree, deleteBranch, setPrefix };