mirror of
https://github.com/TriliumNext/Trilium.git
synced 2025-12-10 03:53:37 -06:00
refactor(type_widgets): use API architecture for relation map
This commit is contained in:
parent
614fc66890
commit
1c1243912b
8
apps/client/src/types.d.ts
vendored
8
apps/client/src/types.d.ts
vendored
@ -115,11 +115,17 @@ declare global {
|
|||||||
filterKey: (e: { altKey: boolean }, dx: number, dy: number, dz: number) => void;
|
filterKey: (e: { altKey: boolean }, dx: number, dy: number, dz: number) => void;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
interface PanZoomTransform {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
scale: number;
|
||||||
|
}
|
||||||
|
|
||||||
interface PanZoom {
|
interface PanZoom {
|
||||||
zoomTo(x: number, y: number, scale: number);
|
zoomTo(x: number, y: number, scale: number);
|
||||||
moveTo(x: number, y: number);
|
moveTo(x: number, y: number);
|
||||||
on(event: string, callback: () => void);
|
on(event: string, callback: () => void);
|
||||||
getTransform(): unknown;
|
getTransform(): PanZoomTransform;
|
||||||
dispose(): void;
|
dispose(): void;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,19 +15,7 @@ import toast from "../../../services/toast";
|
|||||||
import { CreateChildrenResponse } from "@triliumnext/commons";
|
import { CreateChildrenResponse } 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";
|
||||||
interface MapData {
|
|
||||||
notes: {
|
|
||||||
noteId: string;
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
}[];
|
|
||||||
transform: {
|
|
||||||
x: number,
|
|
||||||
y: number,
|
|
||||||
scale: number
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Clipboard {
|
interface Clipboard {
|
||||||
noteId: string;
|
noteId: string;
|
||||||
@ -50,7 +38,9 @@ const uniDirectionalOverlays: OverlaySpec[] = [
|
|||||||
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);
|
||||||
const apiRef = useRef<jsPlumbInstance>(null);
|
const mapApiRef = useRef<RelationMapApi>(null);
|
||||||
|
const pbApiRef = useRef<jsPlumbInstance>(null);
|
||||||
|
|
||||||
const spacedUpdate = useEditorSpacedUpdate({
|
const spacedUpdate = useEditorSpacedUpdate({
|
||||||
note,
|
note,
|
||||||
getData() {
|
getData() {
|
||||||
@ -61,7 +51,14 @@ export default function RelationMap({ note, ntxId }: TypeWidgetProps) {
|
|||||||
onContentChange(content) {
|
onContentChange(content) {
|
||||||
if (content) {
|
if (content) {
|
||||||
try {
|
try {
|
||||||
setData(JSON.parse(content));
|
const data = JSON.parse(content);
|
||||||
|
setData(data);
|
||||||
|
mapApiRef.current = new RelationMapApi(note, data, (newData, refreshUi) => {
|
||||||
|
if (refreshUi) {
|
||||||
|
setData(newData);
|
||||||
|
}
|
||||||
|
spacedUpdate.scheduleUpdate();
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("Could not parse content: ", e);
|
console.log("Could not parse content: ", e);
|
||||||
@ -87,24 +84,17 @@ export default function RelationMap({ note, ntxId }: TypeWidgetProps) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const onTransform = useCallback((pzInstance: PanZoom) => {
|
const onTransform = useCallback((pzInstance: PanZoom) => {
|
||||||
if (!containerRef.current || !apiRef.current || !data) return;
|
if (!containerRef.current || !mapApiRef.current || !pbApiRef.current || !data) return;
|
||||||
const zoom = getZoom(containerRef.current);
|
const zoom = getZoom(containerRef.current);
|
||||||
apiRef.current.setZoom(zoom);
|
mapApiRef.current.setTransform(pzInstance.getTransform());
|
||||||
data.transform = JSON.parse(JSON.stringify(pzInstance.getTransform()));
|
pbApiRef.current.setZoom(zoom);
|
||||||
spacedUpdate.scheduleUpdate();
|
|
||||||
}, [ data ]);
|
}, [ data ]);
|
||||||
|
|
||||||
const onNewItem = useCallback((newNote: MapData["notes"][number]) => {
|
|
||||||
if (!data) return;
|
|
||||||
data.notes.push(newNote);
|
|
||||||
setData({ ...data });
|
|
||||||
spacedUpdate.scheduleUpdate();
|
|
||||||
}, [ data, spacedUpdate ]);
|
|
||||||
const clickCallback = useNoteCreation({
|
const clickCallback = useNoteCreation({
|
||||||
containerRef,
|
containerRef,
|
||||||
note,
|
note,
|
||||||
ntxId,
|
ntxId,
|
||||||
onCreate: onNewItem
|
mapApiRef
|
||||||
});
|
});
|
||||||
|
|
||||||
usePanZoom({
|
usePanZoom({
|
||||||
@ -129,7 +119,7 @@ export default function RelationMap({ note, ntxId }: TypeWidgetProps) {
|
|||||||
<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}>
|
||||||
<JsPlumb
|
<JsPlumb
|
||||||
apiRef={apiRef}
|
apiRef={pbApiRef}
|
||||||
containerRef={containerRef}
|
containerRef={containerRef}
|
||||||
className="relation-map-container"
|
className="relation-map-container"
|
||||||
props={{
|
props={{
|
||||||
@ -140,7 +130,7 @@ export default function RelationMap({ note, ntxId }: TypeWidgetProps) {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{data?.notes.map(note => (
|
{data?.notes.map(note => (
|
||||||
<NoteBox {...note} />
|
<NoteBox {...note} mapApiRef={mapApiRef} />
|
||||||
))}
|
))}
|
||||||
</JsPlumb>
|
</JsPlumb>
|
||||||
</div>
|
</div>
|
||||||
@ -193,11 +183,11 @@ function usePanZoom({ ntxId, containerRef, options, transformData, onTransform }
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function useNoteCreation({ ntxId, note, containerRef, onCreate }: {
|
function useNoteCreation({ ntxId, note, containerRef, mapApiRef }: {
|
||||||
ntxId: string | null | undefined;
|
ntxId: string | null | undefined;
|
||||||
note: FNote;
|
note: FNote;
|
||||||
containerRef: RefObject<HTMLDivElement>;
|
containerRef: RefObject<HTMLDivElement>;
|
||||||
onCreate: (newNote: MapData["notes"][number]) => void;
|
mapApiRef: RefObject<RelationMapApi>;
|
||||||
}) {
|
}) {
|
||||||
const clipboardRef = useRef<Clipboard>(null);
|
const clipboardRef = useRef<Clipboard>(null);
|
||||||
useTriliumEvent("relationMapCreateChildNote", async ({ ntxId: eventNtxId }) => {
|
useTriliumEvent("relationMapCreateChildNote", async ({ ntxId: eventNtxId }) => {
|
||||||
@ -227,10 +217,10 @@ function useNoteCreation({ ntxId, note, containerRef, onCreate }: {
|
|||||||
x -= 80;
|
x -= 80;
|
||||||
y -= 15;
|
y -= 15;
|
||||||
|
|
||||||
onCreate({ noteId: clipboard.noteId, x, y });
|
mapApiRef.current?.createItem({ noteId: clipboard.noteId, x, y });
|
||||||
clipboardRef.current = null;
|
clipboardRef.current = null;
|
||||||
}
|
}
|
||||||
}, [ onCreate ]);
|
}, []);
|
||||||
return onClickHandler;
|
return onClickHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -267,7 +257,7 @@ function JsPlumb({ className, props, children, containerRef: externalContainerRe
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function NoteBox({ noteId, x, y }: MapData["notes"][number]) {
|
function NoteBox({ noteId, x, y, mapApiRef }: MapDataNoteEntry & { mapApiRef: RefObject<RelationMapApi> }) {
|
||||||
const [ note, setNote ] = useState<FNote | null>();
|
const [ note, setNote ] = useState<FNote | null>();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
froca.getNote(noteId).then(setNote);
|
froca.getNote(noteId).then(setNote);
|
||||||
@ -286,12 +276,19 @@ function NoteBox({ noteId, x, y }: MapData["notes"][number]) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("relation_map.remove_note"),
|
title: t("relation_map.remove_note"),
|
||||||
uiIcon: "bx bx-trash"
|
uiIcon: "bx bx-trash",
|
||||||
|
handler: async () => {
|
||||||
|
if (!note) return;
|
||||||
|
const result = await dialog.confirmDeleteNoteBoxWithNote(note.title);
|
||||||
|
if (typeof result !== "object" || !result.confirmed) return;
|
||||||
|
|
||||||
|
mapApiRef.current?.removeItem(noteId, result.isDeleteNoteChecked);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
selectMenuItemHandler() {}
|
selectMenuItemHandler() {}
|
||||||
})
|
})
|
||||||
}, [ noteId ]);
|
}, [ note ]);
|
||||||
|
|
||||||
return note && (
|
return note && (
|
||||||
<div
|
<div
|
||||||
|
|||||||
61
apps/client/src/widgets/type_widgets/relation_map/api.ts
Normal file
61
apps/client/src/widgets/type_widgets/relation_map/api.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import FNote from "../../../entities/fnote";
|
||||||
|
import server from "../../../services/server";
|
||||||
|
import utils from "../../../services/utils";
|
||||||
|
|
||||||
|
export interface MapDataNoteEntry {
|
||||||
|
noteId: string;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MapData {
|
||||||
|
notes: MapDataNoteEntry[];
|
||||||
|
transform: PanZoomTransform;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DELTA = 0.0001;
|
||||||
|
|
||||||
|
export default class RelationMapApi {
|
||||||
|
|
||||||
|
private data: MapData;
|
||||||
|
private relations: any[];
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
createItem(newNote: MapDataNoteEntry) {
|
||||||
|
this.data.notes.push(newNote);
|
||||||
|
this.onDataChange(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeItem(noteId: string, deleteNoteToo: boolean) {
|
||||||
|
console.log("Remove ", noteId, deleteNoteToo);
|
||||||
|
if (deleteNoteToo) {
|
||||||
|
const taskId = utils.randomString(10);
|
||||||
|
await server.remove(`notes/${noteId}?taskId=${taskId}&last=true`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.data) {
|
||||||
|
this.data.notes = this.data.notes.filter((note) => note.noteId !== noteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.relations) {
|
||||||
|
this.relations = this.relations.filter((relation) => relation.sourceNoteId !== noteId && relation.targetNoteId !== noteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.onDataChange(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
setTransform(transform: PanZoomTransform) {
|
||||||
|
if (this.data.transform.scale - transform.scale > DELTA
|
||||||
|
|| this.data.transform.x - transform.x > DELTA
|
||||||
|
|| this.data.transform.y - transform.y > DELTA) {
|
||||||
|
this.data.transform = { ...transform };
|
||||||
|
this.onDataChange(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -173,31 +173,6 @@ export default class RelationMapTypeWidget extends TypeWidget {
|
|||||||
const $title = $noteBox.find(".title a");
|
const $title = $noteBox.find(".title a");
|
||||||
const noteId = this.idToNoteId($noteBox.prop("id"));
|
const noteId = this.idToNoteId($noteBox.prop("id"));
|
||||||
|
|
||||||
if (command === "openInNewTab") {
|
|
||||||
} else if (command === "remove") {
|
|
||||||
const result = await dialogService.confirmDeleteNoteBoxWithNote($title.text());
|
|
||||||
|
|
||||||
if (typeof result !== "object" || !result.confirmed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.jsPlumbInstance?.remove(this.noteIdToId(noteId));
|
|
||||||
|
|
||||||
if (result.isDeleteNoteChecked) {
|
|
||||||
const taskId = utils.randomString(10);
|
|
||||||
|
|
||||||
await server.remove(`notes/${noteId}?taskId=${taskId}&last=true`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.mapData) {
|
|
||||||
this.mapData.notes = this.mapData.notes.filter((note) => note.noteId !== noteId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.relations) {
|
|
||||||
this.relations = this.relations.filter((relation) => relation.sourceNoteId !== noteId && relation.targetNoteId !== noteId);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.saveData();
|
|
||||||
} else if (command === "editTitle") {
|
} else if (command === "editTitle") {
|
||||||
const title = await dialogService.prompt({
|
const title = await dialogService.prompt({
|
||||||
title: t("relation_map.rename_note"),
|
title: t("relation_map.rename_note"),
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user