chore(react/type_widgets): reintroduce relation creation

This commit is contained in:
Elian Doran 2025-10-04 09:37:14 +03:00
parent d076d54170
commit c469fffb6e
No known key found for this signature in database
4 changed files with 76 additions and 59 deletions

View File

@ -1,6 +1,6 @@
import { useCallback, useEffect, useRef, useState } from "preact/hooks";
import { TypeWidgetProps } from "../type_widget";
import { jsPlumbInstance } from "jsplumb";
import { jsPlumbInstance, OnConnectionBindInfo } from "jsplumb";
import { useEditorSpacedUpdate, useTriliumEvent, useTriliumEvents } from "../../react/hooks";
import FNote from "../../../entities/fnote";
import { RefObject } from "preact";
@ -14,8 +14,10 @@ import { CreateChildrenResponse, RelationMapPostResponse, RelationMapRelation }
import RelationMapApi, { MapData, MapDataNoteEntry } from "./api";
import setupOverlays, { uniDirectionalOverlays } from "./overlays";
import { JsPlumb } from "./jsplumb";
import { getMousePosition, getZoom, noteIdToId } from "./utils";
import { getMousePosition, getZoom, idToNoteId, noteIdToId } from "./utils";
import { NoteBox } from "./NoteBox";
import utils from "../../../services/utils";
import attribute_autocomplete from "../../../services/attribute_autocomplete";
interface Clipboard {
noteId: string;
@ -95,6 +97,8 @@ export default function RelationMap({ note, ntxId }: TypeWidgetProps) {
mapApiRef
});
const connectionCallback = useRelationCreation({ mapApiRef, jsPlumbApiRef: pbApiRef });
usePanZoom({
ntxId,
containerRef,
@ -129,6 +133,7 @@ export default function RelationMap({ note, ntxId }: TypeWidgetProps) {
HoverPaintStyle: { stroke: "#777", strokeWidth: 1 },
}}
onInstanceCreated={setupOverlays}
onConnection={connectionCallback}
>
{data?.notes.map(note => (
<NoteBox {...note} mapApiRef={mapApiRef} />
@ -304,3 +309,49 @@ function useNoteCreation({ ntxId, note, containerRef, mapApiRef }: {
}, []);
return onClickHandler;
}
function useRelationCreation({ mapApiRef, jsPlumbApiRef }: { mapApiRef: RefObject<RelationMapApi>, jsPlumbApiRef: RefObject<jsPlumbInstance> }) {
const connectionCallback = useCallback(async (info: OnConnectionBindInfo, originalEvent: Event) => {
// 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 }) => {
if (!$answer) {
return;
}
$answer.on("keyup", () => {
// invalid characters are simply ignored (from user perspective they are not even entered)
const attrName = utils.filterAttributeName($answer.val() as string);
$answer.val(attrName);
});
attribute_autocomplete.initAttributeNameAutocomplete({
$el: $answer,
attributeType: "relation",
open: true
});
}
});
// Delete the newly created connection if the dialog was dismissed.
if (!name || !name.trim()) {
jsPlumbApiRef.current?.deleteConnection(connection);
return;
}
const targetNoteId = idToNoteId(connection.target.id);
const sourceNoteId = idToNoteId(connection.source.id);
const result = await mapApiRef.current.connect(name, sourceNoteId, targetNoteId);
if (!result) {
await dialog.info(t("relation_map.connection_exists", { name }));
jsPlumbApiRef.current?.deleteConnection(connection);
}
}, []);
return connectionCallback;
}

View File

@ -79,4 +79,14 @@ export default class RelationMapApi {
this.onDataChange(false);
}
async connect(name: string, sourceNoteId: string, targetNoteId: string) {
name = utils.filterAttributeName(name);
const relationExists = this.relations?.some((rel) => rel.targetNoteId === targetNoteId && rel.sourceNoteId === sourceNoteId && rel.name === name);
if (relationExists) return false;
await server.put(`notes/${sourceNoteId}/relations/${name}/to/${targetNoteId}`);
this.onDataChange(true);
return true;
}
}

View File

