浏览代码

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)
tags/v0.7.0
balibabu 1年前
父节点
当前提交
4cda40c3ef
没有帐户链接到提交者的电子邮件

+ 2
- 2
web/src/locales/zh-traditional.ts 查看文件

以上就是你需要總結的內容。`, 以上就是你需要總結的內容。`,
maxToken: '最大token數', maxToken: '最大token數',
maxTokenMessage: '最大token數是必填項', maxTokenMessage: '最大token數是必填項',
threshold: '臨界點',
thresholdMessage: '臨界點是必填項',
threshold: '閾值',
thresholdMessage: '閾值是必填項',
maxCluster: '最大聚類數', maxCluster: '最大聚類數',
maxClusterMessage: '最大聚類數是必填項', maxClusterMessage: '最大聚類數是必填項',
randomSeed: '隨機種子', randomSeed: '隨機種子',

+ 2
- 2
web/src/locales/zh.ts 查看文件

以上就是你需要总结的内容。`, 以上就是你需要总结的内容。`,
maxToken: '最大token数', maxToken: '最大token数',
maxTokenMessage: '最大token数是必填项', maxTokenMessage: '最大token数是必填项',
threshold: '临界点',
thresholdMessage: '临界点是必填项',
threshold: '阈值',
thresholdMessage: '阈值是必填项',
maxCluster: '最大聚类数', maxCluster: '最大聚类数',
maxClusterMessage: '最大聚类数是必填项', maxClusterMessage: '最大聚类数是必填项',
randomSeed: '随机种子', randomSeed: '随机种子',

+ 18
- 0
web/src/pages/flow/canvas/context-menu/index.less 查看文件

.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
- 0
web/src/pages/flow/canvas/context-menu/index.tsx 查看文件

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

+ 32
- 9
web/src/pages/flow/canvas/index.tsx 查看文件

Controls, Controls,
Edge, Edge,
Node, Node,
NodeMouseHandler,
OnConnect, OnConnect,
OnEdgesChange, OnEdgesChange,
OnNodesChange, OnNodesChange,
} from 'reactflow'; } from 'reactflow';
import 'reactflow/dist/style.css'; 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'; import { TextUpdaterNode } from './node';


const nodeTypes = { textUpdater: TextUpdaterNode }; const nodeTypes = { textUpdater: TextUpdaterNode };
{ id: '1-2', source: '1', target: '2', label: 'to the', type: 'step' }, { 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 [nodes, setNodes] = useState<Node[]>(initialNodes);
const [edges, setEdges] = useState<Edge[]>(initialEdges); const [edges, setEdges] = useState<Edge[]>(initialEdges);
const { ref, menu, onNodeContextMenu, onPaneClick } =
useHandleNodeContextMenu(sideWidth);
const { drawerVisible, hideDrawer, showDrawer } = useShowDrawer();


const onNodesChange: OnNodesChange = useCallback( const onNodesChange: OnNodesChange = useCallback(
(changes) => setNodes((nds) => applyNodeChanges(changes, nds)), (changes) => setNodes((nds) => applyNodeChanges(changes, nds)),
[], [],
); );


const { handleDrop, allowDrop } = useHandleDrop(setNodes);
const onNodeClick: NodeMouseHandler = useCallback(() => {
showDrawer();
}, [showDrawer]);

const { onDrop, onDragOver, setReactFlowInstance } = useHandleDrop(setNodes);


useEffect(() => { useEffect(() => {
console.info('nodes:', nodes); console.info('nodes:', nodes);
}, [nodes, edges]); }, [nodes, edges]);


return ( return (
<div
style={{ height: '100%', width: '100%' }}
onDrop={handleDrop}
onDragOver={allowDrop}
>
<div style={{ height: '100%', width: '100%' }}>
<ReactFlow <ReactFlow
ref={ref}
nodes={nodes} nodes={nodes}
onNodesChange={onNodesChange} onNodesChange={onNodesChange}
onNodeContextMenu={onNodeContextMenu}
edges={edges} edges={edges}
onEdgesChange={onEdgesChange} onEdgesChange={onEdgesChange}
// fitView
fitView
onConnect={onConnect} onConnect={onConnect}
nodeTypes={nodeTypes} nodeTypes={nodeTypes}
onPaneClick={onPaneClick}
onDrop={onDrop}
onDragOver={onDragOver}
onNodeClick={onNodeClick}
onInit={setReactFlowInstance}
> >
<Background /> <Background />
<Controls /> <Controls />
{Object.keys(menu).length > 0 && (
<NodeContextMenu onClick={onPaneClick} {...(menu as any)} />
)}
</ReactFlow> </ReactFlow>
<FlowDrawer visible={drawerVisible} hideModal={hideDrawer}></FlowDrawer>
</div> </div>
); );
} }

+ 20
- 0
web/src/pages/flow/flow-drawer/index.tsx 查看文件

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;

+ 9
- 6
web/src/pages/flow/flow-sider/index.tsx 查看文件

import { Avatar, Card, Flex, Layout, Space } from 'antd'; import { Avatar, Card, Flex, Layout, Space } from 'antd';
import classNames from 'classnames'; import classNames from 'classnames';
import { useState } from 'react';
import { componentList } from '../mock'; import { componentList } from '../mock';


import { useHandleDrag } from '../hooks'; import { useHandleDrag } from '../hooks';


const { Sider } = Layout; 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 ( return (
<Sider <Sider
hoverable hoverable
draggable draggable
className={classNames(styles.operatorCard)} className={classNames(styles.operatorCard)}
onDragStart={handleDrag(x.name)}
onDragStart={handleDragStart(x.name)}
> >
<Flex justify="space-between" align="center"> <Flex justify="space-between" align="center">
<Space size={15}> <Space size={15}>
); );
}; };


export default FlowSider;
export default FlowSide;

+ 59
- 31
web/src/pages/flow/hooks.ts 查看文件

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 = () => { export const useHandleDrag = () => {
const handleDrag = useCallback(
const handleDragStart = useCallback(
(operatorId: string) => (ev: React.DragEvent<HTMLDivElement>) => { (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[]>>) => { export const useHandleDrop = (setNodes: Dispatch<SetStateAction<Node[]>>) => {
const allowDrop = (ev: React.DragEvent<HTMLDivElement>) => {
ev.preventDefault();
};
const [reactFlowInstance, setReactFlowInstance] =
useState<ReactFlowInstance<any, any>>();

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


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 },
});
// 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,
};
}; };

+ 13
- 7
web/src/pages/flow/index.tsx 查看文件

import { Layout } from 'antd'; import { Layout } from 'antd';
import { useState } from 'react';
import { ReactFlowProvider } from 'reactflow';
import FlowCanvas from './canvas'; import FlowCanvas from './canvas';
import Sider from './flow-sider'; import Sider from './flow-sider';


const { Content } = Layout; const { Content } = Layout;


function RagFlow() { function RagFlow() {
const [collapsed, setCollapsed] = useState(false);

return ( 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> </Layout>
); );
} }

正在加载...
取消
保存