feat: fixed issue with threshold translation #882 and add NodeContextMenu (#906)

### 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:
balibabu 2024-05-23 18:53:04 +08:00 committed by GitHub
parent 1e5c5abe58
commit 4cda40c3ef
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 263 additions and 57 deletions

View File

@ -247,8 +247,8 @@ export default {
`,
maxToken: '最大token數',
maxTokenMessage: '最大token數是必填項',
threshold: '臨界點',
thresholdMessage: '臨界點是必填項',
threshold: '閾值',
thresholdMessage: '閾值是必填項',
maxCluster: '最大聚類數',
maxClusterMessage: '最大聚類數是必填項',
randomSeed: '隨機種子',

View File

@ -264,8 +264,8 @@ export default {
`,
maxToken: '最大token数',
maxTokenMessage: '最大token数是必填项',
threshold: '临界点',
thresholdMessage: '临界点是必填项',
threshold: '阈值',
thresholdMessage: '阈值是必填项',
maxCluster: '最大聚类数',
maxClusterMessage: '最大聚类数是必填项',
randomSeed: '随机种子',

View 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;
}
}

View 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 };
};

View File

@ -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>
);
}

View File

@ -0,0 +1,20 @@
import { IModalProps } from '@/interfaces/common';
import { Drawer } from 'antd';
const FlowDrawer = ({ visible, hideModal }: IModalProps<any>) => {
return (
<Drawer
title="Basic Drawer"
placement="right"
// closable={false}
onClose={hideModal}
open={visible}
getContainer={false}
mask={false}
>
<p>Some contents...</p>
</Drawer>
);
};
export default FlowDrawer;

View File

@ -1,6 +1,5 @@
import { Avatar, Card, Flex, Layout, Space } from 'antd';
import classNames from 'classnames';
import { useState } from 'react';
import { componentList } from '../mock';
import { useHandleDrag } from '../hooks';
@ -8,9 +7,13 @@ import styles from './index.less';
const { Sider } = Layout;
const FlowSider = () => {
const [collapsed, setCollapsed] = useState(true);
const { handleDrag } = useHandleDrag();
interface IProps {
setCollapsed: (width: boolean) => void;
collapsed: boolean;
}
const FlowSide = ({ setCollapsed, collapsed }: IProps) => {
const { handleDragStart } = useHandleDrag();
return (
<Sider
@ -27,7 +30,7 @@ const FlowSider = () => {
hoverable
draggable
className={classNames(styles.operatorCard)}
onDragStart={handleDrag(x.name)}
onDragStart={handleDragStart(x.name)}
>
<Flex justify="space-between" align="center">
<Space size={15}>
@ -45,4 +48,4 @@ const FlowSider = () => {
);
};
export default FlowSider;
export default FlowSide;

View File

@ -1,47 +1,75 @@
import React, { Dispatch, SetStateAction, useCallback } from 'react';
import { Node } from 'reactflow';
import { useSetModalState } from '@/hooks/commonHooks';
import React, { Dispatch, SetStateAction, useCallback, useState } from 'react';
import { Node, ReactFlowInstance } from 'reactflow';
import { v4 as uuidv4 } from 'uuid';
export const useHandleDrag = () => {
const handleDrag = useCallback(
const handleDragStart = useCallback(
(operatorId: string) => (ev: React.DragEvent<HTMLDivElement>) => {
console.info(ev.clientX, ev.pageY);
ev.dataTransfer.setData('operatorId', operatorId);
ev.dataTransfer.setData('startClientX', ev.clientX.toString());
ev.dataTransfer.setData('startClientY', ev.clientY.toString());
ev.dataTransfer.setData('application/reactflow', operatorId);
ev.dataTransfer.effectAllowed = 'move';
},
[],
);
return { handleDrag };
return { handleDragStart };
};
export const useHandleDrop = (setNodes: Dispatch<SetStateAction<Node[]>>) => {
const allowDrop = (ev: React.DragEvent<HTMLDivElement>) => {
ev.preventDefault();
};
const [reactFlowInstance, setReactFlowInstance] =
useState<ReactFlowInstance<any, any>>();
const handleDrop = useCallback(
(ev: React.DragEvent<HTMLDivElement>) => {
ev.preventDefault();
const operatorId = ev.dataTransfer.getData('operatorId');
const startClientX = ev.dataTransfer.getData('startClientX');
const startClientY = ev.dataTransfer.getData('startClientY');
console.info(operatorId);
console.info(ev.pageX, ev.pageY);
console.info(ev.clientX, ev.clientY);
console.info(ev.movementX, ev.movementY);
const x = ev.clientX - 200;
const y = ev.clientY - 72;
setNodes((pre) => {
return pre.concat({
id: operatorId,
position: { x, y },
data: { label: operatorId },
});
const onDragOver = useCallback((event: React.DragEvent<HTMLDivElement>) => {
event.preventDefault();
event.dataTransfer.dropEffect = 'move';
}, []);
const onDrop = useCallback(
(event: React.DragEvent<HTMLDivElement>) => {
event.preventDefault();
const type = event.dataTransfer.getData('application/reactflow');
// check if the dropped element is valid
if (typeof type === 'undefined' || !type) {
return;
}
// reactFlowInstance.project was renamed to reactFlowInstance.screenToFlowPosition
// and you don't need to subtract the reactFlowBounds.left/top anymore
// details: https://reactflow.dev/whats-new/2023-11-10
const position = reactFlowInstance?.screenToFlowPosition({
x: event.clientX,
y: event.clientY,
});
const newNode = {
id: uuidv4(),
type,
position: position || {
x: 0,
y: 0,
},
data: { label: `${type} node` },
};
setNodes((nds) => nds.concat(newNode));
},
[setNodes],
[reactFlowInstance, setNodes],
);
return { handleDrop, allowDrop };
return { onDrop, onDragOver, setReactFlowInstance };
};
export const useShowDrawer = () => {
const {
visible: drawerVisible,
hideModal: hideDrawer,
showModal: showDrawer,
} = useSetModalState();
return {
drawerVisible,
hideDrawer,
showDrawer,
};
};

View File

@ -1,18 +1,24 @@
import { Layout } from 'antd';
import { useState } from 'react';
import { ReactFlowProvider } from 'reactflow';
import FlowCanvas from './canvas';
import Sider from './flow-sider';
const { Content } = Layout;
function RagFlow() {
const [collapsed, setCollapsed] = useState(false);
return (
<Layout style={{ minHeight: '100vh' }}>
<Sider></Sider>
<Layout>
<Content style={{ margin: '0 16px' }}>
<FlowCanvas></FlowCanvas>
</Content>
</Layout>
<Layout>
<ReactFlowProvider>
<Sider setCollapsed={setCollapsed} collapsed={collapsed}></Sider>
<Layout>
<Content style={{ margin: '0 16px' }}>
<FlowCanvas sideWidth={collapsed ? 0 : 200}></FlowCanvas>
</Content>
</Layout>
</ReactFlowProvider>
</Layout>
);
}