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
1 change: 1 addition & 0 deletions docs/xflow/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ Handle 配置继承自 React Flow 的 Handle 配置,用于控制节点连接
| getSettingSchema | 动态获取节点的业务配置信息,返回值同settingSchema。同时设置`settingSchema`和`getSettingSchema`只生效`getSettingSchema` | `(nodeId: string, nodeType: string, nodeItem: TNodeItem, nodeData: any, form: ReturnType<typeof useForm>) => Promise<Schema>` | |
| renderHandle | 自定义渲染节点的handle,以实现某个节点自定义的出口数量.`sourceHandle`是原handle组件 | `(sourceHandle: SourceHandleType,sourceHandleProps:ComponentProps<SourceHandleType>,nodeProps: {id: string;type: string;data: any;layout: 'LR';isConnectable: boolean;readOnly: boolean;}) => React.JSX.Element`| |
| disabledShortcutDelete | 是否禁用该节点的快捷键删除功能 | `boolean` | false |
| disabledShortcutCopy | 是否禁用该节点的快捷键复制功能 | `boolean` | false |


## TNodeSelector
Expand Down
1 change: 1 addition & 0 deletions docs/xflow/demo/quickStart/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export default () => {
type: 'Start',
hidden: true,
disabledShortcutDelete: true,
disabledShortcutCopy: true,
targetHandleHidden: true,
icon: {
type: 'icon-start',
Expand Down
211 changes: 78 additions & 133 deletions packages/x-flow/src/XFlow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
import '@xyflow/react/dist/style.css';
import { useEventListener, useMemoizedFn } from 'ahooks';
import { produce, setAutoFreeze } from 'immer';
import { debounce, isFunction } from 'lodash';
import { isFunction, isString } from 'lodash';
import type { FC } from 'react';
import React, {
memo,
Expand Down Expand Up @@ -87,7 +87,8 @@ const XFlow: FC<FlowProps> = memo(props => {
);
const { record } = useTemporalStore();
const [activeNode, setActiveNode] = useState<any>(null);
const { settingMap, globalConfig, readOnly, logPanel } = useContext(ConfigContext);
const { settingMap, globalConfig, readOnly, logPanel } =
useContext(ConfigContext);
const [openPanel, setOpenPanel] = useState<boolean>(true);
const [openLogPanel, setOpenLogPanel] = useState<boolean>(true);
const {
Expand All @@ -101,8 +102,6 @@ const XFlow: FC<FlowProps> = memo(props => {
const nodeEditorRef = useRef(null);
const { copyNode, pasteNodeSimple } = useFlow();
const { undo, redo } = useTemporalStore();
const isNodeCopyingRef = useRef(false); // 是否正在进行节点复制
const lastClickedNodeRef = useRef(false); // 最后一次点击是否是节点

useEffect(() => {
zoomTo(0.8);
Expand All @@ -116,7 +115,7 @@ const XFlow: FC<FlowProps> = memo(props => {
};
}, []);

useEventListener('keydown', e => {
const handleKeyDown = useMemoizedFn((e: KeyboardEvent) => {
if ((e.key === 'd' || e.key === 'D') && (e.ctrlKey || e.metaKey))
e.preventDefault();
if ((e.key === 'z' || e.key === 'Z') && (e.ctrlKey || e.metaKey)) {
Expand All @@ -130,74 +129,53 @@ 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 selectedNode = nodes?.find(node => node.selected);
// 获取当前选中的文本(非节点的内容)
const selectedText = window.getSelection()?.toString();
// 如果最后点击的是节点 且 有节点被选中 则 复制节点
if (selectedNode && lastClickedNodeRef.current) {
// 复制节点
isNodeCopyingRef.current = true; // 标记为节点复制
copyNode(selectedNode.id);
e.preventDefault();
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);

// 清空系统剪贴板,确保粘贴时使用节点而非之前的文本
try {
navigator.clipboard.writeText('').catch(() => {});
} catch (err) {}
} else if (selectedText) {
// 复制文本
// 清除之前的节点复制状态
const { copyNodes, copyTimeoutId } = storeApi.getState();
if (copyNodes?.length > 0) {
if (copyTimeoutId) {
clearTimeout(copyTimeoutId);
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;
}
storeApi.setState({
copyNodes: [],
copyTimeoutId: null,
isAddingNode: false,
});
}
// 复制节点
e.preventDefault();
copyNode(selectedNode.id);
}
} else if ((e.key === 'v' || e.key === 'V') && (e.ctrlKey || e.metaKey)) {
const { copyNodes } = storeApi.getState();
// 只有在有节点复制状态时才拦截粘贴操作
if (copyNodes?.length > 0) {
pasteNodeSimple();
e.preventDefault();
pasteNodeSimple();
}
} else if (copyNodes.length > 0) {
// 只在有复制节点时才检查其他操作
const { copyTimeoutId } = storeApi.getState();
if (copyTimeoutId) {
clearTimeout(copyTimeoutId);
storeApi.setState({
copyTimeoutId: null,
isAddingNode: false,
});
}
} else if (e.key === 'Escape'){
setOpenPanel(false)
} else if (e.key === 'Escape') {
setOpenPanel(false);
workflowContainerRef.current?.focus();
}
});

// 添加 copy 事件监听,获取实际复制的内容
useEventListener('copy', (e: ClipboardEvent) => {
if (!isNodeCopyingRef.current) {
// 清除节点复制状态,因为用户复制了其他内容
const { copyNodes, copyTimeoutId } = storeApi.getState();
if (copyNodes?.length > 0) {
if (copyTimeoutId) {
clearTimeout(copyTimeoutId);
}
storeApi.setState({
copyNodes: [],
copyTimeoutId: null,
isAddingNode: false,
});
}
}
isNodeCopyingRef.current = false;
useEventListener('keydown', handleKeyDown, {
target: workflowContainerRef,
});

useEventListener(
Expand All @@ -221,28 +199,6 @@ const XFlow: FC<FlowProps> = memo(props => {
}
);

// 当点击非节点区域时重置标记
useEventListener('mousedown', (e: MouseEvent) => {
const target = e.target as HTMLElement;
const isClickingNode =
target.closest('.xflow-node-container') ||
target.closest('.candidate-node');
// 如果点击的不是节点,重置标记
if (!isClickingNode) {
lastClickedNodeRef.current = false;

// 清除复制状态
const { copyTimeoutId, copyNodes } = storeApi.getState();
if (copyTimeoutId && copyNodes?.length > 0) {
clearTimeout(copyTimeoutId);
storeApi.setState({
copyTimeoutId: null,
isAddingNode: false,
});
}
}
});

const { eventEmitter } = useEventEmitterContextContext();
eventEmitter?.useSubscription((v: any) => {
// 整理画布
Expand Down Expand Up @@ -278,28 +234,6 @@ const XFlow: FC<FlowProps> = memo(props => {
setCandidateNode(newNode);
};

// 插入节点
// const handleInsertNode = () => {
// const newNode = {
// id: uuid(),
// data: { label: 'new node' },
// position: {
// x: 0,
// y: 0,
// },
// };
// addNodes(newNode);
// addEdges({
// id: uuid(),
// source: '2',
// target: newNode.id,
// });
// const targetEdge = edges.find(edge => edge.source === '2');
// updateEdge(targetEdge?.id as string, {
// source: newNode.id,
// });
// };

// edge 移入/移出效果
const getUpdateEdgeConfig = useMemoizedFn((edge: any, color: string) => {
const newEdges = produce(edges, draft => {
Expand All @@ -323,7 +257,6 @@ const XFlow: FC<FlowProps> = memo(props => {
const { _nodeType, _status, ...restData } = data || {};
const nodeSetting = settingMap[_nodeType] || {};
const showPanel = nodeSetting?.nodePanel?.showPanel ?? true;

return (
<CustomNode
{...rest}
Expand All @@ -333,9 +266,6 @@ const XFlow: FC<FlowProps> = memo(props => {
layout={layout}
status={_status}
onClick={async e => {
// 记录用户点击了节点
lastClickedNodeRef.current = true;

if (nodeEditorRef?.current?.validateForm) {
const result = await nodeEditorRef?.current?.validateForm();
if (!result) {
Expand All @@ -356,7 +286,7 @@ const XFlow: FC<FlowProps> = memo(props => {
}
setOpenLogPanel(true);
}}
onDelete={()=>{
onDelete={() => {
// 删除节点并关闭弹窗
setActiveNode(null);
}}
Expand Down Expand Up @@ -400,7 +330,14 @@ const XFlow: FC<FlowProps> = memo(props => {
const panelonClose = globalConfig?.nodePanel?.onClose;

return (
<div id="xflow-container" ref={workflowContainerRef}>
<div
id="xflow-container"
ref={workflowContainerRef}
tabIndex={0}
onMouseDown={() => {
workflowContainerRef.current?.focus();
}}
>
<ReactFlow
panOnDrag={panOnDrag}
nodeTypes={nodeTypes}
Expand All @@ -424,7 +361,7 @@ const XFlow: FC<FlowProps> = memo(props => {
},
deletable: deletable, //默认连线属性受此项控制
}}
onBeforeDelete={async (elements) => {
onBeforeDelete={async elements => {
if (readOnly) {
return false;
}
Expand All @@ -436,10 +373,14 @@ const XFlow: FC<FlowProps> = memo(props => {
: false;
});
if (blockedNodes?.length > 0) {
message.warning(`${blockedNodes.map(n => n.data?.title || n.id).join(', ')}节点不允许删除!`);
message.warning(
`${blockedNodes
.map(n => n.data?.title || n.id)
.join(', ')}节点不允许删除!`
);
return false;
}
return true
return true;
}}
onConnect={onConnect}
onNodesChange={changes => {
Expand All @@ -465,24 +406,24 @@ const XFlow: FC<FlowProps> = memo(props => {
});
}}
onEdgeMouseEnter={(_, edge: any) => {
if(!edge.style.stroke || edge.style.stroke === '#c9c9c9'){
if (!edge.style.stroke || edge.style.stroke === '#c9c9c9') {
getUpdateEdgeConfig(edge, '#2970ff');
}
}}
onEdgeMouseLeave={(_, edge) => {
if(['#2970ff',"#c9c9c9"].includes(edge.style.stroke)){
if (['#2970ff', '#c9c9c9'].includes(edge.style.stroke)) {
getUpdateEdgeConfig(edge, '#c9c9c9');
}
}}
onNodesDelete={() => {
setActiveNode(null);
setActiveNode(null);
}}
onNodeClick={(event, node) => {
onNodeClick && onNodeClick(event, node);
}}
deleteKeyCode={globalConfig?.deleteKeyCode}
onEdgeClick={(event,edge)=>{
onEdgeClick && onEdgeClick(event,edge)
onEdgeClick={(event, edge) => {
onEdgeClick && onEdgeClick(event, edge);
}}
>
<CandidateNode />
Expand All @@ -504,6 +445,7 @@ const XFlow: FC<FlowProps> = memo(props => {
return;
}
setOpenPanel(false);
workflowContainerRef.current?.focus();

// 如果日志面板关闭
if (!isTruthy(activeNode?._status) || !openLogPanel) {
Expand All @@ -520,19 +462,22 @@ const XFlow: FC<FlowProps> = memo(props => {
{NodeEditorWrap}
</PanelContainer>
)}
{isTruthy(activeNode?._status) && openLogPanel && Boolean(logPanel?.enable ?? true) && (
<PanelStatusLogContainer
id={activeNode?.id}
nodeType={activeNode?._nodeType}
onClose={() => {
setOpenLogPanel(false);
!openPanel && setActiveNode(null);
}}
data={activeNode?.values}
>
{NodeLogWrap}
</PanelStatusLogContainer>
)}
{isTruthy(activeNode?._status) &&
openLogPanel &&
Boolean(logPanel?.enable ?? true) && (
<PanelStatusLogContainer
id={activeNode?.id}
nodeType={activeNode?._nodeType}
onClose={() => {
setOpenLogPanel(false);
!openPanel && setActiveNode(null);
workflowContainerRef.current?.focus();
}}
data={activeNode?.values}
>
{NodeLogWrap}
</PanelStatusLogContainer>
)}
</ReactFlow>
</div>
);
Expand Down
1 change: 1 addition & 0 deletions packages/x-flow/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export interface TNodeItem {
showTestingBtn?: boolean; // 是否显示单点调试按钮
className?: string;// 自定义节点class
disabledShortcutDelete?: boolean; // 是否禁用快捷键删除
disabledShortcutCopy?: boolean; // 是否禁用快捷键复制
}

export interface TNodeGroup {
Expand Down
Loading