fix: canvas loop bug and node placement

This commit is contained in:
Ida 2026-05-26 10:47:26 +02:00
parent 7da7ad5041
commit 1c539076e5
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 [versions, setVersions] = useState<AutoVersion[]>([]);
const [currentVersionId, setCurrentVersionId] = useState<string | null>(null); const [currentVersionId, setCurrentVersionId] = useState<string | null>(null);
const [versionLoading, setVersionLoading] = useState(false); const [versionLoading, setVersionLoading] = useState(false);
const didBootstrapEmptyCanvasRef = useRef(false);
const [targetFeatureInstanceId, setTargetFeatureInstanceId] = useState<string | null>(instanceId); const [targetFeatureInstanceId, setTargetFeatureInstanceId] = useState<string | null>(instanceId);
@ -598,8 +599,22 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({ in
useEffect(() => { useEffect(() => {
if (loading || nodeTypes.length === 0) return; if (loading || nodeTypes.length === 0) return;
if (currentWorkflowId || initialWorkflowId) return; if (currentWorkflowId || initialWorkflowId) {
if (canvasNodes.length > 0) return; 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: [] }, [], { applyGraphWithSync({ nodes: [], connections: [] }, [], {
skipHistory: true, skipHistory: true,
}); });
@ -609,8 +624,9 @@ export const Automation2FlowEditor: React.FC<Automation2FlowEditorProps> = ({ in
currentWorkflowId, currentWorkflowId,
initialWorkflowId, initialWorkflowId,
canvasNodes.length, canvasNodes.length,
canvasConnections.length,
invocations.length,
applyGraphWithSync, applyGraphWithSync,
t,
]); ]);
const toggleCategory = useCallback((id: string) => { const toggleCategory = useCallback((id: string) => {

View file

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