@ -1,19 +1,21 @@
import { jsPlumb, Defaults, jsPlumbInstance, DragOptions } from "jsplumb";
import { jsPlumb, Defaults, jsPlumbInstance, DragOptions, OnConnectionBindInfo } from "jsplumb";
import { ComponentChildren, createContext, RefObject } from "preact";
import { HTMLProps } from "preact/compat";
import { useContext, useEffect, useRef } from "preact/hooks";
const JsPlumbInstance = createContext<RefObject<jsPlumbInstance> | undefined>(undefined);
export function JsPlumb({ className, props, children, containerRef: externalContainerRef, apiRef, onInstanceCreated }: {
export function JsPlumb({ className, props, children, containerRef: externalContainerRef, apiRef, onInstanceCreated, onConnection }: {
className?: string;
props: Omit<Defaults, "container">;
children: ComponentChildren;
containerRef?: RefObject<HTMLElement>;
apiRef?: RefObject<jsPlumbInstance>;
onInstanceCreated?: (jsPlumbInstance: jsPlumbInstance) => void;
onConnection?: (info: OnConnectionBindInfo, originalEvent: Event) => void;
}) {
const containerRef = useRef<HTMLDivElement>(null);
const jsPlumbRef = useRef<jsPlumbInstance>();
useEffect(() => {
if (!containerRef.current) return;
@ -28,6 +30,7 @@ export function JsPlumb({ className, props, children, containerRef: externalCont
if (apiRef) {
apiRef.current = jsPlumbInstance;
}
jsPlumbRef.current = jsPlumbInstance;
onInstanceCreated?.(jsPlumbInstance);
return () => {
@ -36,6 +39,14 @@ export function JsPlumb({ className, props, children, containerRef: externalCont
};
}, [ apiRef ]);
useEffect(() => {
const jsPlumbInstance = jsPlumbRef.current;
if (!jsPlumbInstance || !onConnection) return;
jsPlumbInstance.bind("connection", onConnection);
return () => jsPlumbInstance.unbind("connection", onConnection);
}, [ onConnection ]);
return (
<div ref={containerRef} className={className}>
<JsPlumbInstance.Provider value={apiRef}>

View File

@ -116,12 +116,9 @@ export default class RelationMapTypeWidget extends TypeWidget {
if (!this.jsPlumbInstance) {
return;
}
this.jsPlumbInstance.bind("connection", (info, originalEvent) => this.connectionCreatedHandler(info, originalEvent));
}
async connectionCreatedHandler(info: ConnectionMadeEventInfo, originalEvent: Event) {
const connection = info.connection;
connection.bind("contextmenu", (obj: unknown, event: MouseEvent) => {
if (connection.getType().includes("link")) {
@ -156,58 +153,6 @@ export default class RelationMapTypeWidget extends TypeWidget {
});
}
});
// if there's no event, then this has been triggered programmatically
if (!originalEvent || !this.jsPlumbInstance) {
return;
}
let name = await dialogService.prompt({
message: t("relation_map.specify_new_relation_name"),
shown: ({ $answer }) => {
if (!$answer) {
return;
}
$answer.on("keyup", () => {
// invalid characters are simply ignored (from user perspective they are not even entered)
const attrName = utils.filterAttributeName($answer.val() as string);
$answer.val(attrName);
});
attributeAutocompleteService.initAttributeNameAutocomplete({
$el: $answer,
attributeType: "relation",
open: true
});
}
});
if (!name || !name.trim()) {
this.jsPlumbInstance.deleteConnection(connection);
return;
}
name = utils.filterAttributeName(name);
const targetNoteId = this.idToNoteId(connection.target.id);
const sourceNoteId = this.idToNoteId(connection.source.id);
const relationExists = this.relations?.some((rel) => rel.targetNoteId === targetNoteId && rel.sourceNoteId === sourceNoteId && rel.name === name);
if (relationExists) {
await dialogService.info(t("relation_map.connection_exists", { name }));
this.jsPlumbInstance.deleteConnection(connection);
return;
}
await server.put(`notes/${sourceNoteId}/relations/${name}/to/${targetNoteId}`);
this.loadNotesAndRelations();
}
saveData() {