feat: add cmdk dependency and enhance canvas node creation with edge splitting functionality

- Introduced the cmdk package for improved command palette capabilities.
- Enhanced the canvas placement context to support creating nodes with edge splitting, allowing for more dynamic node interactions.
- Updated the canvas inner component to utilize optimistic updates for node creation, improving user experience during interactions.
- Refactored node handling logic to incorporate new mutation types and streamline data management.
This commit is contained in:
Matthias
2026-03-27 23:40:31 +01:00
parent 4e84e7f76f
commit 6e866f2df6
15 changed files with 1037 additions and 112 deletions

View File

@@ -26,11 +26,12 @@ import { msg } from "@/lib/toast-messages";
import { useConvexAuth, useMutation, useQuery } from "convex/react";
import { api } from "@/convex/_generated/api";
import type { Id } from "@/convex/_generated/dataModel";
import type { Doc, Id } from "@/convex/_generated/dataModel";
import { authClient } from "@/lib/auth-client";
import { nodeTypes } from "./node-types";
import {
convexNodeDocWithMergedStorageUrl,
convexNodeToRF,
convexEdgeToRF,
NODE_DEFAULTS,
@@ -38,6 +39,7 @@ import {
resolveMediaAspectRatio,
} from "@/lib/canvas-utils";
import CanvasToolbar from "@/components/canvas/canvas-toolbar";
import { CanvasCommandPalette } from "@/components/canvas/canvas-command-palette";
import { CanvasPlacementProvider } from "@/components/canvas/canvas-placement-context";
interface CanvasInnerProps {
@@ -338,6 +340,10 @@ function CanvasInner({ canvasId }: CanvasInnerProps) {
api.edges.list,
shouldSkipCanvasQueries ? "skip" : { canvasId },
);
const storageUrlsById = useQuery(
api.storage.batchGetUrlsForCanvas,
shouldSkipCanvasQueries ? "skip" : { canvasId },
);
const canvas = useQuery(
api.canvases.get,
shouldSkipCanvasQueries ? "skip" : { canvasId },
@@ -347,7 +353,40 @@ function CanvasInner({ canvasId }: CanvasInnerProps) {
const moveNode = useMutation(api.nodes.move);
const resizeNode = useMutation(api.nodes.resize);
const batchMoveNodes = useMutation(api.nodes.batchMove);
const createNode = useMutation(api.nodes.create);
const createNode = useMutation(api.nodes.create).withOptimisticUpdate(
(localStore, args) => {
const current = localStore.getQuery(api.nodes.list, {
canvasId: args.canvasId,
});
if (current === undefined) return;
const tempId =
`optimistic_${Date.now()}_${Math.random().toString(36).slice(2, 11)}` as Id<"nodes">;
const synthetic: Doc<"nodes"> = {
_id: tempId,
_creationTime: Date.now(),
canvasId: args.canvasId,
type: args.type as Doc<"nodes">["type"],
positionX: args.positionX,
positionY: args.positionY,
width: args.width,
height: args.height,
status: "idle",
retryCount: 0,
data: args.data,
parentId: args.parentId,
zIndex: args.zIndex,
};
localStore.setQuery(
api.nodes.list,
{ canvasId: args.canvasId },
[...current, synthetic],
);
},
);
const createNodeWithEdgeSplit = useMutation(api.nodes.createWithEdgeSplit);
const batchRemoveNodes = useMutation(api.nodes.batchRemove);
const createEdge = useMutation(api.edges.create);
const removeEdge = useMutation(api.edges.remove);
@@ -432,14 +471,27 @@ function CanvasInner({ canvasId }: CanvasInnerProps) {
useEffect(() => {
if (!convexNodes || isDragging.current) return;
setNodes((previousNodes) => {
const incomingNodes = withResolvedCompareData(convexNodes.map(convexNodeToRF), edges);
const prevDataById = new Map(
previousNodes.map((node) => [node.id, node.data as Record<string, unknown>]),
);
const enriched = convexNodes.map((node) =>
convexNodeDocWithMergedStorageUrl(
node,
storageUrlsById,
prevDataById,
),
);
const incomingNodes = withResolvedCompareData(
enriched.map(convexNodeToRF),
edges,
);
// Nodes, die gerade optimistisch gelöscht werden, nicht wiederherstellen
const filteredIncoming = deletingNodeIds.current.size > 0
? incomingNodes.filter((node) => !deletingNodeIds.current.has(node.id))
: incomingNodes;
return mergeNodesPreservingLocalState(previousNodes, filteredIncoming);
});
}, [convexNodes, edges]);
}, [convexNodes, edges, storageUrlsById]);
useEffect(() => {
if (!convexEdges) return;
@@ -976,9 +1028,14 @@ function CanvasInner({ canvasId }: CanvasInnerProps) {
}
return (
<CanvasPlacementProvider canvasId={canvasId}>
<CanvasPlacementProvider
canvasId={canvasId}
createNode={createNode}
createNodeWithEdgeSplit={createNodeWithEdgeSplit}
>
<div className="relative h-full w-full">
<CanvasToolbar canvasName={canvas?.name ?? "canvas"} />
<CanvasCommandPalette />
<ReactFlow
nodes={nodes}
edges={edges}