mirror of
https://github.com/zadam/trilium.git
synced 2025-11-11 08:58:58 +01:00
chore(type_widgets): get relations to render
This commit is contained in:
parent
082ea7b5c1
commit
7a2d91e7de
@ -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 (
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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: [],
|
||||||
|
|||||||
@ -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>;
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user