feat(nodes): Center pasted nodes at mouse location (#4595)

* Initial commit.  Feature works, but code might need some cleanup

* Cleaned up diff

* Made mousePosition a XYPosition again so its nicely typed

* Fixed yarn issues

* Paste now properly takes node width/height into account when pasting

* feat(ui): use react's types in the `onMouseMove` `reactflow` handler

* feat(ui): use refs to access `reactflow`'s DOM elements

* feat(ui): use a ref to store cursor position in nodes

---------

Co-authored-by: psychedelicious <4822129+psychedelicious@users.noreply.github.com>
This commit is contained in:
CrypticWit 2023-09-21 13:16:15 +12:00 committed by GitHub
parent 183e2c3ee0
commit f87b042162
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 44 additions and 6 deletions

View File

@ -5,7 +5,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions';
import { $flow } from 'features/nodes/store/reactFlowInstance';
import { contextMenusClosed } from 'features/ui/store/uiSlice';
import { useCallback } from 'react';
import { MouseEvent, useCallback, useRef } from 'react';
import { useHotkeys } from 'react-hotkeys-hook';
import {
Background,
@ -21,6 +21,7 @@ import {
OnSelectionChangeFunc,
ProOptions,
ReactFlow,
XYPosition,
} from 'reactflow';
import { useIsValidConnection } from '../../hooks/useIsValidConnection';
import {
@ -79,7 +80,8 @@ export const Flow = () => {
const edges = useAppSelector((state) => state.nodes.edges);
const viewport = useAppSelector((state) => state.nodes.viewport);
const { shouldSnapToGrid, selectionMode } = useAppSelector(selector);
const flowWrapper = useRef<HTMLDivElement>(null);
const cursorPosition = useRef<XYPosition>();
const isValidConnection = useIsValidConnection();
const [borderRadius] = useToken('radii', ['base']);
@ -154,6 +156,17 @@ export const Flow = () => {
flow.fitView();
}, []);
const onMouseMove = useCallback((event: MouseEvent<HTMLDivElement>) => {
const bounds = flowWrapper.current?.getBoundingClientRect();
if (bounds) {
const pos = $flow.get()?.project({
x: event.clientX - bounds.left,
y: event.clientY - bounds.top,
});
cursorPosition.current = pos;
}
}, []);
useHotkeys(['Ctrl+c', 'Meta+c'], (e) => {
e.preventDefault();
dispatch(selectionCopied());
@ -166,18 +179,20 @@ export const Flow = () => {
useHotkeys(['Ctrl+v', 'Meta+v'], (e) => {
e.preventDefault();
dispatch(selectionPasted());
dispatch(selectionPasted({ cursorPosition: cursorPosition.current }));
});
return (
<ReactFlow
id="workflow-editor"
ref={flowWrapper}
defaultViewport={viewport}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
nodes={nodes}
edges={edges}
onInit={onInit}
onMouseMove={onMouseMove}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onEdgesDelete={onEdgesDelete}

View File

@ -16,6 +16,7 @@ import {
OnConnectStartParams,
SelectionMode,
Viewport,
XYPosition,
} from 'reactflow';
import { receivedOpenAPISchema } from 'services/api/thunks/schema';
import { sessionCanceled, sessionInvoked } from 'services/api/thunks/session';
@ -715,8 +716,30 @@ const nodesSlice = createSlice({
selectionCopied: (state) => {
state.nodesToCopy = state.nodes.filter((n) => n.selected).map(cloneDeep);
state.edgesToCopy = state.edges.filter((e) => e.selected).map(cloneDeep);
if (state.nodesToCopy.length > 0) {
const averagePosition = { x: 0, y: 0 };
state.nodesToCopy.forEach((e) => {
const xOffset = 0.15 * (e.width ?? 0);
const yOffset = 0.5 * (e.height ?? 0);
averagePosition.x += e.position.x + xOffset;
averagePosition.y += e.position.y + yOffset;
});
averagePosition.x /= state.nodesToCopy.length;
averagePosition.y /= state.nodesToCopy.length;
state.nodesToCopy.forEach((e) => {
e.position.x -= averagePosition.x;
e.position.y -= averagePosition.y;
});
}
},
selectionPasted: (state) => {
selectionPasted: (
state,
action: PayloadAction<{ cursorPosition?: XYPosition }>
) => {
const { cursorPosition } = action.payload;
const newNodes = state.nodesToCopy.map(cloneDeep);
const oldNodeIds = newNodes.map((n) => n.data.id);
const newEdges = state.edgesToCopy
@ -745,8 +768,8 @@ const nodesSlice = createSlice({
const position = findUnoccupiedPosition(
state.nodes,
node.position.x,
node.position.y
node.position.x + (cursorPosition?.x ?? 0),
node.position.y + (cursorPosition?.y ?? 0)
);
node.position = position;