feat: implement createNodeWithEdgeToTarget mutation for enhanced node connections

- Added a new mutation to create nodes connected to existing nodes, allowing for more dynamic interactions on the canvas.
- Updated the CanvasPlacementContext to include the new mutation, improving the workflow for node creation and edge management.
- Enhanced optimistic updates for immediate UI feedback during node and edge creation processes.
- Refactored related components to support the new connection method, streamlining user interactions.
This commit is contained in:
Matthias
2026-03-28 17:50:45 +01:00
parent b3a1ed54db
commit 9694c50195
6 changed files with 552 additions and 85 deletions

View File

@@ -0,0 +1,112 @@
"use client";
import { useEffect, useRef, type CSSProperties } from "react";
import { CanvasNodeTemplatePicker } from "@/components/canvas/canvas-node-template-picker";
import {
Command,
CommandEmpty,
CommandInput,
CommandList,
} from "@/components/ui/command";
import { cn } from "@/lib/utils";
import type { CanvasNodeTemplate } from "@/lib/canvas-node-templates";
import type { Id } from "@/convex/_generated/dataModel";
export type ConnectionDropMenuState = {
screenX: number;
screenY: number;
flowX: number;
flowY: number;
fromNodeId: Id<"nodes">;
fromHandleId: string | undefined;
fromHandleType: "source" | "target";
};
type CanvasConnectionDropMenuProps = {
state: ConnectionDropMenuState | null;
onClose: () => void;
onPick: (template: CanvasNodeTemplate) => void;
};
const PANEL_MAX_W = 360;
const PANEL_MAX_H = 420;
export function CanvasConnectionDropMenu({
state,
onClose,
onPick,
}: CanvasConnectionDropMenuProps) {
const panelRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!state) return;
const onEscape = (e: KeyboardEvent) => {
if (e.key === "Escape") onClose();
};
document.addEventListener("keydown", onEscape);
const onPointerDownCapture = (e: PointerEvent) => {
const panel = panelRef.current;
if (panel && !panel.contains(e.target as Node)) {
onClose();
}
};
document.addEventListener("pointerdown", onPointerDownCapture, true);
return () => {
document.removeEventListener("keydown", onEscape);
document.removeEventListener("pointerdown", onPointerDownCapture, true);
};
}, [state, onClose]);
if (!state) return null;
const vw =
typeof window !== "undefined" ? window.innerWidth : PANEL_MAX_W + 16;
const vh =
typeof window !== "undefined" ? window.innerHeight : PANEL_MAX_H + 16;
const left = Math.max(
8,
Math.min(state.screenX, vw - PANEL_MAX_W - 8),
);
const top = Math.max(
8,
Math.min(state.screenY, vh - PANEL_MAX_H - 8),
);
return (
<div
ref={panelRef}
role="dialog"
aria-label="Knoten wählen zur Verbindung"
className={cn(
"fixed z-100 flex max-h-(--panel-max-h) w-[min(100vw-1rem,360px)] max-w-[calc(100vw-1rem)] flex-col overflow-hidden rounded-xl bg-popover text-popover-foreground ring-1 ring-foreground/10 shadow-lg",
)}
style={
{
left,
top,
"--panel-max-h": `${PANEL_MAX_H}px`,
} as CSSProperties
}
onPointerDown={(e) => e.stopPropagation()}
onMouseDown={(e) => e.stopPropagation()}
>
<Command className="rounded-xl! bg-popover">
<CommandInput placeholder="Knoten suchen …" autoFocus />
<CommandList className="max-h-72">
<CommandEmpty>Keine Treffer.</CommandEmpty>
<CanvasNodeTemplatePicker
onPick={(template) => {
onPick(template);
onClose();
}}
groupHeading="Knoten"
/>
</CommandList>
</Command>
</div>
);
}