feat: enhance dashboard and canvas components with improved state management and resizing logic
- Added client mount state to the dashboard to prevent premature interactions before the component is fully loaded. - Updated button disabling logic to ensure it reflects the component's readiness and user session state. - Introduced zIndex handling in canvas placement context for better node layering. - Enhanced asset and image nodes with improved resizing logic to maintain aspect ratios during adjustments. - Refactored node components to streamline rendering and improve performance during dynamic updates.
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import { NodeResizeControl } from "@xyflow/react";
|
||||
import { NodeResizeControl, NodeToolbar, Position, useNodeId, useReactFlow } from "@xyflow/react";
|
||||
import { Trash2, Copy } from "lucide-react";
|
||||
import { useCanvasPlacement } from "@/components/canvas/canvas-placement-context";
|
||||
import { NodeErrorBoundary } from "./node-error-boundary";
|
||||
|
||||
interface ResizeConfig {
|
||||
@@ -14,10 +16,10 @@ const RESIZE_CONFIGS: Record<string, ResizeConfig> = {
|
||||
frame: { minWidth: 200, minHeight: 150 },
|
||||
group: { minWidth: 150, minHeight: 100 },
|
||||
image: { minWidth: 140, minHeight: 120, keepAspectRatio: true },
|
||||
asset: { minWidth: 140, minHeight: 120, keepAspectRatio: true },
|
||||
asset: { minWidth: 140, minHeight: 208, keepAspectRatio: false },
|
||||
"ai-image": { minWidth: 200, minHeight: 200 },
|
||||
compare: { minWidth: 300, minHeight: 200 },
|
||||
prompt: { minWidth: 260, minHeight: 200 },
|
||||
prompt: { minWidth: 260, minHeight: 220 },
|
||||
text: { minWidth: 220, minHeight: 90 },
|
||||
note: { minWidth: 200, minHeight: 90 },
|
||||
};
|
||||
@@ -31,6 +33,117 @@ const CORNERS = [
|
||||
"bottom-right",
|
||||
] as const;
|
||||
|
||||
/** Internal fields to strip when duplicating a node */
|
||||
const INTERNAL_FIELDS = new Set([
|
||||
"_status",
|
||||
"_statusMessage",
|
||||
"retryCount",
|
||||
"url",
|
||||
"canvasId",
|
||||
]);
|
||||
|
||||
function NodeToolbarActions() {
|
||||
const nodeId = useNodeId();
|
||||
const { deleteElements, getNode, getNodes, setNodes } = useReactFlow();
|
||||
const { createNodeWithIntersection } = useCanvasPlacement();
|
||||
|
||||
const handleDelete = () => {
|
||||
if (!nodeId) return;
|
||||
void deleteElements({ nodes: [{ id: nodeId }] });
|
||||
};
|
||||
|
||||
const handleDuplicate = async () => {
|
||||
if (!nodeId) return;
|
||||
const node = getNode(nodeId);
|
||||
if (!node) return;
|
||||
|
||||
// Strip internal/runtime fields, keep only user content
|
||||
const originalData = (node.data ?? {}) as Record<string, unknown>;
|
||||
const cleanedData: Record<string, unknown> = {};
|
||||
for (const [key, value] of Object.entries(originalData)) {
|
||||
if (!INTERNAL_FIELDS.has(key)) {
|
||||
cleanedData[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
const originalPosition = node.position ?? { x: 0, y: 0 };
|
||||
const width = typeof node.style?.width === "number" ? node.style.width : undefined;
|
||||
const height = typeof node.style?.height === "number" ? node.style.height : undefined;
|
||||
|
||||
// Find the highest zIndex across all nodes to ensure the duplicate renders on top
|
||||
const allNodes = getNodes();
|
||||
const maxZIndex = allNodes.reduce(
|
||||
(max, n) => Math.max(max, n.zIndex ?? 0),
|
||||
0,
|
||||
);
|
||||
|
||||
const createdNodeId = await createNodeWithIntersection({
|
||||
type: node.type ?? "text",
|
||||
position: {
|
||||
x: originalPosition.x + 50,
|
||||
y: originalPosition.y + 50,
|
||||
},
|
||||
width,
|
||||
height,
|
||||
data: cleanedData,
|
||||
zIndex: maxZIndex + 1,
|
||||
});
|
||||
|
||||
const selectCreatedNode = (attempt = 0) => {
|
||||
const createdNode = getNode(createdNodeId);
|
||||
if (!createdNode) {
|
||||
if (attempt < 10) {
|
||||
requestAnimationFrame(() => selectCreatedNode(attempt + 1));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
setNodes((nodes) =>
|
||||
nodes.map((n) => {
|
||||
if (n.id === nodeId) {
|
||||
return { ...n, selected: false };
|
||||
}
|
||||
if (n.id === createdNodeId) {
|
||||
return { ...n, selected: true };
|
||||
}
|
||||
return n;
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
selectCreatedNode();
|
||||
};
|
||||
|
||||
const stopPropagation = (e: React.MouseEvent | React.PointerEvent) => {
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
return (
|
||||
<NodeToolbar position={Position.Top} offset={8}>
|
||||
<div className="flex items-center gap-1 rounded-lg border bg-card p-1 shadow-md">
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => { stopPropagation(e); handleDuplicate(); }}
|
||||
onPointerDown={stopPropagation}
|
||||
title="Duplicate"
|
||||
className="flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
|
||||
>
|
||||
<Copy size={14} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => { stopPropagation(e); handleDelete(); }}
|
||||
onPointerDown={stopPropagation}
|
||||
title="Delete"
|
||||
className="flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-destructive/10 hover:text-destructive"
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</NodeToolbar>
|
||||
);
|
||||
}
|
||||
|
||||
interface BaseNodeWrapperProps {
|
||||
nodeType: string;
|
||||
selected?: boolean;
|
||||
@@ -128,6 +241,7 @@ export default function BaseNodeWrapper({
|
||||
{statusMessage}
|
||||
</div>
|
||||
)}
|
||||
<NodeToolbarActions />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user