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:
23
convex/node-type-validator.ts
Normal file
23
convex/node-type-validator.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { v, type Validator } from "convex/values";
|
||||
|
||||
import {
|
||||
ADJUSTMENT_NODE_TYPES,
|
||||
CANVAS_NODE_TYPES,
|
||||
PHASE1_CANVAS_NODE_TYPES,
|
||||
} from "../lib/canvas-node-types";
|
||||
|
||||
function buildNodeTypeUnion<
|
||||
const TValues extends readonly [string, string, ...string[]],
|
||||
>(values: TValues): Validator<TValues[number], "required", string> {
|
||||
return v.union(
|
||||
...values.map((value) => v.literal(value)) as [
|
||||
Validator<TValues[number], "required", string>,
|
||||
Validator<TValues[number], "required", string>,
|
||||
...Validator<TValues[number], "required", string>[],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
export const phase1NodeTypeValidator = buildNodeTypeUnion(PHASE1_CANVAS_NODE_TYPES);
|
||||
export const nodeTypeValidator = buildNodeTypeUnion(CANVAS_NODE_TYPES);
|
||||
export const adjustmentNodeTypeValidator = buildNodeTypeUnion(ADJUSTMENT_NODE_TYPES);
|
||||
@@ -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() });
|
||||
},
|
||||
|
||||
@@ -2,76 +2,23 @@
|
||||
import { defineSchema, defineTable } from "convex/server";
|
||||
import { v } from "convex/values";
|
||||
|
||||
import {
|
||||
nodeTypeValidator,
|
||||
phase1NodeTypeValidator,
|
||||
} from "./node-type-validator";
|
||||
|
||||
// ============================================================================
|
||||
// Node Types
|
||||
// ============================================================================
|
||||
|
||||
// Phase 1 Node Types
|
||||
const phase1NodeTypes = v.union(
|
||||
// Quelle
|
||||
v.literal("image"),
|
||||
v.literal("text"),
|
||||
v.literal("prompt"),
|
||||
// KI-Ausgabe
|
||||
v.literal("ai-image"),
|
||||
// Canvas & Layout
|
||||
v.literal("group"),
|
||||
v.literal("frame"),
|
||||
v.literal("note"),
|
||||
v.literal("compare")
|
||||
);
|
||||
const phase1NodeTypes = phase1NodeTypeValidator;
|
||||
|
||||
// Alle Node Types (Phase 1 + spätere Phasen)
|
||||
// Phase 2+3 Typen sind hier schon definiert, damit das Schema nicht bei
|
||||
// jedem Phasenübergang migriert werden muss. Die UI zeigt nur die Typen
|
||||
// der jeweiligen Phase an.
|
||||
const nodeType = v.union(
|
||||
// Quelle (Phase 1)
|
||||
v.literal("image"),
|
||||
v.literal("text"),
|
||||
v.literal("prompt"),
|
||||
// Quelle (Phase 2)
|
||||
v.literal("color"),
|
||||
v.literal("video"),
|
||||
v.literal("asset"),
|
||||
// KI-Ausgabe (Phase 1)
|
||||
v.literal("ai-image"),
|
||||
// KI-Ausgabe (Phase 2)
|
||||
v.literal("ai-text"),
|
||||
v.literal("ai-video"),
|
||||
// KI-Ausgabe (Phase 3)
|
||||
v.literal("agent-output"),
|
||||
// Transformation (Phase 2)
|
||||
v.literal("crop"),
|
||||
v.literal("bg-remove"),
|
||||
v.literal("upscale"),
|
||||
// Transformation (Phase 3)
|
||||
v.literal("style-transfer"),
|
||||
v.literal("face-restore"),
|
||||
// Bildbearbeitung (Phase 2)
|
||||
v.literal("curves"),
|
||||
v.literal("color-adjust"),
|
||||
v.literal("light-adjust"),
|
||||
v.literal("detail-adjust"),
|
||||
v.literal("render"),
|
||||
// Steuerung (Phase 2)
|
||||
v.literal("splitter"),
|
||||
v.literal("loop"),
|
||||
v.literal("agent"),
|
||||
// Steuerung (Phase 3)
|
||||
v.literal("mixer"),
|
||||
v.literal("switch"),
|
||||
// Canvas & Layout (Phase 1)
|
||||
v.literal("group"),
|
||||
v.literal("frame"),
|
||||
v.literal("note"),
|
||||
v.literal("compare"),
|
||||
// Canvas & Layout (Phase 2)
|
||||
v.literal("text-overlay"),
|
||||
// Canvas & Layout (Phase 3)
|
||||
v.literal("comment"),
|
||||
v.literal("presentation")
|
||||
);
|
||||
const nodeType = nodeTypeValidator;
|
||||
|
||||
// Node Status — direkt am Node sichtbar (UX-Strategie aus dem PRD)
|
||||
const nodeStatus = v.union(
|
||||
|
||||
Reference in New Issue
Block a user