mirror of
https://github.com/zadam/trilium.git
synced 2025-11-11 17:08:58 +01:00
chore(react/type_widgets): reintroduce relation creation
This commit is contained in:
parent
d076d54170
commit
c469fffb6e
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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}>
|
||||
|
||||
@ -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() {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user