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 FlowPoint = { x: number; y: number };
|
||||||
|
|
||||||
type CreateNodeWithIntersectionInput = {
|
type CreateNodeWithIntersectionInput = {
|
||||||
@@ -72,10 +95,19 @@ type CreateNodeWithIntersectionInput = {
|
|||||||
clientRequestId?: string;
|
clientRequestId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type CreateNodeConnectedFromSourceInput = CreateNodeWithIntersectionInput & {
|
||||||
|
sourceNodeId: Id<"nodes">;
|
||||||
|
sourceHandle?: string;
|
||||||
|
targetHandle?: string;
|
||||||
|
};
|
||||||
|
|
||||||
type CanvasPlacementContextValue = {
|
type CanvasPlacementContextValue = {
|
||||||
createNodeWithIntersection: (
|
createNodeWithIntersection: (
|
||||||
input: CreateNodeWithIntersectionInput,
|
input: CreateNodeWithIntersectionInput,
|
||||||
) => Promise<Id<"nodes">>;
|
) => Promise<Id<"nodes">>;
|
||||||
|
createNodeConnectedFromSource: (
|
||||||
|
input: CreateNodeConnectedFromSourceInput,
|
||||||
|
) => Promise<Id<"nodes">>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const CanvasPlacementContext = createContext<CanvasPlacementContextValue | null>(
|
const CanvasPlacementContext = createContext<CanvasPlacementContextValue | null>(
|
||||||
@@ -135,6 +167,7 @@ interface CanvasPlacementProviderProps {
|
|||||||
canvasId: Id<"canvases">;
|
canvasId: Id<"canvases">;
|
||||||
createNode: CreateNodeMutation;
|
createNode: CreateNodeMutation;
|
||||||
createNodeWithEdgeSplit: CreateNodeWithEdgeSplitMutation;
|
createNodeWithEdgeSplit: CreateNodeWithEdgeSplitMutation;
|
||||||
|
createNodeWithEdgeFromSource: CreateNodeWithEdgeFromSourceMutation;
|
||||||
onCreateNodeSettled?: (payload: {
|
onCreateNodeSettled?: (payload: {
|
||||||
clientRequestId?: string;
|
clientRequestId?: string;
|
||||||
realId: Id<"nodes">;
|
realId: Id<"nodes">;
|
||||||
@@ -146,6 +179,7 @@ export function CanvasPlacementProvider({
|
|||||||
canvasId,
|
canvasId,
|
||||||
createNode,
|
createNode,
|
||||||
createNodeWithEdgeSplit,
|
createNodeWithEdgeSplit,
|
||||||
|
createNodeWithEdgeFromSource,
|
||||||
onCreateNodeSettled,
|
onCreateNodeSettled,
|
||||||
children,
|
children,
|
||||||
}: CanvasPlacementProviderProps) {
|
}: 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(
|
const value = useMemo(
|
||||||
() => ({ createNodeWithIntersection }),
|
() => ({ createNodeWithIntersection, createNodeConnectedFromSource }),
|
||||||
[createNodeWithIntersection],
|
[createNodeConnectedFromSource, createNodeWithIntersection],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -38,6 +38,12 @@ import {
|
|||||||
NODE_HANDLE_MAP,
|
NODE_HANDLE_MAP,
|
||||||
resolveMediaAspectRatio,
|
resolveMediaAspectRatio,
|
||||||
} from "@/lib/canvas-utils";
|
} 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 CanvasToolbar from "@/components/canvas/canvas-toolbar";
|
||||||
import { CanvasCommandPalette } from "@/components/canvas/canvas-command-palette";
|
import { CanvasCommandPalette } from "@/components/canvas/canvas-command-palette";
|
||||||
import { CanvasPlacementProvider } from "@/components/canvas/canvas-placement-context";
|
import { CanvasPlacementProvider } from "@/components/canvas/canvas-placement-context";
|
||||||
@@ -47,6 +53,7 @@ interface CanvasInnerProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const OPTIMISTIC_NODE_PREFIX = "optimistic_";
|
const OPTIMISTIC_NODE_PREFIX = "optimistic_";
|
||||||
|
const OPTIMISTIC_EDGE_PREFIX = "optimistic_edge_";
|
||||||
|
|
||||||
function isOptimisticNodeId(id: string): boolean {
|
function isOptimisticNodeId(id: string): boolean {
|
||||||
return id.startsWith(OPTIMISTIC_NODE_PREFIX);
|
return id.startsWith(OPTIMISTIC_NODE_PREFIX);
|
||||||
@@ -250,7 +257,10 @@ function mergeNodesPreservingLocalState(
|
|||||||
typeof (previousNode as { resizing?: boolean }).resizing === "boolean"
|
typeof (previousNode as { resizing?: boolean }).resizing === "boolean"
|
||||||
? (previousNode as { resizing?: boolean }).resizing
|
? (previousNode as { resizing?: boolean }).resizing
|
||||||
: false;
|
: false;
|
||||||
const isMediaNode = incomingNode.type === "asset" || incomingNode.type === "image";
|
const isMediaNode =
|
||||||
|
incomingNode.type === "asset" ||
|
||||||
|
incomingNode.type === "image" ||
|
||||||
|
incomingNode.type === "ai-image";
|
||||||
const shouldPreserveInteractivePosition =
|
const shouldPreserveInteractivePosition =
|
||||||
isMediaNode && (Boolean(previousNode.selected) || Boolean(previousNode.dragging) || previousResizing);
|
isMediaNode && (Boolean(previousNode.selected) || Boolean(previousNode.dragging) || previousResizing);
|
||||||
const shouldPreserveInteractiveSize =
|
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 createNodeWithEdgeSplit = useMutation(api.nodes.createWithEdgeSplit);
|
||||||
const batchRemoveNodes = useMutation(api.nodes.batchRemove);
|
const batchRemoveNodes = useMutation(api.nodes.batchRemove);
|
||||||
const createEdge = useMutation(api.edges.create);
|
const createEdge = useMutation(api.edges.create);
|
||||||
@@ -580,12 +650,14 @@ function CanvasInner({ canvasId }: CanvasInnerProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const node = nds.find((candidate) => candidate.id === change.id);
|
const node = nds.find((candidate) => candidate.id === change.id);
|
||||||
if (!node || node.type !== "asset") {
|
if (!node) {
|
||||||
return change;
|
return change;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isActiveResize =
|
const isActiveResize =
|
||||||
change.resizing === true || change.resizing === false;
|
change.resizing === true || change.resizing === false;
|
||||||
|
|
||||||
|
if (node.type === "asset") {
|
||||||
if (!isActiveResize) {
|
if (!isActiveResize) {
|
||||||
return change;
|
return change;
|
||||||
}
|
}
|
||||||
@@ -660,6 +732,87 @@ function CanvasInner({ canvasId }: CanvasInnerProps) {
|
|||||||
height: enforcedHeight,
|
height: enforcedHeight,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return change;
|
||||||
})
|
})
|
||||||
.filter((change): change is NodeChange => change !== null);
|
.filter((change): change is NodeChange => change !== null);
|
||||||
|
|
||||||
@@ -1114,6 +1267,7 @@ function CanvasInner({ canvasId }: CanvasInnerProps) {
|
|||||||
canvasId={canvasId}
|
canvasId={canvasId}
|
||||||
createNode={createNode}
|
createNode={createNode}
|
||||||
createNodeWithEdgeSplit={createNodeWithEdgeSplit}
|
createNodeWithEdgeSplit={createNodeWithEdgeSplit}
|
||||||
|
createNodeWithEdgeFromSource={createNodeWithEdgeFromSource}
|
||||||
onCreateNodeSettled={({ clientRequestId, realId }) =>
|
onCreateNodeSettled={({ clientRequestId, realId }) =>
|
||||||
syncPendingMoveForClientRequest(clientRequestId, realId)
|
syncPendingMoveForClientRequest(clientRequestId, realId)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ const RESIZE_CONFIGS: Record<string, ResizeConfig> = {
|
|||||||
group: { minWidth: 150, minHeight: 100 },
|
group: { minWidth: 150, minHeight: 100 },
|
||||||
image: { minWidth: 140, minHeight: 120, keepAspectRatio: true },
|
image: { minWidth: 140, minHeight: 120, keepAspectRatio: true },
|
||||||
asset: { minWidth: 140, minHeight: 208, keepAspectRatio: false },
|
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 },
|
compare: { minWidth: 300, minHeight: 200 },
|
||||||
prompt: { minWidth: 260, minHeight: 220 },
|
prompt: { minWidth: 260, minHeight: 220 },
|
||||||
text: { minWidth: 220, minHeight: 90 },
|
text: { minWidth: 220, minHeight: 90 },
|
||||||
|
|||||||
@@ -118,9 +118,8 @@ export default function PromptNode({
|
|||||||
availableCredits !== null && availableCredits >= creditCost;
|
availableCredits !== null && availableCredits >= creditCost;
|
||||||
|
|
||||||
const updateData = useMutation(api.nodes.updateData);
|
const updateData = useMutation(api.nodes.updateData);
|
||||||
const createEdge = useMutation(api.edges.create);
|
|
||||||
const generateImage = useAction(api.ai.generateImage);
|
const generateImage = useAction(api.ai.generateImage);
|
||||||
const { createNodeWithIntersection } = useCanvasPlacement();
|
const { createNodeConnectedFromSource } = useCanvasPlacement();
|
||||||
|
|
||||||
const debouncedSave = useDebouncedCallback(() => {
|
const debouncedSave = useDebouncedCallback(() => {
|
||||||
const raw = dataRef.current as Record<string, unknown>;
|
const raw = dataRef.current as Record<string, unknown>;
|
||||||
@@ -215,7 +214,9 @@ export default function PromptNode({
|
|||||||
const viewport = getImageViewportSize(aspectRatio);
|
const viewport = getImageViewportSize(aspectRatio);
|
||||||
const outer = getAiImageNodeOuterSize(viewport);
|
const outer = getAiImageNodeOuterSize(viewport);
|
||||||
|
|
||||||
const aiNodeId = await createNodeWithIntersection({
|
const clientRequestId = crypto.randomUUID();
|
||||||
|
|
||||||
|
const aiNodeId = await createNodeConnectedFromSource({
|
||||||
type: "ai-image",
|
type: "ai-image",
|
||||||
position: { x: posX, y: posY },
|
position: { x: posX, y: posY },
|
||||||
width: outer.width,
|
width: outer.width,
|
||||||
@@ -229,13 +230,8 @@ export default function PromptNode({
|
|||||||
outputWidth: viewport.width,
|
outputWidth: viewport.width,
|
||||||
outputHeight: viewport.height,
|
outputHeight: viewport.height,
|
||||||
},
|
},
|
||||||
clientRequestId: crypto.randomUUID(),
|
clientRequestId,
|
||||||
});
|
|
||||||
|
|
||||||
await createEdge({
|
|
||||||
canvasId,
|
|
||||||
sourceNodeId: id as Id<"nodes">,
|
sourceNodeId: id as Id<"nodes">,
|
||||||
targetNodeId: aiNodeId,
|
|
||||||
sourceHandle: "prompt-out",
|
sourceHandle: "prompt-out",
|
||||||
targetHandle: "prompt-in",
|
targetHandle: "prompt-in",
|
||||||
});
|
});
|
||||||
@@ -274,8 +270,7 @@ export default function PromptNode({
|
|||||||
id,
|
id,
|
||||||
getEdges,
|
getEdges,
|
||||||
getNode,
|
getNode,
|
||||||
createNodeWithIntersection,
|
createNodeConnectedFromSource,
|
||||||
createEdge,
|
|
||||||
generateImage,
|
generateImage,
|
||||||
creditCost,
|
creditCost,
|
||||||
availableCredits,
|
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.
|
* Node-Position auf dem Canvas verschieben.
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user