fix: canvas loop bug and node placement
All checks were successful
Deploy Nyla Frontend INT / build-and-deploy (push) Successful in 10m31s

This commit is contained in:
Ida 2026-05-26 10:47:26 +02:00
parent 90e1fbc40f
commit fe7321c84d
2 changed files with 61 additions and 10 deletions

View file

@ -158,6 +158,7 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({ in
const [versions, setVersions] = useState<AutoVersion[]>([]);
const [currentVersionId, setCurrentVersionId] = useState<string | null>(null);
const [versionLoading, setVersionLoading] = useState(false);
const didBootstrapEmptyCanvasRef = useRef(false);
const [targetFeatureInstanceId, setTargetFeatureInstanceId] = useState<string | null>(instanceId);
@ -598,8 +599,22 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({ in
useEffect(() => {
if (loading || nodeTypes.length === 0) return;
if (currentWorkflowId || initialWorkflowId) return;
if (canvasNodes.length > 0) return;
if (currentWorkflowId || initialWorkflowId) {
didBootstrapEmptyCanvasRef.current = false;
return;
}
if (didBootstrapEmptyCanvasRef.current) return;
didBootstrapEmptyCanvasRef.current = true;
if (canvasNodes.length === 0 && canvasConnections.length === 0 && invocations.length === 0) {
return;
}
console.debug(`${LOG} bootstrapping empty canvas`, {
currentWorkflowId,
initialWorkflowId,
canvasNodes: canvasNodes.length,
canvasConnections: canvasConnections.length,
invocations: invocations.length,
});
applyGraphWithSync({ nodes: [], connections: [] }, [], {
skipHistory: true,
});
@ -609,8 +624,9 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({ in
currentWorkflowId,
initialWorkflowId,
canvasNodes.length,
canvasConnections.length,
invocations.length,
applyGraphWithSync,
t,
]);
const toggleCategory = useCallback((id: string) => {

View file

@ -20,6 +20,8 @@ import { useLanguage } from '../../../providers/language/LanguageContext';
import { AiBadge } from '../nodes/shared/AiBadge';
import { switchOutputLabel } from '../nodes/shared/graphUtils';
const LOG = '[FlowCanvas]';
export interface CanvasNode {
id: string;
type: string;
@ -842,6 +844,8 @@ export const FlowCanvas = forwardRef<FlowCanvasHandle, FlowCanvasProps>(function
const onHistoryCheckpointRef = useRef(onHistoryCheckpoint);
onHistoryCheckpointRef.current = onHistoryCheckpoint;
const onSelectionChangeRef = useRef(onSelectionChange);
onSelectionChangeRef.current = onSelectionChange;
const emitHistoryCheckpoint = useCallback(() => {
onHistoryCheckpointRef.current?.();
@ -1019,12 +1023,19 @@ export const FlowCanvas = forwardRef<FlowCanvasHandle, FlowCanvasProps>(function
]
);
const lastEmittedSelectionRef = useRef<{ nodeId: string | null; signature: string | null }>({
nodeId: null,
signature: null,
});
useEffect(() => {
if (onSelectionChange) {
const node = selectedNodeId ? nodes.find((n) => n.id === selectedNodeId) ?? null : null;
onSelectionChange(node);
}
}, [selectedNodeId, nodes, onSelectionChange]);
const node = selectedNodeId ? nodes.find((n) => n.id === selectedNodeId) ?? null : null;
const signature = node ? JSON.stringify(node) : null;
const last = lastEmittedSelectionRef.current;
if (last.nodeId === selectedNodeId && last.signature === signature) return;
lastEmittedSelectionRef.current = { nodeId: selectedNodeId, signature };
onSelectionChangeRef.current?.(node);
}, [selectedNodeId, nodes]);
const handleConnectionClick = useCallback((e: React.MouseEvent, connId: string) => {
e.stopPropagation();
@ -1088,6 +1099,11 @@ export const FlowCanvas = forwardRef<FlowCanvasHandle, FlowCanvasProps>(function
const handleDrop = useCallback(
async (e: React.DragEvent) => {
e.preventDefault();
console.debug(`${LOG} drop received`, {
types: Array.from(e.dataTransfer.types),
clientX: e.clientX,
clientY: e.clientY,
});
// 1) externe Drop-Targets (z. B. ``application/json+workflow`` aus UDB-FilesTab)
if (onExternalDrop) {
const reservedMimes = new Set([
@ -1113,16 +1129,35 @@ export const FlowCanvas = forwardRef<FlowCanvasHandle, FlowCanvasProps>(function
}
// 2) Standard: Node-Type aus der NodeSidebar
const raw = e.dataTransfer.getData('application/json');
if (!raw || !containerRef.current) return;
if (!raw || !containerRef.current) {
console.debug(`${LOG} drop ignored`, {
hasRaw: Boolean(raw),
hasContainer: Boolean(containerRef.current),
});
return;
}
try {
const { type } = JSON.parse(raw);
const el = containerRef.current;
const rect = el.getBoundingClientRect();
const x = (e.clientX - rect.left - panOffset.x) / zoom - NODE_WIDTH / 2;
const y = (e.clientY - rect.top - panOffset.y) / zoom - NODE_HEIGHT / 2;
console.debug(`${LOG} placing node from drop`, {
type,
raw,
dropX: x,
dropY: y,
panOffset,
zoom,
});
onDropNodeType(type, Math.max(0, x), Math.max(0, y));
emitHistoryCheckpoint();
} catch (_) {}
} catch (error) {
console.debug(`${LOG} drop parse failed`, {
raw,
error,
});
}
},
[onDropNodeType, onExternalDrop, panOffset, zoom, emitHistoryCheckpoint]
);