fix(ui): rework edge update logic

This commit is contained in:
psychedelicious 2024-05-18 18:55:37 +10:00
parent 9f7841a04b
commit 6b4e464d17
2 changed files with 46 additions and 41 deletions

View File

@ -8,12 +8,13 @@ import { useIsValidConnection } from 'features/nodes/hooks/useIsValidConnection'
import { useWorkflowWatcher } from 'features/nodes/hooks/useWorkflowWatcher'; import { useWorkflowWatcher } from 'features/nodes/hooks/useWorkflowWatcher';
import { import {
$cursorPos, $cursorPos,
$didUpdateEdge,
$isAddNodePopoverOpen, $isAddNodePopoverOpen,
$isUpdatingEdge, $isUpdatingEdge,
$lastEdgeUpdateMouseEvent,
$pendingConnection, $pendingConnection,
$viewport, $viewport,
connectionMade, connectionMade,
edgeAdded,
edgeDeleted, edgeDeleted,
edgesChanged, edgesChanged,
edgesDeleted, edgesDeleted,
@ -24,6 +25,7 @@ import {
undo, undo,
} from 'features/nodes/store/nodesSlice'; } from 'features/nodes/store/nodesSlice';
import { $flow } from 'features/nodes/store/reactFlowInstance'; import { $flow } from 'features/nodes/store/reactFlowInstance';
import { isString } from 'lodash-es';
import type { CSSProperties, MouseEvent } from 'react'; import type { CSSProperties, MouseEvent } from 'react';
import { memo, useCallback, useMemo, useRef } from 'react'; import { memo, useCallback, useMemo, useRef } from 'react';
import { useHotkeys } from 'react-hotkeys-hook'; import { useHotkeys } from 'react-hotkeys-hook';
@ -39,7 +41,7 @@ import type {
ReactFlowProps, ReactFlowProps,
ReactFlowState, ReactFlowState,
} from 'reactflow'; } from 'reactflow';
import { Background, ReactFlow, useStore as useReactFlowStore } from 'reactflow'; import { Background, ReactFlow, useStore as useReactFlowStore, useUpdateNodeInternals } from 'reactflow';
import CustomConnectionLine from './connectionLines/CustomConnectionLine'; import CustomConnectionLine from './connectionLines/CustomConnectionLine';
import InvocationCollapsedEdge from './edges/InvocationCollapsedEdge'; import InvocationCollapsedEdge from './edges/InvocationCollapsedEdge';
@ -81,6 +83,7 @@ export const Flow = memo(() => {
const flowWrapper = useRef<HTMLDivElement>(null); const flowWrapper = useRef<HTMLDivElement>(null);
const isValidConnection = useIsValidConnection(); const isValidConnection = useIsValidConnection();
const cancelConnection = useReactFlowStore(selectCancelConnection); const cancelConnection = useReactFlowStore(selectCancelConnection);
const updateNodeInternals = useUpdateNodeInternals();
useWorkflowWatcher(); useWorkflowWatcher();
useSyncExecutionState(); useSyncExecutionState();
const [borderRadius] = useToken('radii', ['base']); const [borderRadius] = useToken('radii', ['base']);
@ -157,45 +160,46 @@ export const Flow = memo(() => {
* where the edge is deleted if you click it accidentally). * where the edge is deleted if you click it accidentally).
*/ */
// We have a ref for cursor position, but it is the *projected* cursor position. const onEdgeUpdateStart: NonNullable<ReactFlowProps['onEdgeUpdateStart']> = useCallback((e, _edge, _handleType) => {
// Easiest to just keep track of the last mouse event for this particular feature $isUpdatingEdge.set(true);
const edgeUpdateMouseEvent = useRef<MouseEvent>(); $didUpdateEdge.set(false);
$lastEdgeUpdateMouseEvent.set(e);
const onEdgeUpdateStart: NonNullable<ReactFlowProps['onEdgeUpdateStart']> = useCallback( }, []);
(e, edge, _handleType) => {
$isUpdatingEdge.set(true);
// update mouse event
edgeUpdateMouseEvent.current = e;
// always delete the edge when starting an updated
dispatch(edgeDeleted(edge.id));
},
[dispatch]
);
const onEdgeUpdate: OnEdgeUpdateFunc = useCallback( const onEdgeUpdate: OnEdgeUpdateFunc = useCallback(
(_oldEdge, newConnection) => { (edge, newConnection) => {
// Because we deleted the edge when the update started, we must create a new edge from the connection // This event is fired when an edge update is successful
$didUpdateEdge.set(true);
// When an edge update is successful, we need to delete the old edge and create a new one
dispatch(edgeDeleted(edge.id));
dispatch(connectionMade(newConnection)); dispatch(connectionMade(newConnection));
// Because we shift the position of handles depending on whether a field is connected or not, we must use
// updateNodeInternals to tell reactflow to recalculate the positions of the handles
const nodesToUpdate = [edge.source, edge.target, newConnection.source, newConnection.target].filter(isString);
updateNodeInternals(nodesToUpdate);
}, },
[dispatch] [dispatch, updateNodeInternals]
); );
const onEdgeUpdateEnd: NonNullable<ReactFlowProps['onEdgeUpdateEnd']> = useCallback( const onEdgeUpdateEnd: NonNullable<ReactFlowProps['onEdgeUpdateEnd']> = useCallback(
(e, edge, _handleType) => { (e, edge, _handleType) => {
$isUpdatingEdge.set(false); const didUpdateEdge = $didUpdateEdge.get();
$pendingConnection.set(null); // Fall back to a reasonable default event
// Handle the case where user begins a drag but didn't move the cursor - we deleted the edge when starting const lastEvent = $lastEdgeUpdateMouseEvent.get() ?? { clientX: 0, clientY: 0 };
// the edge update - we need to add it back // We have to narrow this event down to MouseEvents - could be TouchEvent
if ( const didMouseMove =
// ignore touch events !('touches' in e) && Math.hypot(e.clientX - lastEvent.clientX, e.clientY - lastEvent.clientY) > 5;
!('touches' in e) &&
edgeUpdateMouseEvent.current?.clientX === e.clientX && // If we got this far and did not successfully update an edge, and the mouse moved away from the handle,
edgeUpdateMouseEvent.current?.clientY === e.clientY // the user probably intended to delete the edge
) { if (!didUpdateEdge && didMouseMove) {
dispatch(edgeAdded(edge)); dispatch(edgeDeleted(edge.id));
} }
// reset mouse event
edgeUpdateMouseEvent.current = undefined; $isUpdatingEdge.set(false);
$didUpdateEdge.set(false);
$pendingConnection.set(null);
$lastEdgeUpdateMouseEvent.set(null);
}, },
[dispatch] [dispatch]
); );
@ -255,9 +259,11 @@ export const Flow = memo(() => {
useHotkeys(['meta+shift+z', 'ctrl+shift+z'], onRedoHotkey); useHotkeys(['meta+shift+z', 'ctrl+shift+z'], onRedoHotkey);
const onEscapeHotkey = useCallback(() => { const onEscapeHotkey = useCallback(() => {
$pendingConnection.set(null); if (!$isUpdatingEdge.get()) {
$isAddNodePopoverOpen.set(false); $pendingConnection.set(null);
cancelConnection(); $isAddNodePopoverOpen.set(false);
cancelConnection();
}
}, [cancelConnection]); }, [cancelConnection]);
useHotkeys('esc', onEscapeHotkey); useHotkeys('esc', onEscapeHotkey);

View File

@ -47,6 +47,7 @@ import {
import type { AnyNode, InvocationNodeEdge } from 'features/nodes/types/invocation'; import type { AnyNode, InvocationNodeEdge } from 'features/nodes/types/invocation';
import { isInvocationNode, isNotesNode } from 'features/nodes/types/invocation'; import { isInvocationNode, isNotesNode } from 'features/nodes/types/invocation';
import { atom } from 'nanostores'; import { atom } from 'nanostores';
import type { MouseEvent } from 'react';
import type { Connection, Edge, EdgeChange, EdgeRemoveChange, Node, NodeChange, Viewport, XYPosition } from 'reactflow'; import type { Connection, Edge, EdgeChange, EdgeRemoveChange, Node, NodeChange, Viewport, XYPosition } from 'reactflow';
import { addEdge, applyEdgeChanges, applyNodeChanges, getConnectedEdges, getIncomers, getOutgoers } from 'reactflow'; import { addEdge, applyEdgeChanges, applyNodeChanges, getConnectedEdges, getIncomers, getOutgoers } from 'reactflow';
import type { UndoableOptions } from 'redux-undo'; import type { UndoableOptions } from 'redux-undo';
@ -125,9 +126,6 @@ export const nodesSlice = createSlice({
edgesChanged: (state, action: PayloadAction<EdgeChange[]>) => { edgesChanged: (state, action: PayloadAction<EdgeChange[]>) => {
state.edges = applyEdgeChanges(action.payload, state.edges); state.edges = applyEdgeChanges(action.payload, state.edges);
}, },
edgeAdded: (state, action: PayloadAction<Edge>) => {
state.edges = addEdge(action.payload, state.edges);
},
connectionMade: (state, action: PayloadAction<Connection>) => { connectionMade: (state, action: PayloadAction<Connection>) => {
state.edges = addEdge({ ...action.payload, type: 'default' }, state.edges); state.edges = addEdge({ ...action.payload, type: 'default' }, state.edges);
}, },
@ -495,7 +493,6 @@ export const {
notesNodeValueChanged, notesNodeValueChanged,
selectedAll, selectedAll,
selectionPasted, selectionPasted,
edgeAdded,
undo, undo,
redo, redo,
} = nodesSlice.actions; } = nodesSlice.actions;
@ -507,6 +504,9 @@ export const $copiedEdges = atom<InvocationNodeEdge[]>([]);
export const $edgesToCopiedNodes = atom<InvocationNodeEdge[]>([]); export const $edgesToCopiedNodes = atom<InvocationNodeEdge[]>([]);
export const $pendingConnection = atom<PendingConnection | null>(null); export const $pendingConnection = atom<PendingConnection | null>(null);
export const $isUpdatingEdge = atom(false); export const $isUpdatingEdge = atom(false);
export const $didUpdateEdge = atom(false);
export const $lastEdgeUpdateMouseEvent = atom<MouseEvent | null>(null);
export const $viewport = atom<Viewport>({ x: 0, y: 0, zoom: 1 }); export const $viewport = atom<Viewport>({ x: 0, y: 0, zoom: 1 });
export const $isAddNodePopoverOpen = atom(false); export const $isAddNodePopoverOpen = atom(false);
export const closeAddNodePopover = () => { export const closeAddNodePopover = () => {
@ -609,6 +609,5 @@ export const isAnyNodeOrEdgeMutation = isAnyOf(
nodesDeleted, nodesDeleted,
nodeUseCacheChanged, nodeUseCacheChanged,
notesNodeValueChanged, notesNodeValueChanged,
selectionPasted, selectionPasted
edgeAdded
); );