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

@@ -47,29 +47,10 @@ export const list = query({
const user = await requireAuth(ctx);
await getCanvasOrThrow(ctx, canvasId, user.userId);
const nodes = await ctx.db
return await ctx.db
.query("nodes")
.withIndex("by_canvas", (q) => q.eq("canvasId", canvasId))
.collect();
return Promise.all(
nodes.map(async (node) => {
const data = node.data as Record<string, unknown> | undefined;
if (!data?.storageId) {
return node;
}
const url = await ctx.storage.getUrl(data.storageId as Id<"_storage">);
return {
...node,
data: {
...data,
url: url ?? undefined,
},
};
})
);
},
});
@@ -173,6 +154,72 @@ export const create = mutation({
},
});
/**
* Neuen Node erzeugen und eine bestehende Kante in zwei Kanten aufteilen (ein Roundtrip).
*/
export const createWithEdgeSplit = mutation({
args: {
canvasId: v.id("canvases"),
type: v.string(),
positionX: v.number(),
positionY: v.number(),
width: v.number(),
height: v.number(),
data: v.any(),
parentId: v.optional(v.id("nodes")),
zIndex: v.optional(v.number()),
splitEdgeId: v.id("edges"),
newNodeTargetHandle: v.optional(v.string()),
newNodeSourceHandle: v.optional(v.string()),
splitSourceHandle: v.optional(v.string()),
splitTargetHandle: v.optional(v.string()),
},
handler: async (ctx, args) => {
const user = await requireAuth(ctx);
await getCanvasOrThrow(ctx, args.canvasId, user.userId);
const edge = await ctx.db.get(args.splitEdgeId);
if (!edge || edge.canvasId !== args.canvasId) {
throw new Error("Edge not found");
}
const nodeId = await ctx.db.insert("nodes", {
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,
});
await ctx.db.insert("edges", {
canvasId: args.canvasId,
sourceNodeId: edge.sourceNodeId,
targetNodeId: nodeId,
sourceHandle: args.splitSourceHandle,
targetHandle: args.newNodeTargetHandle,
});
await ctx.db.insert("edges", {
canvasId: args.canvasId,
sourceNodeId: nodeId,
targetNodeId: edge.targetNodeId,
sourceHandle: args.newNodeSourceHandle,
targetHandle: args.splitTargetHandle,
});
await ctx.db.delete(args.splitEdgeId);
await ctx.db.patch(args.canvasId, { updatedAt: Date.now() });
return nodeId;
},
});
/**
* Node-Position auf dem Canvas verschieben.
*/
@@ -409,8 +456,6 @@ export const batchRemove = mutation({
if (!firstNode) throw new Error("Node not found");
await getCanvasOrThrow(ctx, firstNode.canvasId, user.userId);
const nodeIdSet = new Set(nodeIds.map((id) => id.toString()));
for (const nodeId of nodeIds) {
const node = await ctx.db.get(nodeId);
if (!node) continue;

View File

@@ -1,5 +1,7 @@
import { mutation } from "./_generated/server";
import { mutation, query } from "./_generated/server";
import { v } from "convex/values";
import { requireAuth } from "./helpers";
import type { Id } from "./_generated/dataModel";
export const generateUploadUrl = mutation({
args: {},
@@ -8,3 +10,41 @@ export const generateUploadUrl = mutation({
return await ctx.storage.generateUploadUrl();
},
});
/**
* Signierte URLs für alle Storage-Assets eines Canvas (gebündelt).
* `nodes.list` liefert keine URLs mehr, damit Node-Liste schnell bleibt.
*/
export const batchGetUrlsForCanvas = query({
args: { canvasId: v.id("canvases") },
handler: async (ctx, { canvasId }) => {
const user = await requireAuth(ctx);
const canvas = await ctx.db.get(canvasId);
if (!canvas || canvas.ownerId !== user.userId) {
throw new Error("Canvas not found");
}
const nodes = await ctx.db
.query("nodes")
.withIndex("by_canvas", (q) => q.eq("canvasId", canvasId))
.collect();
const ids = new Set<Id<"_storage">>();
for (const node of nodes) {
const data = node.data as Record<string, unknown> | undefined;
const sid = data?.storageId;
if (typeof sid === "string" && sid.length > 0) {
ids.add(sid as Id<"_storage">);
}
}
const entries = await Promise.all(
[...ids].map(
async (id) =>
[id, (await ctx.storage.getUrl(id)) ?? undefined] as const,
),
);
return Object.fromEntries(entries) as Record<string, string | undefined>;
},
});