chore(type_widgets): get relations to render

This commit is contained in:
Elian Doran 2025-09-29 22:31:53 +03:00
parent 082ea7b5c1
commit 7a2d91e7de
No known key found for this signature in database
5 changed files with 114 additions and 103 deletions

View File

@ -12,7 +12,7 @@ import panzoom, { PanZoomOptions } from "panzoom";
import dialog from "../../../services/dialog"; import dialog from "../../../services/dialog";
import server from "../../../services/server"; import server from "../../../services/server";
import toast from "../../../services/toast"; import toast from "../../../services/toast";
import { CreateChildrenResponse } from "@triliumnext/commons"; import { CreateChildrenResponse, RelationMapPostResponse, RelationMapRelation } from "@triliumnext/commons";
import contextMenu from "../../../menus/context_menu"; import contextMenu from "../../../menus/context_menu";
import appContext from "../../../components/app_context"; import appContext from "../../../components/app_context";
import RelationMapApi, { MapData, MapDataNoteEntry } from "./api"; import RelationMapApi, { MapData, MapDataNoteEntry } from "./api";
@ -23,6 +23,13 @@ interface Clipboard {
title: string; title: string;
} }
type RelationType = "uniDirectional" | "biDirectional" | "inverse";
interface ClientRelation extends RelationMapRelation {
type: RelationType;
render: boolean;
}
export default function RelationMap({ note, ntxId }: TypeWidgetProps) { export default function RelationMap({ note, ntxId }: TypeWidgetProps) {
const [ data, setData ] = useState<MapData>(); const [ data, setData ] = useState<MapData>();
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
@ -103,6 +110,8 @@ export default function RelationMap({ note, ntxId }: TypeWidgetProps) {
onTransform onTransform
}); });
useRelationData(note.noteId, data, mapApiRef, pbApiRef);
return ( return (
<div className="note-detail-relation-map note-detail-printable"> <div className="note-detail-relation-map note-detail-printable">
<div className="relation-map-wrapper" onClick={clickCallback}> <div className="relation-map-wrapper" onClick={clickCallback}>
@ -172,6 +181,84 @@ function usePanZoom({ ntxId, containerRef, options, transformData, onTransform }
}); });
} }
async function useRelationData(noteId: string, mapData: MapData | undefined, mapApiRef: RefObject<RelationMapApi>, jsPlumbRef: RefObject<jsPlumbInstance>) {
const noteIds = mapData?.notes.map((note) => note.noteId);
const [ relations, setRelations ] = useState<ClientRelation[]>();
const [ inverseRelations, setInverseRelations ] = useState<RelationMapPostResponse["inverseRelations"]>();
async function refresh() {
const data = await server.post<RelationMapPostResponse>("relation-map", { noteIds, relationMapNoteId: noteId });
const relations: ClientRelation[] = [];
for (const _relation of data.relations) {
const relation = _relation as ClientRelation; // we inject a few variables.
const match = relations.find(
(rel) =>
rel.name === data.inverseRelations[relation.name] &&
((rel.sourceNoteId === relation.sourceNoteId && rel.targetNoteId === relation.targetNoteId) ||
(rel.sourceNoteId === relation.targetNoteId && rel.targetNoteId === relation.sourceNoteId))
);
if (match) {
match.type = relation.type = relation.name === data.inverseRelations[relation.name] ? "biDirectional" : "inverse";
relation.render = false; // don't render second relation
} else {
relation.type = "uniDirectional";
relation.render = true;
}
relations.push(relation);
setInverseRelations(data.inverseRelations);
}
setRelations(relations);
mapApiRef.current?.cleanupOtherNotes(Object.keys(data.noteTitles));
}
useEffect(() => {
refresh();
}, [ noteId, mapData, jsPlumbInstance ]);
// Refresh on the canvas.
useEffect(() => {
const jsPlumbInstance = jsPlumbRef.current;
if (!jsPlumbInstance) return;
jsPlumbInstance.batch(async () => {
if (!mapData || !relations) {
return;
}
jsPlumbInstance.deleteEveryEndpoint();
for (const relation of relations) {
if (!relation.render) {
continue;
}
const connection = jsPlumbInstance.connect({
source: noteIdToId(relation.sourceNoteId),
target: noteIdToId(relation.targetNoteId),
type: relation.type
});
// TODO: Does this actually do anything.
//@ts-expect-error
connection.id = relation.attributeId;
if (relation.type === "inverse") {
connection.getOverlay("label-source").setLabel(relation.name);
connection.getOverlay("label-target").setLabel(inverseRelations?.[relation.name] ?? "");
} else {
connection.getOverlay("label").setLabel(relation.name);
}
connection.canvas.setAttribute("data-connection-id", connection.id);
}
});
}, [ relations, mapData ]);
}
function useNoteCreation({ ntxId, note, containerRef, mapApiRef }: { function useNoteCreation({ ntxId, note, containerRef, mapApiRef }: {
ntxId: string | null | undefined; ntxId: string | null | undefined;
note: FNote; note: FNote;
@ -238,7 +325,10 @@ function JsPlumb({ className, props, children, containerRef: externalContainerRe
} }
onInstanceCreated?.(jsPlumbInstance); onInstanceCreated?.(jsPlumbInstance);
return () => jsPlumbInstance.cleanupListeners(); return () => {
jsPlumbInstance.deleteEveryEndpoint();
jsPlumbInstance.cleanupListeners()
};
}, [ apiRef ]); }, [ apiRef ]);
return ( return (

View File

@ -49,6 +49,13 @@ export default class RelationMapApi {
this.onDataChange(true); 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;
this.data.notes = filteredNotes;
this.onDataChange(true);
}
setTransform(transform: PanZoomTransform) { setTransform(transform: PanZoomTransform) {
if (this.data.transform.scale - transform.scale > DELTA if (this.data.transform.scale - transform.scale > DELTA
|| this.data.transform.x - transform.x > DELTA || this.data.transform.x - transform.x > DELTA

View File

@ -31,24 +31,6 @@ declare module "jsplumb" {
let containerCounter = 1; let containerCounter = 1;
export type RelationType = "uniDirectional" | "biDirectional" | "inverse";
interface Relation {
name: string;
attributeId: string;
sourceNoteId: string;
targetNoteId: string;
type: RelationType;
render: boolean;
}
// TODO: Deduplicate.
interface RelationMapPostResponse {
relations: Relation[];
inverseRelations: Record<string, string>;
noteTitles: Record<string, string>;
}
type MenuCommands = "openInNewTab" | "remove" | "editTitle"; type MenuCommands = "openInNewTab" | "remove" | "editTitle";
export default class RelationMapTypeWidget extends TypeWidget { export default class RelationMapTypeWidget extends TypeWidget {
@ -113,77 +95,6 @@ export default class RelationMapTypeWidget extends TypeWidget {
this.$relationMapContainer.empty(); this.$relationMapContainer.empty();
} }
async loadNotesAndRelations() {
if (!this.mapData || !this.jsPlumbInstance) {
return;
}
const noteIds = this.mapData.notes.map((note) => note.noteId);
const data = await server.post<RelationMapPostResponse>("relation-map", { noteIds, relationMapNoteId: this.noteId });
this.relations = [];
for (const relation of data.relations) {
const match = this.relations.find(
(rel) =>
rel.name === data.inverseRelations[relation.name] &&
((rel.sourceNoteId === relation.sourceNoteId && rel.targetNoteId === relation.targetNoteId) ||
(rel.sourceNoteId === relation.targetNoteId && rel.targetNoteId === relation.sourceNoteId))
);
if (match) {
match.type = relation.type = relation.name === data.inverseRelations[relation.name] ? "biDirectional" : "inverse";
relation.render = false; // don't render second relation
} else {
relation.type = "uniDirectional";
relation.render = true;
}
this.relations.push(relation);
}
this.mapData.notes = this.mapData.notes.filter((note) => note.noteId in data.noteTitles);
this.jsPlumbInstance.batch(async () => {
if (!this.jsPlumbInstance || !this.mapData || !this.relations) {
return;
}
this.clearMap();
for (const note of this.mapData.notes) {
const title = data.noteTitles[note.noteId];
await this.createNoteBox(note.noteId, title, note.x, note.y);
}
for (const relation of this.relations) {
if (!relation.render) {
continue;
}
const connection = this.jsPlumbInstance.connect({
source: this.noteIdToId(relation.sourceNoteId),
target: this.noteIdToId(relation.targetNoteId),
type: relation.type
});
// TODO: Does this actually do anything.
//@ts-expect-error
connection.id = relation.attributeId;
if (relation.type === "inverse") {
connection.getOverlay("label-source").setLabel(relation.name);
connection.getOverlay("label-target").setLabel(data.inverseRelations[relation.name]);
} else {
connection.getOverlay("label").setLabel(relation.name);
}
connection.canvas.setAttribute("data-connection-id", connection.id);
}
});
}
cleanup() { cleanup() {
if (this.jsPlumbInstance) { if (this.jsPlumbInstance) {
this.clearMap(); this.clearMap();

View File

@ -1,22 +1,12 @@
import type { Request } from "express"; import type { Request } from "express";
import becca from "../../becca/becca.js"; import becca from "../../becca/becca.js";
import sql from "../../services/sql.js"; import sql from "../../services/sql.js";
import { RelationMapPostResponse } from "@triliumnext/commons";
interface ResponseData {
noteTitles: Record<string, string>;
relations: {
attributeId: string;
sourceNoteId: string;
targetNoteId: string;
name: string;
}[];
inverseRelations: Record<string, string>;
}
function getRelationMap(req: Request) { function getRelationMap(req: Request) {
const { relationMapNoteId, noteIds } = req.body; const { relationMapNoteId, noteIds } = req.body;
const resp: ResponseData = { const resp: RelationMapPostResponse = {
// noteId => title // noteId => title
noteTitles: {}, noteTitles: {},
relations: [], relations: [],

View File

@ -247,3 +247,16 @@ export interface SchemaResponse {
type: string; type: string;
}[]; }[];
} }
export interface RelationMapRelation {
name: string;
attributeId: string;
sourceNoteId: string;
targetNoteId: string;
}
export interface RelationMapPostResponse {
noteTitles: Record<string, string>;
relations: RelationMapRelation[];
inverseRelations: Record<string, string>;
}