Enhance canvas components with improved error handling and aspect ratio normalization

- Added error name tracking in NodeErrorBoundary for better debugging.
- Introduced aspect ratio normalization in PromptNode to ensure valid values are used.
- Updated debounced state management in CanvasInner for improved performance.
- Enhanced SelectContent component to support optional portal rendering.
This commit is contained in:
Matthias
2026-04-02 08:26:06 +02:00
parent 2142249ed5
commit 624beac6dc
10 changed files with 552 additions and 100 deletions

View File

@@ -2,6 +2,8 @@ import { query, mutation, QueryCtx, MutationCtx } from "./_generated/server";
import { v } from "convex/values";
import { requireAuth } from "./helpers";
import type { Doc, Id } from "./_generated/dataModel";
import { isAdjustmentNodeType } from "../lib/canvas-node-types";
import { nodeTypeValidator } from "./node-type-validator";
// ============================================================================
// Interne Helpers
@@ -40,6 +42,35 @@ type NodeCreateMutationName =
| "nodes.createWithEdgeFromSource"
| "nodes.createWithEdgeToTarget";
const DISALLOWED_ADJUSTMENT_DATA_KEYS = [
"storageId",
"url",
"blob",
"blobUrl",
"imageData",
] as const;
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function assertNoAdjustmentImagePayload(
nodeType: Doc<"nodes">["type"],
data: unknown,
): void {
if (!isAdjustmentNodeType(nodeType) || !isRecord(data)) {
return;
}
for (const key of DISALLOWED_ADJUSTMENT_DATA_KEYS) {
if (key in data) {
throw new Error(
`Adjustment nodes accept parameter data only. '${key}' is not allowed in data.`,
);
}
}
}
async function getIdempotentNodeCreateResult(
ctx: MutationCtx,
args: {
@@ -159,7 +190,7 @@ export const get = query({
export const listByType = query({
args: {
canvasId: v.id("canvases"),
type: v.string(),
type: nodeTypeValidator,
},
handler: async (ctx, { canvasId, type }) => {
const user = await requireAuth(ctx);
@@ -187,7 +218,7 @@ export const listByType = query({
export const create = mutation({
args: {
canvasId: v.id("canvases"),
type: v.string(),
type: nodeTypeValidator,
positionX: v.number(),
positionY: v.number(),
width: v.number(),
@@ -212,6 +243,8 @@ export const create = mutation({
return existingNodeId;
}
assertNoAdjustmentImagePayload(args.type, args.data);
const nodeId = await ctx.db.insert("nodes", {
canvasId: args.canvasId,
type: args.type as Doc<"nodes">["type"],
@@ -246,7 +279,7 @@ export const create = mutation({
export const createWithEdgeSplit = mutation({
args: {
canvasId: v.id("canvases"),
type: v.string(),
type: nodeTypeValidator,
positionX: v.number(),
positionY: v.number(),
width: v.number(),
@@ -280,6 +313,8 @@ export const createWithEdgeSplit = mutation({
throw new Error("Edge not found");
}
assertNoAdjustmentImagePayload(args.type, args.data);
const nodeId = await ctx.db.insert("nodes", {
canvasId: args.canvasId,
type: args.type as Doc<"nodes">["type"],
@@ -434,7 +469,7 @@ export const splitEdgeAtExistingNode = mutation({
export const createWithEdgeFromSource = mutation({
args: {
canvasId: v.id("canvases"),
type: v.string(),
type: nodeTypeValidator,
positionX: v.number(),
positionY: v.number(),
width: v.number(),
@@ -466,6 +501,8 @@ export const createWithEdgeFromSource = mutation({
throw new Error("Source node not found");
}
assertNoAdjustmentImagePayload(args.type, args.data);
const nodeId = await ctx.db.insert("nodes", {
canvasId: args.canvasId,
type: args.type as Doc<"nodes">["type"],
@@ -508,7 +545,7 @@ export const createWithEdgeFromSource = mutation({
export const createWithEdgeToTarget = mutation({
args: {
canvasId: v.id("canvases"),
type: v.string(),
type: nodeTypeValidator,
positionX: v.number(),
positionY: v.number(),
width: v.number(),
@@ -540,6 +577,8 @@ export const createWithEdgeToTarget = mutation({
throw new Error("Target node not found");
}
assertNoAdjustmentImagePayload(args.type, args.data);
const nodeId = await ctx.db.insert("nodes", {
canvasId: args.canvasId,
type: args.type as Doc<"nodes">["type"],
@@ -659,6 +698,7 @@ export const updateData = mutation({
if (!node) throw new Error("Node not found");
await getCanvasOrThrow(ctx, node.canvasId, user.userId);
assertNoAdjustmentImagePayload(node.type, data);
await ctx.db.patch(nodeId, { data });
await ctx.db.patch(node.canvasId, { updatedAt: Date.now() });
},