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 4b6fa4a73..ed62dce7d 100644 --- a/apps/client/src/widgets/type_widgets/relation_map/RelationMap.tsx +++ b/apps/client/src/widgets/type_widgets/relation_map/RelationMap.tsx @@ -11,26 +11,20 @@ import dialog from "../../../services/dialog"; import server from "../../../services/server"; import toast from "../../../services/toast"; import { CreateChildrenResponse, RelationMapPostResponse, RelationMapRelation } from "@triliumnext/commons"; -import RelationMapApi, { MapData, MapDataNoteEntry } from "./api"; +import RelationMapApi, { ClientRelation, MapData, MapDataNoteEntry } from "./api"; import setupOverlays, { uniDirectionalOverlays } from "./overlays"; import { JsPlumb } from "./jsplumb"; import { getMousePosition, getZoom, idToNoteId, noteIdToId } from "./utils"; import { NoteBox } from "./NoteBox"; import utils from "../../../services/utils"; import attribute_autocomplete from "../../../services/attribute_autocomplete"; +import { buildRelationContextMenuHandler } from "./context_menu"; interface Clipboard { noteId: string; title: string; } -type RelationType = "uniDirectional" | "biDirectional" | "inverse"; - -interface ClientRelation extends RelationMapRelation { - type: RelationType; - render: boolean; -} - export default function RelationMap({ note, ntxId }: TypeWidgetProps) { const [ data, setData ] = useState(); const containerRef = useRef(null); @@ -222,6 +216,7 @@ async function useRelationData(noteId: string, mapData: MapData | undefined, map } setRelations(relations); + mapApiRef.current?.loadRelations(relations); mapApiRef.current?.cleanupOtherNotes(Object.keys(data.noteTitles)); } @@ -312,10 +307,15 @@ function useNoteCreation({ ntxId, note, containerRef, mapApiRef }: { function useRelationCreation({ mapApiRef, jsPlumbApiRef }: { mapApiRef: RefObject, jsPlumbApiRef: RefObject }) { const connectionCallback = useCallback(async (info: OnConnectionBindInfo, originalEvent: Event) => { + const connection = info.connection; + + // Called whenever a connection is created, either initially or manually when added by the user. + const handler = buildRelationContextMenuHandler(connection, mapApiRef); + connection.bind("contextmenu", handler); + // if there's no event, then this has been triggered programmatically if (!originalEvent || !mapApiRef.current) return; - const connection = info.connection; let name = await dialog.prompt({ message: t("relation_map.specify_new_relation_name"), shown: ({ $answer }) => { diff --git a/apps/client/src/widgets/type_widgets/relation_map/api.ts b/apps/client/src/widgets/type_widgets/relation_map/api.ts index 693aeecd2..cf9c2c280 100644 --- a/apps/client/src/widgets/type_widgets/relation_map/api.ts +++ b/apps/client/src/widgets/type_widgets/relation_map/api.ts @@ -1,7 +1,9 @@ +import { Connection } from "jsplumb"; import FNote from "../../../entities/fnote"; import { t } from "../../../services/i18n"; import server from "../../../services/server"; import utils from "../../../services/utils"; +import { RelationMapRelation } from "@triliumnext/commons"; export interface MapDataNoteEntry { noteId: string; @@ -14,17 +16,29 @@ export interface MapData { transform: PanZoomTransform; } +export type RelationType = "uniDirectional" | "biDirectional" | "inverse"; + +export interface ClientRelation extends RelationMapRelation { + type: RelationType; + render: boolean; +} + const DELTA = 0.0001; export default class RelationMapApi { private data: MapData; - private relations: any[]; + private relations: ClientRelation[]; private onDataChange: (refreshUi: boolean) => void; constructor(note: FNote, initialMapData: MapData, onDataChange: (newData: MapData, refreshUi: boolean) => void) { this.data = initialMapData; this.onDataChange = (refreshUi) => onDataChange({ ...this.data }, refreshUi); + this.relations = []; + } + + loadRelations(relations: ClientRelation[]) { + this.relations = relations; } createItem(newNote: MapDataNoteEntry) { @@ -50,6 +64,16 @@ export default class RelationMapApi { this.onDataChange(true); } + async removeRelation(connection: Connection) { + const relation = this.relations.find((rel) => rel.attributeId === connection.id); + + if (relation) { + await server.remove(`notes/${relation.sourceNoteId}/relations/${relation.name}/to/${relation.targetNoteId}`); + } + + this.onDataChange(true); + } + cleanupOtherNotes(noteIds: string[]) { const filteredNotes = this.data.notes.filter((note) => noteIds.includes(note.noteId)); if (filteredNotes.length === this.data.notes.length) return; diff --git a/apps/client/src/widgets/type_widgets/relation_map/context_menu.ts b/apps/client/src/widgets/type_widgets/relation_map/context_menu.ts index 3c27cb13c..4dd4b2e68 100644 --- a/apps/client/src/widgets/type_widgets/relation_map/context_menu.ts +++ b/apps/client/src/widgets/type_widgets/relation_map/context_menu.ts @@ -6,6 +6,7 @@ import dialog from "../../../services/dialog"; import { t } from "../../../services/i18n"; import server from "../../../services/server"; import RelationMapApi from "./api"; +import { Connection } from "jsplumb"; export function buildNoteContextMenuHandler(note: FNote | null | undefined, mapApiRef: RefObject) { return (e: MouseEvent) => { @@ -54,3 +55,31 @@ export function buildNoteContextMenuHandler(note: FNote | null | undefined, mapA }) }; } + +export function buildRelationContextMenuHandler(connection: Connection, mapApiRef: RefObject) { + return (_, event: MouseEvent) => { + if (connection.getType().includes("link")) { + // don't create context menu if it's a link since there's nothing to do with link from relation map + // (don't open browser menu either) + event.preventDefault(); + } else { + event.preventDefault(); + event.stopPropagation(); + + contextMenu.show({ + x: event.pageX, + y: event.pageY, + items: [{ title: t("relation_map.remove_relation"), command: "remove", uiIcon: "bx bx-trash" }], + selectMenuItemHandler: async ({ command }) => { + if (command === "remove") { + if (!(await dialog.confirm(t("relation_map.confirm_remove_relation")))) { + return; + } + + mapApiRef.current?.removeRelation(connection); + } + } + }); + } + }; +} 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 467fd0f73..c414cdcac 100644 --- a/apps/client/src/widgets/type_widgets_old/relation_map.ts +++ b/apps/client/src/widgets/type_widgets_old/relation_map.ts @@ -118,43 +118,6 @@ export default class RelationMapTypeWidget extends TypeWidget { } } - async connectionCreatedHandler(info: ConnectionMadeEventInfo, originalEvent: Event) { - - connection.bind("contextmenu", (obj: unknown, event: MouseEvent) => { - if (connection.getType().includes("link")) { - // don't create context menu if it's a link since there's nothing to do with link from relation map - // (don't open browser menu either) - event.preventDefault(); - } else { - event.preventDefault(); - event.stopPropagation(); - - contextMenu.show({ - x: event.pageX, - y: event.pageY, - items: [{ title: t("relation_map.remove_relation"), command: "remove", uiIcon: "bx bx-trash" }], - selectMenuItemHandler: async ({ command }) => { - if (command === "remove") { - if (!(await dialogService.confirm(t("relation_map.confirm_remove_relation"))) || !this.relations) { - return; - } - - const relation = this.relations.find((rel) => rel.attributeId === connection.id); - - if (relation) { - await server.remove(`notes/${relation.sourceNoteId}/relations/${relation.name}/to/${relation.targetNoteId}`); - } - - this.jsPlumbInstance?.deleteConnection(connection); - - this.relations = this.relations.filter((relation) => relation.attributeId !== connection.id); - } - } - }); - } - }); - } - saveData() { this.spacedUpdate.scheduleUpdate(); }