Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 45 additions & 32 deletions packages/x-flow/src/XFlow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,13 +94,15 @@ const XFlow: FC<FlowProps> = 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(() => {
Expand Down Expand Up @@ -129,43 +131,54 @@ const XFlow: FC<FlowProps> = 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();
Expand Down Expand Up @@ -324,7 +337,7 @@ const XFlow: FC<FlowProps> = 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 (
Expand Down
75 changes: 50 additions & 25 deletions packages/x-flow/src/hooks/useFlow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
// 清除超时定时器
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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('请先复制节点!');
}
});
Expand Down Expand Up @@ -259,6 +282,8 @@ export const useFlow = () => {
pasteNode,
pasteNodeSimple,
deleteNode,
copyFLowNodes,
pasteFLowNodes,
}),
[instance]
);
Expand Down
33 changes: 19 additions & 14 deletions packages/x-flow/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,22 +145,25 @@ export interface THandle{
// isConnectableEnd?:boolean
isValidConnection?:(edges:Parameters<HandleProps['isValidConnection']>[0],edgeType:'source'|'target',nodeType:string)=>boolean
}

type node = {
id: string;
type: string;
data: Record<string, any>;
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<string, any>;
position: {
x: number;
y: number;
};
}>;
edges: Array<{
id: string;
source: string;
target: string;
}>;
nodes: Array<node>;
edges: Array<edge>;
};
layout?: 'LR' | 'TB';
/**
Expand All @@ -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<string, any>) => void) => void;
zoomOnScroll?: boolean;
Expand Down
Loading