From 2cd3e3f9c8759e982627de5716f8e5ba50f574d9 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 29 Sep 2025 20:06:22 +0300 Subject: [PATCH] chore(type_widgets): bring back relation map note creation --- .../src/widgets/type_widgets/RelationMap.tsx | 80 ++++++++++++++++++- .../widgets/type_widgets_old/relation_map.ts | 64 --------------- 2 files changed, 76 insertions(+), 68 deletions(-) diff --git a/apps/client/src/widgets/type_widgets/RelationMap.tsx b/apps/client/src/widgets/type_widgets/RelationMap.tsx index a992da2b8..53df60acc 100644 --- a/apps/client/src/widgets/type_widgets/RelationMap.tsx +++ b/apps/client/src/widgets/type_widgets/RelationMap.tsx @@ -1,7 +1,7 @@ import { useCallback, useEffect, useRef, useState } from "preact/hooks"; import { TypeWidgetProps } from "./type_widget"; import { Defaults, jsPlumb, jsPlumbInstance, OverlaySpec } from "jsplumb"; -import { useEditorSpacedUpdate, useNoteBlob } from "../react/hooks"; +import { useEditorSpacedUpdate, useNoteBlob, useTriliumEvent } from "../react/hooks"; import FNote from "../../entities/fnote"; import { ComponentChildren, RefObject } from "preact"; import froca from "../../services/froca"; @@ -9,6 +9,10 @@ import NoteLink from "../react/NoteLink"; import "./RelationMap.css"; import { t } from "../../services/i18n"; import panzoom, { PanZoomOptions } from "panzoom"; +import dialog from "../../services/dialog"; +import server from "../../services/server"; +import toast from "../../services/toast"; +import { CreateChildrenResponse } from "@triliumnext/commons"; interface MapData { notes: { @@ -23,6 +27,11 @@ interface MapData { } } +interface Clipboard { + noteId: string; + title: string; +} + const uniDirectionalOverlays: OverlaySpec[] = [ [ "Arrow", @@ -36,7 +45,7 @@ const uniDirectionalOverlays: OverlaySpec[] = [ ["Label", { label: "", id: "label", cssClass: "connection-label" }] ]; -export default function RelationMap({ note }: TypeWidgetProps) { +export default function RelationMap({ note, ntxId }: TypeWidgetProps) { const [ data, setData ] = useState(); const containerRef = useRef(null); const apiRef = useRef(null); @@ -83,12 +92,25 @@ export default function RelationMap({ note }: TypeWidgetProps) { spacedUpdate.scheduleUpdate(); }, [ data ]); + const onNewItem = useCallback((newNote: MapData["notes"][number]) => { + if (!data) return; + data.notes.push(newNote); + setData({ ...data }); + spacedUpdate.scheduleUpdate(); + }, [ data, spacedUpdate ]); + const clickCallback = useNoteCreation({ + containerRef, + note, + ntxId, + onCreate: onNewItem + }); + usePanZoom({ containerRef, options: { maxZoom: 2, minZoom: 0.3, - smoothScroll: false, + smoothScroll: false, //@ts-expect-error Upstream incorrectly mentions no arguments. filterKey: function (e: KeyboardEvent) { // if ALT is pressed, then panzoom should bubble the event up @@ -102,7 +124,7 @@ export default function RelationMap({ note }: TypeWidgetProps) { return (
-
+
; + onCreate: (newNote: MapData["notes"][number]) => void; +}) { + const clipboardRef = useRef(null); + useTriliumEvent("relationMapCreateChildNote", async ({ ntxId: eventNtxId }) => { + if (eventNtxId !== ntxId) return; + const title = await dialog.prompt({ message: t("relation_map.enter_title_of_new_note"), defaultValue: t("relation_map.default_new_note_title") }); + if (!title?.trim()) return; + + const { note: createdNote } = await server.post(`notes/${note.noteId}/children?target=into`, { + title, + content: "", + type: "text" + }); + + toast.showMessage(t("relation_map.click_on_canvas_to_place_new_note")); + clipboardRef.current = { + noteId: createdNote.noteId, + title + }; + }); + const onClickHandler = useCallback((e: MouseEvent) => { + const clipboard = clipboardRef.current; + if (clipboard && containerRef.current) { + const zoom = getZoom(containerRef.current); + let { x, y } = getMousePosition(e, containerRef.current, zoom); + + // modifying position so that the cursor is on the top-center of the box + x -= 80; + y -= 15; + + onCreate({ noteId: clipboard.noteId, x, y }); + clipboardRef.current = null; + } + }, [ onCreate ]); + return onClickHandler; +} + function JsPlumb({ className, props, children, containerRef: externalContainerRef, apiRef }: { className?: string; props: Omit; @@ -229,3 +292,12 @@ function getZoom(container: HTMLDivElement) { return parseFloat(matches[1]); } + +function getMousePosition(evt: MouseEvent, container: HTMLDivElement, zoom: number) { + const rect = container.getBoundingClientRect(); + + return { + x: ((evt.clientX ?? 0) - rect.left) / zoom, + y: ((evt.clientY ?? 0) - rect.top) / zoom + }; +} diff --git a/apps/client/src/widgets/type_widgets_old/relation_map.ts b/apps/client/src/widgets/type_widgets_old/relation_map.ts index f8cfa85bd..c3902c9d5 100644 --- a/apps/client/src/widgets/type_widgets_old/relation_map.ts +++ b/apps/client/src/widgets/type_widgets_old/relation_map.ts @@ -90,11 +90,6 @@ const linkOverlays = [ let containerCounter = 1; -interface Clipboard { - noteId: string; - title: string; -} - export type RelationType = "uniDirectional" | "biDirectional" | "inverse"; interface Relation { @@ -106,13 +101,6 @@ interface Relation { render: boolean; } -// TODO: Deduplicate. -interface PostNoteResponse { - note: { - noteId: string; - }; -} - // TODO: Deduplicate. interface RelationMapPostResponse { relations: Relation[]; @@ -149,21 +137,7 @@ export default class RelationMapTypeWidget extends TypeWidget { this.$relationMapWrapper = this.$widget.find(".relation-map-wrapper"); this.$relationMapWrapper.on("click", (event) => { - if (this.clipboard && this.mapData) { - let { x, y } = this.getMousePosition(event); - // modifying position so that the cursor is on the top-center of the box - x -= 80; - y -= 15; - - this.createNoteBox(this.clipboard.noteId, this.clipboard.title, x, y); - - this.mapData.notes.push({ noteId: this.clipboard.noteId, x, y }); - - this.saveData(); - - this.clipboard = null; - } return true; }); @@ -249,12 +223,7 @@ export default class RelationMapTypeWidget extends TypeWidget { async doRefresh(note: FNote) { - await this.loadMapData(); - await this.initJsPlumbInstance(); - - await this.initPanZoom(); - this.loadNotesAndRelations(); } @@ -558,39 +527,6 @@ export default class RelationMapTypeWidget extends TypeWidget { this.loadNotesAndRelations(); } - getMousePosition(evt: JQuery.ClickEvent | JQuery.DropEvent) { - const rect = this.$relationMapContainer[0].getBoundingClientRect(); - - const zoom = this.getZoom(); - - return { - x: ((evt.clientX ?? 0) - rect.left) / zoom, - y: ((evt.clientY ?? 0) - rect.top) / zoom - }; - } - - async relationMapCreateChildNoteEvent({ ntxId }: EventData<"relationMapCreateChildNote">) { - if (!this.isNoteContext(ntxId)) { - return; - } - - const title = await dialogService.prompt({ message: t("relation_map.enter_title_of_new_note"), defaultValue: t("relation_map.default_new_note_title") }); - - if (!title?.trim()) { - return; - } - - const { note } = await server.post(`notes/${this.noteId}/children?target=into`, { - title, - content: "", - type: "text" - }); - - toastService.showMessage(t("relation_map.click_on_canvas_to_place_new_note")); - - this.clipboard = { noteId: note.noteId, title }; - } - relationMapResetPanZoomEvent({ ntxId }: EventData<"relationMapResetPanZoom">) { if (!this.isNoteContext(ntxId)) { return;