- Introduced a new CSS transition for edge insertion reflowing to improve visual feedback during node adjustments. - Enhanced the connection validation logic to include options for optimistic edges, ensuring better handling of edge creation scenarios. - Updated the canvas connection drop menu to support additional templates and improved edge insertion handling. - Refactored edge insertion logic to accommodate local node position adjustments during reflow operations. - Added tests for new edge insertion features and connection validation improvements.
121 lines
3.2 KiB
TypeScript
121 lines
3.2 KiB
TypeScript
"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";
|
|
};
|
|
|
|
export type CanvasMenuAnchor = {
|
|
screenX: number;
|
|
screenY: number;
|
|
};
|
|
|
|
type CanvasConnectionDropMenuProps = {
|
|
anchor: CanvasMenuAnchor | null;
|
|
onClose: () => void;
|
|
onPick: (template: CanvasNodeTemplate) => void;
|
|
templates?: readonly CanvasNodeTemplate[];
|
|
};
|
|
|
|
const PANEL_MAX_W = 360;
|
|
const PANEL_MAX_H = 420;
|
|
|
|
export function CanvasConnectionDropMenu({
|
|
anchor,
|
|
onClose,
|
|
onPick,
|
|
templates,
|
|
}: CanvasConnectionDropMenuProps) {
|
|
const panelRef = useRef<HTMLDivElement>(null);
|
|
|
|
useEffect(() => {
|
|
if (!anchor) 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);
|
|
};
|
|
}, [anchor, onClose]);
|
|
|
|
if (!anchor) 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(anchor.screenX, vw - PANEL_MAX_W - 8),
|
|
);
|
|
const top = Math.max(
|
|
8,
|
|
Math.min(anchor.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"
|
|
templates={templates}
|
|
/>
|
|
</CommandList>
|
|
</Command>
|
|
</div>
|
|
);
|
|
}
|