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..3c8cfe0b2 100644 --- a/packages/x-flow/src/XFlow.tsx +++ b/packages/x-flow/src/XFlow.tsx @@ -83,7 +83,7 @@ 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; @@ -131,7 +131,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); } @@ -270,6 +274,20 @@ 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; + }; + console.log('121212', nodes, getNodesJ(nodes),edges); + 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 => { @@ -350,8 +368,8 @@ const XFlow: FC = memo(props => { if (!isTruthy(activeNode?._status) || !openLogPanel) { setActiveNode(null); } - if(isFunction(panelonClose)){ - panelonClose(activeNode?.id) + if (isFunction(panelonClose)) { + panelonClose(activeNode?.id); } }} node={activeNode}