mirror of
https://github.com/infiniflow/ragflow.git
synced 2025-12-23 15:06:50 +08:00
feat: add custom edge (#1061)
### What problem does this PR solve? feat: add custom edge feat: add flow card feat: add store for canvas #918 ### Type of change - [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
@ -86,7 +86,7 @@ export const useHandleNodeContextMenu = (sideWidth: number) => {
|
||||
|
||||
setMenu({
|
||||
id: node.id,
|
||||
top: event.clientY - 72,
|
||||
top: event.clientY - 144,
|
||||
left: event.clientX - sideWidth,
|
||||
// top: event.clientY < pane.height - 200 ? event.clientY - 72 : 0,
|
||||
// left: event.clientX < pane.width - 200 ? event.clientX : 0,
|
||||
|
||||
15
web/src/pages/flow/canvas/edge/index.less
Normal file
15
web/src/pages/flow/canvas/edge/index.less
Normal file
@ -0,0 +1,15 @@
|
||||
.edgeButton {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background: #eee;
|
||||
border: 1px solid #fff;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
border-radius: 50%;
|
||||
font-size: 10px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.edgeButton:hover {
|
||||
box-shadow: 0 0 2px 2px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
72
web/src/pages/flow/canvas/edge/index.tsx
Normal file
72
web/src/pages/flow/canvas/edge/index.tsx
Normal file
@ -0,0 +1,72 @@
|
||||
import {
|
||||
BaseEdge,
|
||||
EdgeLabelRenderer,
|
||||
EdgeProps,
|
||||
getBezierPath,
|
||||
} from 'reactflow';
|
||||
import useStore from '../../store';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import styles from './index.less';
|
||||
|
||||
export function ButtonEdge({
|
||||
id,
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
sourcePosition,
|
||||
targetPosition,
|
||||
style = {},
|
||||
markerEnd,
|
||||
selected,
|
||||
}: EdgeProps) {
|
||||
const deleteEdgeById = useStore((state) => state.deleteEdgeById);
|
||||
const [edgePath, labelX, labelY] = getBezierPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
sourcePosition,
|
||||
targetX,
|
||||
targetY,
|
||||
targetPosition,
|
||||
});
|
||||
|
||||
const selectedStyle = useMemo(() => {
|
||||
return selected ? { strokeWidth: 1, stroke: '#1677ff' } : {};
|
||||
}, [selected]);
|
||||
|
||||
const onEdgeClick = () => {
|
||||
deleteEdgeById(id);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<BaseEdge
|
||||
path={edgePath}
|
||||
markerEnd={markerEnd}
|
||||
style={{ ...style, ...selectedStyle }}
|
||||
/>
|
||||
<EdgeLabelRenderer>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
|
||||
fontSize: 12,
|
||||
// everything inside EdgeLabelRenderer has no pointer events by default
|
||||
// if you have an interactive element, set pointer-events: all
|
||||
pointerEvents: 'all',
|
||||
}}
|
||||
className="nodrag nopan"
|
||||
>
|
||||
<button
|
||||
className={styles.edgeButton}
|
||||
type="button"
|
||||
onClick={onEdgeClick}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
</EdgeLabelRenderer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
4
web/src/pages/flow/canvas/index.less
Normal file
4
web/src/pages/flow/canvas/index.less
Normal file
@ -0,0 +1,4 @@
|
||||
.canvasWrapper {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
}
|
||||
@ -1,76 +1,64 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import ReactFlow, {
|
||||
Background,
|
||||
Controls,
|
||||
Edge,
|
||||
Node,
|
||||
MarkerType,
|
||||
NodeMouseHandler,
|
||||
OnConnect,
|
||||
OnEdgesChange,
|
||||
OnNodesChange,
|
||||
addEdge,
|
||||
applyEdgeChanges,
|
||||
applyNodeChanges,
|
||||
} from 'reactflow';
|
||||
import 'reactflow/dist/style.css';
|
||||
|
||||
import { NodeContextMenu, useHandleNodeContextMenu } from './context-menu';
|
||||
import { ButtonEdge } from './edge';
|
||||
|
||||
import FlowDrawer from '../flow-drawer';
|
||||
import {
|
||||
useHandleDrop,
|
||||
useHandleKeyUp,
|
||||
useHandleSelectionChange,
|
||||
useSelectCanvasData,
|
||||
useShowDrawer,
|
||||
} from '../hooks';
|
||||
import { dsl } from '../mock';
|
||||
import { TextUpdaterNode } from './node';
|
||||
|
||||
import styles from './index.less';
|
||||
|
||||
const nodeTypes = { textUpdater: TextUpdaterNode };
|
||||
|
||||
const edgeTypes = {
|
||||
buttonEdge: ButtonEdge,
|
||||
};
|
||||
|
||||
interface IProps {
|
||||
sideWidth: number;
|
||||
}
|
||||
|
||||
function FlowCanvas({ sideWidth }: IProps) {
|
||||
const [nodes, setNodes] = useState<Node[]>(dsl.graph.nodes);
|
||||
const [edges, setEdges] = useState<Edge[]>(dsl.graph.edges);
|
||||
|
||||
const { selectedEdges, selectedNodes } = useHandleSelectionChange();
|
||||
const {
|
||||
nodes,
|
||||
edges,
|
||||
onConnect,
|
||||
onEdgesChange,
|
||||
onNodesChange,
|
||||
onSelectionChange,
|
||||
} = useSelectCanvasData();
|
||||
|
||||
const { ref, menu, onNodeContextMenu, onPaneClick } =
|
||||
useHandleNodeContextMenu(sideWidth);
|
||||
const { drawerVisible, hideDrawer, showDrawer } = useShowDrawer();
|
||||
const { drawerVisible, hideDrawer, showDrawer, clickedNode } =
|
||||
useShowDrawer();
|
||||
|
||||
const onNodesChange: OnNodesChange = useCallback(
|
||||
(changes) => setNodes((nds) => applyNodeChanges(changes, nds)),
|
||||
[],
|
||||
);
|
||||
const onEdgesChange: OnEdgesChange = useCallback(
|
||||
(changes) => setEdges((eds) => applyEdgeChanges(changes, eds)),
|
||||
[],
|
||||
const onNodeClick: NodeMouseHandler = useCallback(
|
||||
(e, node) => {
|
||||
showDrawer(node);
|
||||
},
|
||||
[showDrawer],
|
||||
);
|
||||
|
||||
const onConnect: OnConnect = useCallback(
|
||||
(params) => setEdges((eds) => addEdge(params, eds)),
|
||||
[],
|
||||
);
|
||||
const { onDrop, onDragOver, setReactFlowInstance } = useHandleDrop();
|
||||
|
||||
const onNodeClick: NodeMouseHandler = useCallback(() => {
|
||||
showDrawer();
|
||||
}, [showDrawer]);
|
||||
|
||||
const { onDrop, onDragOver, setReactFlowInstance } = useHandleDrop(setNodes);
|
||||
|
||||
const { handleKeyUp } = useHandleKeyUp(selectedEdges, selectedNodes);
|
||||
|
||||
useEffect(() => {
|
||||
console.info('nodes:', nodes);
|
||||
console.info('edges:', edges);
|
||||
}, [nodes, edges]);
|
||||
const { handleKeyUp } = useHandleKeyUp();
|
||||
|
||||
return (
|
||||
<div style={{ height: '100%', width: '100%' }}>
|
||||
<div className={styles.canvasWrapper}>
|
||||
<ReactFlow
|
||||
ref={ref}
|
||||
nodes={nodes}
|
||||
@ -81,12 +69,21 @@ function FlowCanvas({ sideWidth }: IProps) {
|
||||
fitView
|
||||
onConnect={onConnect}
|
||||
nodeTypes={nodeTypes}
|
||||
edgeTypes={edgeTypes}
|
||||
onPaneClick={onPaneClick}
|
||||
onDrop={onDrop}
|
||||
onDragOver={onDragOver}
|
||||
onNodeClick={onNodeClick}
|
||||
onInit={setReactFlowInstance}
|
||||
onKeyUp={handleKeyUp}
|
||||
onSelectionChange={onSelectionChange}
|
||||
nodeOrigin={[0.5, 0]}
|
||||
defaultEdgeOptions={{
|
||||
type: 'buttonEdge',
|
||||
markerEnd: {
|
||||
type: MarkerType.ArrowClosed,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Background />
|
||||
<Controls />
|
||||
@ -94,7 +91,11 @@ function FlowCanvas({ sideWidth }: IProps) {
|
||||
<NodeContextMenu onClick={onPaneClick} {...(menu as any)} />
|
||||
)}
|
||||
</ReactFlow>
|
||||
<FlowDrawer visible={drawerVisible} hideModal={hideDrawer}></FlowDrawer>
|
||||
<FlowDrawer
|
||||
node={clickedNode}
|
||||
visible={drawerVisible}
|
||||
hideModal={hideDrawer}
|
||||
></FlowDrawer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
.textUpdaterNode {
|
||||
// height: 50px;
|
||||
border: 1px solid black;
|
||||
border: 1px solid gray;
|
||||
padding: 5px;
|
||||
border-radius: 5px;
|
||||
background: white;
|
||||
@ -10,3 +10,12 @@
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
.selectedNode {
|
||||
border-color: #1677ff;
|
||||
}
|
||||
|
||||
.handle {
|
||||
display: inline-flex;
|
||||
text-align: center;
|
||||
// align-items: center;
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import classNames from 'classnames';
|
||||
import { Handle, NodeProps, Position } from 'reactflow';
|
||||
|
||||
import styles from './index.less';
|
||||
@ -5,19 +6,30 @@ import styles from './index.less';
|
||||
export function TextUpdaterNode({
|
||||
data,
|
||||
isConnectable = true,
|
||||
selected,
|
||||
}: NodeProps<{ label: string }>) {
|
||||
return (
|
||||
<div className={styles.textUpdaterNode}>
|
||||
<div
|
||||
className={classNames(styles.textUpdaterNode, {
|
||||
[styles.selectedNode]: selected,
|
||||
})}
|
||||
>
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Left}
|
||||
isConnectable={isConnectable}
|
||||
/>
|
||||
className={styles.handle}
|
||||
>
|
||||
{/* <PlusCircleOutlined style={{ fontSize: 10 }} /> */}
|
||||
</Handle>
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
isConnectable={isConnectable}
|
||||
/>
|
||||
className={styles.handle}
|
||||
>
|
||||
{/* <PlusCircleOutlined style={{ fontSize: 10 }} /> */}
|
||||
</Handle>
|
||||
<div>{data.label}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user