diff --git a/packages/x-flow/src/XFlow.tsx b/packages/x-flow/src/XFlow.tsx index 77dfd86ef..3f479303f 100644 --- a/packages/x-flow/src/XFlow.tsx +++ b/packages/x-flow/src/XFlow.tsx @@ -94,13 +94,15 @@ const XFlow: FC = memo(props => { const { onNodeClick, onEdgeClick, + onPasteCompleted, + onCopyCompleted, zoomOnScroll = true, panOnScroll = false, preventScrolling = true, connectionLineComponent, } = props; const nodeEditorRef = useRef(null); - const { copyNode, pasteNodeSimple } = useFlow(); + const { copyNode, pasteNodeSimple, copyFLowNodes, pasteFLowNodes } = useFlow(); const { undo, redo } = useTemporalStore(); useEffect(() => { @@ -129,43 +131,54 @@ const XFlow: FC = memo(props => { if ((e.key === 's' || e.key === 'S') && (e.ctrlKey || e.metaKey)) e.preventDefault(); if ((e.key === 'c' || e.key === 'C') && (e.ctrlKey || e.metaKey)) { - const latestNodes = storeApi.getState().nodes; - let isNodeCopyEvent = false; - if (e.target instanceof HTMLElement) { - const target = e.target as HTMLElement; - if ( - (isString(target.tagName) && - target.tagName.toLowerCase() === 'body') || - (target.tagName.toLowerCase() === 'div' && - target.classList && - isFunction(target.classList.contains) && - (target.classList.contains('ant-drawer') || - target.classList.contains('react-flow__node') || - target.id === 'xflow-container')) - ) { - isNodeCopyEvent = true; - } - } - const selectedNode = latestNodes?.find(node => node.selected); - if (isNodeCopyEvent && selectedNode?.id) { - const nodeType = selectedNode?.data?._nodeType; - if (isString(nodeType) && nodeType) { - const nodeConfig = settingMap[nodeType]; - if (nodeConfig?.disabledShortcutCopy) { - message.warning( - `${selectedNode.data?.title || selectedNode.id}节点不允许复制` - ); - return; + // let isNodeCopyEvent = false; + // if (e.target instanceof HTMLElement) { + // const target = e.target as HTMLElement; + + // if ( + // (isString(target.tagName) && + // target.tagName.toLowerCase() === 'body') || + // (target.tagName.toLowerCase() === 'div' && + // target.classList && + // isFunction(target.classList.contains) && + // (target.classList.contains('ant-drawer') || + // target.classList.contains('react-flow__node') || + // target.id === 'xflow-container')) || + // (target.tagName.toLowerCase() === 'div' && target.className === 'react-flow__nodesselection-rect') + // ) { + // isNodeCopyEvent = true; + // } + // } + const selectedNodes = latestNodes?.filter(node => node.selected); + if ((document.activeElement === workflowContainerRef.current || document.activeElement.className === 'react-flow__nodesselection-rect') && selectedNodes.length > 0) { + const flag = selectedNodes.some(selectedNode => { + const nodeType = selectedNode?.data?._nodeType; + if (isString(nodeType) && nodeType) { + const nodeConfig = settingMap[nodeType]; + if (nodeConfig?.disabledShortcutCopy) { + message.warning( + `${selectedNode.data?.title || selectedNode.id}节点不允许复制` + ); + + return true; + } } + }); + if (flag) { + return; } + //copyNode(selectedNode.id); // 复制节点 e.preventDefault(); - copyNode(selectedNode.id); + copyFLowNodes(selectedNodes,onCopyCompleted); } } else if ((e.key === 'v' || e.key === 'V') && (e.ctrlKey || e.metaKey)) { - e.preventDefault(); - pasteNodeSimple(); + if(document.activeElement === workflowContainerRef.current){ + e.preventDefault(); + //pasteNodeSimple(); + pasteFLowNodes(onPasteCompleted) + } } else if (e.key === 'Escape') { setOpenPanel(false); workflowContainerRef.current?.focus(); @@ -324,7 +337,7 @@ const XFlow: FC = memo(props => { }, [activeNode?.id]); const deletable = globalConfig?.edge?.deletable ?? true; - const strokeWidth = globalConfig?.edge?.strokeWidth ?? 1.5 + const strokeWidth = globalConfig?.edge?.strokeWidth ?? 1.5; const panelonClose = globalConfig?.nodePanel?.onClose; return ( diff --git a/packages/x-flow/src/hooks/useFlow.ts b/packages/x-flow/src/hooks/useFlow.ts index b0e5a7f08..5fb3fdbc5 100644 --- a/packages/x-flow/src/hooks/useFlow.ts +++ b/packages/x-flow/src/hooks/useFlow.ts @@ -113,17 +113,34 @@ export const useFlow = () => { storeApi.setState({ copyTimeoutId: null }); }, 30000); - navigator.clipboard - .writeText(safeJsonStringify(copyNodes)) - .catch((err) => { - message.error('复制失败~'); - }) storeApi.setState({ copyNodes, copyTimeoutId: timeoutId, }); }); + const copyFLowNodes = useMemoizedFn((nodes: any[],onCopyCompleted:(nodes:any[])=>void) => { + const copyNodes = nodes.map(node => { + return { + id: uuid(), + type: node.type, + data: { + ...node.data, + }, + position: { x: 0, y: 0 }, + sourceId: node.id, + ...node + }; + }); + + navigator.clipboard.writeText(safeJsonStringify(copyNodes)).catch(err => { + message.error('节点复制失败'); + }).then(()=>{ + message.success('节点复制成功'); + onCopyCompleted && onCopyCompleted(copyNodes) + }) + }); + const pasteNode = useMemoizedFn((nodeId: string, data: any) => { if (storeApi.getState().copyNodes.length > 0) { // 清除超时定时器 @@ -153,9 +170,32 @@ export const useFlow = () => { } }); - const pasteNodeSimple = useMemoizedFn(async() => { + const pasteFLowNodes = useMemoizedFn(async (onPasteCompleted:(nodes:any[])=>void) => { + const text = await navigator.clipboard.readText().catch(() => null); + const mousePosition = storeApi.getState().mousePosition; + if (text) { + const copyNodeData = safeJSONParse(text, []); + const flowPos = screenToFlowPosition({ + x: mousePosition.elementX, + y: mousePosition.elementY, + }); + const copyNodes = copyNodeData.map((node, index) => ({ + ...node, + id: uuid(), + position: { + x: flowPos.x + index * 200, + y: flowPos.y + index * 200, + }, + })); + record(() => { + storeApi.getState().addNodes(copyNodes, false); + }); + onPasteCompleted && onPasteCompleted(copyNodes) + } + }); + + const pasteNodeSimple = useMemoizedFn(() => { const mousePosition = storeApi.getState().mousePosition; - const text = await navigator.clipboard.readText().catch(()=>null) if (storeApi.getState().copyNodes.length > 0) { // 清除超时定时器 if (storeApi.getState().copyTimeoutId) { @@ -184,24 +224,7 @@ export const useFlow = () => { copyTimeoutId: null, }); }); - } else if(text){ - const copyNodeData = safeJSONParse(text,[]) - const flowPos = screenToFlowPosition({ - x: mousePosition.elementX, - y: mousePosition.elementY, - }); - const copyNodes = copyNodeData.map(node => ({ - ...node, - id:uuid(), - position: { - x: flowPos.x, - y: flowPos.y, - }, - })); - record(() => { - storeApi.getState().addNodes(copyNodes, false); - }); - }else{ + } else { // message.warning('请先复制节点!'); } }); @@ -259,6 +282,8 @@ export const useFlow = () => { pasteNode, pasteNodeSimple, deleteNode, + copyFLowNodes, + pasteFLowNodes, }), [instance] ); diff --git a/packages/x-flow/src/types.ts b/packages/x-flow/src/types.ts index 91445e419..22c4d62aa 100644 --- a/packages/x-flow/src/types.ts +++ b/packages/x-flow/src/types.ts @@ -145,22 +145,25 @@ export interface THandle{ // isConnectableEnd?:boolean isValidConnection?:(edges:Parameters[0],edgeType:'source'|'target',nodeType:string)=>boolean } + +type node = { + id: string; + type: string; + data: Record; + position: { + x: number; + y: number; + }; +} +type edge = { + id: string; + source: string; + target: string; +} export interface FlowProps { initialValues?: { - nodes: Array<{ - id: string; - type: string; - data: Record; - position: { - x: number; - y: number; - }; - }>; - edges: Array<{ - id: string; - source: string; - target: string; - }>; + nodes: Array; + edges: Array; }; layout?: 'LR' | 'TB'; /** @@ -186,6 +189,8 @@ export interface FlowProps { readOnly?:boolean//只读模式 onNodeClick?: ReactFlowProps['onNodeClick'] onEdgeClick?:ReactFlowProps['onEdgeClick'] + onPasteCompleted?:(addNodes:node[])=>void + onCopyCompleted?:(addNodes:node[])=>void onMenuItemClick?: (itemInfo: ItemInfo, defaultAction: () => void) => void; clickAddNode?: (type: string, nodeItem: TNodeItem, addNode: (initData?: Record) => void) => void; zoomOnScroll?: boolean;