mirror of
https://github.com/zadam/trilium.git
synced 2025-11-10 15:25:51 +01:00
chore(react): port data part of server API
This commit is contained in:
@@ -1,6 +1,11 @@
|
||||
import { useEffect, useRef, useState } from "preact/hooks";
|
||||
import "./NoteMap.css";
|
||||
import { rgb2hex } from "./utils";
|
||||
import { getMapRootNoteId, NoteMapWidgetMode, rgb2hex } from "./utils";
|
||||
import { RefObject } from "preact";
|
||||
import FNote from "../../entities/fnote";
|
||||
import { useNoteContext, useNoteLabel } from "../react/hooks";
|
||||
import ForceGraph, { LinkObject, NodeObject } from "force-graph";
|
||||
import { loadNotesAndRelations, NotesAndRelationsData } from "./data";
|
||||
|
||||
interface CssData {
|
||||
fontFamily: string;
|
||||
@@ -8,11 +13,20 @@ interface CssData {
|
||||
mutedTextColor: string;
|
||||
}
|
||||
|
||||
export default function NoteMap() {
|
||||
interface NoteMapProps {
|
||||
note: FNote;
|
||||
widgetMode: NoteMapWidgetMode;
|
||||
}
|
||||
|
||||
type MapType = "tree" | "link";
|
||||
|
||||
export default function NoteMap({ note, widgetMode }: NoteMapProps) {
|
||||
console.log("Got note", note);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const styleResolverRef = useRef<HTMLDivElement>(null);
|
||||
const [ cssData, setCssData ] = useState<CssData>();
|
||||
console.log("Got CSS ", cssData);
|
||||
const [ mapTypeRaw ] = useNoteLabel(note, "mapType");
|
||||
const mapType: MapType = mapTypeRaw === "tree" ? "tree" : "link";
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerRef.current || !styleResolverRef.current) return;
|
||||
@@ -22,14 +36,54 @@ export default function NoteMap() {
|
||||
return (
|
||||
<div className="note-map-widget">
|
||||
<div ref={styleResolverRef} class="style-resolver" />
|
||||
|
||||
<div ref={containerRef} className="note-map-container">
|
||||
Container goes here.
|
||||
</div>
|
||||
<NoteGraph containerRef={containerRef} note={note} widgetMode={widgetMode} mapType={mapType} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function NoteGraph({ containerRef, note, widgetMode, mapType }: {
|
||||
containerRef: RefObject<HTMLDivElement>;
|
||||
note: FNote;
|
||||
widgetMode: NoteMapWidgetMode;
|
||||
mapType: MapType;
|
||||
}) {
|
||||
const graphRef = useRef<ForceGraph<NodeObject, LinkObject<NodeObject>>>();
|
||||
const [ data, setData ] = useState<NotesAndRelationsData>();
|
||||
console.log("Got data ", data);
|
||||
|
||||
// Build the note graph instance.
|
||||
useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
if (!container) return;
|
||||
const { width, height } = container.getBoundingClientRect();
|
||||
const graph = new ForceGraph(container)
|
||||
.width(width)
|
||||
.height(height);
|
||||
graphRef.current = graph;
|
||||
|
||||
const mapRootId = getMapRootNoteId(note.noteId, note, widgetMode);
|
||||
if (!mapRootId) return;
|
||||
|
||||
const labelValues = (name: string) => note.getLabels(name).map(l => l.value) ?? [];
|
||||
const excludeRelations = labelValues("mapExcludeRelation");
|
||||
const includeRelations = labelValues("mapIncludeRelation");
|
||||
loadNotesAndRelations(mapRootId, excludeRelations, includeRelations, mapType).then((data) => {
|
||||
console.log("Got data ", data);
|
||||
});
|
||||
|
||||
return () => container.replaceChildren();
|
||||
}, [ note ]);
|
||||
|
||||
// Render the data.
|
||||
useEffect(() => {
|
||||
if (!graphRef.current || !data) return;
|
||||
graphRef.current.graphData(data);
|
||||
}, [ data ]);
|
||||
|
||||
|
||||
return <div ref={containerRef} className="note-map-container" />;
|
||||
}
|
||||
|
||||
function getCssData(container: HTMLElement, styleResolver: HTMLElement): CssData {
|
||||
const containerStyle = window.getComputedStyle(container);
|
||||
const styleResolverStyle = window.getComputedStyle(styleResolver);
|
||||
|
||||
113
apps/client/src/widgets/note_map/data.ts
Normal file
113
apps/client/src/widgets/note_map/data.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import { NoteMapLink, NoteMapPostResponse } from "@triliumnext/commons";
|
||||
import server from "../../services/server";
|
||||
import { NodeObject } from "force-graph";
|
||||
|
||||
type MapType = "tree" | "link";
|
||||
|
||||
interface GroupedLink {
|
||||
id: string;
|
||||
sourceNoteId: string;
|
||||
targetNoteId: string;
|
||||
names: string[];
|
||||
}
|
||||
|
||||
interface Node extends NodeObject {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
export interface NotesAndRelationsData {
|
||||
nodes: Node[];
|
||||
links: {
|
||||
id: string;
|
||||
source: string;
|
||||
target: string;
|
||||
name: string;
|
||||
}[];
|
||||
noteIdToSizeMap: Record<string, number>;
|
||||
}
|
||||
|
||||
export async function loadNotesAndRelations(mapRootNoteId: string, excludeRelations: string[], includeRelations: string[], mapType: MapType): Promise<NotesAndRelationsData> {
|
||||
const resp = await server.post<NoteMapPostResponse>(`note-map/${mapRootNoteId}/${mapType}`, {
|
||||
excludeRelations, includeRelations
|
||||
});
|
||||
|
||||
const noteIdToSizeMap = calculateNodeSizes(resp, mapType);
|
||||
const links = getGroupedLinks(resp.links);
|
||||
const nodes = resp.notes.map(([noteId, title, type, color]) => ({
|
||||
id: noteId,
|
||||
name: title,
|
||||
type: type,
|
||||
color: color
|
||||
}));
|
||||
|
||||
return {
|
||||
noteIdToSizeMap,
|
||||
nodes,
|
||||
links: links.map((link) => ({
|
||||
id: `${link.sourceNoteId}-${link.targetNoteId}`,
|
||||
source: link.sourceNoteId,
|
||||
target: link.targetNoteId,
|
||||
name: link.names.join(", ")
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
function calculateNodeSizes(resp: NoteMapPostResponse, mapType: MapType) {
|
||||
const noteIdToSizeMap: Record<string, number> = {};
|
||||
|
||||
if (mapType === "tree") {
|
||||
const { noteIdToDescendantCountMap } = resp;
|
||||
|
||||
for (const noteId in noteIdToDescendantCountMap) {
|
||||
noteIdToSizeMap[noteId] = 4;
|
||||
|
||||
const count = noteIdToDescendantCountMap[noteId];
|
||||
|
||||
if (count > 0) {
|
||||
noteIdToSizeMap[noteId] += 1 + Math.round(Math.log(count) / Math.log(1.5));
|
||||
}
|
||||
}
|
||||
} else if (mapType === "link") {
|
||||
const noteIdToLinkCount: Record<string, number> = {};
|
||||
|
||||
for (const link of resp.links) {
|
||||
noteIdToLinkCount[link.targetNoteId] = 1 + (noteIdToLinkCount[link.targetNoteId] || 0);
|
||||
}
|
||||
|
||||
for (const [noteId] of resp.notes) {
|
||||
noteIdToSizeMap[noteId] = 4;
|
||||
|
||||
if (noteId in noteIdToLinkCount) {
|
||||
noteIdToSizeMap[noteId] += Math.min(Math.pow(noteIdToLinkCount[noteId], 0.5), 15);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return noteIdToSizeMap;
|
||||
}
|
||||
|
||||
function getGroupedLinks(links: NoteMapLink[]): GroupedLink[] {
|
||||
const linksGroupedBySourceTarget: Record<string, GroupedLink> = {};
|
||||
|
||||
for (const link of links) {
|
||||
const key = `${link.sourceNoteId}-${link.targetNoteId}`;
|
||||
|
||||
if (key in linksGroupedBySourceTarget) {
|
||||
if (!linksGroupedBySourceTarget[key].names.includes(link.name)) {
|
||||
linksGroupedBySourceTarget[key].names.push(link.name);
|
||||
}
|
||||
} else {
|
||||
linksGroupedBySourceTarget[key] = {
|
||||
id: key,
|
||||
sourceNoteId: link.sourceNoteId,
|
||||
targetNoteId: link.targetNoteId,
|
||||
names: [link.name]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return Object.values(linksGroupedBySourceTarget);
|
||||
}
|
||||
@@ -1,6 +1,28 @@
|
||||
import appContext from "../../components/app_context";
|
||||
import FNote from "../../entities/fnote";
|
||||
import hoisted_note from "../../services/hoisted_note";
|
||||
|
||||
export type NoteMapWidgetMode = "ribbon" | "hoisted";
|
||||
|
||||
export function rgb2hex(rgb: string) {
|
||||
return `#${(rgb.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/) || [])
|
||||
.slice(1)
|
||||
.map((n) => parseInt(n, 10).toString(16).padStart(2, "0"))
|
||||
.join("")}`;
|
||||
}
|
||||
|
||||
export function getMapRootNoteId(noteId: string, note: FNote, widgetMode: NoteMapWidgetMode): string | null {
|
||||
if (noteId && widgetMode === "ribbon") {
|
||||
return noteId;
|
||||
}
|
||||
|
||||
let mapRootNoteId = note?.getLabelValue("mapRootNoteId");
|
||||
|
||||
if (mapRootNoteId === "hoisted") {
|
||||
mapRootNoteId = hoisted_note.getHoistedNoteId();
|
||||
} else if (!mapRootNoteId) {
|
||||
mapRootNoteId = appContext.tabManager.getActiveContext()?.parentNoteId ?? null;
|
||||
}
|
||||
|
||||
return mapRootNoteId;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user