chore(react/type_widgets): reintroduce relation context menu

This commit is contained in:
Elian Doran 2025-10-04 09:54:47 +03:00
parent c469fffb6e
commit 1eca9f6541
No known key found for this signature in database
4 changed files with 63 additions and 47 deletions

View File

@ -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<MapData>();
const containerRef = useRef<HTMLDivElement>(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<RelationMapApi>, jsPlumbApiRef: RefObject<jsPlumbInstance> }) {
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 }) => {

View File

@ -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;

View File

@ -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<RelationMapApi>) {
return (e: MouseEvent) => {
@ -54,3 +55,31 @@ export function buildNoteContextMenuHandler(note: FNote | null | undefined, mapA
})
};
}
export function buildRelationContextMenuHandler(connection: Connection, mapApiRef: RefObject<RelationMapApi>) {
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);
}
}
});
}
};
}

View File

@ -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();
}