feat: implement createNodeConnectedFromSource functionality for enhanced node creation
- Added a new mutation to create nodes connected to existing nodes, improving the canvas interaction model. - Updated the CanvasPlacementContext to include createNodeConnectedFromSource, allowing for seamless node connections. - Refactored the PromptNode component to utilize the new connection method, enhancing the workflow for AI image generation. - Introduced optimistic updates for immediate UI feedback during node creation and connection processes.
This commit is contained in:
@@ -58,6 +58,29 @@ type CreateNodeWithEdgeSplitMutation = ReactMutation<
|
||||
>
|
||||
>;
|
||||
|
||||
type CreateNodeWithEdgeFromSourceMutation = ReactMutation<
|
||||
FunctionReference<
|
||||
"mutation",
|
||||
"public",
|
||||
{
|
||||
canvasId: Id<"canvases">;
|
||||
type: string;
|
||||
positionX: number;
|
||||
positionY: number;
|
||||
width: number;
|
||||
height: number;
|
||||
data: unknown;
|
||||
parentId?: Id<"nodes">;
|
||||
zIndex?: number;
|
||||
clientRequestId?: string;
|
||||
sourceNodeId: Id<"nodes">;
|
||||
sourceHandle?: string;
|
||||
targetHandle?: string;
|
||||
},
|
||||
Id<"nodes">
|
||||
>
|
||||
>;
|
||||
|
||||
type FlowPoint = { x: number; y: number };
|
||||
|
||||
type CreateNodeWithIntersectionInput = {
|
||||
@@ -72,10 +95,19 @@ type CreateNodeWithIntersectionInput = {
|
||||
clientRequestId?: string;
|
||||
};
|
||||
|
||||
export type CreateNodeConnectedFromSourceInput = CreateNodeWithIntersectionInput & {
|
||||
sourceNodeId: Id<"nodes">;
|
||||
sourceHandle?: string;
|
||||
targetHandle?: string;
|
||||
};
|
||||
|
||||
type CanvasPlacementContextValue = {
|
||||
createNodeWithIntersection: (
|
||||
input: CreateNodeWithIntersectionInput,
|
||||
) => Promise<Id<"nodes">>;
|
||||
createNodeConnectedFromSource: (
|
||||
input: CreateNodeConnectedFromSourceInput,
|
||||
) => Promise<Id<"nodes">>;
|
||||
};
|
||||
|
||||
const CanvasPlacementContext = createContext<CanvasPlacementContextValue | null>(
|
||||
@@ -135,6 +167,7 @@ interface CanvasPlacementProviderProps {
|
||||
canvasId: Id<"canvases">;
|
||||
createNode: CreateNodeMutation;
|
||||
createNodeWithEdgeSplit: CreateNodeWithEdgeSplitMutation;
|
||||
createNodeWithEdgeFromSource: CreateNodeWithEdgeFromSourceMutation;
|
||||
onCreateNodeSettled?: (payload: {
|
||||
clientRequestId?: string;
|
||||
realId: Id<"nodes">;
|
||||
@@ -146,6 +179,7 @@ export function CanvasPlacementProvider({
|
||||
canvasId,
|
||||
createNode,
|
||||
createNodeWithEdgeSplit,
|
||||
createNodeWithEdgeFromSource,
|
||||
onCreateNodeSettled,
|
||||
children,
|
||||
}: CanvasPlacementProviderProps) {
|
||||
@@ -250,9 +284,57 @@ export function CanvasPlacementProvider({
|
||||
],
|
||||
);
|
||||
|
||||
const createNodeConnectedFromSource = useCallback(
|
||||
async ({
|
||||
type,
|
||||
position,
|
||||
width,
|
||||
height,
|
||||
data,
|
||||
zIndex,
|
||||
clientRequestId,
|
||||
sourceNodeId,
|
||||
sourceHandle,
|
||||
targetHandle,
|
||||
}: CreateNodeConnectedFromSourceInput) => {
|
||||
const defaults = NODE_DEFAULTS[type] ?? {
|
||||
width: 200,
|
||||
height: 100,
|
||||
data: {},
|
||||
};
|
||||
|
||||
const effectiveWidth = width ?? defaults.width;
|
||||
const effectiveHeight = height ?? defaults.height;
|
||||
|
||||
const payload = {
|
||||
canvasId,
|
||||
type,
|
||||
positionX: position.x,
|
||||
positionY: position.y,
|
||||
width: effectiveWidth,
|
||||
height: effectiveHeight,
|
||||
data: {
|
||||
...defaults.data,
|
||||
...(data ?? {}),
|
||||
canvasId,
|
||||
},
|
||||
...(zIndex !== undefined ? { zIndex } : {}),
|
||||
...(clientRequestId !== undefined ? { clientRequestId } : {}),
|
||||
sourceNodeId,
|
||||
sourceHandle,
|
||||
targetHandle,
|
||||
};
|
||||
|
||||
const realId = await createNodeWithEdgeFromSource(payload);
|
||||
onCreateNodeSettled?.({ clientRequestId, realId });
|
||||
return realId;
|
||||
},
|
||||
[canvasId, createNodeWithEdgeFromSource, onCreateNodeSettled],
|
||||
);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({ createNodeWithIntersection }),
|
||||
[createNodeWithIntersection],
|
||||
() => ({ createNodeWithIntersection, createNodeConnectedFromSource }),
|
||||
[createNodeConnectedFromSource, createNodeWithIntersection],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -38,6 +38,12 @@ import {
|
||||
NODE_HANDLE_MAP,
|
||||
resolveMediaAspectRatio,
|
||||
} from "@/lib/canvas-utils";
|
||||
import {
|
||||
AI_IMAGE_NODE_FOOTER_PX,
|
||||
AI_IMAGE_NODE_HEADER_PX,
|
||||
DEFAULT_ASPECT_RATIO,
|
||||
parseAspectRatioString,
|
||||
} from "@/lib/image-formats";
|
||||
import CanvasToolbar from "@/components/canvas/canvas-toolbar";
|
||||
import { CanvasCommandPalette } from "@/components/canvas/canvas-command-palette";
|
||||
import { CanvasPlacementProvider } from "@/components/canvas/canvas-placement-context";
|
||||
@@ -47,6 +53,7 @@ interface CanvasInnerProps {
|
||||
}
|
||||
|
||||
const OPTIMISTIC_NODE_PREFIX = "optimistic_";
|
||||
const OPTIMISTIC_EDGE_PREFIX = "optimistic_edge_";
|
||||
|
||||
function isOptimisticNodeId(id: string): boolean {
|
||||
return id.startsWith(OPTIMISTIC_NODE_PREFIX);
|
||||
@@ -250,7 +257,10 @@ function mergeNodesPreservingLocalState(
|
||||
typeof (previousNode as { resizing?: boolean }).resizing === "boolean"
|
||||
? (previousNode as { resizing?: boolean }).resizing
|
||||
: false;
|
||||
const isMediaNode = incomingNode.type === "asset" || incomingNode.type === "image";
|
||||
const isMediaNode =
|
||||
incomingNode.type === "asset" ||
|
||||
incomingNode.type === "image" ||
|
||||
incomingNode.type === "ai-image";
|
||||
const shouldPreserveInteractivePosition =
|
||||
isMediaNode && (Boolean(previousNode.selected) || Boolean(previousNode.dragging) || previousResizing);
|
||||
const shouldPreserveInteractiveSize =
|
||||
@@ -441,6 +451,66 @@ function CanvasInner({ canvasId }: CanvasInnerProps) {
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
const createNodeWithEdgeFromSource = useMutation(
|
||||
api.nodes.createWithEdgeFromSource,
|
||||
).withOptimisticUpdate((localStore, args) => {
|
||||
const nodeList = localStore.getQuery(api.nodes.list, {
|
||||
canvasId: args.canvasId,
|
||||
});
|
||||
const edgeList = localStore.getQuery(api.edges.list, {
|
||||
canvasId: args.canvasId,
|
||||
});
|
||||
if (nodeList === undefined || edgeList === undefined) return;
|
||||
|
||||
const tempNodeId = (
|
||||
args.clientRequestId
|
||||
? `${OPTIMISTIC_NODE_PREFIX}${args.clientRequestId}`
|
||||
: `${OPTIMISTIC_NODE_PREFIX}${Date.now()}_${Math.random().toString(36).slice(2, 11)}`
|
||||
) as Id<"nodes">;
|
||||
|
||||
const tempEdgeId = (
|
||||
args.clientRequestId
|
||||
? `${OPTIMISTIC_EDGE_PREFIX}${args.clientRequestId}`
|
||||
: `${OPTIMISTIC_EDGE_PREFIX}${Date.now()}_${Math.random().toString(36).slice(2, 11)}`
|
||||
) as Id<"edges">;
|
||||
|
||||
const syntheticNode: Doc<"nodes"> = {
|
||||
_id: tempNodeId,
|
||||
_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,
|
||||
};
|
||||
|
||||
const syntheticEdge: Doc<"edges"> = {
|
||||
_id: tempEdgeId,
|
||||
_creationTime: Date.now(),
|
||||
canvasId: args.canvasId,
|
||||
sourceNodeId: args.sourceNodeId,
|
||||
targetNodeId: tempNodeId,
|
||||
sourceHandle: args.sourceHandle,
|
||||
targetHandle: args.targetHandle,
|
||||
};
|
||||
|
||||
localStore.setQuery(api.nodes.list, { canvasId: args.canvasId }, [
|
||||
...nodeList,
|
||||
syntheticNode,
|
||||
]);
|
||||
localStore.setQuery(api.edges.list, { canvasId: args.canvasId }, [
|
||||
...edgeList,
|
||||
syntheticEdge,
|
||||
]);
|
||||
});
|
||||
|
||||
const createNodeWithEdgeSplit = useMutation(api.nodes.createWithEdgeSplit);
|
||||
const batchRemoveNodes = useMutation(api.nodes.batchRemove);
|
||||
const createEdge = useMutation(api.edges.create);
|
||||
@@ -580,86 +650,169 @@ function CanvasInner({ canvasId }: CanvasInnerProps) {
|
||||
}
|
||||
|
||||
const node = nds.find((candidate) => candidate.id === change.id);
|
||||
if (!node || node.type !== "asset") {
|
||||
if (!node) {
|
||||
return change;
|
||||
}
|
||||
|
||||
const isActiveResize =
|
||||
change.resizing === true || change.resizing === false;
|
||||
if (!isActiveResize) {
|
||||
return change;
|
||||
|
||||
if (node.type === "asset") {
|
||||
if (!isActiveResize) {
|
||||
return change;
|
||||
}
|
||||
|
||||
const nodeData = node.data as {
|
||||
intrinsicWidth?: number;
|
||||
intrinsicHeight?: number;
|
||||
orientation?: string;
|
||||
};
|
||||
const hasIntrinsicRatioInput =
|
||||
typeof nodeData.intrinsicWidth === "number" &&
|
||||
nodeData.intrinsicWidth > 0 &&
|
||||
typeof nodeData.intrinsicHeight === "number" &&
|
||||
nodeData.intrinsicHeight > 0;
|
||||
if (!hasIntrinsicRatioInput) {
|
||||
return change;
|
||||
}
|
||||
|
||||
const targetRatio = resolveMediaAspectRatio(
|
||||
nodeData.intrinsicWidth,
|
||||
nodeData.intrinsicHeight,
|
||||
nodeData.orientation,
|
||||
);
|
||||
|
||||
if (!Number.isFinite(targetRatio) || targetRatio <= 0) {
|
||||
return change;
|
||||
}
|
||||
|
||||
const previousWidth =
|
||||
typeof node.style?.width === "number"
|
||||
? node.style.width
|
||||
: change.dimensions.width;
|
||||
const previousHeight =
|
||||
typeof node.style?.height === "number"
|
||||
? node.style.height
|
||||
: change.dimensions.height;
|
||||
|
||||
const widthDelta = Math.abs(change.dimensions.width - previousWidth);
|
||||
const heightDelta = Math.abs(change.dimensions.height - previousHeight);
|
||||
|
||||
let constrainedWidth = change.dimensions.width;
|
||||
let constrainedHeight = change.dimensions.height;
|
||||
|
||||
// Axis with larger delta drives resize; the other axis is ratio-locked.
|
||||
if (heightDelta > widthDelta) {
|
||||
constrainedWidth = constrainedHeight * targetRatio;
|
||||
} else {
|
||||
constrainedHeight = constrainedWidth / targetRatio;
|
||||
}
|
||||
|
||||
const assetChromeHeight = 88;
|
||||
const assetMinPreviewHeight = 120;
|
||||
const assetMinNodeHeight = assetChromeHeight + assetMinPreviewHeight;
|
||||
const assetMinNodeWidth = 140;
|
||||
|
||||
const minWidthFromHeight = assetMinNodeHeight * targetRatio;
|
||||
const minimumAllowedWidth = Math.max(assetMinNodeWidth, minWidthFromHeight);
|
||||
const minimumAllowedHeight = minimumAllowedWidth / targetRatio;
|
||||
|
||||
const enforcedWidth = Math.max(constrainedWidth, minimumAllowedWidth);
|
||||
const enforcedHeight = Math.max(
|
||||
constrainedHeight,
|
||||
minimumAllowedHeight,
|
||||
assetMinNodeHeight,
|
||||
);
|
||||
|
||||
return {
|
||||
...change,
|
||||
dimensions: {
|
||||
...change.dimensions,
|
||||
width: enforcedWidth,
|
||||
height: enforcedHeight,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const nodeData = node.data as {
|
||||
intrinsicWidth?: number;
|
||||
intrinsicHeight?: number;
|
||||
orientation?: string;
|
||||
};
|
||||
const hasIntrinsicRatioInput =
|
||||
typeof nodeData.intrinsicWidth === "number" &&
|
||||
nodeData.intrinsicWidth > 0 &&
|
||||
typeof nodeData.intrinsicHeight === "number" &&
|
||||
nodeData.intrinsicHeight > 0;
|
||||
if (!hasIntrinsicRatioInput) {
|
||||
return change;
|
||||
if (node.type === "ai-image") {
|
||||
if (!isActiveResize) {
|
||||
return change;
|
||||
}
|
||||
|
||||
const nodeData = node.data as { aspectRatio?: string };
|
||||
const arLabel =
|
||||
typeof nodeData.aspectRatio === "string" && nodeData.aspectRatio.trim()
|
||||
? nodeData.aspectRatio.trim()
|
||||
: DEFAULT_ASPECT_RATIO;
|
||||
|
||||
let arW: number;
|
||||
let arH: number;
|
||||
try {
|
||||
const parsed = parseAspectRatioString(arLabel);
|
||||
arW = parsed.w;
|
||||
arH = parsed.h;
|
||||
} catch {
|
||||
return change;
|
||||
}
|
||||
|
||||
const chrome = AI_IMAGE_NODE_HEADER_PX + AI_IMAGE_NODE_FOOTER_PX;
|
||||
const hPerW = arH / arW;
|
||||
|
||||
const previousWidth =
|
||||
typeof node.style?.width === "number"
|
||||
? node.style.width
|
||||
: change.dimensions.width;
|
||||
const previousHeight =
|
||||
typeof node.style?.height === "number"
|
||||
? node.style.height
|
||||
: change.dimensions.height;
|
||||
|
||||
const widthDelta = Math.abs(change.dimensions.width - previousWidth);
|
||||
const heightDelta = Math.abs(change.dimensions.height - previousHeight);
|
||||
|
||||
let constrainedWidth = change.dimensions.width;
|
||||
let constrainedHeight = change.dimensions.height;
|
||||
|
||||
if (heightDelta > widthDelta) {
|
||||
const viewportH = Math.max(1, constrainedHeight - chrome);
|
||||
constrainedWidth = viewportH * (arW / arH);
|
||||
constrainedHeight = chrome + viewportH;
|
||||
} else {
|
||||
constrainedHeight = chrome + constrainedWidth * hPerW;
|
||||
}
|
||||
|
||||
const aiMinViewport = 120;
|
||||
const aiMinOuterHeight = chrome + aiMinViewport;
|
||||
const aiMinOuterWidthBase = 200;
|
||||
const minimumAllowedWidth = Math.max(
|
||||
aiMinOuterWidthBase,
|
||||
aiMinViewport * (arW / arH),
|
||||
);
|
||||
const minimumAllowedHeight = Math.max(
|
||||
aiMinOuterHeight,
|
||||
chrome + minimumAllowedWidth * hPerW,
|
||||
);
|
||||
|
||||
let enforcedWidth = Math.max(constrainedWidth, minimumAllowedWidth);
|
||||
let enforcedHeight = chrome + enforcedWidth * hPerW;
|
||||
if (enforcedHeight < minimumAllowedHeight) {
|
||||
enforcedHeight = minimumAllowedHeight;
|
||||
enforcedWidth = (enforcedHeight - chrome) * (arW / arH);
|
||||
}
|
||||
enforcedWidth = Math.max(enforcedWidth, minimumAllowedWidth);
|
||||
enforcedHeight = chrome + enforcedWidth * hPerW;
|
||||
|
||||
return {
|
||||
...change,
|
||||
dimensions: {
|
||||
...change.dimensions,
|
||||
width: enforcedWidth,
|
||||
height: enforcedHeight,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const targetRatio = resolveMediaAspectRatio(
|
||||
nodeData.intrinsicWidth,
|
||||
nodeData.intrinsicHeight,
|
||||
nodeData.orientation,
|
||||
);
|
||||
|
||||
if (!Number.isFinite(targetRatio) || targetRatio <= 0) {
|
||||
return change;
|
||||
}
|
||||
|
||||
const previousWidth =
|
||||
typeof node.style?.width === "number"
|
||||
? node.style.width
|
||||
: change.dimensions.width;
|
||||
const previousHeight =
|
||||
typeof node.style?.height === "number"
|
||||
? node.style.height
|
||||
: change.dimensions.height;
|
||||
|
||||
const widthDelta = Math.abs(change.dimensions.width - previousWidth);
|
||||
const heightDelta = Math.abs(change.dimensions.height - previousHeight);
|
||||
|
||||
let constrainedWidth = change.dimensions.width;
|
||||
let constrainedHeight = change.dimensions.height;
|
||||
|
||||
// Axis with larger delta drives resize; the other axis is ratio-locked.
|
||||
if (heightDelta > widthDelta) {
|
||||
constrainedWidth = constrainedHeight * targetRatio;
|
||||
} else {
|
||||
constrainedHeight = constrainedWidth / targetRatio;
|
||||
}
|
||||
|
||||
const assetChromeHeight = 88;
|
||||
const assetMinPreviewHeight = 120;
|
||||
const assetMinNodeHeight = assetChromeHeight + assetMinPreviewHeight;
|
||||
const assetMinNodeWidth = 140;
|
||||
|
||||
const minWidthFromHeight = assetMinNodeHeight * targetRatio;
|
||||
const minimumAllowedWidth = Math.max(assetMinNodeWidth, minWidthFromHeight);
|
||||
const minimumAllowedHeight = minimumAllowedWidth / targetRatio;
|
||||
|
||||
const enforcedWidth = Math.max(constrainedWidth, minimumAllowedWidth);
|
||||
const enforcedHeight = Math.max(
|
||||
constrainedHeight,
|
||||
minimumAllowedHeight,
|
||||
assetMinNodeHeight,
|
||||
);
|
||||
|
||||
return {
|
||||
...change,
|
||||
dimensions: {
|
||||
...change.dimensions,
|
||||
width: enforcedWidth,
|
||||
height: enforcedHeight,
|
||||
},
|
||||
};
|
||||
return change;
|
||||
})
|
||||
.filter((change): change is NodeChange => change !== null);
|
||||
|
||||
@@ -1114,6 +1267,7 @@ function CanvasInner({ canvasId }: CanvasInnerProps) {
|
||||
canvasId={canvasId}
|
||||
createNode={createNode}
|
||||
createNodeWithEdgeSplit={createNodeWithEdgeSplit}
|
||||
createNodeWithEdgeFromSource={createNodeWithEdgeFromSource}
|
||||
onCreateNodeSettled={({ clientRequestId, realId }) =>
|
||||
syncPendingMoveForClientRequest(clientRequestId, realId)
|
||||
}
|
||||
|
||||
@@ -17,7 +17,8 @@ const RESIZE_CONFIGS: Record<string, ResizeConfig> = {
|
||||
group: { minWidth: 150, minHeight: 100 },
|
||||
image: { minWidth: 140, minHeight: 120, keepAspectRatio: true },
|
||||
asset: { minWidth: 140, minHeight: 208, keepAspectRatio: false },
|
||||
"ai-image": { minWidth: 200, minHeight: 200 },
|
||||
// Chrome 88 + min. Viewport 120 → äußere Mindesthöhe 208 (siehe canvas onNodesChange)
|
||||
"ai-image": { minWidth: 200, minHeight: 208, keepAspectRatio: false },
|
||||
compare: { minWidth: 300, minHeight: 200 },
|
||||
prompt: { minWidth: 260, minHeight: 220 },
|
||||
text: { minWidth: 220, minHeight: 90 },
|
||||
|
||||
@@ -118,9 +118,8 @@ export default function PromptNode({
|
||||
availableCredits !== null && availableCredits >= creditCost;
|
||||
|
||||
const updateData = useMutation(api.nodes.updateData);
|
||||
const createEdge = useMutation(api.edges.create);
|
||||
const generateImage = useAction(api.ai.generateImage);
|
||||
const { createNodeWithIntersection } = useCanvasPlacement();
|
||||
const { createNodeConnectedFromSource } = useCanvasPlacement();
|
||||
|
||||
const debouncedSave = useDebouncedCallback(() => {
|
||||
const raw = dataRef.current as Record<string, unknown>;
|
||||
@@ -215,7 +214,9 @@ export default function PromptNode({
|
||||
const viewport = getImageViewportSize(aspectRatio);
|
||||
const outer = getAiImageNodeOuterSize(viewport);
|
||||
|
||||
const aiNodeId = await createNodeWithIntersection({
|
||||
const clientRequestId = crypto.randomUUID();
|
||||
|
||||
const aiNodeId = await createNodeConnectedFromSource({
|
||||
type: "ai-image",
|
||||
position: { x: posX, y: posY },
|
||||
width: outer.width,
|
||||
@@ -229,13 +230,8 @@ export default function PromptNode({
|
||||
outputWidth: viewport.width,
|
||||
outputHeight: viewport.height,
|
||||
},
|
||||
clientRequestId: crypto.randomUUID(),
|
||||
});
|
||||
|
||||
await createEdge({
|
||||
canvasId,
|
||||
clientRequestId,
|
||||
sourceNodeId: id as Id<"nodes">,
|
||||
targetNodeId: aiNodeId,
|
||||
sourceHandle: "prompt-out",
|
||||
targetHandle: "prompt-in",
|
||||
});
|
||||
@@ -274,8 +270,7 @@ export default function PromptNode({
|
||||
id,
|
||||
getEdges,
|
||||
getNode,
|
||||
createNodeWithIntersection,
|
||||
createEdge,
|
||||
createNodeConnectedFromSource,
|
||||
generateImage,
|
||||
creditCost,
|
||||
availableCredits,
|
||||
|
||||
@@ -224,6 +224,64 @@ export const createWithEdgeSplit = mutation({
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Neuen Node erstellen und sofort mit einem bestehenden Node verbinden
|
||||
* (ein Roundtrip — z. B. Prompt → neue AI-Image-Node).
|
||||
*/
|
||||
export const createWithEdgeFromSource = 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()),
|
||||
clientRequestId: v.optional(v.string()),
|
||||
sourceNodeId: v.id("nodes"),
|
||||
sourceHandle: v.optional(v.string()),
|
||||
targetHandle: v.optional(v.string()),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const user = await requireAuth(ctx);
|
||||
await getCanvasOrThrow(ctx, args.canvasId, user.userId);
|
||||
void args.clientRequestId;
|
||||
|
||||
const source = await ctx.db.get(args.sourceNodeId);
|
||||
if (!source || source.canvasId !== args.canvasId) {
|
||||
throw new Error("Source node 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: args.sourceNodeId,
|
||||
targetNodeId: nodeId,
|
||||
sourceHandle: args.sourceHandle,
|
||||
targetHandle: args.targetHandle,
|
||||
});
|
||||
|
||||
await ctx.db.patch(args.canvasId, { updatedAt: Date.now() });
|
||||
|
||||
return nodeId;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Node-Position auf dem Canvas verschieben.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user