From 1c1243912bc84329ca6d3e4fb1b2d58a1eec7789 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Mon, 29 Sep 2025 21:05:29 +0300 Subject: [PATCH] refactor(type_widgets): use API architecture for relation map --- apps/client/src/types.d.ts | 8 ++- .../type_widgets/relation_map/RelationMap.tsx | 67 +++++++++---------- .../widgets/type_widgets/relation_map/api.ts | 61 +++++++++++++++++ .../widgets/type_widgets_old/relation_map.ts | 25 ------- 4 files changed, 100 insertions(+), 61 deletions(-) create mode 100644 apps/client/src/widgets/type_widgets/relation_map/api.ts diff --git a/apps/client/src/types.d.ts b/apps/client/src/types.d.ts index fedad1662..201064002 100644 --- a/apps/client/src/types.d.ts +++ b/apps/client/src/types.d.ts @@ -115,11 +115,17 @@ declare global { filterKey: (e: { altKey: boolean }, dx: number, dy: number, dz: number) => void; }); + interface PanZoomTransform { + x: number; + y: number; + scale: number; + } + interface PanZoom { zoomTo(x: number, y: number, scale: number); moveTo(x: number, y: number); on(event: string, callback: () => void); - getTransform(): unknown; + getTransform(): PanZoomTransform; dispose(): void; } } diff --git a/apps/client/src/widgets/type_widgets/relation_map/RelationMap.tsx b/apps/client/src/widgets/type_widgets/relation_map/RelationMap.tsx index 9ab687384..753cfa5da 100644 --- a/apps/client/src/widgets/type_widgets/relation_map/RelationMap.tsx +++ b/apps/client/src/widgets/type_widgets/relation_map/RelationMap.tsx @@ -15,19 +15,7 @@ import toast from "../../../services/toast"; import { CreateChildrenResponse } from "@triliumnext/commons"; import contextMenu from "../../../menus/context_menu"; import appContext from "../../../components/app_context"; - -interface MapData { - notes: { - noteId: string; - x: number; - y: number; - }[]; - transform: { - x: number, - y: number, - scale: number - } -} +import RelationMapApi, { MapData, MapDataNoteEntry } from "./api"; interface Clipboard { noteId: string; @@ -50,7 +38,9 @@ const uniDirectionalOverlays: OverlaySpec[] = [ export default function RelationMap({ note, ntxId }: TypeWidgetProps) { const [ data, setData ] = useState(); const containerRef = useRef(null); - const apiRef = useRef(null); + const mapApiRef = useRef(null); + const pbApiRef = useRef(null); + const spacedUpdate = useEditorSpacedUpdate({ note, getData() { @@ -61,7 +51,14 @@ export default function RelationMap({ note, ntxId }: TypeWidgetProps) { onContentChange(content) { if (content) { try { - setData(JSON.parse(content)); + const data = JSON.parse(content); + setData(data); + mapApiRef.current = new RelationMapApi(note, data, (newData, refreshUi) => { + if (refreshUi) { + setData(newData); + } + spacedUpdate.scheduleUpdate(); + }); return; } catch (e) { console.log("Could not parse content: ", e); @@ -87,24 +84,17 @@ export default function RelationMap({ note, ntxId }: TypeWidgetProps) { }); const onTransform = useCallback((pzInstance: PanZoom) => { - if (!containerRef.current || !apiRef.current || !data) return; + if (!containerRef.current || !mapApiRef.current || !pbApiRef.current || !data) return; const zoom = getZoom(containerRef.current); - apiRef.current.setZoom(zoom); - data.transform = JSON.parse(JSON.stringify(pzInstance.getTransform())); - spacedUpdate.scheduleUpdate(); + mapApiRef.current.setTransform(pzInstance.getTransform()); + pbApiRef.current.setZoom(zoom); }, [ 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 + mapApiRef }); usePanZoom({ @@ -129,7 +119,7 @@ export default function RelationMap({ note, ntxId }: TypeWidgetProps) {
{data?.notes.map(note => ( - + ))}
@@ -193,11 +183,11 @@ function usePanZoom({ ntxId, containerRef, options, transformData, onTransform } }); } -function useNoteCreation({ ntxId, note, containerRef, onCreate }: { +function useNoteCreation({ ntxId, note, containerRef, mapApiRef }: { ntxId: string | null | undefined; note: FNote; containerRef: RefObject; - onCreate: (newNote: MapData["notes"][number]) => void; + mapApiRef: RefObject; }) { const clipboardRef = useRef(null); useTriliumEvent("relationMapCreateChildNote", async ({ ntxId: eventNtxId }) => { @@ -227,10 +217,10 @@ function useNoteCreation({ ntxId, note, containerRef, onCreate }: { x -= 80; y -= 15; - onCreate({ noteId: clipboard.noteId, x, y }); + mapApiRef.current?.createItem({ noteId: clipboard.noteId, x, y }); clipboardRef.current = null; } - }, [ onCreate ]); + }, []); return onClickHandler; } @@ -267,7 +257,7 @@ function JsPlumb({ className, props, children, containerRef: externalContainerRe ) } -function NoteBox({ noteId, x, y }: MapData["notes"][number]) { +function NoteBox({ noteId, x, y, mapApiRef }: MapDataNoteEntry & { mapApiRef: RefObject }) { const [ note, setNote ] = useState(); useEffect(() => { froca.getNote(noteId).then(setNote); @@ -286,12 +276,19 @@ function NoteBox({ noteId, x, y }: MapData["notes"][number]) { }, { title: t("relation_map.remove_note"), - uiIcon: "bx bx-trash" + uiIcon: "bx bx-trash", + handler: async () => { + if (!note) return; + const result = await dialog.confirmDeleteNoteBoxWithNote(note.title); + if (typeof result !== "object" || !result.confirmed) return; + + mapApiRef.current?.removeItem(noteId, result.isDeleteNoteChecked); + } } ], selectMenuItemHandler() {} }) - }, [ noteId ]); + }, [ note ]); return note && (
void; + + constructor(note: FNote, initialMapData: MapData, onDataChange: (newData: MapData, refreshUi: boolean) => void) { + this.data = initialMapData; + this.onDataChange = (refreshUi) => onDataChange({ ...this.data }, refreshUi); + } + + createItem(newNote: MapDataNoteEntry) { + this.data.notes.push(newNote); + this.onDataChange(true); + } + + async removeItem(noteId: string, deleteNoteToo: boolean) { + console.log("Remove ", noteId, deleteNoteToo); + if (deleteNoteToo) { + const taskId = utils.randomString(10); + await server.remove(`notes/${noteId}?taskId=${taskId}&last=true`); + } + + if (this.data) { + this.data.notes = this.data.notes.filter((note) => note.noteId !== noteId); + } + + if (this.relations) { + this.relations = this.relations.filter((relation) => relation.sourceNoteId !== noteId && relation.targetNoteId !== noteId); + } + + this.onDataChange(true); + } + + setTransform(transform: PanZoomTransform) { + if (this.data.transform.scale - transform.scale > DELTA + || this.data.transform.x - transform.x > DELTA + || this.data.transform.y - transform.y > DELTA) { + this.data.transform = { ...transform }; + this.onDataChange(false); + } + } + +} 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 36ae9082d..5430f9bd5 100644 --- a/apps/client/src/widgets/type_widgets_old/relation_map.ts +++ b/apps/client/src/widgets/type_widgets_old/relation_map.ts @@ -173,31 +173,6 @@ export default class RelationMapTypeWidget extends TypeWidget { const $title = $noteBox.find(".title a"); const noteId = this.idToNoteId($noteBox.prop("id")); - if (command === "openInNewTab") { - } else if (command === "remove") { - const result = await dialogService.confirmDeleteNoteBoxWithNote($title.text()); - - if (typeof result !== "object" || !result.confirmed) { - return; - } - - this.jsPlumbInstance?.remove(this.noteIdToId(noteId)); - - if (result.isDeleteNoteChecked) { - const taskId = utils.randomString(10); - - await server.remove(`notes/${noteId}?taskId=${taskId}&last=true`); - } - - if (this.mapData) { - this.mapData.notes = this.mapData.notes.filter((note) => note.noteId !== noteId); - } - - if (this.relations) { - this.relations = this.relations.filter((relation) => relation.sourceNoteId !== noteId && relation.targetNoteId !== noteId); - } - - this.saveData(); } else if (command === "editTitle") { const title = await dialogService.prompt({ title: t("relation_map.rename_note"),