feat(breadcrumb): get tree menu to show

This commit is contained in:
Elian Doran 2025-12-16 09:35:19 +02:00
parent 587ea42700
commit 96a6ea4c7a
No known key found for this signature in database
2 changed files with 276 additions and 245 deletions

View File

@ -1,21 +1,23 @@
import NoteColorPicker from "./custom-items/NoteColorPicker.jsx";
import treeService from "../services/tree.js";
import froca from "../services/froca.js";
import clipboard from "../services/clipboard.js";
import noteCreateService from "../services/note_create.js";
import contextMenu, { type MenuCommandItem, type MenuItem } from "./context_menu.js";
import appContext, { type ContextMenuCommandData, type FilteredCommandNames } from "../components/app_context.js";
import type { SelectMenuItemEventListener } from "../components/events.js";
import type FAttachment from "../entities/fattachment.js";
import FBranch from "../entities/fbranch.js";
import FNote from "../entities/fnote.js";
import attributes from "../services/attributes.js";
import { executeBulkActions } from "../services/bulk_action.js";
import clipboard from "../services/clipboard.js";
import dialogService from "../services/dialog.js";
import froca from "../services/froca.js";
import { t } from "../services/i18n.js";
import noteCreateService from "../services/note_create.js";
import noteTypesService from "../services/note_types.js";
import server from "../services/server.js";
import toastService from "../services/toast.js";
import dialogService from "../services/dialog.js";
import { t } from "../services/i18n.js";
import type NoteTreeWidget from "../widgets/note_tree.js";
import type FAttachment from "../entities/fattachment.js";
import type { SelectMenuItemEventListener } from "../components/events.js";
import treeService from "../services/tree.js";
import utils from "../services/utils.js";
import attributes from "../services/attributes.js";
import { executeBulkActions } from "../services/bulk_action.js";
import type NoteTreeWidget from "../widgets/note_tree.js";
import contextMenu, { type MenuCommandItem, type MenuItem } from "./context_menu.js";
import NoteColorPicker from "./custom-items/NoteColorPicker.jsx";
// TODO: Deduplicate once client/server is well split.
interface ConvertToAttachmentResponse {
@ -53,9 +55,93 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
lastTargetNode.classList.add('fancytree-menu-target');
}
async getMenuItems(): Promise<MenuItem<TreeCommandNames>[]> {
async getMenuItems() {
const note = this.node.data.noteId ? await froca.getNote(this.node.data.noteId) : null;
const branch = froca.getBranch(this.node.data.branchId);
const selNodes = this.treeWidget.getSelectedNodes();
return buildTreeMenuItems({
note,
branch: froca.getBranch(this.node.data.branchId),
selectedNotes: await froca.getNotes(selNodes.map(node => node.data.noteId)),
noSelectedNotes: selNodes.length === 0 || (selNodes.length === 1 && selNodes[0] === this.node)
});
}
async selectMenuItemHandler({ command, type, templateNoteId }: MenuCommandItem<TreeCommandNames>) {
const notePath = treeService.getNotePath(this.node);
if (utils.isMobile()) {
this.treeWidget.triggerCommand("setActiveScreen", { screen: "detail" });
}
if (command === "openInTab") {
appContext.tabManager.openTabWithNoteWithHoisting(notePath);
} else if (command === "insertNoteAfter") {
const parentNotePath = treeService.getNotePath(this.node.getParent());
const isProtected = treeService.getParentProtectedStatus(this.node);
noteCreateService.createNote(parentNotePath, {
target: "after",
targetBranchId: this.node.data.branchId,
type,
isProtected,
templateNoteId
});
} else if (command === "insertChildNote") {
const parentNotePath = treeService.getNotePath(this.node);
noteCreateService.createNote(parentNotePath, {
type,
isProtected: this.node.data.isProtected,
templateNoteId
});
} else if (command === "openNoteInSplit") {
const subContexts = appContext.tabManager.getActiveContext()?.getSubContexts();
const { ntxId } = subContexts?.[subContexts.length - 1] ?? {};
this.treeWidget.triggerCommand("openNewNoteSplit", { ntxId, notePath });
} else if (command === "openNoteInPopup") {
appContext.triggerCommand("openInPopup", { noteIdOrPath: notePath });
} else if (command === "convertNoteToAttachment") {
if (!(await dialogService.confirm(t("tree-context-menu.convert-to-attachment-confirm")))) {
return;
}
let converted = 0;
for (const noteId of this.treeWidget.getSelectedOrActiveNoteIds(this.node)) {
const note = await froca.getNote(noteId);
if (note?.isEligibleForConversionToAttachment()) {
const { attachment } = await server.post<ConvertToAttachmentResponse>(`notes/${note.noteId}/convert-to-attachment`);
if (attachment) {
converted++;
}
}
}
toastService.showMessage(t("tree-context-menu.converted-to-attachments", { count: converted }));
} else if (command === "copyNotePathToClipboard") {
navigator.clipboard.writeText(`#${ notePath}`);
} else if (command) {
this.treeWidget.triggerCommand<TreeCommandNames>(command, {
node: this.node,
notePath,
noteId: this.node.data.noteId,
selectedOrActiveBranchIds: this.treeWidget.getSelectedOrActiveBranchIds(this.node),
selectedOrActiveNoteIds: this.treeWidget.getSelectedOrActiveNoteIds(this.node)
});
}
}
}
export async function buildTreeMenuItems({ note, branch, selectedNotes, noSelectedNotes }: {
note: FNote | null;
branch: FBranch | undefined;
selectedNotes: FNote[];
noSelectedNotes: boolean;
}): Promise<MenuItem<TreeCommandNames>[]> {
const isNotRoot = note?.noteId !== "root";
const isHoisted = note?.noteId === appContext.tabManager.getActiveContext()?.hoistedNoteId;
const parentNote = isNotRoot && branch ? await froca.getNote(branch.parentNoteId) : null;
@ -63,14 +149,10 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
// some actions don't support multi-note, so they are disabled when notes are selected,
// the only exception is when the only selected note is the one that was right-clicked, then
// it's clear what the user meant to do.
const selNodes = this.treeWidget.getSelectedNodes();
const selectedNotes = await froca.getNotes(selNodes.map(node => node.data.noteId));
if (note && !selectedNotes.includes(note)) selectedNotes.push(note);
const isArchived = selectedNotes.every(note => note.isArchived);
const canToggleArchived = !selectedNotes.some(note => note.isArchived !== isArchived);
const noSelectedNotes = selNodes.length === 0 || (selNodes.length === 1 && selNodes[0] === this.node);
const notSearch = note?.type !== "search";
const notOptionsOrHelp = !note?.noteId.startsWith("_options") && !note?.noteId.startsWith("_help");
const parentNotSearch = !parentNote || parentNote.type !== "search";
@ -275,72 +357,3 @@ export default class TreeContextMenu implements SelectMenuItemEventListener<Tree
];
return items.filter((row) => row !== null) as MenuItem<TreeCommandNames>[];
}
async selectMenuItemHandler({ command, type, templateNoteId }: MenuCommandItem<TreeCommandNames>) {
const notePath = treeService.getNotePath(this.node);
if (utils.isMobile()) {
this.treeWidget.triggerCommand("setActiveScreen", { screen: "detail" });
}
if (command === "openInTab") {
appContext.tabManager.openTabWithNoteWithHoisting(notePath);
} else if (command === "insertNoteAfter") {
const parentNotePath = treeService.getNotePath(this.node.getParent());
const isProtected = treeService.getParentProtectedStatus(this.node);
noteCreateService.createNote(parentNotePath, {
target: "after",
targetBranchId: this.node.data.branchId,
type: type,
isProtected: isProtected,
templateNoteId: templateNoteId
});
} else if (command === "insertChildNote") {
const parentNotePath = treeService.getNotePath(this.node);
noteCreateService.createNote(parentNotePath, {
type: type,
isProtected: this.node.data.isProtected,
templateNoteId: templateNoteId
});
} else if (command === "openNoteInSplit") {
const subContexts = appContext.tabManager.getActiveContext()?.getSubContexts();
const { ntxId } = subContexts?.[subContexts.length - 1] ?? {};
this.treeWidget.triggerCommand("openNewNoteSplit", { ntxId, notePath });
} else if (command === "openNoteInPopup") {
appContext.triggerCommand("openInPopup", { noteIdOrPath: notePath })
} else if (command === "convertNoteToAttachment") {
if (!(await dialogService.confirm(t("tree-context-menu.convert-to-attachment-confirm")))) {
return;
}
let converted = 0;
for (const noteId of this.treeWidget.getSelectedOrActiveNoteIds(this.node)) {
const note = await froca.getNote(noteId);
if (note?.isEligibleForConversionToAttachment()) {
const { attachment } = await server.post<ConvertToAttachmentResponse>(`notes/${note.noteId}/convert-to-attachment`);
if (attachment) {
converted++;
}
}
}
toastService.showMessage(t("tree-context-menu.converted-to-attachments", { count: converted }));
} else if (command === "copyNotePathToClipboard") {
navigator.clipboard.writeText("#" + notePath);
} else if (command) {
this.treeWidget.triggerCommand<TreeCommandNames>(command, {
node: this.node,
notePath: notePath,
noteId: this.node.data.noteId,
selectedOrActiveBranchIds: this.treeWidget.getSelectedOrActiveBranchIds(this.node),
selectedOrActiveNoteIds: this.treeWidget.getSelectedOrActiveNoteIds(this.node)
});
}
}
}

