### What problem does this PR solve? feat: fixed issue with threshold translation #882 feat: add NodeContextMenu ### Type of change - [ ] New Feature (non-breaking change which adds functionality)
This commit is contained in:
18
web/src/pages/flow/canvas/context-menu/index.less
Normal file
18
web/src/pages/flow/canvas/context-menu/index.less
Normal file
@@ -0,0 +1,18 @@
|
||||
.contextMenu {
|
||||
background: white;
|
||||
border-style: solid;
|
||||
box-shadow: 10px 19px 20px rgba(0, 0, 0, 10%);
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
button {
|
||||
border: none;
|
||||
display: block;
|
||||
padding: 0.5em;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: white;
|
||||
}
|
||||
}
|
||||
108
web/src/pages/flow/canvas/context-menu/index.tsx
Normal file
108
web/src/pages/flow/canvas/context-menu/index.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import { NodeMouseHandler, useReactFlow } from 'reactflow';
|
||||
|
||||
import styles from './index.less';
|
||||
|
||||
export interface INodeContextMenu {
|
||||
id: string;
|
||||
top: number;
|
||||
left: number;
|
||||
right?: number;
|
||||
bottom?: number;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export function NodeContextMenu({
|
||||
id,
|
||||
top,
|
||||
left,
|
||||
right,
|
||||
bottom,
|
||||
...props
|
||||
}: INodeContextMenu) {
|
||||
const { getNode, setNodes, addNodes, setEdges } = useReactFlow();
|
||||
|
||||
const duplicateNode = useCallback(() => {
|
||||
const node = getNode(id);
|
||||
const position = {
|
||||
x: node?.position?.x || 0 + 50,
|
||||
y: node?.position?.y || 0 + 50,
|
||||
};
|
||||
|
||||
addNodes({
|
||||
...(node || {}),
|
||||
data: node?.data,
|
||||
selected: false,
|
||||
dragging: false,
|
||||
id: `${node?.id}-copy`,
|
||||
position,
|
||||
});
|
||||
}, [id, getNode, addNodes]);
|
||||
|
||||
const deleteNode = useCallback(() => {
|
||||
setNodes((nodes) => nodes.filter((node) => node.id !== id));
|
||||
setEdges((edges) => edges.filter((edge) => edge.source !== id));
|
||||
}, [id, setNodes, setEdges]);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{ top, left, right, bottom }}
|
||||
className={styles.contextMenu}
|
||||
{...props}
|
||||
>
|
||||
<p style={{ margin: '0.5em' }}>
|
||||
<small>node: {id}</small>
|
||||
</p>
|
||||
<button onClick={duplicateNode} type={'button'}>
|
||||
duplicate
|
||||
</button>
|
||||
<button onClick={deleteNode} type={'button'}>
|
||||
delete
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const useHandleNodeContextMenu = (sideWidth: number) => {
|
||||
const [menu, setMenu] = useState<INodeContextMenu>({} as INodeContextMenu);
|
||||
const ref = useRef<any>(null);
|
||||
|
||||
const onNodeContextMenu: NodeMouseHandler = useCallback(
|
||||
(event, node) => {
|
||||
// Prevent native context menu from showing
|
||||
event.preventDefault();
|
||||
|
||||
// Calculate position of the context menu. We want to make sure it
|
||||
// doesn't get positioned off-screen.
|
||||
const pane = ref.current?.getBoundingClientRect();
|
||||
// setMenu({
|
||||
// id: node.id,
|
||||
// top: event.clientY < pane.height - 200 ? event.clientY : 0,
|
||||
// left: event.clientX < pane.width - 200 ? event.clientX : 0,
|
||||
// right: event.clientX >= pane.width - 200 ? pane.width - event.clientX : 0,
|
||||
// bottom:
|
||||
// event.clientY >= pane.height - 200 ? pane.height - event.clientY : 0,
|
||||
// });
|
||||
|
||||
console.info('clientX:', event.clientX);
|
||||
console.info('clientY:', event.clientY);
|
||||
|
||||
setMenu({
|
||||
id: node.id,
|
||||
top: event.clientY - 72,
|
||||
left: event.clientX - sideWidth,
|
||||
// top: event.clientY < pane.height - 200 ? event.clientY - 72 : 0,
|
||||
// left: event.clientX < pane.width - 200 ? event.clientX : 0,
|
||||
});
|
||||
},
|
||||
[sideWidth],
|
||||
);
|
||||
|
||||
// Close the context menu if it's open whenever the window is clicked.
|
||||
const onPaneClick = useCallback(
|
||||
() => setMenu({} as INodeContextMenu),
|
||||
[setMenu],
|
||||
);
|
||||
|
||||
return { onNodeContextMenu, menu, onPaneClick, ref };
|
||||
};
|
||||
@@ -4,6 +4,7 @@ import ReactFlow, {
|
||||
Controls,
|
||||
Edge,
|
||||
Node,
|
||||
NodeMouseHandler,
|
||||
OnConnect,
|
||||
OnEdgesChange,
|
||||
OnNodesChange,
|
||||
@@ -13,7 +14,10 @@ import ReactFlow, {
|
||||
} from 'reactflow';
|
||||
import 'reactflow/dist/style.css';
|
||||
|
||||
import { useHandleDrop } from '../hooks';
|
||||
import { NodeContextMenu, useHandleNodeContextMenu } from './context-menu';
|
||||
|
||||
import FlowDrawer from '../flow-drawer';
|
||||
import { useHandleDrop, useShowDrawer } from '../hooks';
|
||||
import { TextUpdaterNode } from './node';
|
||||
|
||||
const nodeTypes = { textUpdater: TextUpdaterNode };
|
||||
@@ -42,9 +46,17 @@ const initialEdges = [
|
||||
{ id: '1-2', source: '1', target: '2', label: 'to the', type: 'step' },
|
||||
];
|
||||
|
||||
function FlowCanvas() {
|
||||
interface IProps {
|
||||
sideWidth: number;
|
||||
showDrawer(): void;
|
||||
}
|
||||
|
||||
function FlowCanvas({ sideWidth }: IProps) {
|
||||
const [nodes, setNodes] = useState<Node[]>(initialNodes);
|
||||
const [edges, setEdges] = useState<Edge[]>(initialEdges);
|
||||
const { ref, menu, onNodeContextMenu, onPaneClick } =
|
||||
useHandleNodeContextMenu(sideWidth);
|
||||
const { drawerVisible, hideDrawer, showDrawer } = useShowDrawer();
|
||||
|
||||
const onNodesChange: OnNodesChange = useCallback(
|
||||
(changes) => setNodes((nds) => applyNodeChanges(changes, nds)),
|
||||
@@ -60,7 +72,11 @@ function FlowCanvas() {
|
||||
[],
|
||||
);
|
||||
|
||||
const { handleDrop, allowDrop } = useHandleDrop(setNodes);
|
||||
const onNodeClick: NodeMouseHandler = useCallback(() => {
|
||||
showDrawer();
|
||||
}, [showDrawer]);
|
||||
|
||||
const { onDrop, onDragOver, setReactFlowInstance } = useHandleDrop(setNodes);
|
||||
|
||||
useEffect(() => {
|
||||
console.info('nodes:', nodes);
|
||||
@@ -68,23 +84,30 @@ function FlowCanvas() {
|
||||
}, [nodes, edges]);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{ height: '100%', width: '100%' }}
|
||||
onDrop={handleDrop}
|
||||
onDragOver={allowDrop}
|
||||
>
|
||||
<div style={{ height: '100%', width: '100%' }}>
|
||||
<ReactFlow
|
||||
ref={ref}
|
||||
nodes={nodes}
|
||||
onNodesChange={onNodesChange}
|
||||
onNodeContextMenu={onNodeContextMenu}
|
||||
edges={edges}
|
||||
onEdgesChange={onEdgesChange}
|
||||
// fitView
|
||||
fitView
|
||||
onConnect={onConnect}
|
||||
nodeTypes={nodeTypes}
|
||||
onPaneClick={onPaneClick}
|
||||
onDrop={onDrop}
|
||||
onDragOver={onDragOver}
|
||||
onNodeClick={onNodeClick}
|
||||
onInit={setReactFlowInstance}
|
||||
>
|
||||
<Background />
|
||||
<Controls />
|
||||
{Object.keys(menu).length > 0 && (
|
||||
<NodeContextMenu onClick={onPaneClick} {...(menu as any)} />
|
||||
)}
|
||||
</ReactFlow>
|
||||
<FlowDrawer visible={drawerVisible} hideModal={hideDrawer}></FlowDrawer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user