diff --git a/docs/xflow/best-practices.md b/docs/xflow/best-practices.md
new file mode 100644
index 000000000..dcaba3b88
--- /dev/null
+++ b/docs/xflow/best-practices.md
@@ -0,0 +1,16 @@
+---
+order: 1
+title: '最佳实践'
+mobile: false
+group:
+ title: 最佳实践
+ order: 1
+---
+
+# 在项目中使用
+
+
+
+
+
+
diff --git a/docs/xflow/demo/best/basic/TextEllipsis/index.less b/docs/xflow/demo/best/basic/TextEllipsis/index.less
new file mode 100644
index 000000000..56831a83d
--- /dev/null
+++ b/docs/xflow/demo/best/basic/TextEllipsis/index.less
@@ -0,0 +1,13 @@
+.text-ellipsis {
+ display: inline-block;
+ max-width: 100%;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+}
+
+.paragraph-ellipsis {
+ display: block;
+ width: 100%;
+ word-break: break-all;
+}
diff --git a/docs/xflow/demo/best/basic/TextEllipsis/index.tsx b/docs/xflow/demo/best/basic/TextEllipsis/index.tsx
new file mode 100644
index 000000000..d10131e31
--- /dev/null
+++ b/docs/xflow/demo/best/basic/TextEllipsis/index.tsx
@@ -0,0 +1,101 @@
+import { Tooltip, TooltipProps } from 'antd';
+import React, { FC, memo, useEffect, useState } from 'react';
+import './index.less';
+
+interface ITextEllipsisProps {
+ text: string;
+ style?: object;
+ className?: string;
+ toolTipProps?: TooltipProps;
+ type?: 'text' | 'paragraph';
+ rows?: number;
+}
+const TextEllipsis: FC = ({
+ text,
+ style,
+ toolTipProps,
+ type = 'text',
+ rows = 1,
+ className,
+}) => {
+ const typographyRef = React.useRef(null);
+ const [isEllipse, setIsEllipse] = useState(false);
+
+ const component =
+ type === 'paragraph' ? (
+
+ {text}
+
+ ) : (
+
+ {text}
+
+ );
+
+ useEffect(() => {
+ if (text) {
+ if (type === 'paragraph') {
+ const { offsetHeight, scrollHeight, clientHeight } =
+ typographyRef.current;
+ setIsEllipse(scrollHeight > clientHeight);
+ } else {
+ const isEllipse = isEleEllipsis(typographyRef?.current);
+ setIsEllipse(isEllipse);
+ }
+ }
+ }, [text]);
+
+ const isEleEllipsis = (ele: HTMLElement): boolean => {
+ const childDiv = document.createElement('em');
+ ele.appendChild(childDiv);
+
+ const rect = ele.getBoundingClientRect();
+ const childRect = childDiv.getBoundingClientRect();
+
+ ele.removeChild(childDiv);
+
+ return (
+ rect.left > childRect.left ||
+ childRect.right > rect.right ||
+ rect.top > childRect.top ||
+ childRect.bottom > rect.bottom
+ );
+ };
+ if (isEllipse) {
+ return (
+
+ document.getElementById('xflow-container') as HTMLElement
+ }
+ color="#ffff"
+ overlayInnerStyle={{
+ color: '#354052',
+ fontSize: '12px',
+ borderRadius: '8px',
+ }}
+ placement="bottomRight"
+ {...toolTipProps}
+ >
+ {component}
+
+ );
+ }
+ return component;
+};
+
+export default memo(TextEllipsis);
diff --git a/docs/xflow/demo/best/basic/const.tsx b/docs/xflow/demo/best/basic/const.tsx
new file mode 100644
index 000000000..b48bcfb07
--- /dev/null
+++ b/docs/xflow/demo/best/basic/const.tsx
@@ -0,0 +1,342 @@
+export const nodes = [
+ {
+ id: 'y01s4993gdvyknzf',
+ type: 'startEvent',
+ position: {
+ x: 0,
+ y: 120,
+ },
+ measured: {
+ width: 268,
+ height: 54,
+ },
+ selected: false,
+ dragging: false,
+ data: {
+ _isCandidate: false,
+ title: '开始',
+ string: 'hello',
+ string2: 'test',
+ string3: '16',
+ string4: '17',
+ string5: '19',
+ string6: 'hello world',
+ string7: 'test',
+ },
+ },
+ {
+ id: 'bzydz67rlmv1n871',
+ type: 'Switch',
+ position: {
+ x: 354,
+ y: 120,
+ },
+ measured: {
+ width: 268,
+ height: 222,
+ },
+ selected: false,
+ dragging: false,
+ data: {
+ _isCandidate: false,
+ title: '条件分支',
+ list: [
+ {
+ _id: 'yjrqixxjwfdn47of',
+ name: '条件一',
+ type: '类型一',
+ value: 'a==12',
+ },
+ {
+ name: '条件二',
+ type: '类型一',
+ value: 'a!=12',
+ _id: 'id_wmu3t8gkanwg271a',
+ },
+ ],
+ },
+ },
+ {
+ id: 'mjw4se36aadccnbt',
+ type: 'serviceTask',
+ position: {
+ x: 708,
+ y: 22.5,
+ },
+ measured: {
+ width: 268,
+ height: 54,
+ },
+ selected: false,
+ dragging: false,
+ data: {
+ _isCandidate: false,
+ title: '知识检索_ncjg',
+ properties: [
+ {
+ name: 'name1',
+ value: 'value1',
+ },
+ ],
+ },
+ },
+ {
+ id: '6w52vnb5kwwq7pdj',
+ type: 'receiveTask',
+ position: {
+ x: 708,
+ y: 217.5,
+ },
+ measured: {
+ width: 268,
+ height: 73,
+ },
+ selected: false,
+ data: {
+ _isCandidate: false,
+ title: '问题分类器_ysa6',
+ desc: '问题分类描述',
+ properties: [
+ {
+ name: '问题1',
+ value: '值1',
+ },
+ ],
+ },
+ },
+ {
+ id: 'dm6eebudu8tmb4v3',
+ type: 'Parallel',
+ position: {
+ x: 1062,
+ y: 120,
+ },
+ measured: {
+ width: 268,
+ height: 196,
+ },
+ selected: false,
+ dragging: false,
+ data: {
+ _isCandidate: false,
+ title: '并行事件_49pn',
+ properties: [
+ {
+ name: '属性',
+ value: 'value',
+ },
+ ],
+ list: [
+ {
+ _id: 'id_7m3vitc4qy476axa',
+ name: '事件1',
+ value: '描述1',
+ },
+ {
+ _id: 'id_m26wo9298g3imnes',
+ name: '事件2',
+ value: '描述2',
+ },
+ ],
+ },
+ },
+ {
+ id: 'walnrnko6rjn56bx',
+ type: 'userTask',
+ position: {
+ x: 1416,
+ y: 22.5,
+ },
+ measured: {
+ width: 268,
+ height: 54,
+ },
+ selected: false,
+ data: {
+ _isCandidate: false,
+ title: '代码执行_flcv',
+ properties: [
+ {
+ name: 'name',
+ value: 'value',
+ },
+ ],
+ },
+ },
+ {
+ id: 'rxoar6yr0whfr7ym',
+ type: 'callActivity',
+ position: {
+ x: 1416,
+ y: 217.5,
+ },
+ measured: {
+ width: 268,
+ height: 54,
+ },
+ selected: false,
+ data: {
+ _isCandidate: false,
+ title: 'HTTP请求_pmkf',
+ properties: [
+ {
+ name: 'name',
+ value: 'value',
+ },
+ ],
+ },
+ },
+ {
+ id: 'qjmv9gxija93gxkb',
+ type: 'endEvent',
+ position: {
+ x: 1770,
+ y: 120,
+ },
+ measured: {
+ width: 268,
+ height: 54,
+ },
+ selected: true,
+ dragging: false,
+ data: {
+ _isCandidate: false,
+ title: '结束_af4u',
+ nodeDesc: '流程结束',
+ },
+ },
+];
+
+export const edges = [
+ {
+ type: 'buttonedge',
+ style: {
+ strokeWidth: 1.5,
+ stroke: '#c9c9c9',
+ },
+ markerEnd: {
+ type: 'arrowclosed',
+ color: '#c9c9c9',
+ },
+ deletable: true,
+ source: 'y01s4993gdvyknzf',
+ target: 'bzydz67rlmv1n871',
+ id: 'xy-edge__y01s4993gdvyknzf-bzydz67rlmv1n871',
+ },
+ {
+ type: 'buttonedge',
+ style: {
+ strokeWidth: 1.5,
+ stroke: '#c9c9c9',
+ },
+ markerEnd: {
+ type: 'arrowclosed',
+ color: '#c9c9c9',
+ },
+ deletable: true,
+ source: 'bzydz67rlmv1n871',
+ sourceHandle: 'yjrqixxjwfdn47of',
+ target: 'mjw4se36aadccnbt',
+ id: 'xy-edge__bzydz67rlmv1n871yjrqixxjwfdn47of-mjw4se36aadccnbt',
+ },
+ {
+ type: 'buttonedge',
+ style: {
+ strokeWidth: 1.5,
+ stroke: '#c9c9c9',
+ },
+ markerEnd: {
+ type: 'arrowclosed',
+ color: '#c9c9c9',
+ },
+ deletable: true,
+ source: 'bzydz67rlmv1n871',
+ sourceHandle: 'id_wmu3t8gkanwg271a',
+ target: '6w52vnb5kwwq7pdj',
+ id: 'xy-edge__bzydz67rlmv1n871id_wmu3t8gkanwg271a-6w52vnb5kwwq7pdj',
+ },
+ {
+ type: 'buttonedge',
+ style: {
+ strokeWidth: 1.5,
+ },
+ markerEnd: {
+ type: 'arrowclosed',
+ },
+ deletable: true,
+ source: 'mjw4se36aadccnbt',
+ target: 'dm6eebudu8tmb4v3',
+ id: 'xy-edge__mjw4se36aadccnbt-dm6eebudu8tmb4v3',
+ },
+ {
+ type: 'buttonedge',
+ style: {
+ strokeWidth: 1.5,
+ stroke: '#c9c9c9',
+ },
+ markerEnd: {
+ type: 'arrowclosed',
+ color: '#c9c9c9',
+ },
+ deletable: true,
+ source: '6w52vnb5kwwq7pdj',
+ target: 'dm6eebudu8tmb4v3',
+ id: 'xy-edge__6w52vnb5kwwq7pdj-dm6eebudu8tmb4v3',
+ },
+ {
+ type: 'buttonedge',
+ style: {
+ strokeWidth: 1.5,
+ stroke: '#c9c9c9',
+ },
+ markerEnd: {
+ type: 'arrowclosed',
+ color: '#c9c9c9',
+ },
+ deletable: true,
+ source: 'dm6eebudu8tmb4v3',
+ sourceHandle: 'id_7m3vitc4qy476axa',
+ target: 'walnrnko6rjn56bx',
+ id: 'xy-edge__dm6eebudu8tmb4v3id_7m3vitc4qy476axa-walnrnko6rjn56bx',
+ },
+ {
+ type: 'buttonedge',
+ style: {
+ strokeWidth: 1.5,
+ },
+ markerEnd: {
+ type: 'arrowclosed',
+ },
+ deletable: true,
+ source: 'dm6eebudu8tmb4v3',
+ sourceHandle: 'id_m26wo9298g3imnes',
+ target: 'rxoar6yr0whfr7ym',
+ id: 'xy-edge__dm6eebudu8tmb4v3id_m26wo9298g3imnes-rxoar6yr0whfr7ym',
+ },
+ {
+ type: 'buttonedge',
+ style: {
+ strokeWidth: 1.5,
+ },
+ markerEnd: {
+ type: 'arrowclosed',
+ },
+ deletable: true,
+ source: 'walnrnko6rjn56bx',
+ target: 'qjmv9gxija93gxkb',
+ id: 'xy-edge__walnrnko6rjn56bx-qjmv9gxija93gxkb',
+ },
+ {
+ type: 'buttonedge',
+ style: {
+ strokeWidth: 1.5,
+ },
+ markerEnd: {
+ type: 'arrowclosed',
+ },
+ deletable: true,
+ source: 'rxoar6yr0whfr7ym',
+ target: 'qjmv9gxija93gxkb',
+ id: 'xy-edge__rxoar6yr0whfr7ym-qjmv9gxija93gxkb',
+ },
+];
diff --git a/docs/xflow/demo/best/basic/header.tsx b/docs/xflow/demo/best/basic/header.tsx
new file mode 100644
index 000000000..9bb9c253e
--- /dev/null
+++ b/docs/xflow/demo/best/basic/header.tsx
@@ -0,0 +1,21 @@
+import React ,{ FC } from 'react';
+import './index.less';
+
+const Header: FC<{ data: any }> = ({ data }) => {
+ // const container = (document.getElementById('xflow-container') as HTMLElement);
+ return (
+
+
+

+
+ XFlow
+
+
+
+ );
+};
+
+export default Header;
diff --git a/docs/xflow/demo/best/basic/index.less b/docs/xflow/demo/best/basic/index.less
new file mode 100644
index 000000000..f86033d46
--- /dev/null
+++ b/docs/xflow/demo/best/basic/index.less
@@ -0,0 +1,79 @@
+.settingSchemaStyle {
+ .ant-col {
+ width: 100% !important;
+
+ .ant-form-item-label label {
+ font-weight: 500;
+ }
+
+ .ant-form-item-explain-error {
+ font-size: 12px;
+ }
+ }
+
+ .child-title {
+ .ant-form-item-label label {
+ font-size: 12px;
+ }
+ }
+
+ .fr-list-simple {
+ display: block;
+ }
+
+ .parallel-wrap {
+ .fr-list-simple .fr-inline-field {
+ min-width: 200px;
+ }
+ }
+
+ .switch-list {
+ .fr-list-simple .fr-inline-field {
+ width: 140px;
+ min-width: 140px;
+ }
+ }
+
+ .ant-form-item-label>label {
+ color: #096dd9;
+ }
+}
+
+
+.switch-custom-node {
+ width: 100%;
+ min-height: 20px;
+ padding: 0.25rem;
+ text-align: justify;
+ word-wrap: break-word;
+ background-color: #f2f4f7;
+ border-radius: 0.375rem;
+
+ .condition-label {
+ display: flex;
+ }
+}
+
+
+.process-header-card {
+ position: absolute;
+ top: 14px;
+ left: 10px;
+ z-index: 1;
+ padding: 6px;
+ font-size: 12px;
+}
+
+
+.tools {
+ position: absolute;
+ top: 14px;
+ right: 10px;
+ z-index: 1;
+
+ .tools-btn {
+ height: 29px;
+ font-size: 12px;
+ border-radius: 6px;
+ }
+}
diff --git a/docs/xflow/demo/best/basic/index.tsx b/docs/xflow/demo/best/basic/index.tsx
new file mode 100644
index 000000000..27b54f3c9
--- /dev/null
+++ b/docs/xflow/demo/best/basic/index.tsx
@@ -0,0 +1,25 @@
+import XFlow from '@xrenders/xflow';
+import { settings } from './setting';
+import { nodes,edges } from './const';
+import React from 'react';
+import './index.less';
+import showSwitchNode from './showSwitchNode';
+import Header from './header';
+import { Tools } from './tools';
+
+export default () => {
+ return (
+
+
+
+
+
+ );
+};
diff --git a/docs/xflow/demo/best/basic/setting.tsx b/docs/xflow/demo/best/basic/setting.tsx
new file mode 100644
index 000000000..bcc182be8
--- /dev/null
+++ b/docs/xflow/demo/best/basic/setting.tsx
@@ -0,0 +1,405 @@
+export const settingSchema = {
+ properties: {
+ type: 'array',
+ widget: 'simpleList',
+ props: {
+ hideCopy: true,
+ hideMove: true,
+ },
+ className: 'parallel-wrap',
+ items: {
+ type: 'object',
+ properties: {
+ name: {
+ title: '名称',
+ type: 'string',
+ props: {
+ allowClear: true,
+ },
+ className: 'child-title',
+ readOnlyWidget: 'ReadOnlyPanel',
+ },
+ value: {
+ title: 'value',
+ type: 'string',
+ props: {
+ allowClear: true,
+ },
+ className: 'child-title',
+ readOnlyWidget: 'ReadOnlyPanel',
+ },
+ },
+ },
+ },
+};
+
+export const activitySchema = {
+ title: '工具',
+ type: '_group',
+ items: [
+ {
+ title: '知识检索',
+ type: 'serviceTask',
+ description: '允许你从知识库中查询与用户问题相关的文本内容',
+ icon: {
+ type: 'icon-knowledge',
+ bgColor: '#fa541c',
+ },
+ hideDesc: true,
+ nodePanel: {
+ width: 510,
+ },
+ settingSchema: {
+ type: 'object',
+ className: 'settingSchemaStyle',
+ properties: {
+ ...settingSchema,
+ },
+ },
+ },
+ {
+ title: '问题分类器',
+ type: 'receiveTask',
+ description: '定义问题的分类条件',
+ icon: {
+ type: 'icon-prompt',
+ bgColor: '#875BF7',
+ },
+ hideDesc: true,
+ nodePanel: {
+ width: 510,
+ },
+ settingSchema: {
+ type: 'object',
+ className: 'settingSchemaStyle',
+ properties: {
+ ...settingSchema,
+ },
+ },
+ },
+ {
+ title: '代码执行',
+ type: 'userTask', // exclusiveGateway
+ description: '执行一段代码实现自定义逻辑',
+ icon: {
+ type: 'icon-code',
+ bgColor: 'pink',
+ },
+ hideDesc: true,
+ nodePanel: {
+ width: 510,
+ },
+ settingSchema: {
+ type: 'object',
+ className: 'settingSchemaStyle',
+ properties: {
+ ...settingSchema,
+ },
+ },
+ },
+ {
+ title: 'HTTP请求',
+ type: 'callActivity',
+ description: '允许通过 HTTP 协议发送服务器请求',
+ icon: {
+ type: 'icon-http',
+ bgColor: '#2E90FA',
+ },
+ hideDesc: true,
+ nodePanel: {
+ width: 510,
+ },
+ settingSchema: {
+ type: 'object',
+ className: 'settingSchemaStyle',
+ properties: {
+ ...settingSchema,
+ },
+ },
+ },
+ ],
+};
+
+export const settings = [
+ {
+ title: '事件',
+ type: '_group',
+ items: [
+ {
+ title: '开始',
+ type: 'startEvent',
+ description: '流程开始的节点,一个流程只允许有一个开始节点',
+ icon: {
+ type: 'icon-start',
+ bgColor: '#17B26A',
+ },
+ targetHandleHidden: true,
+ settingSchema: {
+ type: 'object',
+ className: 'settingSchemaStyle',
+ properties: {
+ string: {
+ title: '字符串',
+ description: '带清空x按钮',
+ type: 'string',
+ default: 'hello world',
+ props: {
+ allowClear: true,
+ },
+ },
+ string2: {
+ title: '复杂校验',
+ description: 'pattern和message的用法',
+ type: 'string',
+ rules: [
+ {
+ pattern: '^[A-Za-z0-9]+$',
+ message: '请输入数字或英文字母',
+ },
+ ],
+ placeholder: '请输入数字或英文',
+ },
+ string3: {
+ title: '长度控制',
+ description: '长度在5-15个字之间',
+ type: 'string',
+ minLength: 5,
+ maxLength: 15,
+ },
+ string4: {
+ title: '前置/后置标签',
+ type: 'string',
+ props: {
+ addonBefore: '长度',
+ addonAfter: 'px',
+ },
+ },
+ string5: {
+ title: '前后缀',
+ type: 'string',
+ rules: [
+ {
+ pattern: '^[0-9]+$',
+ message: '请输入数字',
+ },
+ ],
+ props: {
+ prefix: '¥',
+ suffix: 'RMB',
+ },
+ },
+ string6: {
+ title: '置灰的输入框',
+ type: 'string',
+ disabled: true,
+ default: 'hello world',
+ },
+ string7: {
+ title: '文本框',
+ description: '固定高度',
+ type: 'string',
+ format: 'textarea',
+ props: {
+ row: 4,
+ },
+ },
+ },
+ required: ['string4', 'string5'],
+ },
+ },
+ {
+ title: '结束',
+ type: 'endEvent',
+ description: '表示流程结束节点,可以有多个结束节点',
+ icon: {
+ type: 'icon-end',
+ bgColor: '#F79009',
+ },
+ sourceHandleHidden: true,
+ settingSchema: {
+ type: 'object',
+ className: 'settingSchemaStyle',
+ properties: {
+ nodeDesc: {
+ title: '结束描述',
+ type: 'string',
+ format: 'textarea',
+ placeholder: '根据内容缩放',
+ props: {
+ autoSize: {
+ minRows: 3,
+ maxRows: 5,
+ },
+ },
+ readOnlyWidget: 'ReadOnlyPanel',
+ },
+ },
+ },
+ },
+ ],
+ },
+ {
+ title: '逻辑',
+ type: '_group',
+ items: [
+ {
+ title: '条件分支',
+ type: 'Switch',
+ description: '允许你根据 if/else 条件将 workflow 拆分成两个分支',
+ icon: {
+ type: 'icon-fenzhi',
+ bgColor: '#6172F3',
+ },
+ nodePanel: {
+ width: 550,
+ },
+ hideDesc: true,
+ switchExtra: {
+ hideElse: true,
+ titleKey: 'name',
+ },
+ settingSchema: {
+ type: 'object',
+ className: 'settingSchemaStyle',
+ properties: {
+ list: {
+ // title: '高级属性',
+ type: 'array',
+ widget: 'simpleList',
+ props: {
+ hideCopy: true,
+ hideMove: true,
+ },
+ className: 'switch-list',
+ items: {
+ type: 'object',
+ properties: {
+ name: {
+ title: '条件名称', // 条件描述
+ type: 'string',
+ props: {
+ allowClear: true,
+ },
+ className: 'child-title',
+ readOnlyWidget: 'ReadOnlyPanel',
+ },
+ type: {
+ title: '条件类型',
+ type: 'string',
+ widget: 'select',
+ props: {
+ allowClear: true,
+ },
+ enum: ['类型一'],
+ enumNames: ['类型一'],
+ className: 'child-title',
+ readOnlyWidget: 'ReadOnlyPanel',
+ },
+ value: {
+ title: '条件语句',
+ type: 'string',
+ props: {
+ allowClear: true,
+ },
+ className: 'child-title',
+ readOnlyWidget: 'ReadOnlyPanel',
+ },
+ },
+ },
+ },
+ },
+ },
+ nodeWidget: 'showSwitchNode',
+ },
+ {
+ title: '并行事件',
+ type: 'Parallel',
+ description: '支持多个分支同时执行',
+ icon: {
+ type: 'icon-parallel',
+ bgColor: '#06aed4',
+ },
+ hideDesc: true,
+ nodePanel: {
+ width: 510,
+ },
+ parallelExtra: {
+ titleKey: 'name',
+ },
+ settingSchema: {
+ type: 'object',
+ className: 'settingSchemaStyle',
+ properties: {
+ properties: {
+ title: 'Properties',
+ type: 'array',
+ widget: 'simpleList',
+ props: {
+ hideCopy: true,
+ hideMove: true,
+ },
+ className: 'parallel-wrap',
+ items: {
+ type: 'object',
+ properties: {
+ name: {
+ title: '属性名称',
+ type: 'string',
+ props: {
+ allowClear: true,
+ },
+ className: 'child-title',
+ readOnlyWidget: 'ReadOnlyPanel',
+ },
+ value: {
+ title: 'value',
+ type: 'string',
+ props: {
+ allowClear: true,
+ },
+ className: 'child-title',
+ readOnlyWidget: 'ReadOnlyPanel',
+ },
+ },
+ },
+ },
+ list: {
+ title: '并行事件',
+ type: 'array',
+ widget: 'simpleList',
+ props: {
+ hideCopy: true,
+ hideMove: true,
+ },
+ className: 'parallel-wrap',
+ items: {
+ type: 'object',
+ properties: {
+ name: {
+ title: '事件名称',
+ type: 'string',
+ props: {
+ allowClear: true,
+ },
+ className: 'child-title',
+ readOnlyWidget: 'ReadOnlyPanel',
+ },
+ value: {
+ title: '事件描述',
+ type: 'string',
+ props: {
+ allowClear: true,
+ },
+ className: 'child-title',
+ readOnlyWidget: 'ReadOnlyPanel',
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ ],
+ },
+ { ...activitySchema },
+];
diff --git a/docs/xflow/demo/best/basic/showSwitchNode.tsx b/docs/xflow/demo/best/basic/showSwitchNode.tsx
new file mode 100644
index 000000000..ec32bf22c
--- /dev/null
+++ b/docs/xflow/demo/best/basic/showSwitchNode.tsx
@@ -0,0 +1,30 @@
+import { Space } from 'antd';
+import React from 'react';
+import './index.less';
+import TextEllipsis from './TextEllipsis';
+
+const showSwitchNode = ({ data, index }) => {
+ const { type, value } = data;
+ if (!type && !value) {
+ return ;
+ }
+
+ return (
+
+ {type && (
+
+ )}
+ {value && (
+
+ )}
+
+ );
+};
+
+export default showSwitchNode;
diff --git a/docs/xflow/demo/best/basic/tools.tsx b/docs/xflow/demo/best/basic/tools.tsx
new file mode 100644
index 000000000..d87f8b154
--- /dev/null
+++ b/docs/xflow/demo/best/basic/tools.tsx
@@ -0,0 +1,19 @@
+import { Button, Space } from 'antd';
+import React from 'react'
+
+
+export const Tools = () => {
+ return (
+
+
+
+
+
+ );
+};
diff --git a/packages/x-flow/src/XFlow.tsx b/packages/x-flow/src/XFlow.tsx
index 170559a9f..f07833623 100644
--- a/packages/x-flow/src/XFlow.tsx
+++ b/packages/x-flow/src/XFlow.tsx
@@ -32,6 +32,7 @@ import FlowProps from './types';
import { isTruthy, uuid, uuid4 } from './utils';
import autoLayoutNodes from './utils/autoLayoutNodes';
+import { message } from 'antd';
import { shallow } from 'zustand/shallow';
import NodeEditor from './components/NodeEditor';
import NodeLogPanel from './components/NodeLogPanel';
@@ -83,10 +84,11 @@ const XFlow: FC = memo(props => {
);
const { record } = useTemporalStore();
const [activeNode, setActiveNode] = useState(null);
- const { settingMap, globalConfig,readOnly } = useContext(ConfigContext);
+ const { settingMap, globalConfig, readOnly } = useContext(ConfigContext);
const [openPanel, setOpenPanel] = useState(true);
const [openLogPanel, setOpenLogPanel] = useState(true);
const { onNodeClick } = props;
+ const nodeEditorRef = useRef(null);
useEffect(() => {
zoomTo(0.8);
@@ -131,7 +133,11 @@ const XFlow: FC = memo(props => {
eventEmitter?.useSubscription((v: any) => {
// 整理画布
if (v.type === 'auto-layout-nodes') {
- const newNodes: any = autoLayoutNodes(storeApi.getState().nodes, edges, layout);
+ const newNodes: any = autoLayoutNodes(
+ storeApi.getState().nodes,
+ edges,
+ layout
+ );
setNodes(newNodes, false);
}
@@ -222,7 +228,14 @@ const XFlow: FC = memo(props => {
type={_nodeType}
layout={layout}
status={_status}
- onClick={e => {
+ onClick={async e => {
+ if (nodeEditorRef?.current?.validateForm) {
+ const result = await nodeEditorRef?.current?.validateForm();
+ if (!result) {
+ message.error('请检查必填项!');
+ return;
+ }
+ }
setActiveNode({
id,
_nodeType,
@@ -245,6 +258,7 @@ const XFlow: FC = memo(props => {
onChange={handleNodeValueChange}
nodeType={activeNode?._nodeType}
id={activeNode?.id}
+ ref={nodeEditorRef}
/>
);
}, [activeNode?.id]);
@@ -270,6 +284,19 @@ const XFlow: FC = memo(props => {
const deletable = globalConfig?.edge?.deletable ?? true;
const panelonClose = globalConfig?.nodePanel?.onClose;
+ const getNodesJ = nodes => {
+ const result = nodes.map(item => {
+ const { data, ...rest } = item;
+ const { _nodeType, ...restData } = data;
+ return {
+ ...rest,
+ data: restData,
+ type: _nodeType,
+ };
+ });
+ return result;
+ };
+
return (
= memo(props => {
},
deletable: deletable, //默认连线属性受此项控制
}}
- onBeforeDelete={async()=>{
- if(readOnly){
- return false
+ onBeforeDelete={async () => {
+ if (readOnly) {
+ return false;
}
- return true
+ return true;
}}
onConnect={onConnect}
onNodesChange={changes => {
@@ -344,14 +371,20 @@ const XFlow: FC = memo(props => {
{
+ onClose={async () => {
+ // 面板关闭校验表单
+ const result = await nodeEditorRef?.current?.validateForm();
+ if (!result) {
+ return;
+ }
setOpenPanel(false);
+
// 如果日志面板关闭
if (!isTruthy(activeNode?._status) || !openLogPanel) {
setActiveNode(null);
}
- if(isFunction(panelonClose)){
- panelonClose(activeNode?.id)
+ if (isFunction(panelonClose)) {
+ panelonClose(activeNode?.id);
}
}}
node={activeNode}
diff --git a/packages/x-flow/src/components/NodeEditor/index.tsx b/packages/x-flow/src/components/NodeEditor/index.tsx
index bca9c7d86..fad585027 100644
--- a/packages/x-flow/src/components/NodeEditor/index.tsx
+++ b/packages/x-flow/src/components/NodeEditor/index.tsx
@@ -1,7 +1,14 @@
import FormRender, { Schema, useForm } from 'form-render';
import produce from 'immer';
import { debounce, isFunction } from 'lodash';
-import React, { FC, useContext, useEffect, useState } from 'react';
+import React, {
+ FC,
+ forwardRef,
+ useContext,
+ useEffect,
+ useImperativeHandle,
+ useState,
+} from 'react';
import { shallow } from 'zustand/shallow';
import { useStore } from '../../hooks/useStore';
import { ConfigContext } from '../../models/context';
@@ -14,7 +21,7 @@ interface INodeEditorProps {
id: string;
}
-const NodeEditor: FC = (props: any) => {
+const NodeEditor: FC = forwardRef((props, ref) => {
const { data, onChange, nodeType, id } = props;
const form = useForm();
// // 1.获取节点配置信息
@@ -26,6 +33,26 @@ const NodeEditor: FC = (props: any) => {
const getSettingSchema = nodeSetting['getSettingSchema'];
const [asyncSchema, setAsyncSchema] = useState({});
+ useImperativeHandle(ref, () => ({
+ validateForm: async () => {
+ let result = true;
+ if (
+ nodeSetting?.settingSchema ||
+ (isFunction(getSettingSchema) && Object.keys(asyncSchema).length > 0)
+ ) {
+ result = await form
+ .validateFields()
+ .then(() => {
+ return true;
+ })
+ .catch(err => {
+ return false;
+ });
+ }
+ return result;
+ },
+ }));
+
async function getSchema() {
const shema = await getSettingSchema(
id,
@@ -158,6 +185,6 @@ const NodeEditor: FC = (props: any) => {
} else {
return null;
}
-};
+});
export default NodeEditor;