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 { useCallback, useEffect, useRef, useState } from "preact/hooks";
|
||||||
import { TypeWidgetProps } from "../type_widget";
|
import { TypeWidgetProps } from "../type_widget";
|
||||||
import { jsPlumbInstance } from "jsplumb";
|
import { jsPlumbInstance, OnConnectionBindInfo } from "jsplumb";
|
||||||
import { useEditorSpacedUpdate, useTriliumEvent, useTriliumEvents } from "../../react/hooks";
|
import { useEditorSpacedUpdate, useTriliumEvent, useTriliumEvents } from "../../react/hooks";
|
||||||
import FNote from "../../../entities/fnote";
|
import FNote from "../../../entities/fnote";
|
||||||
import { RefObject } from "preact";
|
import { RefObject } from "preact";
|
||||||
@ -14,8 +14,10 @@ import { CreateChildrenResponse, RelationMapPostResponse, RelationMapRelation }
|
|||||||
import RelationMapApi, { MapData, MapDataNoteEntry } from "./api";
|
import RelationMapApi, { MapData, MapDataNoteEntry } from "./api";
|
||||||
import setupOverlays, { uniDirectionalOverlays } from "./overlays";
|
import setupOverlays, { uniDirectionalOverlays } from "./overlays";
|
||||||
import { JsPlumb } from "./jsplumb";
|
import { JsPlumb } from "./jsplumb";
|
||||||
import { getMousePosition, getZoom, noteIdToId } from "./utils";
|
import { getMousePosition, getZoom, idToNoteId, noteIdToId } from "./utils";
|
||||||
import { NoteBox } from "./NoteBox";
|
import { NoteBox } from "./NoteBox";
|
||||||
|
import utils from "../../../services/utils";
|
||||||
|
import attribute_autocomplete from "../../../services/attribute_autocomplete";
|
||||||
|
|
||||||
interface Clipboard {
|
interface Clipboard {
|
||||||
noteId: string;
|
noteId: string;
|
||||||
@ -95,6 +97,8 @@ export default function RelationMap({ note, ntxId }: TypeWidgetProps) {
|
|||||||
mapApiRef
|
mapApiRef
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const connectionCallback = useRelationCreation({ mapApiRef, jsPlumbApiRef: pbApiRef });
|
||||||
|
|
||||||
usePanZoom({
|
usePanZoom({
|
||||||
ntxId,
|
ntxId,
|
||||||
containerRef,
|
containerRef,
|
||||||
@ -129,6 +133,7 @@ export default function RelationMap({ note, ntxId }: TypeWidgetProps) {
|
|||||||
HoverPaintStyle: { stroke: "#777", strokeWidth: 1 },
|
HoverPaintStyle: { stroke: "#777", strokeWidth: 1 },
|
||||||
}}
|
}}
|
||||||
onInstanceCreated={setupOverlays}
|
onInstanceCreated={setupOverlays}
|
||||||
|
onConnection={connectionCallback}
|
||||||
>
|
>
|
||||||
{data?.notes.map(note => (
|
{data?.notes.map(note => (
|
||||||
<NoteBox {...note} mapApiRef={mapApiRef} />
|
<NoteBox {...note} mapApiRef={mapApiRef} />
|
||||||
@ -304,3 +309,49 @@ function useNoteCreation({ ntxId, note, containerRef, mapApiRef }: {
|
|||||||
}, []);
|
}, []);
|
||||||
return onClickHandler;
|
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);
|
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 { ComponentChildren, createContext, RefObject } from "preact";
|
||||||
import { HTMLProps } from "preact/compat";
|
import { HTMLProps } from "preact/compat";
|
||||||
import { useContext, useEffect, useRef } from "preact/hooks";
|
import { useContext, useEffect, useRef } from "preact/hooks";
|
||||||
|
|
||||||
const JsPlumbInstance = createContext<RefObject<jsPlumbInstance> | undefined>(undefined);
|
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;
|
className?: string;
|
||||||
props: Omit<Defaults, "container">;
|
props: Omit<Defaults, "container">;
|
||||||
children: ComponentChildren;
|
children: ComponentChildren;
|
||||||
containerRef?: RefObject<HTMLElement>;
|
containerRef?: RefObject<HTMLElement>;
|
||||||
apiRef?: RefObject<jsPlumbInstance>;
|
apiRef?: RefObject<jsPlumbInstance>;
|
||||||
onInstanceCreated?: (jsPlumbInstance: jsPlumbInstance) => void;
|
onInstanceCreated?: (jsPlumbInstance: jsPlumbInstance) => void;
|
||||||
|
onConnection?: (info: OnConnectionBindInfo, originalEvent: Event) => void;
|
||||||
}) {
|
}) {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const jsPlumbRef = useRef<jsPlumbInstance>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!containerRef.current) return;
|
if (!containerRef.current) return;
|
||||||
@ -28,6 +30,7 @@ export function JsPlumb({ className, props, children, containerRef: externalCont
|
|||||||
if (apiRef) {
|
if (apiRef) {
|
||||||
apiRef.current = jsPlumbInstance;
|
apiRef.current = jsPlumbInstance;
|
||||||
}
|
}
|
||||||
|
jsPlumbRef.current = jsPlumbInstance;
|
||||||
|
|
||||||
onInstanceCreated?.(jsPlumbInstance);
|
onInstanceCreated?.(jsPlumbInstance);
|
||||||
return () => {
|
return () => {
|
||||||
@ -36,6 +39,14 @@ export function JsPlumb({ className, props, children, containerRef: externalCont
|
|||||||
};
|
};
|
||||||
}, [ apiRef ]);
|
}, [ apiRef ]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const jsPlumbInstance = jsPlumbRef.current;
|
||||||
|
if (!jsPlumbInstance || !onConnection) return;
|
||||||
|
|
||||||
|
jsPlumbInstance.bind("connection", onConnection);
|
||||||
|
return () => jsPlumbInstance.unbind("connection", onConnection);
|
||||||
|
}, [ onConnection ]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={containerRef} className={className}>
|
<div ref={containerRef} className={className}>
|
||||||
<JsPlumbInstance.Provider value={apiRef}>
|
<JsPlumbInstance.Provider value={apiRef}>
|
||||||
|
|||||||
@ -116,12 +116,9 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
|||||||
if (!this.jsPlumbInstance) {
|
if (!this.jsPlumbInstance) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.jsPlumbInstance.bind("connection", (info, originalEvent) => this.connectionCreatedHandler(info, originalEvent));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async connectionCreatedHandler(info: ConnectionMadeEventInfo, originalEvent: Event) {
|
async connectionCreatedHandler(info: ConnectionMadeEventInfo, originalEvent: Event) {
|
||||||
const connection = info.connection;
|
|
||||||
|
|
||||||
connection.bind("contextmenu", (obj: unknown, event: MouseEvent) => {
|
connection.bind("contextmenu", (obj: unknown, event: MouseEvent) => {
|
||||||
if (connection.getType().includes("link")) {
|
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() {
|
saveData() {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user