mirror of
https://github.com/zadam/trilium.git
synced 2025-11-11 08:58:58 +01:00
chore(react/type_widgets): reintroduce relation context menu
This commit is contained in:
parent
c469fffb6e
commit
1eca9f6541
@ -11,26 +11,20 @@ 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, RelationMapPostResponse, RelationMapRelation } from "@triliumnext/commons";
|
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 setupOverlays, { uniDirectionalOverlays } from "./overlays";
|
||||||
import { JsPlumb } from "./jsplumb";
|
import { JsPlumb } from "./jsplumb";
|
||||||
import { getMousePosition, getZoom, idToNoteId, noteIdToId } from "./utils";
|
import { getMousePosition, getZoom, idToNoteId, noteIdToId } from "./utils";
|
||||||
import { NoteBox } from "./NoteBox";
|
import { NoteBox } from "./NoteBox";
|
||||||
import utils from "../../../services/utils";
|
import utils from "../../../services/utils";
|
||||||
import attribute_autocomplete from "../../../services/attribute_autocomplete";
|
import attribute_autocomplete from "../../../services/attribute_autocomplete";
|
||||||
|
import { buildRelationContextMenuHandler } from "./context_menu";
|
||||||
|
|
||||||
interface Clipboard {
|
interface Clipboard {
|
||||||
noteId: string;
|
noteId: string;
|
||||||
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);
|
||||||
@ -222,6 +216,7 @@ async function useRelationData(noteId: string, mapData: MapData | undefined, map
|
|||||||
}
|
}
|
||||||
|
|
||||||
setRelations(relations);
|
setRelations(relations);
|
||||||
|
mapApiRef.current?.loadRelations(relations);
|
||||||
mapApiRef.current?.cleanupOtherNotes(Object.keys(data.noteTitles));
|
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> }) {
|
function useRelationCreation({ mapApiRef, jsPlumbApiRef }: { mapApiRef: RefObject<RelationMapApi>, jsPlumbApiRef: RefObject<jsPlumbInstance> }) {
|
||||||
const connectionCallback = useCallback(async (info: OnConnectionBindInfo, originalEvent: Event) => {
|
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 there's no event, then this has been triggered programmatically
|
||||||
if (!originalEvent || !mapApiRef.current) return;
|
if (!originalEvent || !mapApiRef.current) return;
|
||||||
|
|
||||||
const connection = info.connection;
|
|
||||||
let name = await dialog.prompt({
|
let name = await dialog.prompt({
|
||||||
message: t("relation_map.specify_new_relation_name"),
|
message: t("relation_map.specify_new_relation_name"),
|
||||||
shown: ({ $answer }) => {
|
shown: ({ $answer }) => {
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
|
import { Connection } from "jsplumb";
|
||||||
import FNote from "../../../entities/fnote";
|
import FNote from "../../../entities/fnote";
|
||||||
import { t } from "../../../services/i18n";
|
import { t } from "../../../services/i18n";
|
||||||
import server from "../../../services/server";
|
import server from "../../../services/server";
|
||||||
import utils from "../../../services/utils";
|
import utils from "../../../services/utils";
|
||||||
|
import { RelationMapRelation } from "@triliumnext/commons";
|
||||||
|
|
||||||
export interface MapDataNoteEntry {
|
export interface MapDataNoteEntry {
|
||||||
noteId: string;
|
noteId: string;
|
||||||
@ -14,17 +16,29 @@ export interface MapData {
|
|||||||
transform: PanZoomTransform;
|
transform: PanZoomTransform;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type RelationType = "uniDirectional" | "biDirectional" | "inverse";
|
||||||
|
|
||||||
|
export interface ClientRelation extends RelationMapRelation {
|
||||||
|
type: RelationType;
|
||||||
|
render: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
const DELTA = 0.0001;
|
const DELTA = 0.0001;
|
||||||
|
|
||||||
export default class RelationMapApi {
|
export default class RelationMapApi {
|
||||||
|
|
||||||
private data: MapData;
|
private data: MapData;
|
||||||
private relations: any[];
|
private relations: ClientRelation[];
|
||||||
private onDataChange: (refreshUi: boolean) => void;
|
private onDataChange: (refreshUi: boolean) => void;
|
||||||
|
|
||||||
constructor(note: FNote, initialMapData: MapData, onDataChange: (newData: MapData, refreshUi: boolean) => void) {
|
constructor(note: FNote, initialMapData: MapData, onDataChange: (newData: MapData, refreshUi: boolean) => void) {
|
||||||
this.data = initialMapData;
|
this.data = initialMapData;
|
||||||
this.onDataChange = (refreshUi) => onDataChange({ ...this.data }, refreshUi);
|
this.onDataChange = (refreshUi) => onDataChange({ ...this.data }, refreshUi);
|
||||||
|
this.relations = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
loadRelations(relations: ClientRelation[]) {
|
||||||
|
this.relations = relations;
|
||||||
}
|
}
|
||||||
|
|
||||||
createItem(newNote: MapDataNoteEntry) {
|
createItem(newNote: MapDataNoteEntry) {
|
||||||
@ -50,6 +64,16 @@ export default class RelationMapApi {
|
|||||||
this.onDataChange(true);
|
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[]) {
|
cleanupOtherNotes(noteIds: string[]) {
|
||||||
const filteredNotes = this.data.notes.filter((note) => noteIds.includes(note.noteId));
|
const filteredNotes = this.data.notes.filter((note) => noteIds.includes(note.noteId));
|
||||||
if (filteredNotes.length === this.data.notes.length) return;
|
if (filteredNotes.length === this.data.notes.length) return;
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import dialog from "../../../services/dialog";
|
|||||||
import { t } from "../../../services/i18n";
|
import { t } from "../../../services/i18n";
|
||||||
import server from "../../../services/server";
|
import server from "../../../services/server";
|
||||||
import RelationMapApi from "./api";
|
import RelationMapApi from "./api";
|
||||||
|
import { Connection } from "jsplumb";
|
||||||
|
|
||||||
export function buildNoteContextMenuHandler(note: FNote | null | undefined, mapApiRef: RefObject<RelationMapApi>) {
|
export function buildNoteContextMenuHandler(note: FNote | null | undefined, mapApiRef: RefObject<RelationMapApi>) {
|
||||||
return (e: MouseEvent) => {
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@ -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() {
|
saveData() {
|
||||||
this.spacedUpdate.scheduleUpdate();
|
this.spacedUpdate.scheduleUpdate();
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user