From 3354bd669f0d75ff90753749b031d7c4e47a5b16 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 10 Jan 2026 12:26:51 +0200 Subject: [PATCH] fix(client/tree): toast displayed when doing operations outside of tree --- apps/client/src/services/branches.ts | 16 +++--- apps/client/src/services/load_results.ts | 1 - apps/client/src/types-fancytree.d.ts | 21 ++++---- apps/client/src/widgets/note_tree.ts | 68 +++++++++++------------- 4 files changed, 49 insertions(+), 57 deletions(-) diff --git a/apps/client/src/services/branches.ts b/apps/client/src/services/branches.ts index e5a5158e1..cd1f5c6e7 100644 --- a/apps/client/src/services/branches.ts +++ b/apps/client/src/services/branches.ts @@ -1,12 +1,12 @@ -import utils from "./utils.js"; -import server from "./server.js"; -import toastService, { type ToastOptionsWithRequiredId } from "./toast.js"; +import appContext from "../components/app_context.js"; +import type { ResolveOptions } from "../widgets/dialogs/delete_notes.js"; import froca from "./froca.js"; import hoistedNoteService from "./hoisted_note.js"; -import ws from "./ws.js"; -import appContext from "../components/app_context.js"; import { t } from "./i18n.js"; -import type { ResolveOptions } from "../widgets/dialogs/delete_notes.js"; +import server from "./server.js"; +import toastService, { type ToastOptionsWithRequiredId } from "./toast.js"; +import utils from "./utils.js"; +import ws from "./ws.js"; // TODO: Deduplicate type with server interface Response { @@ -66,7 +66,7 @@ async function moveAfterBranch(branchIdsToMove: string[], afterBranchId: string) } } -async function moveToParentNote(branchIdsToMove: string[], newParentBranchId: string) { +async function moveToParentNote(branchIdsToMove: string[], newParentBranchId: string, componentId?: string) { const newParentBranch = froca.getBranch(newParentBranchId); if (!newParentBranch) { return; @@ -86,7 +86,7 @@ async function moveToParentNote(branchIdsToMove: string[], newParentBranchId: st continue; } - const resp = await server.put(`branches/${branchIdToMove}/move-to/${newParentBranchId}`); + const resp = await server.put(`branches/${branchIdToMove}/move-to/${newParentBranchId}`, undefined, componentId); if (!resp.success) { toastService.showError(resp.message); diff --git a/apps/client/src/services/load_results.ts b/apps/client/src/services/load_results.ts index 5b6589c65..b5706705b 100644 --- a/apps/client/src/services/load_results.ts +++ b/apps/client/src/services/load_results.ts @@ -132,7 +132,6 @@ export default class LoadResults { } addBranch(branchId: string, componentId: string) { - console.log("Got branch with ", branchId, componentId); this.branchRows.push({ branchId, componentId }); } diff --git a/apps/client/src/types-fancytree.d.ts b/apps/client/src/types-fancytree.d.ts index cb709ea07..a8a151b55 100644 --- a/apps/client/src/types-fancytree.d.ts +++ b/apps/client/src/types-fancytree.d.ts @@ -69,7 +69,7 @@ declare namespace Fancytree { debug(msg: any): void; /** Expand (or collapse) all parent nodes. */ - expandAll(flag?: boolean, options?: Object): void; + expandAll(flag?: boolean, options?: object): void; /** [ext-filter] Dimm or hide whole branches. * @returns {integer} count @@ -221,6 +221,7 @@ declare namespace Fancytree { branchId: string; isProtected: boolean; noteType: NoteType; + subtreeHidden: boolean; } interface FancytreeNewNode extends FancytreeNodeData { @@ -369,7 +370,7 @@ declare namespace Fancytree { * @param mode 'before', 'after', or 'child' (default='child') * @param init NodeData (or simple title string) */ - editCreateNode(mode?: string, init?: Object): void; + editCreateNode(mode?: string, init?: object): void; /** [ext-edit] Stop inline editing. * @@ -526,7 +527,7 @@ declare namespace Fancytree { * * @param opts passed to `setExpanded()`. Defaults to {noAnimation: false, noEvents: false, scrollIntoView: true} */ - makeVisible(opts?: Object): JQueryPromise; + makeVisible(opts?: object): JQueryPromise; /** Move this node to targetNode. * @@ -589,25 +590,25 @@ declare namespace Fancytree { * @param effects animation options. * @param options {topNode: null, effects: ..., parent: ...} this node will remain visible in any case, even if `this` is outside the scroll pane. */ - scrollIntoView(effects?: boolean, options?: Object): JQueryPromise; + scrollIntoView(effects?: boolean, options?: object): JQueryPromise; /** * @param effects animation options. * @param options {topNode: null, effects: ..., parent: ...} this node will remain visible in any case, even if `this` is outside the scroll pane. */ - scrollIntoView(effects?: Object, options?: Object): JQueryPromise; + scrollIntoView(effects?: object, options?: object): JQueryPromise; /** * @param flag pass false to deactivate * @param opts additional options. Defaults to {noEvents: false} */ - setActive(flag?: boolean, opts?: Object): JQueryPromise; + setActive(flag?: boolean, opts?: object): JQueryPromise; /** * @param flag pass false to collapse. * @param opts additional options. Defaults to {noAnimation:false, noEvents:false} */ - setExpanded(flag?: boolean, opts?: Object): JQueryPromise; + setExpanded(flag?: boolean, opts?: object): JQueryPromise; /** * Set keyboard focus to this node. @@ -1109,7 +1110,7 @@ declare namespace Fancytree { /** class names added to the node markup (separate with space) */ extraClasses?: string | undefined; /** all properties from will be copied to `node.data` */ - data?: Object | undefined; + data?: object | undefined; /** Will be added as title attribute of the node's icon span,thus enabling a tooltip. */ iconTooltip?: string | undefined; @@ -1160,7 +1161,7 @@ declare namespace Fancytree { escapeHtml(s: string): string; - getEventTarget(event: Event): Object; + getEventTarget(event: Event): object; getEventTargetType(event: Event): string; @@ -1179,7 +1180,7 @@ declare namespace Fancytree { parseHtml($ul: JQuery): NodeData[]; /** Add Fancytree extension definition to the list of globally available extensions. */ - registerExtension(definition: Object): void; + registerExtension(definition: object): void; unescapeHtml(s: string): string; diff --git a/apps/client/src/widgets/note_tree.ts b/apps/client/src/widgets/note_tree.ts index f6af3c374..c7078b874 100644 --- a/apps/client/src/widgets/note_tree.ts +++ b/apps/client/src/widgets/note_tree.ts @@ -555,7 +555,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget { } else if (data.hitMode === "after") { branchService.moveAfterBranch(selectedBranchIds, node.data.branchId); } else if (data.hitMode === "over") { - branchService.moveToParentNote(selectedBranchIds, node.data.branchId); + branchService.moveToParentNote(selectedBranchIds, node.data.branchId, this.componentId); } else { throw new Error(`Unknown hitMode '${data.hitMode}'`); } @@ -758,12 +758,6 @@ export default class NoteTreeWidget extends NoteContextAwareWidget { return; } - const parentNote = froca.getNoteFromCache(branch.parentNoteId); - if (parentNote?.isLabelTruthy("subtreeHidden")) { - node.remove(); - return "removed-due-to-subtree-hidden"; - } - const title = `${branch.prefix ? `${branch.prefix} - ` : ""}${note.title}`; node.data.isProtected = note.isProtected; @@ -805,6 +799,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget { lazy: true, folder: isFolder, expanded: !!branch.isExpanded && note.type !== "search", + subtreeHidden: note.isLabelTruthy("subtreeHidden"), key: utils.randomString(12) // this should prevent some "duplicate key" errors }; @@ -1339,18 +1334,34 @@ export default class NoteTreeWidget extends NoteContextAwareWidget { } else if (frocaBranch) { // make sure it's loaded // we're forcing lazy since it's not clear if the whole required subtree is in froca - const newNode = this.prepareNode(frocaBranch, true); - if (newNode) { - parentNode.addChildren([newNode]); - } + if (!parentNode.data.subtreeHidden) { + const newNode = this.prepareNode(frocaBranch, true); + if (newNode) { + parentNode.addChildren([newNode]); + } - if (frocaBranch?.isExpanded && note && note.hasChildren()) { - refreshCtx.noteIdsToReload.add(frocaBranch.noteId); - } + if (frocaBranch?.isExpanded && note && note.hasChildren()) { + refreshCtx.noteIdsToReload.add(frocaBranch.noteId); + } - this.sortChildren(parentNode); + this.sortChildren(parentNode); + } else if (branchRow.componentId === this.componentId) { + // Display the toast and focus to parent note only if we know for sure that the operation comes from the tree. + const parentNote = froca.getNoteFromCache(parentNode.data.noteId || ""); + toastService.showPersistent({ + id: `subtree-hidden-moved`, + title: t("note_tree.subtree-hidden-moved-title", { title: parentNote?.title }), + message: parentNote?.type === "book" + ? t("note_tree.subtree-hidden-moved-description-collection") + : t("note_tree.subtree-hidden-moved-description-other"), + icon: "bx bx-hide", + timeout: 5_000, + }); + parentNode.setActive(true); + } // this might be a first child which would force an icon change + // also update the count if the subtree is hidden. if (branchRow.parentNoteId) { refreshCtx.noteIdsToUpdate.add(branchRow.parentNoteId); } @@ -1385,30 +1396,11 @@ export default class NoteTreeWidget extends NoteContextAwareWidget { }); // for some reason, node update cannot be in the batchUpdate() block (node is not re-rendered) - let removedFromSubtreeParent: Fancytree.FancytreeNode | null = null; for (const noteId of refreshCtx.noteIdsToUpdate) { for (const node of this.getNodesByNoteId(noteId)) { - const parent = node.parent; - const result = await this.updateNode(node); - if (result === "removed-due-to-subtree-hidden") { - removedFromSubtreeParent = parent; - } + await this.updateNode(node); } } - - if (removedFromSubtreeParent) { - removedFromSubtreeParent.setActive(true, { noEvents: true }); - const targetNote = froca.getNoteFromCache(removedFromSubtreeParent.data.noteId || ""); - toastService.showPersistent({ - id: `subtree-hidden-moved`, - title: t("note_tree.subtree-hidden-moved-title", { title: targetNote?.title }), - message: targetNote?.type === "book" - ? t("note_tree.subtree-hidden-moved-description-collection") - : t("note_tree.subtree-hidden-moved-description-other"), - icon: "bx bx-hide", - timeout: 5_000, - }); - } } async #setActiveNode(activeNotePath: string | null, activeNodeFocused: boolean, movedActiveNode: Fancytree.FancytreeNode | null, parentsOfAddedNodes: Fancytree.FancytreeNode[]) { @@ -1665,7 +1657,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget { const toNode = node.getPrevSibling(); if (toNode !== null) { - branchService.moveToParentNote([node.data.branchId], toNode.data.branchId); + branchService.moveToParentNote([node.data.branchId], toNode.data.branchId, this.componentId); } } @@ -1802,12 +1794,12 @@ export default class NoteTreeWidget extends NoteContextAwareWidget { #moveLaunchers(selectedOrActiveBranchIds: string[], desktopParent: string, mobileParent: string) { const desktopLaunchersToMove = selectedOrActiveBranchIds.filter((branchId) => !branchId.startsWith("_lbMobile")); if (desktopLaunchersToMove) { - branchService.moveToParentNote(desktopLaunchersToMove, `_lbRoot_${ desktopParent}`); + branchService.moveToParentNote(desktopLaunchersToMove, `_lbRoot_${ desktopParent}`, this.componentId); } const mobileLaunchersToMove = selectedOrActiveBranchIds.filter((branchId) => branchId.startsWith("_lbMobile")); if (mobileLaunchersToMove) { - branchService.moveToParentNote(mobileLaunchersToMove, `_lbMobileRoot_${ mobileParent}`); + branchService.moveToParentNote(mobileLaunchersToMove, `_lbMobileRoot_${mobileParent}`, this.componentId); } }