View File

@ -8,6 +8,7 @@ import NoteContext from "../../components/note_context";
import FNote from "../../entities/fnote";
import contextMenu from "../../menus/context_menu";
import link_context_menu from "../../menus/link_context_menu";
import { buildTreeMenuItems } from "../../menus/tree_context_menu";
import { getReadableTextColor } from "../../services/css_class_manager";
import froca from "../../services/froca";
import hoisted_note from "../../services/hoisted_note";
@ -154,14 +155,31 @@ function BreadcrumbItem({ index, notePath, noteContext, notePathLength }: { inde
return <NoteLink
notePath={notePath}
noContextMenu
onContextMenu={(e) => {
onContextMenu={async (e) => {
e.preventDefault();
const notePathArray = notePath.split("/");
const parentNoteId = notePathArray.at(-2);
const childNoteId = notePathArray.at(-1);
console.log(parentNoteId, childNoteId);
if (!parentNoteId || !childNoteId) return;
const branchId = await froca.getBranchId(parentNoteId, childNoteId);
if (!branchId) return;
const branch = froca.getBranch(branchId);
const note = await branch?.getNote();
if (!branch || !note) return;
const items = await buildTreeMenuItems({
branch,
note,
noSelectedNotes: true,
selectedNotes: []
});
contextMenu.show({
items: [
{
title: "Foo"
}
],
items,
x: e.pageX,
y: e.pageY
});