From b800a39c3ab8cd4a12da9a2aa11d27c18bc6302c Mon Sep 17 00:00:00 2001 From: "zhanbo.lh" Date: Wed, 13 Nov 2024 13:29:11 +0800 Subject: [PATCH 01/38] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0xflow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .dumirc.ts | 1 + docs/xflow/data/basic.ts | 51 ++ docs/xflow/index.md | 53 ++ docs/xflow/schema/basic.ts | 165 ++++++ packages/x-flow/.fatherrc.js | 35 ++ packages/x-flow/CHANGELOG.md | 2 + packages/x-flow/CONTRIBUTING.md | 43 ++ packages/x-flow/LICENSE | 21 + packages/x-flow/README.md | 126 +++++ packages/x-flow/__tests__/core-utils.spec.ts | 28 ++ packages/x-flow/__tests__/demo.tsx | 61 +++ packages/x-flow/__tests__/form-demo.tsx | 112 +++++ .../x-flow/__tests__/form-fields.spec.tsx | 129 +++++ packages/x-flow/__tests__/form.spec.tsx | 28 ++ .../x-flow/__tests__/get-descriptor.spec.ts | 144 ++++++ packages/x-flow/__tests__/schema.ts | 310 ++++++++++++ packages/x-flow/__tests__/utils.spec.ts | 38 ++ packages/x-flow/package.json | 79 +++ .../components/CandidateNode/index.tsx | 83 +++ .../components/CustomEdge/index.less | 34 ++ .../components/CustomEdge/index.tsx | 90 ++++ .../components/CustomHtml/index.tsx | 64 +++ .../components/CustomNode/index.less | 41 ++ .../components/CustomNode/index.tsx | 41 ++ .../FlowEditor/components/CustomNode/utils.ts | 264 ++++++++++ .../components/FAutoComplete/index.tsx | 89 ++++ .../components/FlowDebugDrawer/index.less | 114 +++++ .../components/FlowDebugDrawer/index.tsx | 411 +++++++++++++++ .../components/NodeContainer/index.less | 33 ++ .../components/NodeContainer/index.tsx | 25 + .../components/NodeDebugDrawer/index.less | 114 +++++ .../components/NodeDebugDrawer/index.tsx | 163 ++++++ .../components/NodeSelectPopover/index.less | 49 ++ .../components/NodeSelectPopover/index.tsx | 180 +++++++ .../components/PanelContainer/index.less | 125 +++++ .../components/PanelContainer/index.tsx | 104 ++++ packages/x-flow/src/FlowEditor/constants.ts | 261 ++++++++++ packages/x-flow/src/FlowEditor/context.tsx | 41 ++ packages/x-flow/src/FlowEditor/index.less | 5 + packages/x-flow/src/FlowEditor/index.tsx | 22 + packages/x-flow/src/FlowEditor/main.tsx | 283 +++++++++++ .../FlowEditor/operator/Control/index.less | 48 ++ .../src/FlowEditor/operator/Control/index.tsx | 67 +++ .../FlowEditor/operator/UndoRedo/index.less | 11 + .../FlowEditor/operator/UndoRedo/index.tsx | 31 ++ .../FlowEditor/operator/ZoomInOut/index.less | 35 ++ .../FlowEditor/operator/ZoomInOut/index.tsx | 154 ++++++ .../operator/ZoomInOut/shortcuts-name.tsx | 32 ++ .../x-flow/src/FlowEditor/operator/index.less | 27 + .../x-flow/src/FlowEditor/operator/index.tsx | 30 ++ packages/x-flow/src/FlowEditor/store.ts | 108 ++++ packages/x-flow/src/FlowEditor/types.ts | 5 + .../src/FlowEditor/utils/autoLayoutNodes.ts | 68 +++ packages/x-flow/src/FlowEditor/utils/index.ts | 27 + packages/x-flow/src/index.ts | 23 + packages/x-flow/src/locales/en_US.ts | 27 + packages/x-flow/src/locales/index.ts | 7 + packages/x-flow/src/locales/zh_CN.ts | 27 + packages/x-flow/src/models/bindValues.ts | 175 +++++++ packages/x-flow/src/models/context.ts | 5 + packages/x-flow/src/models/expression.ts | 163 ++++++ .../x-flow/src/models/fieldShouldUpdate.ts | 80 +++ .../x-flow/src/models/filterValuesHidden.ts | 74 +++ .../src/models/filterValuesUndefined.ts | 51 ++ packages/x-flow/src/models/flattenSchema.ts | 87 ++++ packages/x-flow/src/models/formCoreUtils.ts | 188 +++++++ .../x-flow/src/models/formDataSkeleton.ts | 31 ++ packages/x-flow/src/models/layout.ts | 73 +++ packages/x-flow/src/models/mapping.tsx | 134 +++++ packages/x-flow/src/models/sortProperties.ts | 23 + packages/x-flow/src/models/store.ts | 27 + packages/x-flow/src/models/transformProps.ts | 85 ++++ packages/x-flow/src/models/useForm.ts | 360 +++++++++++++ packages/x-flow/src/models/validateMessage.ts | 97 ++++ packages/x-flow/src/models/validates.ts | 138 +++++ packages/x-flow/src/nodes/index.less | 41 ++ packages/x-flow/src/nodes/index.tsx | 2 + .../x-flow/src/nodes/node-input/index.less | 16 + .../x-flow/src/nodes/node-input/index.tsx | 20 + .../src/nodes/node-input/setting/index.tsx | 106 ++++ .../x-flow/src/nodes/node-output/index.less | 16 + .../x-flow/src/nodes/node-output/index.tsx | 22 + .../src/nodes/node-output/setting/index.tsx | 130 +++++ .../x-flow/src/nodes/node-switch/index.less | 16 + .../x-flow/src/nodes/node-switch/index.tsx | 39 ++ .../src/nodes/node-switch/setting/index.tsx | 96 ++++ .../src/nodes/node-switch/setting/schema.ts | 102 ++++ packages/x-flow/src/nodes/utils.ts | 264 ++++++++++ packages/x-flow/src/type.ts | 475 ++++++++++++++++++ packages/x-flow/src/utils/index.ts | 125 +++++ packages/x-flow/src/withProvider.tsx | 85 ++++ packages/x-flow/tsconfig.json | 42 ++ 92 files changed, 8177 insertions(+) create mode 100644 docs/xflow/data/basic.ts create mode 100644 docs/xflow/index.md create mode 100644 docs/xflow/schema/basic.ts create mode 100644 packages/x-flow/.fatherrc.js create mode 100644 packages/x-flow/CHANGELOG.md create mode 100644 packages/x-flow/CONTRIBUTING.md create mode 100644 packages/x-flow/LICENSE create mode 100644 packages/x-flow/README.md create mode 100644 packages/x-flow/__tests__/core-utils.spec.ts create mode 100644 packages/x-flow/__tests__/demo.tsx create mode 100644 packages/x-flow/__tests__/form-demo.tsx create mode 100644 packages/x-flow/__tests__/form-fields.spec.tsx create mode 100644 packages/x-flow/__tests__/form.spec.tsx create mode 100644 packages/x-flow/__tests__/get-descriptor.spec.ts create mode 100644 packages/x-flow/__tests__/schema.ts create mode 100644 packages/x-flow/__tests__/utils.spec.ts create mode 100644 packages/x-flow/package.json create mode 100644 packages/x-flow/src/FlowEditor/components/CandidateNode/index.tsx create mode 100644 packages/x-flow/src/FlowEditor/components/CustomEdge/index.less create mode 100644 packages/x-flow/src/FlowEditor/components/CustomEdge/index.tsx create mode 100644 packages/x-flow/src/FlowEditor/components/CustomHtml/index.tsx create mode 100644 packages/x-flow/src/FlowEditor/components/CustomNode/index.less create mode 100644 packages/x-flow/src/FlowEditor/components/CustomNode/index.tsx create mode 100644 packages/x-flow/src/FlowEditor/components/CustomNode/utils.ts create mode 100644 packages/x-flow/src/FlowEditor/components/FAutoComplete/index.tsx create mode 100644 packages/x-flow/src/FlowEditor/components/FlowDebugDrawer/index.less create mode 100644 packages/x-flow/src/FlowEditor/components/FlowDebugDrawer/index.tsx create mode 100644 packages/x-flow/src/FlowEditor/components/NodeContainer/index.less create mode 100644 packages/x-flow/src/FlowEditor/components/NodeContainer/index.tsx create mode 100644 packages/x-flow/src/FlowEditor/components/NodeDebugDrawer/index.less create mode 100644 packages/x-flow/src/FlowEditor/components/NodeDebugDrawer/index.tsx create mode 100644 packages/x-flow/src/FlowEditor/components/NodeSelectPopover/index.less create mode 100644 packages/x-flow/src/FlowEditor/components/NodeSelectPopover/index.tsx create mode 100644 packages/x-flow/src/FlowEditor/components/PanelContainer/index.less create mode 100644 packages/x-flow/src/FlowEditor/components/PanelContainer/index.tsx create mode 100644 packages/x-flow/src/FlowEditor/constants.ts create mode 100644 packages/x-flow/src/FlowEditor/context.tsx create mode 100644 packages/x-flow/src/FlowEditor/index.less create mode 100644 packages/x-flow/src/FlowEditor/index.tsx create mode 100644 packages/x-flow/src/FlowEditor/main.tsx create mode 100644 packages/x-flow/src/FlowEditor/operator/Control/index.less create mode 100644 packages/x-flow/src/FlowEditor/operator/Control/index.tsx create mode 100644 packages/x-flow/src/FlowEditor/operator/UndoRedo/index.less create mode 100644 packages/x-flow/src/FlowEditor/operator/UndoRedo/index.tsx create mode 100644 packages/x-flow/src/FlowEditor/operator/ZoomInOut/index.less create mode 100644 packages/x-flow/src/FlowEditor/operator/ZoomInOut/index.tsx create mode 100644 packages/x-flow/src/FlowEditor/operator/ZoomInOut/shortcuts-name.tsx create mode 100644 packages/x-flow/src/FlowEditor/operator/index.less create mode 100644 packages/x-flow/src/FlowEditor/operator/index.tsx create mode 100644 packages/x-flow/src/FlowEditor/store.ts create mode 100644 packages/x-flow/src/FlowEditor/types.ts create mode 100644 packages/x-flow/src/FlowEditor/utils/autoLayoutNodes.ts create mode 100644 packages/x-flow/src/FlowEditor/utils/index.ts create mode 100644 packages/x-flow/src/index.ts create mode 100644 packages/x-flow/src/locales/en_US.ts create mode 100644 packages/x-flow/src/locales/index.ts create mode 100644 packages/x-flow/src/locales/zh_CN.ts create mode 100644 packages/x-flow/src/models/bindValues.ts create mode 100644 packages/x-flow/src/models/context.ts create mode 100644 packages/x-flow/src/models/expression.ts create mode 100644 packages/x-flow/src/models/fieldShouldUpdate.ts create mode 100644 packages/x-flow/src/models/filterValuesHidden.ts create mode 100644 packages/x-flow/src/models/filterValuesUndefined.ts create mode 100644 packages/x-flow/src/models/flattenSchema.ts create mode 100644 packages/x-flow/src/models/formCoreUtils.ts create mode 100644 packages/x-flow/src/models/formDataSkeleton.ts create mode 100644 packages/x-flow/src/models/layout.ts create mode 100644 packages/x-flow/src/models/mapping.tsx create mode 100644 packages/x-flow/src/models/sortProperties.ts create mode 100644 packages/x-flow/src/models/store.ts create mode 100644 packages/x-flow/src/models/transformProps.ts create mode 100644 packages/x-flow/src/models/useForm.ts create mode 100644 packages/x-flow/src/models/validateMessage.ts create mode 100644 packages/x-flow/src/models/validates.ts create mode 100644 packages/x-flow/src/nodes/index.less create mode 100644 packages/x-flow/src/nodes/index.tsx create mode 100644 packages/x-flow/src/nodes/node-input/index.less create mode 100644 packages/x-flow/src/nodes/node-input/index.tsx create mode 100644 packages/x-flow/src/nodes/node-input/setting/index.tsx create mode 100644 packages/x-flow/src/nodes/node-output/index.less create mode 100644 packages/x-flow/src/nodes/node-output/index.tsx create mode 100644 packages/x-flow/src/nodes/node-output/setting/index.tsx create mode 100644 packages/x-flow/src/nodes/node-switch/index.less create mode 100644 packages/x-flow/src/nodes/node-switch/index.tsx create mode 100644 packages/x-flow/src/nodes/node-switch/setting/index.tsx create mode 100644 packages/x-flow/src/nodes/node-switch/setting/schema.ts create mode 100644 packages/x-flow/src/nodes/utils.ts create mode 100644 packages/x-flow/src/type.ts create mode 100644 packages/x-flow/src/utils/index.ts create mode 100644 packages/x-flow/src/withProvider.tsx create mode 100644 packages/x-flow/tsconfig.json diff --git a/.dumirc.ts b/.dumirc.ts index b89b92635..d903ccc66 100644 --- a/.dumirc.ts +++ b/.dumirc.ts @@ -76,6 +76,7 @@ export default defineConfig({ 'form-render-mobile': path.resolve(__dirname, 'packages/form-render-mobile/src'), '@xrenders/schema-builder': path.resolve(__dirname, 'tools/schema-builder/src'), '@xrenders/data-render': path.resolve(__dirname, 'packages/data-render/src'), + '@xrenders/x-flow': path.resolve(__dirname, 'packages/x-flow/src'), }, codeSplitting: { jsStrategy: 'granularChunks' }, //...(process.env.NODE_ENV === 'development' ? {} : { ssr: {} }), diff --git a/docs/xflow/data/basic.ts b/docs/xflow/data/basic.ts new file mode 100644 index 000000000..e4e869ffc --- /dev/null +++ b/docs/xflow/data/basic.ts @@ -0,0 +1,51 @@ +export default { + "creator": "清风徐来", + "relevanceCode": "421421", + "desc": "浙江省杭州市工专路", + "create-time": "2019-10-10", + "effective-date": "2019-10-10 ~ 2020-10-31", + "safety": { + "name": "Test demo 001", + "app": "中后台详情页面", + "mode": "代码包", + "yum": "592342323904823489", + "fore": "23" + }, + "operLog": [{ + "type": "创建测试", + "creator": "清风徐来", + "time": "2019-10-30 12:23:45", + "result": "1", + "desc": "这是备注" + }, { + "type": "创建测试", + "creator": "清风徐来", + "time": "2019-10-30 12:23:45", + "result": "1", + "desc": "这是备注" + }, { + "type": "创建测试", + "creator": "清风徐来", + "time": "2019-10-30 12:23:45", + "result": "1", + "desc": "这是备注" + }, { + "type": "创建测试", + "creator": "清风徐来", + "time": "2019-10-30 12:23:45", + "result": "1", + "desc": "这是备注" + }, { + "type": "创建测试", + "creator": "清风徐来", + "time": "2019-10-30 12:23:45", + "result": "1", + "desc": "这是备注" + }, { + "type": "创建测试", + "creator": "清风徐来", + "time": "2019-10-30 12:23:45", + "result": "1", + "desc": "这是备注" + }] +} \ No newline at end of file diff --git a/docs/xflow/index.md b/docs/xflow/index.md new file mode 100644 index 000000000..6fc00c8c2 --- /dev/null +++ b/docs/xflow/index.md @@ -0,0 +1,53 @@ +--- +order: 0 +title: 开始使用 +mobile: false +--- + +
+ logo + DataView +
+

+ + npm + + + NPM downloads + + + NPM all downloads + + + PRs Welcome + +

+ +中后台详情页解决方案,通过 schema 协议渲染页面 + + +## 安装 +```shell +npm i @xrenders/data-render --save +``` + +## 使用方式 + +**函数组件** + +```jsx +/** + * transform: true + * defaultShowCode: true + */ +import React from 'react'; +import DataView from '@xrenders/x-flow'; +import schema from './schema/basic'; +import data from './data/basic'; + +export default () => { + return ( + + ); +} +``` \ No newline at end of file diff --git a/docs/xflow/schema/basic.ts b/docs/xflow/schema/basic.ts new file mode 100644 index 000000000..fd9be2c96 --- /dev/null +++ b/docs/xflow/schema/basic.ts @@ -0,0 +1,165 @@ +export default [ + { + "widget": "FPanel", + "style": { + "paddingTop": "20px", + "paddingLeft": "20px", + "paddingBottom": "20px", + "paddingRight": "20px", + "backgroundColor": "#ffffff", + "marginBottom": "12px" + }, + "children": [ + { + "widget": "FTitle", + "data": "基础信息" + }, + { + "widget": "FDescriptions", + "column": 3, + "items": [ + { + "label": "创建人", + "dataKey": "creator" + }, + { + "label": "关联单据", + "dataKey": "relevanceCode" + }, + { + "label": "单据备注", + "dataKey": "desc" + }, + { + "label": "创建时间", + "dataKey": "create-time" + }, + { + "label": "生效日期", + "dataKey": "effective-date" + }, + { + "label": "描述项", + "showLevel": 1 + } + ], + "style": { + "backgroundColor": "#ffffff", + "paddingTop": "0px", + "paddingLeft": "0px", + "paddingRight": "0px", + "paddingBottom": "0px" + }, + "itemShowLevel": 1, + "getCompProps": "xxxx" + } + ] + }, + { + "widget": "FTabs", + "items": [ + { + "label": "负载均衡(SLB)", + "children": [ + { + "widget": "FPanel", + "style": { + "paddingTop": "20px", + "paddingLeft": "20px", + "paddingBottom": "20px", + "paddingRight": "20px", + "backgroundColor": "#ffffff", + "marginBottom": "12px" + }, + "children": [ + { + "widget": "FTitle", + "data": "安全信息" + }, + { + "widget": "FDescriptions", + "column": 2, + "items": [ + { + "label": "安全构建名称", + "dataKey": "name" + }, + { + "label": "所属应用", + "dataKey": "app" + }, + { + "label": "构建模式", + "dataKey": "mode" + }, + { + "label": "公网域名", + "dataKey": "yum" + }, + { + "label": "保留计算实例", + "dataKey": "fore" + } + ], + "dataKey": "safety" + } + ] + }, + { + "widget": "FPanel", + "style": { + "paddingTop": "20px", + "paddingLeft": "20px", + "paddingBottom": "20px", + "paddingRight": "20px", + "backgroundColor": "#ffffff", + "marginBottom": "12px" + }, + "children": [ + { + "widget": "FTitle", + "data": "操作日志" + }, + { + "widget": "FTable", + "pagination": { + "pageSize": "3" + }, + "style": { + "backgroundColor": "#ffffff" + }, + "dataKey": "operLog", + "column": { + "type": { + "title": "操作类型", + "dataKey": "type" + }, + "creator": { + "title": "操作人", + "dataKey": "creator" + }, + "time": { + "title": "操作时间", + "dataKey": "time" + }, + "result": { + "title": "执行结果", + "dataKey": "result" + }, + "desc": { + "title": "备注", + "dataKey": "desc" + } + } + } + ] + } + ] + }, + { + "label": "云服务器(ECS)", + "children": [] + } + ] + } +] \ No newline at end of file diff --git a/packages/x-flow/.fatherrc.js b/packages/x-flow/.fatherrc.js new file mode 100644 index 000000000..4d442af1f --- /dev/null +++ b/packages/x-flow/.fatherrc.js @@ -0,0 +1,35 @@ +import copy from 'rollup-plugin-copy'; + +export default { + cjs: 'babel', + esm: { + type: 'babel', + importLibToEs: true, + }, + lessInBabelMode: true, + extraRollupPlugins: [ + copy({ + targets: [{ src: 'src/index.d.ts', dest: 'dist/' }], + }), + ], + extraBabelPlugins: [ + [ + 'import', + { + libraryName: 'antd', + libraryDirectory: 'es', + style: true, + }, + 'antd', + ], + [ + 'import', + { + libraryName: '@ant-design/icons', + libraryDirectory: 'lib/icons', + camel2DashComponentName: false, + }, + '@ant-design/icons', + ], + ], +}; diff --git a/packages/x-flow/CHANGELOG.md b/packages/x-flow/CHANGELOG.md new file mode 100644 index 000000000..a8635cefa --- /dev/null +++ b/packages/x-flow/CHANGELOG.md @@ -0,0 +1,2 @@ +# 更新日志 + diff --git a/packages/x-flow/CONTRIBUTING.md b/packages/x-flow/CONTRIBUTING.md new file mode 100644 index 000000000..b44c74a66 --- /dev/null +++ b/packages/x-flow/CONTRIBUTING.md @@ -0,0 +1,43 @@ +# 如何贡献代码 + +欢迎给 FormRender 提优化建议,或者修复已有 Bug,共促其发展 + +## Branch 管理 + +``` +master + ↑ +dev <--- Develop/PR +``` + +- `dev` 分支 + - 所有的开发均在 dev 分支进行 + - 提 PR 时候请提交到 dev 分支 +- `master` 分支 + - `master` 是稳定不改的分支,不会在上面进行代码开发 + - 在 dev 分支 publish 后会 merge 到 master,同时打对应 tag + +## Commit 格式 + +``` +[{action}] {description} +``` + +- `{action}` + - `+` 新增功能 + - `!` 更新或者修复 bug + - `-` 移除功能 +- `{description}` + - 尽可能详细的描述就好 + +for example: + +- [+] 列表选项新增拖拽功能 +- [!] 修复输入框长按闪烁的问题 + +## 更多 + +- 很推荐在提交 PR 前,先在钉钉群里进行讨论,已防止此功能已经有同学在开发了 +- 但是如果是想修复文档和明显代码错误,直接提交 PR 就好 + + diff --git a/packages/x-flow/LICENSE b/packages/x-flow/LICENSE new file mode 100644 index 000000000..a68fd737b --- /dev/null +++ b/packages/x-flow/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019-present XRender Team + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/x-flow/README.md b/packages/x-flow/README.md new file mode 100644 index 000000000..c8988944a --- /dev/null +++ b/packages/x-flow/README.md @@ -0,0 +1,126 @@ +
+ logo +

FormRender

+
+

+ + npm + + + NPM downloads + + + NPM all downloads + + + PRs Welcome + +

+ +> 一站式中后台**表单解决方案** + +## 官网 + + + +FormRender 是中后台开箱即用的表单解决方案,通过 JsonSchema 协议动态渲染表单。为了能切实承接日益复杂的表单场景需求,2.0 我们进行了底层重构。我们的目标是以强大的扩展能力对表单场景 100% 的覆盖支持,同时保持开发者能快速上手,并以表单编辑器、插件、自定义组件等一系列周边产品带来极致的开发体验。在开发 1.0 的道路上,我们做了一系列的取舍,详见[v2 升级方案](https://xrender.fun/form-render/migrate) + +## 安装 + +FormRender 依赖 ant design,单独使用不要忘记同时安装 `antd` + +```shell +npm i form-render --save +``` + +## 使用 + +**最简使用 demo:** + +```jsx +import React from 'react'; +import { Button } from 'antd'; +import FormRender, { connectForm } from 'form-render'; + +const schema = { + type: 'object', + properties: { + input1: { + title: '简单输入框', + type: 'string', + required: true, + }, + select1: { + title: '单选', + type: 'string', + props: { + options: [ + { label: '早', value: 'a' }, + { label: '中', value: 'b' }, + { label: '晚', value: 'c' } + ] + } + }, + }, +}; + +class Demo extends React.Component { + render() { + const { form } = this.props; + return ( +
+ + +
+ ); + } +} + +export default connectForm(Demo); +``` + +**对于函数组件,FormRender 提供了 `useForm` hooks, 书写更为灵活** + +```jsx +import React from 'react'; +import { Button } from 'antd'; +import FormRender, { useForm } from 'form-render'; + +const schema = { + type: 'object', + properties: { + input1: { + title: '简单输入框', + type: 'string', + required: true, + }, + select1: { + title: '单选', + type: 'string', + props: { + options: [ + { label: '早', value: 'a' }, + { label: '中', value: 'b' }, + { label: '晚', value: 'c' } + ] + } + } + } +}; + +const Demo = () => { + const form = useForm(); + return ( +
+ + +
+ ); +}; + +export default Demo; +``` \ No newline at end of file diff --git a/packages/x-flow/__tests__/core-utils.spec.ts b/packages/x-flow/__tests__/core-utils.spec.ts new file mode 100644 index 000000000..e9a04621f --- /dev/null +++ b/packages/x-flow/__tests__/core-utils.spec.ts @@ -0,0 +1,28 @@ +import { describe, it, expect } from 'vitest'; +import { flattenSchema } from '../src/form-render-core/src/utils'; + +describe('Test FormRender Utils', () => { + it('Test flattenSchema', () => { + const schema = { + type: 'object', + properties: { + input1: { + title: '简单输入框', + type: 'string', + order: 2, + required: true, + }, + select1: { + title: '单选', + type: 'string', + order: 1, + enum: ['a', 'b', 'c'], + enumNames: ['早', '中', '晚'], + }, + }, + }; + + const _schema = flattenSchema(schema); + expect(Object.keys(_schema)).toEqual(['select1', 'input1', '#']); + }); +}); diff --git a/packages/x-flow/__tests__/demo.tsx b/packages/x-flow/__tests__/demo.tsx new file mode 100644 index 000000000..eef173d2f --- /dev/null +++ b/packages/x-flow/__tests__/demo.tsx @@ -0,0 +1,61 @@ +import * as React from 'react'; +import { useState } from 'react'; +import FormRender, { useForm } from '../src/index'; +import { normalSchema } from './schema'; + +const SimpleForm = () => { + const form = useForm(); + const [state, setState] = useState({ + input1: 'fr', + select1: 'd', + }); + const onFinish = (formData, errors) => { + setState(formData); + }; + + const watch = { + // # 为全局 + '#': val => { + console.log('表单的实时数据为:', val); + }, + input1: { + handler: val => { + console.log(val); + }, + immediate: true, + }, + onSearch: val => {}, + }; + + const onMount = () => { + form.setValueByPath('link', 'www.baidu.com'); + }; + + const onClick = () => { + form.setValueByPath('link', 'www.baidu.com'); + }; + + return ( +
+ +
+
{state?.input1}
+
{state?.select1}
+
+ + +
+ ); +}; + +export default SimpleForm; diff --git a/packages/x-flow/__tests__/form-demo.tsx b/packages/x-flow/__tests__/form-demo.tsx new file mode 100644 index 000000000..4d368dc7b --- /dev/null +++ b/packages/x-flow/__tests__/form-demo.tsx @@ -0,0 +1,112 @@ +import * as React from 'react'; +import { useState } from 'react'; +import FormRender, { useForm } from '../src/index'; + +const schema = { + type: 'object', + properties: { + input1: { + type: 'object', + properties: { + test: { + title: '简单输入框', + type: 'string', + }, + }, + }, + select1: { + title: '单选', + type: 'string', + enum: ['a', 'b', 'c'], + enumNames: ['早', '中', '晚'], + }, + }, +}; + +const SimpleForm = () => { + const form = useForm(); + const [state, setState] = useState(); + const setRes = data => { + let res; + if (typeof data === 'object') { + res = JSON.stringify(data); + } + res = (res || data) + ''; + setState(res); + }; + + const handleIsFieldTouched = () => { + const res = form.isFieldTouched('input1.test'); + setRes(res); + }; + + const handleIsFieldsTouched = () => { + const res = form.isFieldsTouched(['input1.test', 'select1'], true); + setRes(res); + }; + + const handleIsFieldValidating = () => { + const res = form.isFieldValidating('select1'); + setRes(res); + }; + + const handleSetFields = () => { + form.setFields([ + { + name: 'input1.test', + touched: true, + error: ['set input1.test error'], + value: 'input1.test value', + }, + { + name: 'select1', + validating: true, + value: 'select1 value', + }, + ]); + }; + + const handleGetFieldError = () => { + const res = form.getFieldError('input1.test'); + setRes(res); + }; + + const handleValidateFields = () => { + form.validateFields().then(data => { + // data: + // { + // input1: { + // test: 'input1.test value', + // }, + // select1: 'select1 value, + // } + setRes(data); + }); + }; + + const handleGetValues = () => { + const res = form.getValues(['input1.test', 'select1'], ({ touched }) => { + return touched; + }); + setRes(res); + }; + + return ( +
+ +
+
+
+
+
+
+
+
{state}
+
+ ); +}; + +export default SimpleForm; diff --git a/packages/x-flow/__tests__/form-fields.spec.tsx b/packages/x-flow/__tests__/form-fields.spec.tsx new file mode 100644 index 000000000..03fc4448d --- /dev/null +++ b/packages/x-flow/__tests__/form-fields.spec.tsx @@ -0,0 +1,129 @@ +import { describe, test, afterAll, expect } from 'vitest'; +import * as React from 'react'; +import '@testing-library/jest-dom'; +import { render, act, cleanup } from '@testing-library/react'; +import Demo from './form-demo'; + +function sleep(ms): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +afterAll(cleanup); + +describe('FormRender API', () => { + test('📦 api test setFields and getFieldError success', async () => { + const { getByTestId, unmount } = render(); + // 测试 setFields + getFieldError + act(() => { + getByTestId('setFields').click(); + }); + await act(() => sleep(500)); + act(() => { + getByTestId('getFieldError').click(); + }); + await act(() => sleep(500)); + expect(getByTestId('result')).toHaveTextContent( + JSON.stringify(['set input1.test error']) + ); + act(() => { + unmount(); + }); + }); + test('📦 api test validateFields success', async () => { + const { getByTestId, unmount } = render(); + act(() => { + getByTestId('setFields').click(); + }); + await act(() => sleep(500)); + act(() => { + getByTestId('validateFields').click(); + }); + await act(() => sleep(500)); + expect(getByTestId('result')).toHaveTextContent( + JSON.stringify({ + input1: { + test: 'input1.test value', + }, + select1: 'select1 value', + }) + ); + act(() => { + unmount(); + }); + }); + test('📦 api test isFieldValidating success', async () => { + const { getByTestId, unmount } = render(); + // 测试 isFieldValidating + act(() => { + getByTestId('setFields').click(); + }); + await act(() => sleep(500)); + act(() => { + getByTestId('fieldValidating').click(); + }); + await act(() => sleep(500)); + expect(getByTestId('result')).toHaveTextContent('true'); + act(() => { + unmount(); + }); + }); + + test('📦 api test isFieldTouched success', async () => { + const { getByTestId, unmount } = render(); + // 测试 isFieldTouched + act(() => { + getByTestId('setFields').click(); + }); + await act(() => sleep(500)); + act(() => { + getByTestId('fieldTouched').click(); + }); + await act(() => sleep(500)); + expect(getByTestId('result')).toHaveTextContent('true'); + act(() => { + unmount(); + }); + }); + + test('📦 api test isFieldsTouched success', async () => { + const { getByTestId, unmount } = render(); + act(() => { + getByTestId('setFields').click(); + }); + await act(() => sleep(500)); + // 测试 isFieldsTouched + act(() => { + getByTestId('fieldsTouched').click(); + }); + await act(() => sleep(500)); + expect(getByTestId('result')).toHaveTextContent('false'); + act(() => { + unmount(); + }); + }); + + test('📦 api test getValues success', async () => { + const { getByTestId, unmount } = render(); + + // 测试 getValues + act(() => { + getByTestId('setFields').click(); + }); + await act(() => sleep(500)); + // 测试 getValues + act(() => { + getByTestId('getValues').click(); + }); + await act(() => sleep(500)); + expect(getByTestId('result')).toHaveTextContent( + JSON.stringify({ + input1: { + test: 'input1.test value', + }, + }) + ); + act(() => { + unmount(); + }); + }); +}); diff --git a/packages/x-flow/__tests__/form.spec.tsx b/packages/x-flow/__tests__/form.spec.tsx new file mode 100644 index 000000000..b81a1e34f --- /dev/null +++ b/packages/x-flow/__tests__/form.spec.tsx @@ -0,0 +1,28 @@ +import { describe, it, afterAll, expect } from 'vitest'; +import * as React from 'react'; +import { render, act, cleanup } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import Demo from './demo'; + +function sleep(ms): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +afterAll(cleanup); + +describe('FormRender', () => { + it('📦 Render FR Success', async () => { + const { getByTestId, unmount } = render(); + act(() => { + getByTestId('submit').click(); + getByTestId('test').click(); + }); + await act(() => sleep(500)); + expect(getByTestId('input')).toHaveTextContent('简单输入框'); + expect(getByTestId('select')).toHaveTextContent('a'); + + act(() => { + unmount(); + }); + }); +}); diff --git a/packages/x-flow/__tests__/get-descriptor.spec.ts b/packages/x-flow/__tests__/get-descriptor.spec.ts new file mode 100644 index 000000000..0055c96b3 --- /dev/null +++ b/packages/x-flow/__tests__/get-descriptor.spec.ts @@ -0,0 +1,144 @@ +import { describe, expect, it } from 'vitest'; +import { getDescriptorSimple } from '../src/form-render-core/src/getDescriptorSimple'; + +// 主要对转换格式进行单元测试 +describe('get-descriptor utils', () => { + it('transform test 1', () => { + const schema = { + title: '代号', + type: 'string', + required: true, + rules: [{ pattern: '^[a-z]+$', message: 'incorrect province' }], + }; + + const res = getDescriptorSimple(schema, 'count'); + const expectData = { + count: [ + { required: true, type: 'string' }, + { pattern: /^[a-z]+$/, message: 'incorrect province' }, + ], + }; + + expect(res).toEqual(expectData); + }); + + it('transform test 2', () => { + const schema = { + title: '代号', + type: 'string', + rules: [ + { required: true, message: '必填' }, + { pattern: '^[a-z]+$', message: 'incorrect province' }, + ], + }; + + const res = getDescriptorSimple(schema, 'count'); + const expectData = { + count: [ + { required: true, message: '必填' }, + { pattern: /^[a-z]+$/, message: 'incorrect province' }, + { type: 'string' }, + ], + }; + + expect(res).toEqual(expectData); + }); + + it('transform test 3', () => { + const schema = { + title: '代号', + type: 'string', + rules: [{ pattern: '^[a-z]+$', message: 'incorrect province' }], + }; + + const res = getDescriptorSimple(schema, 'count'); + + const expectData = { + count: [ + { type: 'string' }, + { pattern: /^[a-z]+$/, message: 'incorrect province' }, + ], + }; + + expect(res).toEqual(expectData); + }); + + it('transform test 4', () => { + const schema = { + title: '代号', + type: 'string', + required: true, + }; + + const res = getDescriptorSimple(schema, 'count'); + + const expectData = { + count: [{ type: 'string', required: true }], + }; + + expect(res).toEqual(expectData); + }); + + it('transform test 5', () => { + const schema = { + title: '代号', + type: 'string', + }; + + const res = getDescriptorSimple(schema, 'count'); + + const expectData = { + count: [{ type: 'string' }], + }; + + expect(res).toEqual(expectData); + }); + + it('transform test 6', () => { + const schema = { + title: '时间选择', + type: 'string', + widget: 'site', + format: 'time', + required: true, + }; + + const res = getDescriptorSimple(schema, 'time'); + + const expectData = + '{"time":[{"required":true},{"type":"string","message":"${title}的格式错误"}]}'; + + expect(JSON.stringify(res)).toEqual(expectData); + }); + + it('transform test 7', () => { + const schema = { + title: '时间选择', + type: 'string', + widget: 'site', + format: 'time', + }; + + const res = getDescriptorSimple(schema, 'time'); + const expectData = + '{"time":[{"type":"string","message":"${title}的格式错误"}]}'; + + expect(JSON.stringify(res)).toEqual(expectData); + }); + + it('transform test 9', () => { + const schema = { + title: '时间选择', + type: 'string', + widget: 'site', + format: 'time', + required: true, + rules: [{ pattern: '^[a-z]+$', message: 'incorrect province' }], + }; + + const res = getDescriptorSimple(schema, 'count'); + const expectData = + '{"count":[{"required":true},{"type":"string","message":"${title}的格式错误"},{"pattern":{},"message":"incorrect province"}]}'; + expect(JSON.stringify(res)).toEqual(expectData); + }); +}); diff --git a/packages/x-flow/__tests__/schema.ts b/packages/x-flow/__tests__/schema.ts new file mode 100644 index 000000000..33e454d24 --- /dev/null +++ b/packages/x-flow/__tests__/schema.ts @@ -0,0 +1,310 @@ +export const normalSchema = { + type: 'object', + properties: { + input1: { + title: '简单输入框', + type: 'string', + required: true, + default: '简单输入框', + placeholder: '尝试在此输入', + className: 'input-with-px', + props: { + addonAfter: 'px', + }, + }, + numberDemo: { + title: '数字', + description: '数字输入框', + type: 'number', + min: 10, + max: 100, + step: 10, + }, + textareaDemo: { + title: '输入框', + type: 'string', + widget: 'textarea', + default: 'FormRender\nHello World!', + required: true, + }, + imgDemo: { + title: '图片', + type: 'string', + format: 'image', + default: + 'https://img.alicdn.com/tfs/TB1P8p2uQyWBuNjy0FpXXassXXa-750-1334.png', + }, + uploadDemo: { + title: '文件上传', + type: 'string', + default: + 'https://img.alicdn.com/tfs/TB1P8p2uQyWBuNjy0FpXXassXXa-750-1334.png', + widget: 'upload', + props: { + action: 'https://www.mocky.io/v2/5cc8019d300000980a055e76', + }, + }, + disabledDemo: { + title: '不可用', + type: 'string', + default: '我是一个被 disabled 的值', + disabled: true, + }, + select: { + title: '单选', + type: 'string', + enum: ['a', 'b', 'c'], + enumNames: ['', '中', '晚'], + default: 'a', + props: { + showSearch: true, + onSearch: 'onSearch', + }, + }, + select1: { + title: '单选', + type: 'string', + default: 'a', + props: { + options: [ + { label: '早', value: 'a' }, + { label: '晚', value: 'b' }, + ], + }, + }, + select2: { + title: '复选', + type: 'array', + enum: ['a', 'b', 'c'], + enumNames: ['早', '中', '晚'], + widgets: 'checkboxes', + default: 'a', + }, + select3: { + title: '多选', + type: 'array', + enum: ['a', 'b', 'c', 'd', 'e', 'f', 'g'], + enumNames: ['早', '中', '晚', 'd', 'e', 'f', 'g'], + default: 'a', + }, + radio: { + title: '单选radio', + type: 'string', + enum: ['a', 'b'], + enumNames: ['早', '中'], + default: 'a', + }, + fontSize: { + title: '字体大小', + readOnly: false, + required: false, + type: 'number', + widget: 'slider', + default: 24, + min: 12, + max: 64, + }, + width: { + title: '宽度', + readOnly: false, + required: false, + type: 'number', + widget: 'slider', + default: 280, + min: 100, + max: 560, + props: { + hideInput: true, + }, + }, + time: { + title: '时间', + type: 'string', + format: 'time', + }, + time2: { + title: '时间范围', + type: 'range', + format: 'time', + }, + link: { + title: '链接', + type: 'string', + format: 'url', + props: { + prefix: 'https://', + suffix: '.com', + }, + }, + dateDemo: { + title: '时间', + format: 'dateTime', + type: 'string', + widget: 'date', + width: '50%', + default: '2018-11-22', + required: true, + }, + dateRange: { + title: '时间范围', + format: 'dateTime', + type: 'range', + width: '50%', + }, + objDemo: { + title: '单个对象', + description: '这是一个对象类型', + type: 'object', + properties: { + isLike: { + title: '是否显示颜色选择', + type: 'boolean', + default: true, + }, + background: { + title: '颜色选择', + description: '特殊面板', + format: 'color', + type: 'string', + hidden: '{{rootValue.isLike === false}}', + default: '#ffff00', + }, + wayToTravel: { + title: '旅行方式', + type: 'string', + enum: ['self', 'group'], + enumNames: ['自驾', '跟团'], + widget: 'radio', + }, + canDrive: { + title: '是否拥有驾照', + type: 'boolean', + default: false, + hidden: "{{rootValue.wayToTravel !== 'self'}}", + }, + }, + required: ['background'], + }, + html1: { + title: '纯字符串', + type: 'html', + default: 'hello world', + }, + list: { + title: 'list', + type: 'array', + }, + objectName: { + type: 'object', + description: '这是一个对象类型', + collapsed: false, + properties: { + input1: { + title: '简单输入框', + type: 'string', + required: true, + }, + }, + }, + }, +}; + +export const listSchema = { + type: 'object', + properties: { + listName2: { + title: '对象数组', + description: '对象数组嵌套功能', + type: 'array', + // widget: 'cardList', + items: { + type: 'object', + properties: { + input1: { + title: '简单输入框', + type: 'string', + required: true, + }, + select1: { + title: '单选', + type: 'string', + enum: ['a', 'b', 'c'], + enumNames: ['早', '中', '晚'], + default: 'a', + }, + obj: { + title: '对象', + type: 'object', + properties: { + input1: { + title: '简单输入框', + type: 'string', + required: true, + default: '卡片列表', + }, + select1: { + title: '单选', + type: 'string', + enum: ['a', 'b', 'c'], + enumNames: ['早', '中', '晚'], + }, + }, + }, + }, + }, + }, + listName3: { + title: '对象数组', + description: '对象数组嵌套功能', + type: 'array', + widget: 'simpleList', + items: { + type: 'object', + properties: { + input1: { + title: '简单输入框', + type: 'string', + required: true, + }, + select1: { + title: '单选', + type: 'string', + enum: ['a', 'b', 'c'], + enumNames: ['早', '中', '晚'], + }, + }, + }, + }, + listName4: { + title: '对象数组', + description: '对象数组嵌套功能', + type: 'array', + widget: 'tableList', + items: { + type: 'object', + properties: { + input1: { + title: '简单输入框', + type: 'string', + required: true, + }, + input2: { + title: '简单输入框2', + type: 'string', + }, + input3: { + title: '简单输入框3', + type: 'string', + }, + select1: { + title: '单选', + type: 'string', + enum: ['a', 'b', 'c'], + enumNames: ['早', '中', '晚'], + widget: 'select', + }, + }, + }, + }, + }, +}; diff --git a/packages/x-flow/__tests__/utils.spec.ts b/packages/x-flow/__tests__/utils.spec.ts new file mode 100644 index 000000000..10d3577c2 --- /dev/null +++ b/packages/x-flow/__tests__/utils.spec.ts @@ -0,0 +1,38 @@ +import { describe, test, expect } from 'vitest'; +import { getWidgetName } from '../src/form-render-core/src/mapping'; +import { getArray, getFormat, isUrl } from '../src/utils'; + +describe('Test Utils', () => { + test('Test getFormat', () => { + expect(getFormat('date')).toBe('YYYY-MM-DD'); + expect(getFormat('time')).toBe('HH:mm:ss'); + expect(getFormat('dateTime')).toBe('YYYY-MM-DD HH:mm:ss'); + expect(getFormat('week')).toBe('YYYY-w'); + expect(getFormat('year')).toBe('YYYY'); + expect(getFormat('quarter')).toBe('YYYY-Q'); + expect(getFormat('month')).toBe('YYYY-MM'); + expect(getFormat('YYYY-MM-DD')).toBe('YYYY-MM-DD'); + expect(getFormat(123)).toBe('YYYY-MM-DD'); + }); + + test('Test isUrl', () => { + expect(isUrl('https://github.com/alibaba/x-render')).toBe(true); + expect(isUrl('http://github.com/alibaba/x-render')).toBe(true); + expect(isUrl('github.com/alibaba/x-render')).toBe(false); + expect(isUrl(123)).toBe(false); + }); + + test('Test getArray', () => { + expect(getArray(['hangzhou', 'nanjing'])).toEqual(['hangzhou', 'nanjing']); + expect(getArray('test')).toEqual([]); + }); + + test('Test getWidgetName', () => { + expect( + getWidgetName({ + type: 'string', + format: 'date', + }) + ).toEqual('date'); + }); +}); diff --git a/packages/x-flow/package.json b/packages/x-flow/package.json new file mode 100644 index 000000000..5629f7cc5 --- /dev/null +++ b/packages/x-flow/package.json @@ -0,0 +1,79 @@ +{ + "name": "@xrenders/xflow", + "version": "0.0.1", + "description": "通过 JSON Schema 生成标准 Form,常用于自定义搭建配置界面生成", + "keywords": [ + "Form", + "FormRender", + "Render", + "XRender", + "React", + "Json Schema", + "Ant Design" + ], + "homepage": "https://xrender.fun/form-render", + "bugs": { + "url": "https://github.com/alibaba/x-render/issues" + }, + "repository": { + "type": "git", + "url": "git@github.com:alibaba/form-render.git" + }, + "license": "MIT", + "contributors": [ + { + "name": "lhbxs", + "email": "596850703@qq.com" + } + ], + "main": "lib/index.js", + "module": "es/index.js", + "files": [ + "es", + "lib", + "package.json" + ], + "scripts": { + "beta": "npm publish --tag beta", + "build": "father-build", + "next": "npm publish --tag next", + "prepare": "npm run build", + "prettier": "prettier --write \"**/*.{js,jsx,tsx,ts,less,md,json}\"", + "postpublish": "git push --tags", + "test:ui": "vitest --ui" + }, + "lint-staged": { + "*.{js,jsx,less,md,json}": [ + "prettier --write" + ], + "*.ts?(x)": [ + "prettier --parser=typescript --write" + ] + }, + "dependencies": { + "@ant-design/icons": "^4.0.2", + "async-validator": "^3.5.1", + "classnames": "^2.3.1", + "color": "^3.1.2", + "lodash-es": "^4.17.21", + "dayjs": "^1.11.7", + "rc-color-picker": "^1.2.6", + "virtualizedtableforantd4": "^1.1.2", + "ahooks": "^3.7.5", + "zustand": "^4.5.4", + "@xyflow/react": "^12.3.2" + }, + "devDependencies": { + "deep-equal": "^2.0.3", + "rollup-plugin-copy": "^3.4.0" + }, + "peerDependencies": { + "antd": "4.x || 5.x", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + }, + "gitHooks": { + "pre-commit": "lint-staged" + }, + "sideEffect": false +} \ No newline at end of file diff --git a/packages/x-flow/src/FlowEditor/components/CandidateNode/index.tsx b/packages/x-flow/src/FlowEditor/components/CandidateNode/index.tsx new file mode 100644 index 000000000..28e2f506e --- /dev/null +++ b/packages/x-flow/src/FlowEditor/components/CandidateNode/index.tsx @@ -0,0 +1,83 @@ +import { memo } from 'react'; +import produce from 'immer'; +import { useShallow } from 'zustand/react/shallow'; +import { useReactFlow, useViewport } from '@xyflow/react'; +import { useEventListener } from 'ahooks'; +import CustomNode from '../../../nodes'; +import useStore from '../../store'; + +const CandidateNode = () => { + const reactflow = useReactFlow(); + const { zoom } = useViewport(); + + const { + nodes, + setNodes, + candidateNode, + mousePosition, + setCandidateNode + } = useStore( + useShallow((state: any) => ({ + nodes: state.nodes, + edges: state.edges, + candidateNode: state.candidateNode, + mousePosition: state.mousePosition, + setNodes: state.setNodes, + setEdges: state.setEdges, + setCandidateNode: state.setCandidateNode, + onNodesChange: state.onNodesChange, + onEdgesChange: state.onEdgesChange, + })) + ); + + useEventListener('click', (ev) => { + if (!candidateNode) { + return; + } + ev.preventDefault(); + const { screenToFlowPosition } = reactflow; + const { x, y } = screenToFlowPosition({ x: mousePosition.pageX, y: mousePosition.pageY }); + + const newNodes = produce(nodes, (draft: any) => { + draft.push({ + ...candidateNode, + data: { + ...candidateNode.data, + _isCandidate: false, + }, + position: { x, y } + }); + }); + setNodes(newNodes); + setCandidateNode(null); + }); + + useEventListener('contextmenu', (e) => { + // const { candidateNode } = workflowStore.getState() + // if (candidateNode) { + // e.preventDefault() + // workflowStore.setState({ candidateNode: undefined }) + // } + }) + + if (!candidateNode) { + return null + } + + return ( +
+ +
+ ); +} + +export default memo(CandidateNode); diff --git a/packages/x-flow/src/FlowEditor/components/CustomEdge/index.less b/packages/x-flow/src/FlowEditor/components/CustomEdge/index.less new file mode 100644 index 000000000..1920d5ccd --- /dev/null +++ b/packages/x-flow/src/FlowEditor/components/CustomEdge/index.less @@ -0,0 +1,34 @@ + +.custom-edge-line { + position: absolute; + z-index: 1000; + pointer-events: all; + + .line-content { + width: 60px; + display: flex; + justify-content: space-around; + align-items: center; + } + + .icon-box { + width: 20px; + height: 20px; + border-radius: 20px; + display: flex; + align-items: center; + justify-content: center; + background: #c6c6c6; + visibility: hidden; + } + + .icon-box:hover { + background: #296dff; + } +} + +.custom-edge-line:hover { + .icon-box { + visibility: visible; + } +} \ No newline at end of file diff --git a/packages/x-flow/src/FlowEditor/components/CustomEdge/index.tsx b/packages/x-flow/src/FlowEditor/components/CustomEdge/index.tsx new file mode 100644 index 000000000..8a1489984 --- /dev/null +++ b/packages/x-flow/src/FlowEditor/components/CustomEdge/index.tsx @@ -0,0 +1,90 @@ +import { memo } from 'react'; +import { PlusOutlined, CloseOutlined } from '@ant-design/icons'; +import { BezierEdge, EdgeLabelRenderer, getBezierPath, useReactFlow } from '@xyflow/react'; +import { useShallow } from 'zustand/react/shallow'; +import produce from 'immer'; +import { uuid } from '../../utils'; +import useStore from '../../store'; +import NodeSelectPopover from '../NodeSelectPopover'; +import './index.less'; + +export default memo((edge: any) => { + const { + label, + id, + sourceX, + sourceY, + targetX, + targetY, + data, + selected, + source, + target, + } = edge; + + const reactflow = useReactFlow(); + const [edgePath, labelX, labelY] = getBezierPath({ + sourceX, + sourceY, + targetX, + targetY, + }); + + const { + nodes, + setNodes, + mousePosition, + } = useStore( + useShallow((state: any) => ({ + nodes: state.nodes, + mousePosition: state.mousePosition, + setNodes: state.setNodes + })) + ); + + const handleAddNode = (data: any) => { + const { screenToFlowPosition } = reactflow; + const { x, y } = screenToFlowPosition({ x: mousePosition.pageX, y: mousePosition.pageY }); + + const newNodes = produce(nodes, (draft: any) => { + draft.push({ + id: uuid(), + type: 'custom', + data, + position: { x, y } + }); + }); + setNodes(newNodes); + }; + + return ( + +
+
+
+ +
+ +
+ +
+
+
+
+ + } + /> + ); +}); \ No newline at end of file diff --git a/packages/x-flow/src/FlowEditor/components/CustomHtml/index.tsx b/packages/x-flow/src/FlowEditor/components/CustomHtml/index.tsx new file mode 100644 index 000000000..1ec473fb3 --- /dev/null +++ b/packages/x-flow/src/FlowEditor/components/CustomHtml/index.tsx @@ -0,0 +1,64 @@ +import { Tooltip, Typography } from 'antd'; +import _ from 'lodash'; +import IconView from '@/components/IconView'; + +const { Text } = Typography; + +export default (props: any) => { + const { value, config, showEllipsis, width } = props; + const item = config?.[value]; + + if (showEllipsis && width) { + return ( + + {value} + + ); + } + + if (config?.requiredByValue && props.addons.schemaPath.includes('[]')) { + // 根据当前行的数据判断是否必填 + const values = props.addons.getValues(); + const dataPath = props.addons.dataPath; + const record = _.get(values, dataPath.slice(0, dataPath.lastIndexOf('.'))); + const pathName = dataPath.slice(dataPath.lastIndexOf('.') + 1); + // console.log('🚀 ~ record:', record, pathName); + // 获取当前行的数据 + + return ( + + {record?.required && config?.[pathName] === 'required' ? ( + * + ) : ( + '' + )} + {value} + + ); + } + + if (!item) { + return value; + } + const { required, tooltip } = item; + + return ( + + {required && ( + * + )} + {item?.label || value} + {!!tooltip && ( + + + + )} + + ); +}; diff --git a/packages/x-flow/src/FlowEditor/components/CustomNode/index.less b/packages/x-flow/src/FlowEditor/components/CustomNode/index.less new file mode 100644 index 000000000..b4048db05 --- /dev/null +++ b/packages/x-flow/src/FlowEditor/components/CustomNode/index.less @@ -0,0 +1,41 @@ +.node-container { + border: 2px solid #fff; + border-radius: 14px; + + .react-flow__edge-path, + .react-flow__connection-path { + stroke: #d0d5dc; + stroke-width: 2px; + } +} + +.node-container-selected { + border: 2px solid #296dff; + + .react-flow__handle::after { + display: none; + } +} + +.react-flow__handle { + width: 32px; + height: 32px; + background: transparent; + border-radius: 0; + border: none; + + :hover { + border: 2px solid #00a952; + transform: scale(1.25); + } +} + +.react-flow__handle::after { + content: ''; + --tw-bg-opacity: 1; + background-color: #2970ff; + width: 8px; + height: 2px; + display: block; + margin: 15px 0 0 12px; +} \ No newline at end of file diff --git a/packages/x-flow/src/FlowEditor/components/CustomNode/index.tsx b/packages/x-flow/src/FlowEditor/components/CustomNode/index.tsx new file mode 100644 index 000000000..29ec46395 --- /dev/null +++ b/packages/x-flow/src/FlowEditor/components/CustomNode/index.tsx @@ -0,0 +1,41 @@ +import { memo } from 'react'; +import classNames from 'classnames'; +import { Handle, Position } from '@xyflow/react'; +import './index.less'; + +function capitalize(string: string) { + if (typeof string !== 'string' || string.length === 0) { + return string; + } + return string.charAt(0).toUpperCase() + string.slice(1); +} + +export default memo((props: any) => { + const { data, isConnectable, selected, onClick } = props; + + const NodeComponent = NodeComponentMap[`${capitalize(data?.node)}Node`]; + + return ( +
+ {data?.node !== 'Input' && ( + + )} + onClick(data)} /> + {data?.node !== 'Output' && ( + + )} +
+ ); +}); diff --git a/packages/x-flow/src/FlowEditor/components/CustomNode/utils.ts b/packages/x-flow/src/FlowEditor/components/CustomNode/utils.ts new file mode 100644 index 000000000..da63bab50 --- /dev/null +++ b/packages/x-flow/src/FlowEditor/components/CustomNode/utils.ts @@ -0,0 +1,264 @@ +import { AnyObject } from 'antd/es/_util/type'; +import { Node } from '@antv/x6'; +import { ICard, colorMap } from './constant'; + +const getItemStatus = (item: ICard) => { + const { debugging, result } = item; + let status = 'default'; + + if (debugging) { + status = 'running'; + } else if (result?.error) { + status = 'failed'; + } else if (result) { + status = 'success'; + } + return status; +}; +interface ICell extends Node { + code: string; +} + +export const getInitGraphData = (inputItem: ICard, outputItem: ICard) => [ + { + id: inputItem._id, + code: inputItem.code, + shape: 'dag-node', + x: 290, + y: 110, + data: { + label: inputItem.code, + status: 'default', + borderColor: '#5e606a', + icon: 'icon-input', + }, + ports: [ + { + id: `${inputItem._id}-bottom-port`, + group: 'bottom', + }, + ], + }, + { + id: outputItem._id, + // code后端需要 + code: outputItem.code, + shape: 'dag-node', + x: 290, + y: 110 + 120, + data: { + label: outputItem.code, + status: 'default', + borderColor: '#5e606a', + icon: 'icon-output', + }, + ports: [ + { + id: `${outputItem._id}-top-port`, + group: 'top', + }, + ], + }, +]; + +export class GraphNode { + // 图数据的增删改查 + cells: ICell[]; + instance: any; + + constructor(cells: ICell[], instance?: any) { + this.cells = cells; + this.instance = instance; + } + addCell(node: ICard, flowList: ICard[]) { + const graphCells = this.cells; + const outputIndex = graphCells.findIndex( + (el: any) => el.id.toLowerCase() === 'output', + ); + const output = graphCells.splice(outputIndex, 1)[0]; + graphCells.push({ + id: node._id || node.code, + code: node.code, + shape: 'dag-node', + x: flowList.length % 2 === 1 ? 300 : 280, + y: 110 + 120 * flowList.length, + data: { + label: node.code, + status: getItemStatus(node), + borderColor: colorMap[node.type]?.borderColor, + icon: colorMap[node.type]?.icon, + }, + ports: { + items: [ + { + id: `${node._id}-top-port`, + group: 'top', + }, + { + id: `${node._id}-bottom-port`, + group: 'bottom', + }, + ], + }, + }); + if (!output) return; + output.y = 110 + 120 * (flowList.length + 1); + graphCells.push(output); + } + addCells(nodes: ICard[], flowList: ICard[]) { + nodes.forEach((node, index) => { + this.addCell( + node, + flowList.slice(0, flowList.length - nodes.length + index + 1), + ); + }); + } + removeCell(node: ICard) { + this.instance.removeCell(node._id); + } + updateCellLabel(node: ICard, val: string) { + const graphCells = this.cells; + const index = graphCells.findIndex((el: any) => el.id === node._id); + graphCells[index].data.label = val; + graphCells[index].code = val; + } + + updateCellStatus(node: ICard, status: string) { + // const graphCells = this.cells; + // const index = graphCells.findIndex( + // (el: any) => el.id === node._id || el.code === node._id, + // ); + // if (index !== -1) { + // graphCells[index].data.status = status; + // } + const curNode = this.instance.getCellById(node._id); + if (!curNode) { + console.log('err,cannot find node'); + return; + } + const data = curNode.getData(); + curNode.setData({ + ...data, + status, + }); + } + + updateAllCellStatus(status: string) { + const graphCells = this.cells; + + graphCells.forEach((el: any) => { + if (el.shape === 'dag-node' && el.id !== 'Input' && el.id !== 'Output') { + el.data.status = status; + } + }); + } + getCell() { + return this.cells; + } +} + +export const generateGraphByNodes = (devVersion: any) => { + // 根据 nodes 生成图表节点 + const graphIns = new GraphNode([]); + const nodes = devVersion.nodes || []; + + nodes.forEach((item: any, index: number) => { + graphIns.addCell(item, new Array(index)); + }); + let graphCells = graphIns.getCell(); + const inputNode = graphCells.find( + (item: any) => item.id.toLowerCase() === 'input', + ) as any; + const outputNode = graphCells.find( + (item: any) => item.id.toLowerCase() === 'output', + ) as any; + if (outputNode) { + outputNode.ports.items = outputNode.ports.items.filter((el: any) => { + return el.group !== 'bottom'; + }); + } + if (inputNode) { + inputNode.y = graphCells?.[0].y - 120; + inputNode.ports.items = inputNode.ports.items.filter((el: any) => { + return el.group !== 'top'; + }); + + graphCells = graphCells.filter( + (item: any) => item.id.toLowerCase() !== 'input', + ); + graphCells.unshift(inputNode); + } + return graphCells; +}; + +type IMap = AnyObject; +const typeMap: IMap = { + string: 'STRING', + number: 'INTEGER', + object: 'OBJECT', + array: 'ARRAY', + boolean: 'BOOLEAN', +}; +const getParamType = (param: any) => { + return Object.prototype.toString.call(param).slice(8, -1).toLowerCase(); +}; +export const formatObj2Arr = (obj: any) => { + return Object.entries(obj).map((item) => { + return { + name: item[0] === 'undefined' ? '' : item[0], + value: item[1] ?? '', + dataType: typeMap[getParamType(item[1])] ?? 'STRING', + }; + }); +}; + +export const formatArr2Obj = (arr: any) => { + return arr.reduce((pre: AnyObject, cur: { name: string; value: string }) => { + pre[cur.name] = cur.value; + return pre; + }, {}); +}; + +export function extractFencedCodeBlock(text: string, language: string) { + const regex = new RegExp(`\`\`\`${language}([^]*?)\`\`\``, 'gi'); + let match; + let results = []; + + while ((match = regex.exec(text)) !== null) { + results.push(match[1].trim()); + } + + return results.length > 0 ? results.join('\n') : text; +} + +export function typewriter( + text: string, + callback: (text: string) => void, + typingSpeed = 30, +) { + let currentIndex = -1; + let currentText = ''; + + const type = () => { + if (currentIndex < text.length - 1) { + currentIndex++; + currentText += text[currentIndex]; + callback(currentText); + setTimeout(type, typingSpeed); + } + }; + + type(); +} + +export function capitalizeFirstLetter(str: string) { + return str.charAt(0).toUpperCase() + str.slice(1); +} + +export function isNodeConnected(edges: any, nodeId: string) { + return edges.some((edge: any) => { + const source = edge.source.cell; + const target = edge.target.cell; + return source === nodeId || target === nodeId; + }); +} diff --git a/packages/x-flow/src/FlowEditor/components/FAutoComplete/index.tsx b/packages/x-flow/src/FlowEditor/components/FAutoComplete/index.tsx new file mode 100644 index 000000000..9fe41edb2 --- /dev/null +++ b/packages/x-flow/src/FlowEditor/components/FAutoComplete/index.tsx @@ -0,0 +1,89 @@ +import { useEffect, useState } from 'react'; +import { AutoComplete, InputNumber } from 'antd'; +import _ from 'lodash'; + +const FAutoComplete: React.FC = (props) => { + const { + value, + onChange, + placeholder, + optionList, + width = '100%', + disabled, + } = props; + const [options, setOptions] = useState<{ value: string }[]>(optionList); + + useEffect(() => { + setOptions(optionList); + }, [optionList]); + const handleSearch = async (value: string) => { + if (!props.request) { + return; + } + const res = await props.request(value); + setOptions(res); + }; + + let customDisabled = false; + const dependValues = props.addons.dependValues; + if (dependValues) { + // 知识库组件的特殊处理 + if (dependValues.length > 1) { + customDisabled = dependValues[1] === 'vector_weight' && dependValues[0]; + } + if (dependValues[1] === 'vector_weight') { + const onNumberChange = (val: any) => { + let newValue: string | number = val; + if (val === null || val === undefined) { + newValue = ''; + } + onChange(newValue); + }; + return ( + + ); + } + // if ( + // ['search_attachment', 'with_associated_documents'].includes( + // dependValues[1], + // ) + // ) { + // return ( + // onChange(e.target.checked)} + // style={{ width }} + // disabled={disabled} + // /> + // ); + // } + } + const values = props.addons.getValues(); + const dataPath = props.addons.dataPath; + const record = _.get(values, dataPath.slice(0, dataPath.lastIndexOf('.'))); + const pathName = dataPath.slice(dataPath.lastIndexOf('.') + 1); + + return ( + { + onChange(val); + }} + disabled={disabled} + placeholder={placeholder} + /> + ); +}; +export default FAutoComplete; diff --git a/packages/x-flow/src/FlowEditor/components/FlowDebugDrawer/index.less b/packages/x-flow/src/FlowEditor/components/FlowDebugDrawer/index.less new file mode 100644 index 000000000..1745f21a4 --- /dev/null +++ b/packages/x-flow/src/FlowEditor/components/FlowDebugDrawer/index.less @@ -0,0 +1,114 @@ +.debug-flow-panel { + .ant-drawer-content-wrapper { + top: 54px; + bottom: 14px; + right: 12px; + border-radius: 20px; + } + + .ant-drawer-close { + display: none; + } + + .ant-drawer-header { + padding: 16px; + } + + .ant-drawer-content { + border-radius: 20px; + } + + .ant-drawer-body { + padding: 8px 16px; + } + + .title-box { + display: flex; + align-items: center; + justify-content: space-between; + + .ant-input-outlined { + border-color: #fff; + font-weight: 600; + } + } + + .icon-box { + width: 24px; + height: 24px; + border-radius: 6px; + display: flex; + align-items: center; + justify-content: center; + margin-right: 5px; + } + + .title-actions { + display: flex; + align-items: center; + margin-left: 24px; + } + + .desc-box { + font-size: 12px; + line-height: 32px; + font-weight: normal; + + .ant-input-outlined { + border-color: #fff; + } + + textarea { + margin: 12px 0; + } + } + + .ant-input-outlined:focus-within { + border-color: #3b82f6 !important; + } + + .fr-table-cell-content { + .ant-col { + padding: 0 !important; + } + } + + .ant-collapse-content-box { + padding: 0 !important; + } + + .item-collapse { + border: none; + background-color: #fff; + + .ant-collapse-header { + background: none; + padding: 6px 0; + } + + .ant-collapse-item { + border: none !important; + padding: 0; + } + + .ant-collapse-content { + border: none; + } + } + + .ant-collapse-header-text { + color: #354052; + font-weight: 600; + } + + .ant-table-thead>tr>th { + font-size: 12px; + font-weight: normal; + } + + input, + select, + .ant-select-selector { + font-size: 13px !important; + } +} \ No newline at end of file diff --git a/packages/x-flow/src/FlowEditor/components/FlowDebugDrawer/index.tsx b/packages/x-flow/src/FlowEditor/components/FlowDebugDrawer/index.tsx new file mode 100644 index 000000000..c6207b1fa --- /dev/null +++ b/packages/x-flow/src/FlowEditor/components/FlowDebugDrawer/index.tsx @@ -0,0 +1,411 @@ +import { useEffect, useRef, useState } from 'react'; +import ReactJson from 'react-json-view'; +import { + Button, + Collapse, + Drawer, + Flex, + message, + Select, + Space, + Tabs, + Upload, + UploadFile, + UploadProps, +} from 'antd'; +import { DownloadOutlined, UploadOutlined } from '@ant-design/icons'; +import saveAs from 'file-saver'; +import FormRender, { useForm } from 'form-render'; +import * as XLSX from 'xlsx'; +import { getUrlParams } from '@/utils'; +import api from '@/apis'; +import ExpandInput from '@/components/ExpandInput'; +import IconView from '@/components/IconView'; +import { useFlow } from '@/hooks/useWorkFlow'; +import { transformData } from '@/pages/WorkflowDetail/DebugModal/util'; +import CustomHtml from '../CustomHtml'; +import './index.less'; + +interface IDebugDrawerProp { + visible: boolean; + onClose: () => void; + item: any; +} +const schema = { + type: 'object', + displayType: 'row', + properties: { + list: { + type: 'array', + widget: 'tableList', + readOnly: true, + + items: { + type: 'object', + properties: { + name: { + title: '名称', + type: 'string', + widget: 'html', + width: 140, + props: { + width: 100, + }, + }, + dataType: { + title: '类型', + type: 'string', + widget: 'html', + width: 80, + }, + value: { + title: '值', + type: 'string', + widget: 'ExpandInput', + placeholder: '请输入常量', + // required: true, + }, + }, + }, + }, + }, +}; +const NodeDebugDrawer = (props: IDebugDrawerProp) => { + const { onClose, visible, item } = props; + const [activeKey, setActiveKey] = useState('single'); + const [historyItem, setHistoryItem] = useState(null); + const [historyOptions, setHistoryOptions] = useState([]); + const form = useForm(); + const { id: detailId } = getUrlParams(); + const [activeTab, setActiveTab] = useState('output'); + const { handleDebugOk, handleBatchDebugOk, flowList } = useFlow(); + const [outputResult, setOutputResult] = useState(); + const [flowsResult, setFlowsResult] = useState([]); + + const fileData = useRef(null); + const [uploading, setUploading] = useState(false); + const [loading, setLoading] = useState(false); + const [fileList, setFileList] = useState([]); + const initInputList = (item?.data?.list || []).map((item: any) => { + return { + ...item, + value: '', + }; + }); + + useEffect(() => { + form.setValues({ + list: initInputList || [], + }); + + return () => { + form.resetFields(); + }; + }, [item]); + useEffect(() => { + if (visible) { + // eslint-disable-next-line @typescript-eslint/no-use-before-define + getHistory(); + } + }, [visible]); + useEffect(() => { + const currentHistory = historyOptions.find( + (item: any) => item.value === historyItem, + ); + // input字段修改,如果有历史记录,需要将历史记录的值填充到input中 + const list = initInputList.map((item: any) => { + const current = currentHistory?.inputs?.find( + (i: any) => i.name === item.name, + ); + return { + ...item, + value: current?.value, + }; + }); + + form.setValues({ + list, + }); + }, [historyOptions, historyItem]); + const getHistory = async () => { + if (!detailId) return; + const data = await api.workFlow.getDebugHistoryRecord({ + flowId: detailId, + pageable: { + current: 1, + pageSize: 10, + }, + }); + if (data.success) { + setHistoryOptions( + data.data.list.map((item: any, index: number) => { + return { + label: `调试记录${index + 1}: ${item.gmtCreate}`, + value: item.id, + ...item, + }; + }), + ); + setHistoryItem(data.data.list?.[0]?.id); + } + }; + const handleHistoryChange = (value: any) => { + setHistoryItem(value); + }; + // 批量 + const handleDownload = async () => { + const inputs = item?.data.list; + let data = [inputs.map((i) => `${i.name}|${i.dataType}`)]; + let ws = XLSX.utils.aoa_to_sheet(data); + let wb = XLSX.utils.book_new(); + XLSX.utils.book_append_sheet(wb, ws, 'SheetJS'); + let wbOut = XLSX.write(wb, { + bookType: 'xlsx', + bookSST: true, + type: 'array', + }); + saveAs( + new Blob([wbOut], { type: 'application/octet-stream' }), + 'workflow_batch_test.xlsx', + ); + }; + const handleBeforeUpload: UploadProps['beforeUpload'] = async (item) => { + let newFileList = [...fileList, item]; + if (newFileList.length > 1) { + message.error('一次最多上传一个文件'); + return false; + } + + try { + setUploading(true); + const uploadPromises = newFileList.map(async (file) => { + const reader = new FileReader(); + reader.onload = function (event) { + const arrayBuffer = event.target?.result; + if (!arrayBuffer) return; + const data = new Uint8Array(arrayBuffer as ArrayBuffer); + const workbook = XLSX.read(data, { + type: 'array', + }); + + // 默认处理第一个工作表 + const firstSheetName = workbook.SheetNames[0]; + const worksheet = workbook.Sheets[firstSheetName]; + const json = XLSX.utils.sheet_to_json(worksheet); + fileData.current = transformData(json as any); + setUploading(false); + console.log(fileData.current, json); + }; + reader.readAsArrayBuffer(file); + return { + file, + }; + }); + + const uploadResults = await Promise.allSettled(uploadPromises); + uploadResults.forEach(({ status, value }: any) => { + if (status === 'fulfilled') { + const { file, url } = value; + file.status = 'done'; + file.url = url; + } else if (status === 'rejected') { + message.error('Failed to upload file:', value.reason); + } + }); + } catch (error) { + console.error('Failed to upload files:', error); + } + + setFileList(newFileList); + return false; + }; + const uploadProps = { + accept: '.xlsx', + onRemove: (file: any) => { + const index = fileList.indexOf(file); + const newFileList = fileList.slice(); + newFileList.splice(index, 1); + setFileList(newFileList); + }, + beforeUpload: handleBeforeUpload, + }; + + const handleOk = async () => { + if (activeKey === 'single') { + const formData = await form.validateFields(); + setLoading(true); + await handleDebugOk(formData, true, { + successCb: (res: any) => { + console.log(323, res, flowList); + if (res.taskCode === 'Output') { + setOutputResult(res.output); + } + flowList.forEach((item: any) => { + if (item.code === res.taskCode) { + item.result = res.output; + } + }); + setLoading(false); + }, + errorCb: (err: any) => { + console.log(31221 + 'rr', err); + setLoading(false); + }, + }); + } else { + setLoading(true); + await handleBatchDebugOk(fileData.current); + setLoading(false); + } + }; + const traceItems = flowList.map((item: any) => { + return { + key: item.code, + label: item.name, + children: ( + +

输出结果:

+ +
+ ), + }; + }); + const singleOutItems = [ + { + key: 'output', + label: '结果', + children: ( + + ), + }, + { + key: 'log', + label: '追踪', + children: ( + <> + + + ), + }, + ]; + const items = [ + { + key: 'single', + label: '单次验证', + children: ( + <> + +
+ +
+ , + ExpandInput, + }} + /> + + ), + }, + { + key: 'batch', + label: '批量测试', + children: ( + + + + + + + + + ), + }, + ]; + const handleModalClose = () => { + if (onClose) { + form.resetFields(); + fileData.current = null; + setFileList([]); + onClose(); + } + }; + + return ( + +
流程调试
+ + + } + > + + + + + {activeKey === 'single' && !loading && outputResult && ( + + )} +
+ ); +}; +export default NodeDebugDrawer; diff --git a/packages/x-flow/src/FlowEditor/components/NodeContainer/index.less b/packages/x-flow/src/FlowEditor/components/NodeContainer/index.less new file mode 100644 index 000000000..57ef6d71b --- /dev/null +++ b/packages/x-flow/src/FlowEditor/components/NodeContainer/index.less @@ -0,0 +1,33 @@ +.custom-node-container { + width: 240px; + padding: 13px 12px 5px 12px; + background: #fff; + border-radius: 12px; + + .node-title { + display: flex; + height: 24px; + margin-bottom: 8px; + align-items: center; + font-size: 13px; + color: #1D2939; + span { + font-weight: bold; + } + } + + .node-desc { + color: #676f83; + font-size: 12px; + padding-top: 4px; + } + + .icon-box { + width: 24px; + height: 24px; + border-radius: 6px; + display: flex; + align-items: center; + justify-content: center; + } +} \ No newline at end of file diff --git a/packages/x-flow/src/FlowEditor/components/NodeContainer/index.tsx b/packages/x-flow/src/FlowEditor/components/NodeContainer/index.tsx new file mode 100644 index 000000000..75673ef88 --- /dev/null +++ b/packages/x-flow/src/FlowEditor/components/NodeContainer/index.tsx @@ -0,0 +1,25 @@ +import { memo } from 'react'; +import IconView from '@/components/IconView'; +import classNames from 'classnames'; +import './index.less'; + +export default memo((props: any) => { + const { className, onClick, children, icon, title, desc, hideDesc } = props; + + return ( +
+
+ + {title} +
+
{children}
+ {(!hideDesc && !!desc) && ( +
+ {desc} +
+ )} +
+ ); +}) + + diff --git a/packages/x-flow/src/FlowEditor/components/NodeDebugDrawer/index.less b/packages/x-flow/src/FlowEditor/components/NodeDebugDrawer/index.less new file mode 100644 index 000000000..894a25329 --- /dev/null +++ b/packages/x-flow/src/FlowEditor/components/NodeDebugDrawer/index.less @@ -0,0 +1,114 @@ +.debug-node-panel { + .ant-drawer-content-wrapper { + top: 54px; + bottom: 14px; + right: 12px; + border-radius: 20px; + } + + .ant-drawer-close { + display: none; + } + + .ant-drawer-header { + padding: 16px; + } + + .ant-drawer-content { + border-radius: 20px; + } + + .ant-drawer-body { + padding: 8px 16px; + } + + .title-box { + display: flex; + align-items: center; + justify-content: space-between; + + .ant-input-outlined { + border-color: #fff; + font-weight: 600; + } + } + + .icon-box { + width: 24px; + height: 24px; + border-radius: 6px; + display: flex; + align-items: center; + justify-content: center; + margin-right: 5px; + } + + .title-actions { + display: flex; + align-items: center; + margin-left: 24px; + } + + .desc-box { + font-size: 12px; + line-height: 32px; + font-weight: normal; + + .ant-input-outlined { + border-color: #fff; + } + + textarea { + margin: 12px 0; + } + } + + .ant-input-outlined:focus-within { + border-color: #3b82f6 !important; + } + + .fr-table-cell-content { + .ant-col { + padding: 0 !important; + } + } + + .ant-collapse-content-box { + padding: 0 !important; + } + + .item-collapse { + border: none; + background-color: #fff; + + .ant-collapse-header { + background: none; + padding: 6px 0; + } + + .ant-collapse-item { + border: none !important; + padding: 0; + } + + .ant-collapse-content { + border: none; + } + } + + .ant-collapse-header-text { + color: #354052; + font-weight: 600; + } + + .ant-table-thead>tr>th { + font-size: 12px; + font-weight: normal; + } + + input, + select, + .ant-select-selector { + font-size: 13px !important; + } +} \ No newline at end of file diff --git a/packages/x-flow/src/FlowEditor/components/NodeDebugDrawer/index.tsx b/packages/x-flow/src/FlowEditor/components/NodeDebugDrawer/index.tsx new file mode 100644 index 000000000..6a1d10ce8 --- /dev/null +++ b/packages/x-flow/src/FlowEditor/components/NodeDebugDrawer/index.tsx @@ -0,0 +1,163 @@ +import { useEffect, useState } from 'react'; +import ReactJson from 'react-json-view'; +import { Alert, Button, Drawer, Flex } from 'antd'; +import FormRender, { useForm } from 'form-render'; +import { cloneDeep } from 'lodash'; +import ExpandInput from '@/components/ExpandInput'; +import IconView from '@/components/IconView'; +import { useFlow } from '../../../../../hooks/useWorkFlow'; +import CustomHtml from '../CustomHtml'; +import './index.less'; + +interface IDebugDrawerProp { + visible: boolean; + onClose: () => void; + title: string; + node: any; +} +const schema = { + type: 'object', + displayType: 'row', + properties: { + list: { + type: 'array', + widget: 'tableList', + readOnly: true, + + items: { + type: 'object', + properties: { + name: { + title: '名称', + type: 'string', + widget: 'html', + width: 140, + props: { + width: 100, + }, + }, + dataType: { + title: '类型', + type: 'string', + widget: 'html', + width: 80, + }, + value: { + title: '值', + type: 'string', + widget: 'ExpandInput', + placeholder: '请输入常量', + // required: true, + }, + }, + }, + }, + }, +}; +const NodeDebugDrawer = (props: IDebugDrawerProp) => { + const { visible, onClose, title, node } = props; + const form = useForm(); + const [debugging, setDebugging] = useState(false); + const [debugData, setDebugData] = useState(); + + const { handleDebugOk } = useFlow(); + useEffect(() => { + const inputData = cloneDeep(node); + + if (inputData?.list) { + inputData.list = inputData.list.map((item: any) => { + return { + ...item, + value: '', + }; + }); + } + form.setValues({ + list: inputData?.list || [], + }); + return () => { + form.resetFields(); + }; + }, [node]); + + const handleOk = async () => { + const formData = await form.validateFields(); + + setDebugging(true); + await handleDebugOk({ ...formData, code: node.code }, false, { + successCb: (res: any) => { + if (res.taskCode === node.code) { + setDebugData(res?.output || {}); + } + setDebugging(false); + }, + errorCb: (err: any) => { + setDebugData(err); + setDebugging(false); + }, + }); + }; + return ( + +
{`测试 ${title}`}
+ + + } + > + + , + ExpandInput, + }} + /> + + + + {!debugging && debugData && ( + +

输出结果:

+ +
+ )} +
+ ); +}; +export default NodeDebugDrawer; diff --git a/packages/x-flow/src/FlowEditor/components/NodeSelectPopover/index.less b/packages/x-flow/src/FlowEditor/components/NodeSelectPopover/index.less new file mode 100644 index 000000000..7fecd2365 --- /dev/null +++ b/packages/x-flow/src/FlowEditor/components/NodeSelectPopover/index.less @@ -0,0 +1,49 @@ +.fai-reactflow-addblock { + min-height: 400px; + .node-item { + height: 32px; + color: #101828; + padding: 0 10px; + display: flex; + align-items: center; + cursor: pointer; + } + + .node-item:hover { + background-color: #f9fafb; + border-radius: 8px; + } + + .icon-box { + width: 20px; + height: 20px; + border-radius: 6px; + display: inline-flex; + align-items: center; + justify-content: center; + } +} + +.node-info-tooltip { + width: 200px; + + .title { + color: #101828; + margin-top: 3px; + } + + .description { + font-size: 12px; + font-weight: normal; + color: #101828; + } + + .icon-box-max { + width: 24px; + height: 24px; + border-radius: 6px; + display: flex; + align-items: center; + justify-content: center; + } +} \ No newline at end of file diff --git a/packages/x-flow/src/FlowEditor/components/NodeSelectPopover/index.tsx b/packages/x-flow/src/FlowEditor/components/NodeSelectPopover/index.tsx new file mode 100644 index 000000000..a6250d4f4 --- /dev/null +++ b/packages/x-flow/src/FlowEditor/components/NodeSelectPopover/index.tsx @@ -0,0 +1,180 @@ + +import { useCallback, useState, useRef } from 'react'; +import { Popover, Input, Tabs } from 'antd'; +import { SearchOutlined } from '@ant-design/icons'; +import { useEventListener } from 'ahooks'; +import { useShallow } from 'zustand/react/shallow'; +import { useClickAway } from 'ahooks'; +import { useSet } from '@/utils/hooks'; +import IconView from '@/components/IconView'; +import useStore from '../../store'; +import './index.less'; + +const items: any['items'] = [ + { + key: 'node', + label: '节点', + }, + { + key: 'tools', + label: '工具', + } +]; + +const filterNodeList = (query: string, _nodeList: any[]) => { + if (!query) { + return _nodeList; + } + const searchTerm = query.toLowerCase(); + + function searchNodes(nodes: any, results = []) { + if (nodes.length === 0) { + return results; + } + + const [currentNode, ...restNodes] = nodes; + let newResults: any = [...results]; + + if (currentNode.title.toLowerCase().includes(searchTerm)) { + newResults.push(currentNode); + } else if (currentNode.type === 'group' && currentNode.items) { + const matchingItems = searchNodes(currentNode.items); + if (matchingItems.length > 0) { + newResults.push({ ...currentNode, items: matchingItems }); + } + } + + return searchNodes(restNodes, newResults); + } + + return searchNodes(_nodeList); +}; + +const NodeInfo = ({ icon, title, description }: any) => { + return ( +
+
+ +
+
+ {title} +
+
+ {description} +
+
+ ) +}; + +const SelectNodeView = ({ onCreate, nodeMenus, containerRef }: any) => { + + const [state, setState] = useSet({ + nodeType: 'node', + nodeList: [...nodeMenus] + }); + const { nodeType, nodeList } = state; + + const handleSearchCange = (ev: any) => { + if (nodeType === 'node') { + setState({ nodeList: filterNodeList(ev.target.value, nodeMenus)}) + } else if (nodeType === 'tools') { + // todo 可能要调用接口查询了 + } + }; + + return ( +
+
+ } + style={{ width: '100%' }} + /> +
+
+ setState({ nodeType: type })} + style={{ padding: '0 5px' }} + /> + {nodeType === 'node' ? ( +
+ {nodeList.map((item: any) => item.type === 'group' ? ( +
+
{item.title}
+ {item.items.map(({ icon, title }: any, index: number) => ( +
onCreate(ev, item.type)}> + + {title} +
+ ))} +
+ ) : ( + } placement='right' arrow={false} key={item.type}> +
onCreate(ev, item.type)}> + + + + {item.title} +
+
+ ))} +
+ ) : ( +
工具数据
+ )} +
+
+ ) +}; + +export default (props: any) => { + const { addNode, children, placement='top' } = props; + + const ref = useRef(null); + const closeRef: any = useRef(null); + const [open, setOpen] = useState(false); + const { + nodeMenus, + } = useStore( + useShallow((state) => ({ + nodeMenus: state.nodeMenus, + })) + ); + + useClickAway(() => { + if (closeRef.current) { + setOpen(false); + closeRef.current = false; + } + }, ref); + + const handAddNode = useCallback((ev: any, type: any) => { + ev.stopPropagation(); // 阻止事件冒泡 + addNode({ node: type }); + setOpen(false); + }, []); + + return ( + } + zIndex={1000} + trigger='click' + arrow={false} + open={open} + overlayInnerStyle={{ padding: '12px 6px' }} + placement={placement} + onOpenChange={() => { + setTimeout(() => { + closeRef.current = true; + setOpen(true); + }, 50) + }} + > + {children} + + ); +} \ No newline at end of file diff --git a/packages/x-flow/src/FlowEditor/components/PanelContainer/index.less b/packages/x-flow/src/FlowEditor/components/PanelContainer/index.less new file mode 100644 index 000000000..7679e2fa9 --- /dev/null +++ b/packages/x-flow/src/FlowEditor/components/PanelContainer/index.less @@ -0,0 +1,125 @@ +.custom-node-panel { + .ant-drawer-content-wrapper { + top: 54px; + bottom: 14px; + right: 12px; + border-radius: 20px; + } + + .ant-drawer-close { + display: none; + } + + .ant-drawer-header { + padding: 24px 16px 0 16px; + } + + .ant-drawer-content { + border-radius: 20px; + } + + .ant-drawer-body { + padding: 12px 16px; + } + + .title-box { + display: flex; + align-items: center; + justify-content: space-between; + .ant-input-outlined { + border-color: #fff; + font-weight: 600; + } + } + + .icon-box { + width: 24px; + height: 24px; + border-radius: 6px; + display: flex; + align-items: center; + justify-content: center; + margin-right: 5px; + } + + .title-actions { + display: flex; + align-items: center; + margin-left: 24px; + } + + .desc-box { + font-size: 12px; + line-height: 32px; + font-weight: normal; + .ant-input-outlined { + border-color: #fff; + } + textarea { + margin: 12px 0; + } + } + + .ant-input-outlined:focus-within { + border-color: #3b82f6 !important; + } + + .fr-table-cell-content { + .ant-col { + padding: 0 !important; + } + } + + .ant-collapse-content-box { + padding: 0 !important; + } + + .item-collapse { + border: none; + background-color: #fff; + + .ant-collapse-header { + background: none; + padding: 6px 0; + } + + .ant-collapse-item { + border: none !important; + padding: 0; + } + + .ant-collapse-content { + border: none; + } + } + + .ant-collapse-header-text { + color: #354052; + font-weight: 600; + } + + .ant-table-thead >tr>th { + font-size: 12px; + font-weight: normal; + } + + .ant-table-tbody>tr>td { + padding: 4px !important; + } + + input, select, textarea, .ant-select-selector { + font-size: 12px !important; + min-height: 30px; + } + + input, select, .ant-select-selector { + height: 30px !important; + line-height: 30px !important; + } + + .fr-table-list { + .ant-btn { + font-size: 12px; + } + } +} \ No newline at end of file diff --git a/packages/x-flow/src/FlowEditor/components/PanelContainer/index.tsx b/packages/x-flow/src/FlowEditor/components/PanelContainer/index.tsx new file mode 100644 index 000000000..664c46ff5 --- /dev/null +++ b/packages/x-flow/src/FlowEditor/components/PanelContainer/index.tsx @@ -0,0 +1,104 @@ +import React from 'react'; +import { Divider, Drawer, Input, Space } from 'antd'; +import IconView from '@/components/IconView'; +import { useFlow } from '@/hooks/useWorkFlow'; +import NodeDebugDrawer from '../NodeDebugDrawer'; +import './index.less'; + +const getDescription = (nodeType: string, description: string) => { + if (nodeType === 'Input') { + return '工作流的起始节点,用于设定启动工作流入参信息'; + } + if (nodeType === 'Output') { + return '工作流的最终节点,用于返回工作流运行后的出参信息'; + } + return description || ''; +}; + +const Panel = (props: any) => { + const { onClose, children, title, icon, nodeType, disabled, node } = props; + const { handleItemDebug } = useFlow(); + + const [nodeDebugVisible, setNodeDebugVisible] = React.useState(false); + + const isDisabled = ['Input', 'Output'].includes(nodeType) || disabled; + const description = getDescription(nodeType, props.description); + + const handleNodeDebug = async () => { + const res = await handleItemDebug(node); + if (!res) { + return; + } + setNodeDebugVisible(true); + }; + + return ( + +
+
+ + + + {isDisabled ? ( + {title} + ) : ( + + )} +
+
+ + {!isDisabled && ( + <> + + + + )} + {/* */} + + +
+
+
+ {isDisabled ? ( + description + ) : ( + + )} +
+ + } + > + {children} + setNodeDebugVisible(false)} + /> +
+ ); +}; + +export default React.memo(Panel); diff --git a/packages/x-flow/src/FlowEditor/constants.ts b/packages/x-flow/src/FlowEditor/constants.ts new file mode 100644 index 000000000..72eb85411 --- /dev/null +++ b/packages/x-flow/src/FlowEditor/constants.ts @@ -0,0 +1,261 @@ +import type { Var } from './types' +import { BlockEnum, VarType } from './types'; + +export const CUSTOM_ITERATION_START_NODE = 'custom-iteration-start' + + +export const NODE_WIDTH = 240 +export const X_OFFSET = 60 +export const NODE_WIDTH_X_OFFSET = NODE_WIDTH + X_OFFSET +export const Y_OFFSET = 39 +export const MAX_TREE_DEPTH = 50 +export const START_INITIAL_POSITION = { x: 80, y: 282 } +export const AUTO_LAYOUT_OFFSET = { + x: -42, + y: 243, +} +export const ITERATION_NODE_Z_INDEX = 1 +export const ITERATION_CHILDREN_Z_INDEX = 1002 +export const ITERATION_PADDING = { + top: 65, + right: 16, + bottom: 20, + left: 16, +} +export const PARALLEL_LIMIT = 10 +export const PARALLEL_DEPTH_LIMIT = 3 + +export const RETRIEVAL_OUTPUT_STRUCT = `{ + "content": "", + "title": "", + "url": "", + "icon": "", + "metadata": { + "dataset_id": "", + "dataset_name": "", + "document_id": [], + "document_name": "", + "document_data_source_type": "", + "segment_id": "", + "segment_position": "", + "segment_word_count": "", + "segment_hit_count": "", + "segment_index_node_hash": "", + "score": "" + } +}` + +export const SUPPORT_OUTPUT_VARS_NODE = [ + BlockEnum.Start, BlockEnum.LLM, BlockEnum.KnowledgeRetrieval, BlockEnum.Code, BlockEnum.TemplateTransform, + BlockEnum.HttpRequest, BlockEnum.Tool, BlockEnum.VariableAssigner, BlockEnum.VariableAggregator, BlockEnum.QuestionClassifier, + BlockEnum.ParameterExtractor, BlockEnum.Iteration, +] + +export const LLM_OUTPUT_STRUCT: Var[] = [ + { + variable: 'text', + type: VarType.string, + }, +] + +export const KNOWLEDGE_RETRIEVAL_OUTPUT_STRUCT: Var[] = [ + { + variable: 'result', + type: VarType.arrayObject, + }, +] + +export const TEMPLATE_TRANSFORM_OUTPUT_STRUCT: Var[] = [ + { + variable: 'output', + type: VarType.string, + }, +] + +export const QUESTION_CLASSIFIER_OUTPUT_STRUCT = [ + { + variable: 'class_name', + type: VarType.string, + }, +] + +export const HTTP_REQUEST_OUTPUT_STRUCT: Var[] = [ + { + variable: 'body', + type: VarType.string, + }, + { + variable: 'status_code', + type: VarType.number, + }, + { + variable: 'headers', + type: VarType.object, + }, + { + variable: 'files', + type: VarType.arrayFile, + }, +] + +export const TOOL_OUTPUT_STRUCT: Var[] = [ + { + variable: 'text', + type: VarType.string, + }, + { + variable: 'files', + type: VarType.arrayFile, + }, + { + variable: 'json', + type: VarType.arrayObject, + }, +] + +export const PARAMETER_EXTRACTOR_COMMON_STRUCT: Var[] = [ + { + variable: '__is_success', + type: VarType.number, + }, + { + variable: '__reason', + type: VarType.string, + }, +] + +export const WORKFLOW_DATA_UPDATE = 'WORKFLOW_DATA_UPDATE' +export const CUSTOM_NODE = 'custom' +export const CUSTOM_EDGE = 'custom' +export const DSL_EXPORT_CHECK = 'DSL_EXPORT_CHECK' + + + + +export const iconSettingMap: Record = { + Start: { + icon: { + type: 'icon-start', + style: { fontSize: 14, color: '#fff' }, + bgColor: '#17B26A' + } + }, + End: { + icon: { + type: 'icon-end', + style: { fontSize: 14, color: '#fff' }, + bgColor: '#F79009' + } + }, + Code: { + title: '代码执行', + icon: { + type: 'icon-code', + style: { fontSize: 14, color: '#fff' }, + bgColor: '#2E90FA' + } + }, + Prompt: { + borderColor: '#3b82f6', + bgColor: '#f0f6fe', + icon: 'icon-prompt', + }, + LLM: { + borderColor: '#15afb3', + bgColor: '#e7f7f7', + icon: 'icon-model', + }, + Knowledge: { + borderColor: '#e7365d', + bgColor: '#fad4d7', + icon: 'icon-knowledge', + }, + Switch: { + title: '条件分支', + icon: { + type: 'icon-switch', + style: { fontSize: 14, color: '#fff' }, + bgColor: '#06AED4' + } + }, + HSF: { + title: 'HSF 请求', + icon: { + type: 'icon-hsf', + style: { fontSize: 14, color: '#fff' }, + bgColor: '#875BF7' + } + }, + Http: { + title: 'Http 请求', + icon: { + type: 'icon-http', + style: { fontSize: 14, color: '#fff' }, + bgColor: '#875BF7' + } + }, + Tool: { + title: '工具', + icon: { + type: 'icon-gongju', + style: { fontSize: 14, color: '#fff' }, + bgColor: '#2E90FA' + } + }, +}; + + +export const nodeConfigList = [ + { + title: 'Prompt', + type: 'prompt' + + }, + { + title: 'LLM', + type: 'llm' + }, + { + title: '知识库', + icon: 'icon-knowledge', + type: 'knowledge' + + + }, + { + title: 'Switch', + type: 'switch', + icon: { + type: 'icon-switch', + style: { fontSize: 14, color: '#fff' }, + bgColor: '#06AED4' + } + }, + { + title: 'HSF', + type: 'hsf', + icon: { + type: 'icon-hsf', + style: { fontSize: 14, color: '#fff' }, + bgColor: '#875BF7' + } + }, + { + title: 'Http', + type: 'http', + icon: { + type: 'icon-http', + style: { fontSize: 14, color: '#fff' }, + bgColor: '#875BF7' + } + }, + { + title: '脚步语言', + type: 'group', + items: [ + { title: 'Groovy', icon: 'icon-groovy', type: 'groovy' }, + { title: 'Javascript', icon: 'icon-js', type: 'javascript' }, + { title: 'Pathon', icon: 'icon-pathon', type: 'pathon' }, + ]} +]; + diff --git a/packages/x-flow/src/FlowEditor/context.tsx b/packages/x-flow/src/FlowEditor/context.tsx new file mode 100644 index 000000000..4a320c434 --- /dev/null +++ b/packages/x-flow/src/FlowEditor/context.tsx @@ -0,0 +1,41 @@ +import { createContext, useRef, useContext } from 'react'; +import { createStore } from 'zustand/vanilla' + +type Shape = { + appId: string +} + +export const createWorkflowStore = () => { + return createStore(set => ({ + appId: '', + candidateNode: {} + })) +} + + + +type WorkflowStore = ReturnType +export const WorkflowContext = createContext(null) + +type WorkflowProviderProps = { + children: React.ReactNode +} + + +export const WorkflowContextProvider = ({ children }: WorkflowProviderProps) => { + const storeRef = useRef(); + + if (!storeRef.current) { + storeRef.current = createWorkflowStore(); + } + + return ( + + {children} + + ); +} + +export const useWorkflowStore = () => { + return useContext(WorkflowContext)! +} diff --git a/packages/x-flow/src/FlowEditor/index.less b/packages/x-flow/src/FlowEditor/index.less new file mode 100644 index 000000000..45dc2df69 --- /dev/null +++ b/packages/x-flow/src/FlowEditor/index.less @@ -0,0 +1,5 @@ +#workflow-container { + width: 100%; + background: #F0F2F7; + flex: 1; +} \ No newline at end of file diff --git a/packages/x-flow/src/FlowEditor/index.tsx b/packages/x-flow/src/FlowEditor/index.tsx new file mode 100644 index 000000000..6251c049d --- /dev/null +++ b/packages/x-flow/src/FlowEditor/index.tsx @@ -0,0 +1,22 @@ +import { memo } from 'react'; +import { ReactFlowProvider } from '@xyflow/react'; +import { WorkflowContextProvider } from './context'; +import FlowEditor from './main'; + +const WorkflowContainer = (props: any) => { + const { initialState, nodeMenus } = props; + + return ( + + + + + + ); +}; + +export default memo(WorkflowContainer) diff --git a/packages/x-flow/src/FlowEditor/main.tsx b/packages/x-flow/src/FlowEditor/main.tsx new file mode 100644 index 000000000..77dbcff3d --- /dev/null +++ b/packages/x-flow/src/FlowEditor/main.tsx @@ -0,0 +1,283 @@ +import type { FC } from 'react'; +import { memo, useEffect, useMemo, useRef, useState } from 'react'; +import { useEventListener, useMemoizedFn } from 'ahooks'; +import produce, { setAutoFreeze } from 'immer'; +import { debounce } from 'lodash'; +import { useShallow } from 'zustand/react/shallow'; +import { + Background, + BackgroundVariant, + MarkerType, + ReactFlow, + useReactFlow, + useStoreApi, +} from '@xyflow/react'; +import '@xyflow/react/dist/style.css'; +import { useEventEmitterContextContext } from '../context/event-emitter'; +import CandidateNode from './components/CandidateNode'; +import CustomEdge from './components/CustomEdge'; +import PanelContainer from './components/PanelContainer'; +import './index.less'; +import CustomNodeComponent from '../nodes'; +import { PanelComponentMap } from '../nodes/nodes'; +import Operator from './operator'; +import useStore, { useUndoRedo } from './store'; +import { FlowEditorProps } from './types'; +import { capitalize, uuid } from './utils'; +import autoLayoutNodes from './utils/autoLayoutNodes'; + +const edgeTypes = { buttonedge: memo(CustomEdge) }; +const CustomNode = memo(CustomNodeComponent); +/*** + * + * ReactFlow 入口 + * + */ +const FlowEditor: FC = memo( + ({ nodeMenus, nodes: originalNodes, edges: originalEdges }) => { + const workflowContainerRef = useRef(null); + const store = useStoreApi(); + const { updateEdge, addNodes, addEdges, zoomTo } = useReactFlow(); + const { undo, redo, record } = useUndoRedo(false); + const { + nodes, + edges, + onNodesChange, + onEdgesChange, + onConnect, + setNodes, + setEdges, + setNodeMenus, + setCandidateNode, + setMousePosition, + } = useStore( + useShallow((state) => ({ + nodes: state.nodes, + edges: state.edges, + setNodes: state.setNodes, + setEdges: state.setEdges, + setNodeMenus: state.setNodeMenus, + setMousePosition: state.setMousePosition, + setCandidateNode: state.setCandidateNode, + onNodesChange: state.onNodesChange, + onEdgesChange: state.onEdgesChange, + onConnect: state.onConnect, + })), + ); + + const [activeNode, setActiveNode] = useState(null); + + useEffect(() => { + zoomTo(0.8); + setAutoFreeze(false); + return () => { + setAutoFreeze(true); + }; + }, []); + + useEffect(() => { + setNodeMenus(nodeMenus); + const _nodes: any = autoLayoutNodes(originalNodes, originalEdges); + setNodes(_nodes); + setEdges(originalEdges); + }, [JSON.stringify(originalNodes)]); + + useEventListener('keydown', (e) => { + if ((e.key === 'd' || e.key === 'D') && (e.ctrlKey || e.metaKey)) + e.preventDefault(); + if ((e.key === 'z' || e.key === 'Z') && (e.ctrlKey || e.metaKey)) + e.preventDefault(); + if ((e.key === 'y' || e.key === 'Y') && (e.ctrlKey || e.metaKey)) + e.preventDefault(); + if ((e.key === 's' || e.key === 'S') && (e.ctrlKey || e.metaKey)) + e.preventDefault(); + }); + + useEventListener('mousemove', (e) => { + const containerClientRect = + workflowContainerRef.current?.getBoundingClientRect(); + if (containerClientRect) { + setMousePosition({ + pageX: e.clientX, + pageY: e.clientY, + elementX: e.clientX - containerClientRect.left, + elementY: e.clientY - containerClientRect.top, + }); + } + }); + + const { eventEmitter } = useEventEmitterContextContext(); + eventEmitter?.useSubscription((v: any) => { + // 整理节点 + if (v.type === 'auto-layout-nodes') { + const newNodes: any = autoLayoutNodes(store.getState().nodes, edges); + setNodes(newNodes); + } + }); + + // 新增节点 + const handleAddNode = (data: any) => { + const newNode = { + id: uuid(), + type: 'custom', + data, + position: { + x: 0, + y: 0, + }, + }; + setCandidateNode(newNode); + // record(() => { + // addNodes(newNode); + // addEdges({ + // id: uuid(), + // source: '1', + // target: newNode.id, + // }); + // }); + }; + + // 插入节点 + const handleInsertNode = () => { + const newNode = { + id: uuid(), + data: { label: 'new node' }, + position: { + x: 0, + y: 0, + }, + }; + record(() => { + 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) => { + const currEdge: any = draft.find((e) => e.id === edge.id); + currEdge.style = { + ...edge.style, + stroke: color, + }; + currEdge.markerEnd = { + ...edge?.markerEnd, + color, + }; + }); + setEdges(newEdges); + }); + + const handleNodeValueChange = debounce((data: any) => { + for (let node of nodes) { + if (node.id === activeNode.name) { + node.data = { + ...node?.data, + ...data, + }; + break; + } + } + setNodes([...nodes]); + }, 200); + + const nodeTypes = useMemo(() => { + return { + custom: (props: any) => ( + + ), + }; + }, []); + const { icon, description } = + nodeMenus.find( + (item) => item.type?.toLowerCase() === activeNode?.node?.toLowerCase(), + ) || {}; + + const NodeEditor = + PanelComponentMap[capitalize(`${activeNode?.node}Setting`)]; + + return ( +
+ + + { + const recordTypes = new Set(['add', 'remove']); + changes.forEach((change) => { + if (recordTypes.has(change.type)) { + record(() => { + onNodesChange([change]); + }); + } else { + onNodesChange([change]); + } + }); + }} + onEdgesChange={(changes) => { + const recordTypes = new Set(['add', 'remove']); + changes.forEach((change) => { + if (recordTypes.has(change.type)) { + record(() => { + onEdgesChange([change]); + }); + } else { + onEdgesChange([change]); + } + }); + }} + onEdgeMouseEnter={(_, edge: any) => { + getUpdateEdgeConfig(edge, '#2970ff'); + }} + onEdgeMouseLeave={(_, edge) => { + getUpdateEdgeConfig(edge, '#c9c9c9'); + }} + > + + + {activeNode && ( + setActiveNode(null)} + node={activeNode} + > + + + )} +
+ ); + }, +); + +export default FlowEditor; diff --git a/packages/x-flow/src/FlowEditor/operator/Control/index.less b/packages/x-flow/src/FlowEditor/operator/Control/index.less new file mode 100644 index 000000000..bf99b50e9 --- /dev/null +++ b/packages/x-flow/src/FlowEditor/operator/Control/index.less @@ -0,0 +1,48 @@ +.fai-reactflow-control { + display: flex; + align-items: center; + padding: 2px 1px; + border-radius: 8px; + border: 0.5px solid #f3f4f6; + background-color: #fff; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + color: #667085; + margin-left: 10px; +} + +.control-item { + display: flex; + align-items: center; + justify-content: center; + width: 2rem; + height: 2rem; + border-radius: 0.5rem; + cursor: pointer; + + &.inactive:hover { + background-color: rgba(0, 0, 0, 0.05); + color: #374151; + } + + &.active { + background-color: #ecfdf5; + color: #10b981; + } + + &.disabled { + cursor: not-allowed; + opacity: 0.5; + } +} + +.icon { + width: 1rem; + height: 1rem; +} + +.separator { + margin: 0 0.75rem; + width: 1px; + height: 0.875rem; + background-color: #e5e7eb; +} diff --git a/packages/x-flow/src/FlowEditor/operator/Control/index.tsx b/packages/x-flow/src/FlowEditor/operator/Control/index.tsx new file mode 100644 index 000000000..1d14f91d8 --- /dev/null +++ b/packages/x-flow/src/FlowEditor/operator/Control/index.tsx @@ -0,0 +1,67 @@ +import type { MouseEvent } from 'react'; +import { memo } from 'react'; +import { + RiCursorLine, + RiFunctionAddLine, + RiHand, + RiStickyNoteAddLine, +} from '@remixicon/react'; +import { Tooltip, Button } from 'antd'; +import IconView from '@/components/IconView'; +import { useEventEmitterContextContext } from '../../../context/event-emitter'; +import NodeSelectPopover from '../../components/NodeSelectPopover'; +import './index.less'; + +const Control = (props: any) => { + const { addNode } = props; + + const addNote = (e: MouseEvent) => { + e.stopPropagation(); + }; + + const { eventEmitter } = useEventEmitterContextContext() + + return ( +
+ + +
+ ) +}; + +export default memo(Control); diff --git a/packages/x-flow/src/FlowEditor/operator/UndoRedo/index.less b/packages/x-flow/src/FlowEditor/operator/UndoRedo/index.less new file mode 100644 index 000000000..5994dc756 --- /dev/null +++ b/packages/x-flow/src/FlowEditor/operator/UndoRedo/index.less @@ -0,0 +1,11 @@ +.fai-reactflow-undoredo { + display: flex; + align-items: center; + padding: 2px 1px; + border-radius: 8px; + border: 0.5px solid #f3f4f6; + background-color: #ffffff; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + color: #667085; + margin-left: 10px; +} \ No newline at end of file diff --git a/packages/x-flow/src/FlowEditor/operator/UndoRedo/index.tsx b/packages/x-flow/src/FlowEditor/operator/UndoRedo/index.tsx new file mode 100644 index 000000000..3bc629df4 --- /dev/null +++ b/packages/x-flow/src/FlowEditor/operator/UndoRedo/index.tsx @@ -0,0 +1,31 @@ +import { memo } from 'react'; +import { RiArrowGoBackLine, RiArrowGoForwardFill } from '@remixicon/react' +import { Button, Tooltip } from 'antd'; +import IconView from '@/components/IconView'; +import './index.less'; + +export type UndoRedoProps = { + handleUndo: () => void; + handleRedo: () => void; +}; + +export default memo(({ handleUndo, handleRedo }: UndoRedoProps) => { + return ( +
+ +
+ ); +}) \ No newline at end of file diff --git a/packages/x-flow/src/FlowEditor/operator/ZoomInOut/index.less b/packages/x-flow/src/FlowEditor/operator/ZoomInOut/index.less new file mode 100644 index 000000000..98b4b5a6c --- /dev/null +++ b/packages/x-flow/src/FlowEditor/operator/ZoomInOut/index.less @@ -0,0 +1,35 @@ +.fai-reactflow-zoominout { + display: flex; + align-items: center; + padding: 2px 1px; + border-radius: 8px; + border: 0.5px solid #f3f4f6; + background-color: #ffffff; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + color: #667085; +} + +.fai-reactflow-zoom-select { + + .parting-line { + height: 1px; + background-color: #F3F4F6; + } + + .zoom-item { + display: flex; + align-items: center; + justify-content: space-between; + padding-left: 0.75rem; + padding-right: 0.75rem; + height: 2rem; + border-radius: 0.5rem; + cursor: pointer; + font-size: 0.875rem; + color: #374151; + + &:hover { + background-color: #F9FAFB; + } + } +} \ No newline at end of file diff --git a/packages/x-flow/src/FlowEditor/operator/ZoomInOut/index.tsx b/packages/x-flow/src/FlowEditor/operator/ZoomInOut/index.tsx new file mode 100644 index 000000000..809c76488 --- /dev/null +++ b/packages/x-flow/src/FlowEditor/operator/ZoomInOut/index.tsx @@ -0,0 +1,154 @@ +import type { FC } from 'react'; +import { Fragment, memo } from 'react'; +import { Button, Popover, Tooltip } from 'antd'; +import { useReactFlow, useViewport } from '@xyflow/react'; +import { getKeyboardKeyNameBySystem } from '../../utils'; +import ShortcutsName from './shortcuts-name'; +import IconView from '@/components/IconView'; +import './index.less'; + +enum ZoomType { + zoomIn = 'zoomIn', + zoomOut = 'zoomOut', + zoomToFit = 'zoomToFit', + zoomTo25 = 'zoomTo25', + zoomTo50 = 'zoomTo50', + zoomTo75 = 'zoomTo75', + zoomTo100 = 'zoomTo100', + zoomTo200 = 'zoomTo200' +}; + +const ZOOM_IN_OUT_OPTIONS = [ + [ + { + key: ZoomType.zoomTo200, + text: '200%', + }, + { + key: ZoomType.zoomTo100, + text: '100%', + }, + { + key: ZoomType.zoomTo75, + text: '75%', + }, + { + key: ZoomType.zoomTo50, + text: '50%', + }, + { + key: ZoomType.zoomTo25, + text: '25%', + }, + ], + [ + { + key: ZoomType.zoomToFit, + text: '自适应视图', + }, + ] +]; + +const ZoomSelect = ({ handleZoom }: any) => { + return ( +
+ {ZOOM_IN_OUT_OPTIONS.map((options, i) => ( + + {i !== 0 &&
} +
+ { + options.map(option => ( +
handleZoom(option.key)} + > + {option.text} + { + option.key === ZoomType.zoomToFit && ( + + ) + } + { + option.key === ZoomType.zoomTo50 && ( + + ) + } + { + option.key === ZoomType.zoomTo100 && ( + + ) + } +
+ )) + } +
+ + ))} +
+ ) +}; + + +const ZoomInOut: FC = () => { + const { + zoomIn, + zoomOut, + zoomTo, + fitView, + } = useReactFlow(); + + const { zoom } = useViewport(); + + const handleZoom = (type: string) => { + if (type === ZoomType.zoomToFit) + fitView() + + if (type === ZoomType.zoomTo25) + zoomTo(0.25) + + if (type === ZoomType.zoomTo50) + zoomTo(0.5) + + if (type === ZoomType.zoomTo75) + zoomTo(0.75) + + if (type === ZoomType.zoomTo100) + zoomTo(1) + + if (type === ZoomType.zoomTo200) + zoomTo(2) + }; + + return ( +
+ +
+ ) +}; + +export default memo(ZoomInOut); diff --git a/packages/x-flow/src/FlowEditor/operator/ZoomInOut/shortcuts-name.tsx b/packages/x-flow/src/FlowEditor/operator/ZoomInOut/shortcuts-name.tsx new file mode 100644 index 000000000..ecb291c45 --- /dev/null +++ b/packages/x-flow/src/FlowEditor/operator/ZoomInOut/shortcuts-name.tsx @@ -0,0 +1,32 @@ +import { memo } from 'react' +import { getKeyboardKeyNameBySystem } from '../../utils' +import cn from 'classnames' + +type ShortcutsNameProps = { + keys: string[] + className?: string +} +const ShortcutsName = ({ + keys, + className, +}: ShortcutsNameProps) => { + return ( +
+ { + keys.map(key => ( +
+ {getKeyboardKeyNameBySystem(key)} +
+ )) + } +
+ ) +} + +export default memo(ShortcutsName) diff --git a/packages/x-flow/src/FlowEditor/operator/index.less b/packages/x-flow/src/FlowEditor/operator/index.less new file mode 100644 index 000000000..a8cb678c3 --- /dev/null +++ b/packages/x-flow/src/FlowEditor/operator/index.less @@ -0,0 +1,27 @@ +.fai-reactflow-operator { + position: absolute; + left: 4px; + bottom: 14px; + height: 50px; + z-index: 1; + + .mini-map { + position: absolute; + left: 12px; + bottom: 45px; + margin: 0; + border: 0.5px solid rgba(0, 0, 0, 0.08); + border-radius: 8px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + z-index: 9; + } + + .operator-section { + display: flex; + align-items: center; + position: absolute; + left: 10px; + bottom: 4px; + z-index: 9; + } +} \ No newline at end of file diff --git a/packages/x-flow/src/FlowEditor/operator/index.tsx b/packages/x-flow/src/FlowEditor/operator/index.tsx new file mode 100644 index 000000000..31c753d1b --- /dev/null +++ b/packages/x-flow/src/FlowEditor/operator/index.tsx @@ -0,0 +1,30 @@ +import { memo } from 'react'; +// import UndoRedo from '../header/undo-redo' +import ZoomInOut from './ZoomInOut'; +import UndoRedo from './UndoRedo'; +import Control from './Control'; + +import './index.less'; + +export type OperatorProps = { + handleUndo: () => void + handleRedo: () => void + addNode: any; +} + +const Operator = ({ handleUndo, handleRedo, addNode }: OperatorProps) => { + return ( +
+
+ + + +
+
+ ); +} + +export default memo(Operator) diff --git a/packages/x-flow/src/FlowEditor/store.ts b/packages/x-flow/src/FlowEditor/store.ts new file mode 100644 index 000000000..13cb75e44 --- /dev/null +++ b/packages/x-flow/src/FlowEditor/store.ts @@ -0,0 +1,108 @@ +import { create } from 'zustand'; +import { immer } from 'zustand/middleware/immer'; +import { temporal } from 'zundo'; +import { + addEdge, + applyNodeChanges, + applyEdgeChanges, + Edge, + Node, + OnNodesChange, + OnEdgesChange, + OnConnect, +} from '@xyflow/react'; +import _ from "lodash"; + +export type AppNode = Node; + +export type AppState = { + nodes: AppNode[]; + edges: Edge[]; + nodeMenus: any[]; + candidateNode: any; + mousePosition: any; + onNodesChange: OnNodesChange; + onEdgesChange: OnEdgesChange; + onConnect: OnConnect; + setNodes: (nodes: AppNode[]) => void; + setEdges: (edges: Edge[]) => void; + setNodeMenus: (nodeMenus: any[]) => void; + setCandidateNode: (candidateNode: any) => void; + setMousePosition: (mousePosition: any) => void; +}; + +// 这是我们的 useStore hook,我们可以在我们的组件中使用它来获取 store 并调用动作 +// 注意:immer 使用方式是 create()(immer(() => ({}))) +const useStore = create()( + immer( + temporal( + (set, get) => ({ + nodes: [], + edges: [], + candidateNode: null, + nodeMenus: [], + mousePosition: { pageX: 0, pageY: 0, elementX: 0, elementY: 0 }, + onNodesChange: (changes) => { + set({ + nodes: applyNodeChanges(changes, get().nodes), + }); + }, + onEdgesChange: (changes) => { + set({ + edges: applyEdgeChanges(changes, get().edges), + }); + }, + onConnect: (connection) => { + set({ + edges: addEdge(connection, get().edges), + }); + }, + setNodes: (nodes) => { + set({ nodes }); + }, + setEdges: (edges) => { + set({ edges }); + }, + setNodeMenus: (nodeMenus: any) => { + set({ nodeMenus }); + }, + setCandidateNode: (candidateNode) => { + set({ candidateNode }); + }, + setMousePosition: (mousePosition: any) => { + set({ mousePosition }); + } + }), + { + // 偏函数 + partialize: (state) => { + const { nodes, edges } = state; + return { + edges, + nodes, + }; + }, + }, + ), + ), +); + + +export const useUndoRedo = (isTracking = true) => { + const temporalStore = useStore.temporal.getState(); + if (temporalStore.isTracking) { + // 暂停时间旅行机器, + temporalStore.pause(); + } + + return { + ...temporalStore, + record: (callback: () => void) => { + temporalStore.resume(); + callback(); + temporalStore.pause(); + } + } +}; + +export default useStore; \ No newline at end of file diff --git a/packages/x-flow/src/FlowEditor/types.ts b/packages/x-flow/src/FlowEditor/types.ts new file mode 100644 index 000000000..52263e357 --- /dev/null +++ b/packages/x-flow/src/FlowEditor/types.ts @@ -0,0 +1,5 @@ +export type FlowEditorProps = { + nodes: any[] + edges: any[] + nodeMenus: any[] +} \ No newline at end of file diff --git a/packages/x-flow/src/FlowEditor/utils/autoLayoutNodes.ts b/packages/x-flow/src/FlowEditor/utils/autoLayoutNodes.ts new file mode 100644 index 000000000..289dcb634 --- /dev/null +++ b/packages/x-flow/src/FlowEditor/utils/autoLayoutNodes.ts @@ -0,0 +1,68 @@ +import produce from 'immer'; +import dagre from '@dagrejs/dagre'; +import { cloneDeep } from 'lodash-es' + +export const CUSTOM_NODE = 'custom'; +export const CUSTOM_EDGE = 'custom'; + +export const getLayoutByDagre = (originNodes: any[], originEdges: any[]) => { + const dagreGraph = new dagre.graphlib.Graph() + dagreGraph.setDefaultEdgeLabel(() => ({})) + + const nodes = cloneDeep(originNodes).filter(node => !node.parentId && node.type === CUSTOM_NODE) + const edges = cloneDeep(originEdges).filter(edge => !edge.data?.isInIteration) + dagreGraph.setGraph({ + ranker: 'network-simplex', // 节点分层算法,可选:'tight-tree' 'longest-path' 'network-simplex' + rankdir: 'TB', // 图的延展方向,可选: 'TB' | 'BT' | 'LR' | 'RL' + nodesep: 150, // 同层各个节点之间的间距 + ranksep: 150, // 图的各个层次之间的间距 + // align: '', // 节点对齐方式,可选:'UL' | 'UR' | 'DL' | 'DR' | undefined + }); + + nodes.forEach((node) => { + dagreGraph.setNode(node.id, { + width: node.width || 204, + height: node.height || 45, + }); + }); + + edges.forEach((edge) => { + dagreGraph.setEdge(edge.source, edge.target) + }); + + dagre.layout(dagreGraph); + return dagreGraph; +} + +export default (nodes: any, edges: any) => { + const layout = getLayoutByDagre(nodes, edges); + const rankMap: any = {} as Record; + + nodes.forEach((node: any) => { + if (!node.parentId && node.type === CUSTOM_NODE) { + const rank: any = layout.node(node.id).rank! + + if (!rankMap[rank]) { + rankMap[rank] = node + } + else { + if (rankMap[rank].position.y > node.position.y) + rankMap[rank] = node + } + } + }); + + const newNodes = produce(nodes, (draft: any) => { + draft.forEach((node: any) => { + if (!node.parentId && node.type === CUSTOM_NODE) { + const nodeWithPosition = layout.node(node.id) + node.position = { + x: nodeWithPosition.x - (node.width || 204) / 2, + y: nodeWithPosition.y - (node.height || 45) / 2 + (rankMap[nodeWithPosition.rank!].height || 45) / 2, + } + } + }) + }); + + return newNodes; +} diff --git a/packages/x-flow/src/FlowEditor/utils/index.ts b/packages/x-flow/src/FlowEditor/utils/index.ts new file mode 100644 index 000000000..b203ee0bd --- /dev/null +++ b/packages/x-flow/src/FlowEditor/utils/index.ts @@ -0,0 +1,27 @@ +import { customAlphabet } from 'nanoid'; +export const uuid = customAlphabet('0123456789abcdefghijklmnopqrstuvwxyz', 16); + + +export const isMac = () => { + return navigator.userAgent.toUpperCase().includes('MAC') +} + +const specialKeysNameMap: Record = { + ctrl: '⌘', + alt: '⌥', +} + +export const getKeyboardKeyNameBySystem = (key: string) => { + if (isMac()) + return specialKeysNameMap[key] || key + + return key +} + + +export const capitalize = (string: string) => { + if (typeof string !== 'string' || string.length === 0) { + return string; + } + return string.charAt(0).toUpperCase() + string.slice(1); +} \ No newline at end of file diff --git a/packages/x-flow/src/index.ts b/packages/x-flow/src/index.ts new file mode 100644 index 000000000..267339946 --- /dev/null +++ b/packages/x-flow/src/index.ts @@ -0,0 +1,23 @@ +import FlowEditor from './FlowEditor'; +import withProvider from './withProvider'; +import * as defaultWidgets from './widgets'; +export { default as useForm } from './models/useForm'; + + +export type { + default as FR, + Schema, + FRProps, + FormInstance, + FormParams, + FieldParams, + WatchProperties, + SchemaType, + SchemaBase, + ValidateParams, + ResetParams, + RuleItem, + WidgetProps, +} from './type'; + +export default withProvider(FlowEditor, defaultWidgets); diff --git a/packages/x-flow/src/locales/en_US.ts b/packages/x-flow/src/locales/en_US.ts new file mode 100644 index 000000000..42e529c14 --- /dev/null +++ b/packages/x-flow/src/locales/en_US.ts @@ -0,0 +1,27 @@ +export default { + "copy_max_tip": "The maximum number of table items has been reached and cannot be copied", + "copy": "Copy", + "add_item": "Add a new line", + "confirm_delete": "Are you sure to delete?", + "confirm": "Yes", + "cancel": "No", + "operate": "Operate", + "delete": "Delete", + "edit": "Edit", + "img_src_error": "Image address error", + "upload": "Upload", + "upload_success": "upload success", + "upload_fail": "upload failed", + "uploaded_address": "Uploaded address", + "test_src": "Test address", + "schema_not_match": "Schema does not match the display component:", + "item": "Item", + "search": "Search", + "reset": "Reset", + "expand": "Expand", + "fold": "Fold", + "submit": "Submit", + "save": "Save", + "moveDown": "Move Down", + "moveUp": "Move Up" +} \ No newline at end of file diff --git a/packages/x-flow/src/locales/index.ts b/packages/x-flow/src/locales/index.ts new file mode 100644 index 000000000..0e44ddda6 --- /dev/null +++ b/packages/x-flow/src/locales/index.ts @@ -0,0 +1,7 @@ +import enUS from './en_US'; +import zhCN from './zh_CN'; + +export default { + 'en-US': enUS, + 'zh-CN': zhCN, +} \ No newline at end of file diff --git a/packages/x-flow/src/locales/zh_CN.ts b/packages/x-flow/src/locales/zh_CN.ts new file mode 100644 index 000000000..e7dfc3082 --- /dev/null +++ b/packages/x-flow/src/locales/zh_CN.ts @@ -0,0 +1,27 @@ +export default { + "copy_max_tip": "已达表单项数量上限,无法复制!", + "copy": "复制", + "add_item": "新增一条", + "confirm_delete": "确定删除?", + "confirm": "确定", + "cancel": "取消", + "operate": "操作", + "delete": "删除", + "edit": "编辑", + "img_src_error": "图片地址错误", + "upload": "上传", + "upload_success": "上传成功", + "upload_fail": "上传失败", + "uploaded_address": "已上传地址", + "test_src": "测试链接", + "schema_not_match": "schema未匹配到展示组件:", + "item": "项目", + "search": "查询", + "reset": "重置", + "expand": "展开", + "fold": "收起", + "submit": "提交", + "save": "保存", + "moveDown": "下移", + "moveUp": "上移" +}; \ No newline at end of file diff --git a/packages/x-flow/src/models/bindValues.ts b/packages/x-flow/src/models/bindValues.ts new file mode 100644 index 000000000..aef74294d --- /dev/null +++ b/packages/x-flow/src/models/bindValues.ts @@ -0,0 +1,175 @@ +import { get, set, unset } from 'lodash-es'; +import { + _cloneDeep, + isArray, + isObject, + safeGet +} from '../utils/index'; + +const isMultiBind = (array: string[]) => isArray(array) && array.every(item => typeof item === 'string'); + +// Need to consider list nested controls +const transformPath = (path: string) => { + const result: string[] = []; + + const recursion = (str: string) => { + const index = str.indexOf('[]'); + if (index === -1) { + result.push(str); + return; + } + result.push(str.substring(0, index)); + recursion(str.substring(index+3)) + }; + + recursion(path); + + if (result.length === 1) { + return result[0]; + } + return result; +}; + +const transformValueToBind = (data: any, path: any, bind: false | string | string[]) => { + if (bind === false) { + unset(data, path); + return; + } + + if (typeof bind === 'string') { + let value = get(data, path); + const preValue = get(data, bind); + if (isObject(preValue)) { + value = { ...preValue, ...value }; + } + set(data, bind, value); + unset(data, path); + return; + } + + // The array is converted to multiple fields. + if (isMultiBind(bind)) { + const value = get(data, path); + unset(data, path); + + if (Array.isArray(value)) { + value.forEach((item, index) => { + const bindPath = bind[index]; + bindPath && set(data, bindPath, item); + }); + } + } +} + +const transformBindToValue = (data: any, path: any, bind: any) => { + if (typeof bind === 'string') { + let value = get(data, bind); + const preValue = get(data, path); + if (isObject(preValue)) { + value = { ...preValue, ...value }; + } + set(data, path, value); + unset(data, bind); + return; + } + + // The array is converted to multiple fields. + if (isMultiBind(bind)) { + const value = []; + bind.forEach(key => { + const bindValue = get(data, key); + // if (bindValue != undefined) { + // value.push(bindValue); + // } + value.push(bindValue); + unset(data, key); + }); + + if (value.length > 0) { + set(data, path, value); + } + } +} + + +export const parseValuesToBind = (values: any, flatten: any) => { + // No bind field exists, no processing + if (!JSON.stringify(flatten).includes('bind')) { + return values; + } + + const data = _cloneDeep(values); + + const dealFieldList = (obj: any, [path, ...rest]: any, bind: any) => { + if (rest.length === 1) { + const list = get(obj, path, [])||[]; + list.forEach((item: any, index: number) => { + const value = get(item, rest[0]); + if (bind === 'root') { + list[index] = value; + return; + } + transformValueToBind(item, rest[0], bind); + }); + } + + if (isArray(obj)) { + obj.forEach((item: any) => dealFieldList(item, [path, ...rest], bind)); + } else if (isObject(obj)) { + const value = get(obj, path); + dealFieldList(value, rest, bind); + } + }; + + Object.keys(flatten).forEach(key => { + const bind = flatten[key]?.schema?.bind; + if (bind === undefined) { + return; + } + const path = transformPath(key); + isArray(path) ? dealFieldList(data, path, bind) : transformValueToBind(data, path, bind); + }); + + return data; +}; + +export const parseBindToValues = (values: any, flatten: any) => { + if (!JSON.stringify(flatten).includes('bind')) { + return values; + } + + const data = _cloneDeep(values); + const dealFieldList = (obj: any, [path, ...rest]: any, bind: any) => { + if (rest.length === 1) { + const list = safeGet(obj, path, []); + list.forEach((item: any, index: number) => { + if (bind === 'root') { + list[index] = { [rest[0]] : item }; + return; + } + transformBindToValue(item, rest[0], bind); + }); + } + + if (isArray(obj)) { + obj.forEach((item: any) => dealFieldList(item, [path, ...rest], bind)); + } else if (isObject(obj)) { + const value = get(obj, path); + dealFieldList(value, rest, bind); + } + }; + + Object.keys(flatten).forEach(key => { + const bind = flatten[key]?.schema?.bind; + if (bind === undefined) { + return; + } + const path = transformPath(key); + + isArray(path) ? dealFieldList(data, path, bind) : transformBindToValue(data, path, bind); + }); + + return data; +}; + + diff --git a/packages/x-flow/src/models/context.ts b/packages/x-flow/src/models/context.ts new file mode 100644 index 000000000..1bd2251ff --- /dev/null +++ b/packages/x-flow/src/models/context.ts @@ -0,0 +1,5 @@ +import { createContext } from 'react'; + +export const FRContext = createContext(null); + +export const ConfigContext = createContext(null); \ No newline at end of file diff --git a/packages/x-flow/src/models/expression.ts b/packages/x-flow/src/models/expression.ts new file mode 100644 index 000000000..d4987f513 --- /dev/null +++ b/packages/x-flow/src/models/expression.ts @@ -0,0 +1,163 @@ +import { get } from 'lodash-es'; +import { isObject, _cloneDeep, isArray } from '../utils/index'; +import { createDataSkeleton } from './formDataSkeleton'; + +export const isExpression = (str: string) => { + if (typeof str !== 'string') { + return false; + } + + const pattern = /^{\s*{(.+)}\s*}$/s; + const reg1 = /^{\s*{function\(.+}\s*}$/; + return str.match(pattern) && !str.match(reg1); +} + +export const isHasExpression = (schema: any) => { + const result = Object.keys(schema).some((key: string) => { + const item = schema[key]; + + // 子协议不做递归确认 + if (key === 'properties') { + return false; + } + + const recursionArray = (list: any[]) => { + const result = list.some(ite => { + if (isArray(ite)) { + return recursionArray(ite); + } + + if (isObject(ite)) { + return isHasExpression(ite); + } + return isExpression(ite); + }); + return result; + }; + + if (isArray(item)) { + return recursionArray(item); + } + + if (isObject(item)) { + return isHasExpression(item); + } + + return isExpression(item); + }); + + return result; +}; + +const parseFunc = (funcBody: string) => { + const funcBodyTemp = funcBody.replace(/(\.|\?\.)/g, '?.'); // 将. 和 ?. 统一替换为?. + const funcBodyStr = funcBodyTemp.replace(/(\d+)\?\.(\d+)/g, '$1.$2'); // 排除数字中的?. + const result = [...funcBodyStr].reduce((acc, char, index) => { + if (char === '[') { + if (index > 0 && funcBodyStr[index - 1] !== '\n') { + // 排除开头[] + return `${acc}?.${char}`; + } + } + return `${acc}${char}`; + }, ''); + return result; +}; + +export const parseExpression = ( + func: any, + formData = {}, + parentPath: string | [] +) => { + const parentData = get(formData, parentPath) || {}; + + if (typeof func === 'string') { + const funcBody = func + .replace(/^{\s*{/g, '') + .replace(/}\s*}$/g, '') + .trim(); + let isHandleData = + funcBody?.startsWith('formData') || funcBody?.startsWith('rootValue'); + + let funcBodyStr = isHandleData ? parseFunc(funcBody) : funcBody; + + const funcStr = ` + return ${funcBodyStr + .replace(/formData/g, JSON.stringify(formData)) + .replace(/rootValue/g, JSON.stringify(parentData))} + `; + try { + const result = Function(funcStr)(); + return result; + } catch (error) { + console.log(error, funcStr, parentPath); + return null; // 如果计算有错误,return null 最合适 + } + } + + return func; +} + +export function getRealDataPath(path) { + if (typeof path !== 'string') { + throw Error(`id ${path} is not a string!!! Something wrong here`); + } + + if (path.match(/[$]void_[^.]+$/)) { + return undefined; + } + + return path.replace(/[$]void_[^.]+./g, ''); +} + +export function getValueByPath(formData, path) { + if (path === '#' || !path) { + return formData || {}; + } else if (typeof path === 'string') { + const realPath = getRealDataPath(path); + return realPath && get(formData, realPath); + } else { + console.error('path has to be a string'); + } +} + +export const parseAllExpression = (_schema: any, _formData: any, dataPath: string, formSchema?: any) => { + const schema = _cloneDeep(_schema); + let formData = _formData; + if (formSchema) { + formData = createDataSkeleton(formSchema, formData); + } + + const recursionArray = (list: any[]) => { + const result = list.map(item => { + if (isArray(item)) { + return recursionArray(item); + } + if (isObject(item)) { + return parseAllExpression(item, formData, dataPath); + } + + if (isExpression(item)) { + return parseExpression(item, formData, dataPath); + } + return item; + }); + + return result; + } + + Object.keys(schema).forEach(key => { + const value = schema[key]; + + if (isArray(value)) { + schema[key] = recursionArray(value); + } if (isObject(value) && (value.mustacheParse ?? true)) { + schema[key] = parseAllExpression(value, formData, dataPath); + } else if (isExpression(value)) { + schema[key] = parseExpression(value, formData, dataPath); + } + }); + + return schema; +}; + diff --git a/packages/x-flow/src/models/fieldShouldUpdate.ts b/packages/x-flow/src/models/fieldShouldUpdate.ts new file mode 100644 index 000000000..2d403740d --- /dev/null +++ b/packages/x-flow/src/models/fieldShouldUpdate.ts @@ -0,0 +1,80 @@ +import { parseExpression } from './expression'; + +// 提取 formData. 开头的字符串 +const extractFormDataStrings = (list: string[]) => { + let result = []; + list.forEach(str => { + // TODO: 为啥要拆开来获取? + // const regex = /formData.\w+(.\w+)*(\(.*\))?/g; // 匹配formData.后面跟着字母、数字、下划线间隔的组合 + const regex = /formData(\.\w+|\[\w+\])(\.\w+|\[\w+\])*/g; // 1.同时匹配两种格式 + const matches = str.match(regex); + if (matches) { + result = result.concat( + matches + ); + } + }); + + return result; +}; + +// 提取 rootValue. 开头的字符串 +const extractRootValueStrings = (list: string[]) => { + let result = []; + list.forEach(str => { + // const regex = /rootValue.\w+(.\w+)*(\(.*\))?/g; // 匹配formData.后面跟着字母、数字、下划线间隔的组合 + const regex = /rootValue(\.\w+|\[\w+\])(\.\w+|\[\w+\])*/g; // 1.同时匹配两种格式 + const matches = str.match(regex); + if (matches) { + result = result.concat( + matches + ); + } + }); + return result; +}; + +// 提取 {{ }} 里面的内容 +const findStrList = (str: any, type: string) => { + const regex = /{{(.*?)}}/g; + const matches = []; + let match; + while ((match = regex.exec(str)) !== null) { + matches.push(match[1]); + }; + + if (type === 'formData') { + return extractFormDataStrings(matches); + } + + if (type === 'rootValue') { + return extractRootValueStrings(matches); + } + return []; +}; + +const getListEveryResult = (list: string[], preValue: any, nextValue: any, dataPath: string) => { + return list.every(item => { + const pre = parseExpression(item, preValue, dataPath); + const curr = parseExpression(item, nextValue, dataPath); + return pre === curr; + }); +}; + +export default (str: string, dataPath: string, dependencies: any[], shouldUpdateOpen: boolean) => (preValue: any, nextValue: any) => { + // dependencies 先不处理 + if (dependencies) { + return true; + } + + const formDataList = findStrList(str, 'formData'); + const rootValueList = findStrList(str, 'rootValue'); + const formDataRes = getListEveryResult(formDataList, preValue, nextValue, dataPath); + const rootValueRes = getListEveryResult(rootValueList, preValue, nextValue, dataPath); + + if (formDataRes && rootValueRes) { + return false; + } + + return true; + }; diff --git a/packages/x-flow/src/models/filterValuesHidden.ts b/packages/x-flow/src/models/filterValuesHidden.ts new file mode 100644 index 000000000..1ca7c62c7 --- /dev/null +++ b/packages/x-flow/src/models/filterValuesHidden.ts @@ -0,0 +1,74 @@ +import { isObject, isArray } from '../utils'; + +const transformHidden = (str: any, formData = {}, parentData = {}) => { + if (typeof str !== 'string') { + return !!str; + } + + const funcBody = str.replace(/^{\s*{/g, '').replace(/}\s*}$/g, '').trim(); + const funcStr = ` + return ${funcBody + .replace(/formData/g, JSON.stringify(formData)) + .replace(/rootValue/g, JSON.stringify(parentData))} + `; + try { + const result = Function(funcStr)(); + return result; + } catch (error) { + return false; + } +}; + +/** + * 过滤 field.schema.hidden = true,的值 + */ +export default (_values: any, flattenSchema: object) => { + + const recursiveArray = (list: any[], _path: string) => { + return list.map(item => { + if (isObject(item)) { + return recursiveObj(item, _path, item); + } + return item; + }); + }; + + const recursiveObj = (obj: any, prePath?: string, parentData?: any) => { + + for (let key of Object.keys(obj)) { + const item = obj[key]; + let path = prePath ? `${prePath}.${key}` : key; + let schema = flattenSchema[path]?.schema; + + if (isArray(item) && !schema) { + path = prePath ? `${prePath}.${key}[]` : `${key}[]`; + schema = flattenSchema[path]?.schema; + } + + // 剔除隐藏数据 + if (schema?.hidden) { + const hidden = transformHidden(schema.hidden, _values, parentData); + if (hidden) { + obj[key] = undefined; + continue; + } + } + + if (isObject(item)) { + obj[key] = recursiveObj(item, path, parentData); + continue; + } + + if (isArray(item) && schema?.items) { + obj[key] = recursiveArray(item, path) || []; + continue; + } + + obj[key] = item; + } + + return obj; + }; + + return recursiveObj(_values) || {}; +} \ No newline at end of file diff --git a/packages/x-flow/src/models/filterValuesUndefined.ts b/packages/x-flow/src/models/filterValuesUndefined.ts new file mode 100644 index 000000000..735fb179b --- /dev/null +++ b/packages/x-flow/src/models/filterValuesUndefined.ts @@ -0,0 +1,51 @@ +import { isUndefined, omitBy } from 'lodash-es'; +import { isObject, isArray } from '../utils'; + +export default (values: any, notFilter?: boolean) => { + const recursiveArray = (list: any[]) => { + let result = list.map(item => { + if (isObject(item)) { + return recursiveObj(item, false); + } + if (isArray(item)) { + return recursiveArray(item); + } + return item; + }); + if (Object.keys(result).length === 0) { + return undefined; + } + return result; + }; + + const recursiveObj = (_obj: any, filter = true) => { + if (_obj._isAMomentObject) { + return _obj; + } + + let obj = omitBy(_obj, isUndefined); + Object.keys(obj).forEach(key => { + const item = obj[key]; + + if (isObject(item)) { + obj[key] = recursiveObj(item); + } + + if (isArray(item)) { + const data = recursiveArray(item); + obj[key] = data; + if (!notFilter && data) { + obj[key] = data.filter((item: any) => item !== undefined); + } + } + }); + + obj = omitBy(obj, isUndefined); + if (Object.keys(obj).length === 0 && filter) { + return undefined; + } + return obj; + }; + + return recursiveObj(values) || {}; +}; \ No newline at end of file diff --git a/packages/x-flow/src/models/flattenSchema.ts b/packages/x-flow/src/models/flattenSchema.ts new file mode 100644 index 000000000..411b90e27 --- /dev/null +++ b/packages/x-flow/src/models/flattenSchema.ts @@ -0,0 +1,87 @@ +import { _cloneDeep, isObjType, isListType } from '../utils/index'; +import sortProperties from './sortProperties'; + +export const getKeyFromPath = (path = '#') => { + try { + const arr = path.split('.'); + const last = arr.slice(-1)[0]; + const result = last.replace('[]', ''); + return result; + } catch (error) { + console.error(error, 'getKeyFromPath'); + return ''; + } +}; + +export function getSchemaFromFlatten(flatten: any, path = '#') { + let schema: any = {}; + const item = _cloneDeep(flatten[path]); + + if (!item) { + return schema; + } + + schema = item.schema; + // schema.$id && delete schema.$id; + if (item.children.length > 0) { + item.children.forEach((child: any) => { + if (!flatten[child]) return; + const key = getKeyFromPath(child); + if (isObjType(schema)) { + schema.properties[key] = getSchemaFromFlatten(flatten, child); + } + if (isListType(schema)) { + schema.items.properties[key] = getSchemaFromFlatten(flatten, child); + } + }); + } + + return schema; +} + +// TODO: more tests to make sure weird & wrong schema won't crush +export function flattenSchema(_schema = {}, name?: any, parent?: any, _result?: any) { + // 排序 + // _schema = orderBy(_schema, item => item.order, ['asc']); + + const result = _result || {}; + + const schema: any = _cloneDeep(_schema) || {}; + let _name = name || '#'; + if (!schema.$id) { + schema.$id = _name; // path as $id, for easy access to path in schema + } + const children: any[] = []; + if (isObjType(schema)) { + sortProperties(Object.entries(schema.properties)).forEach( + ([key, value]) => { + const _key = isListType(value) ? key + '[]' : key; + const uniqueName = _name === '#' ? _key : _name + '.' + _key; + children.push(uniqueName); + + flattenSchema(value, uniqueName, _name, result); + } + ); + + schema.properties = {}; + } + if (isListType(schema)) { + sortProperties(Object.entries(schema.items.properties)).forEach( + ([key, value]) => { + const _key = isListType(value) ? key + '[]' : key; + const uniqueName = _name === '#' ? _key : _name + '.' + _key; + children.push(uniqueName); + flattenSchema(value, uniqueName, _name, result); + } + ); + + schema.items.properties = {}; + } + + if (schema.type) { + result[_name] = { parent, schema, children }; + } + + return result; +} + diff --git a/packages/x-flow/src/models/formCoreUtils.ts b/packages/x-flow/src/models/formCoreUtils.ts new file mode 100644 index 000000000..fc42ba022 --- /dev/null +++ b/packages/x-flow/src/models/formCoreUtils.ts @@ -0,0 +1,188 @@ +import { isObject, isArray, _get, _has, isFunction, isObjType } from '../utils'; + +const executeCallBack = (watchItem: any, value: any, path: string, index?: any) => { + if (isFunction(watchItem)) { + try { + watchItem(value, index); + } catch (error) { + console.log(`${path}对应的watch函数执行报错:`, error); + } + } + + if (isFunction(watchItem?.handler)) { + try { + watchItem.handler(value, index); + } catch (error) { + console.log(`${path}对应的watch函数执行报错:`, error); + } + } +}; + +const traverseValues = ({ changedValues, allValues, flatValues }) => { + + const traverseArray = (list: any[], fullList: any, path: string, index: number[]) => { + if (!list.length) { + return + } + + const _path = path += '[]'; + const filterLength = list.filter(item => (item || item === undefined)).length; + + let flag = filterLength !== fullList.length || list.length === 1; + let isRemove = false; + if (filterLength > 1 && filterLength < fullList.length) { + flag = false; + isRemove = true; + } + + list.forEach((item: any, idx: number) => { + if (!isRemove) { + flatValues[_path] = { value: fullList[idx], index }; + } + if (isObject(item)) { + traverseObj(item, fullList[idx], _path, [...index, idx], !flag); + } + if (isArray(item)) { + traverseArray(item, fullList[idx], _path, [...index, idx]); + } + }); + }; + + const traverseObj = (obj: any, fullObj: any, path: string, index: number[], flag?: boolean) => { + Object.keys(obj).forEach((key: string) => { + const item = obj[key]; + const fullItem = fullObj?.[key]; + let value = item; + + const _path = path ? (path + '.' + key) : key; + + let last = true; + + if (isArray(item)) { + value = fullItem ? [...fullItem] : fullItem; + last = false; + traverseArray(item, fullItem, _path, index); + } + + if (isObject(item)) { + last = false; + traverseObj(item, fullItem, _path, index, flag); + } + + if (!last || !flag) { + flatValues[_path] = { value, index }; + } + }); + }; + + traverseObj(changedValues, allValues, null, []); +}; + +export const valuesWatch = (changedValues: any, allValues: any, watch: any) => { + if (Object.keys(watch || {})?.length === 0) { + return; + } + + const flatValues = { + '#': { value: allValues, index: changedValues } + }; + + traverseValues({ changedValues, allValues, flatValues }); + + Object.keys(watch).forEach(path => { + if (!_has(flatValues, path)) { + return; + } + const { value, index } = _get(flatValues, path) as { value: any; index: any; }; + const item = watch[path]; + executeCallBack(item, value, path, index) + }); +}; + +export const transformFieldsData = (_fieldsError: any, getFieldName: any) => { + let fieldsError = _fieldsError; + if (isObject(fieldsError)) { + fieldsError = [fieldsError]; + } + + if (!(isArray(fieldsError) && fieldsError.length > 0)) { + return; + } + + return fieldsError.map((field: any) => ({ errors: field.error, ...field, name: getFieldName(field.name) })); +}; + +export const immediateWatch = (watch: any, values: any) => { + if (Object.keys(watch || {})?.length === 0) { + return; + } + + const watchObj = {}; + Object.keys(watch).forEach(key => { + const watchItem = watch[key]; + if (watchItem?.immediate && isFunction(watchItem?.handler)) { + watchObj[key] = watchItem; + } + }); + + valuesWatch(values, values, watchObj); +}; + +export const getSchemaFullPath = (path: string, schema: any) => { + if (!path || !path.includes('.')) { + return 'properties.' + path; + } + + // 补全 list 类型 path 路径 + while(path.includes('[]')) { + const index = path.indexOf('[]'); + path = path.substring(0, index) + '.items' + path.substring(index + 2); + } + + // 补全 object 类型 path 路径 + let result = 'properties'; + const pathList = path.split('.'); + pathList.forEach((item, index) => { + const key = result + '.' + item; + const itemSchema = _get(schema, key, {}); + if (isObjType(itemSchema) && index !== pathList.length-1) { + result = key + '.properties'; + return ; + } + result = key; + }); + + return result; +}; + +export function yymmdd(timeStamp) { + const date_ob = new Date(Number(timeStamp)); + const adjustZero = num => ('0' + num).slice(-2); + let day = adjustZero(date_ob.getDate()); + let month = adjustZero(date_ob.getMonth()); + let year = date_ob.getFullYear(); + let hours = adjustZero(date_ob.getHours()); + let minutes = adjustZero(date_ob.getMinutes()); + let seconds = adjustZero(date_ob.getSeconds()); + return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; +} + +export function msToTime(duration) { + let seconds: any = Math.floor((duration / 1000) % 60); + let minutes: any = Math.floor((duration / (1000 * 60)) % 60); + let hours: any = Math.floor((duration / (1000 * 60 * 60)) % 24); + + hours = hours < 10 ? '0' + hours : hours; + minutes = minutes < 10 ? '0' + minutes : minutes; + seconds = seconds < 10 ? '0' + seconds : seconds; + return hours + ':' + minutes + ':' + seconds; +} + +export const getSessionItem = (key: string) => { + return Number(sessionStorage.getItem(key) || 0); +} + +export const setSessionItem = (key: string, data: any) => { + sessionStorage.setItem(key, data +''); +} + diff --git a/packages/x-flow/src/models/formDataSkeleton.ts b/packages/x-flow/src/models/formDataSkeleton.ts new file mode 100644 index 000000000..30ffdc4fd --- /dev/null +++ b/packages/x-flow/src/models/formDataSkeleton.ts @@ -0,0 +1,31 @@ +import { _cloneDeep, isObjType, isListType } from '../utils/index'; + +export const createDataSkeleton = (schema: any, formData?: any) => { + let _formData = _cloneDeep(formData); + let result = _formData; + + if (isObjType(schema)) { + if (_formData === undefined || typeof _formData !== 'object') { + _formData = {}; + result = {}; + } + Object.keys(schema.properties).forEach(key => { + const childSchema = schema.properties[key]; + const childData = _formData[key]; + const childResult = createDataSkeleton(childSchema, childData); + result[key] = childResult; + }); + } else if (_formData !== undefined) { + // result = _formData; + } else if (schema.default !== undefined) { + result = _cloneDeep(schema.default); + } else if (isListType(schema)) { + result = [createDataSkeleton(schema.items)]; + } else if (schema.type === 'boolean' && !schema.widget) { + // result = false; + result = undefined; + } else { + result = undefined; + } + return result; +}; \ No newline at end of file diff --git a/packages/x-flow/src/models/layout.ts b/packages/x-flow/src/models/layout.ts new file mode 100644 index 000000000..5f8d8322b --- /dev/null +++ b/packages/x-flow/src/models/layout.ts @@ -0,0 +1,73 @@ +export const getFormItemLayout = (column: number, schema: any, { labelWidth, displayType, _labelCol, _fieldCol }: any) => { + let labelCol: any = { span: 5 }; + let fieldCol: any = { span: 9 }; + + if (column === 2) { + labelCol = { span: 6 }; + fieldCol = { span: 14 } + } + + if (column > 2) { + labelCol = { span: 7 }; + fieldCol = { span: 16 } + } + + if (displayType === 'column') { + // labelCol = { xl: 9, xxl: 6 }; + // if (column > 1) { + // labelCol = {}; + // fieldCol = {}; + // } + labelCol = {}; + fieldCol = {}; + } + + if (_labelCol) { + labelCol = _labelCol; + if (displayType === 'column') { + labelCol = {}; + } + } + + if (_fieldCol) { + fieldCol = _fieldCol; + if (typeof _fieldCol === 'number') { + fieldCol = { span: _fieldCol } + } + } + + if (displayType === 'inline') { + labelCol = {}; + fieldCol = {}; + } + + // 兼容一下 1.0 版本 + if ((labelWidth || labelWidth === 0) && displayType !== 'column') { + labelCol = { flex : labelWidth + 'px' }; + fieldCol = { flex: 'auto' }; + } + + // 自定义进行覆盖 + if (schema.cellSpan) { + fieldCol = {}; + } + + + if (schema.labelCol || schema.labelCol === 0) { + labelCol = schema.labelCol; + } + + if (schema.fieldCol || schema.fieldCol === 0) { + fieldCol = schema.fieldCol; + } + + if (typeof labelCol === 'number') { + labelCol = { span: labelCol } + } + + if (typeof fieldCol === 'number') { + fieldCol = { span: fieldCol } + } + + return { labelCol, fieldCol } +} \ No newline at end of file diff --git a/packages/x-flow/src/models/mapping.tsx b/packages/x-flow/src/models/mapping.tsx new file mode 100644 index 000000000..24905c4dc --- /dev/null +++ b/packages/x-flow/src/models/mapping.tsx @@ -0,0 +1,134 @@ +export const mapping = { + default: 'input', + string: 'input', + array: 'list', + boolean: 'checkbox', + integer: 'number', + number: 'inputNumber', + object: 'map', + html: 'html', + card: 'card', + collapse: 'collapse', + lineTitle: 'lineTitle', + line: 'line', + subItem: 'subItem', + panel: 'panel', + 'string:upload': 'upload', + 'string:url': 'urlInput', + 'string:dateTime': 'datePicker', + 'string:date': 'datePicker', + 'string:year': 'datePicker', + 'string:month': 'datePicker', + 'string:week': 'datePicker', + 'string:quarter': 'datePicker', + 'string:time': 'timePicker', + 'string:textarea': 'textArea', + 'string:color': 'color', + 'string:image': 'imageInput', + 'range:time': 'timeRange', + 'range:dateTime': 'dateRange', + 'range:date': 'dateRange', + 'range:year': 'dateRange', + 'range:month': 'dateRange', + 'range:week': 'dateRange', + 'range:quarter': 'dateRange', + '*?enum': 'radio', + '*?enum_long': 'select', + 'array?enum': 'checkboxes', + 'array?enum_long': 'multiSelect', + '*?readOnly': 'html', // TODO: html widgets for list / object +}; + +export function getWidgetName(schema, _mapping = mapping) { + const { type, format, enum: enums, readOnly, widget, props } = schema; + + //如果已经注明了渲染widget,那最好 + if (schema['ui:widget'] || schema.widget) { + return schema['ui:widget'] || schema.widget; + } + + const list: string[] = []; + if (readOnly) { + list.push(`${type}?readOnly`); + list.push('*?readOnly'); + } + + if (enums) { + // 根据 enum 长度来智能选择控件 + if ( + Array.isArray(enums) && + ((type === 'array' && enums.length > 6) || + (type !== 'array' && enums.length > 2)) + ) { + list.push(`${type}?enum_long`); + list.push('*?enum_long'); + } else { + list.push(`${type}?enum`); + // array 默认使用 list,array?enum 默认使用 checkboxes,*?enum 默认使用select + list.push('*?enum'); + } + } + + if (props?.options) { + if ((type === 'array' && props.options.length > 6) || (type !== 'array' && props.options.length > 2)) { + + list.push(`${type}?enum_long`); + list.push('*?enum_long'); + } else { + list.push(`${type}?enum`); + // array 默认使用 list,array?enum 默认使用 checkboxes,*?enum 默认使用select + list.push('*?enum'); + } + } + + const _widget = format; + if (_widget) { + list.push(`${type}:${_widget}`); + } + + if (type === 'object') { + list.push((schema.theme === 'tile' ? 'lineTitle' : schema.theme) || 'collapse'); + } else { + list.push(type); // 放在最后兜底,其他都不match时使用type默认的组件 + } + + let widgetName = ''; + list.some(item => { + widgetName = _mapping[item]; + return !!widgetName; + }); + + return widgetName; +} + + +function capitalizeFirstLetter(str: any) { + if (!str) { + return str; + } + return str.charAt(0).toUpperCase() + str.slice(1); +} + +export const getWidget = (name: string, widgets: any) => { + let widget = widgets[name]; + + // name 转成首字母大写 + if (!widget) { + widget = widgets[capitalizeFirstLetter(name)]; + } + + if (!widget) { + widget = widgets['Html'] || null; + } + + return widget; +} + +export const extraSchemaList = { + checkbox: { + valuePropName: 'checked', + }, + switch: { + valuePropName: 'checked', + }, +}; diff --git a/packages/x-flow/src/models/sortProperties.ts b/packages/x-flow/src/models/sortProperties.ts new file mode 100644 index 000000000..1c0263d87 --- /dev/null +++ b/packages/x-flow/src/models/sortProperties.ts @@ -0,0 +1,23 @@ +export default (properties, orderKey = 'order') => { + const orderHash = new Map(); + // order不为数字的数据 + const unsortedList: any[] = []; + const insert = (item: any) => { + const [, value] = item; + if (typeof value[orderKey] !== 'number') { + unsortedList.push(item); + return; + } + if (orderHash.has(value[orderKey])) { + orderHash.get(value[orderKey]).push(item); + } else { + orderHash.set(value[orderKey], [item]); + } + }; + + properties.forEach(item => insert(item)); + const sortedList = Array.from(orderHash.entries()) + .sort(([order1], [order2]) => order1 - order2) // order值越小越靠前 + .flatMap(([, items]) => items); + return sortedList.concat(unsortedList); +} \ No newline at end of file diff --git a/packages/x-flow/src/models/store.ts b/packages/x-flow/src/models/store.ts new file mode 100644 index 000000000..814bb7490 --- /dev/null +++ b/packages/x-flow/src/models/store.ts @@ -0,0 +1,27 @@ +import { createStore as createx } from 'zustand'; + +type FormStore = { + schema?: any; + flattenSchema: any; + context?: any; + initialized: boolean, + init?: (schema: FormStore['schema']) => any; + setContext: (context: any) => any; +}; + +// 将 useStore 改为 createStore, 并把它改为 create 方法 +export const createStore = () => createx((setState: any, get: any) => ({ + initialized: false, + schema: {}, + flattenSchema: {}, + context: {}, + init: data => { + return setState({ + initialized: true, + ...data + }); + }, + setContext: context => { + return setState({ context }); + } +})); \ No newline at end of file diff --git a/packages/x-flow/src/models/transformProps.ts b/packages/x-flow/src/models/transformProps.ts new file mode 100644 index 000000000..4c9bd956a --- /dev/null +++ b/packages/x-flow/src/models/transformProps.ts @@ -0,0 +1,85 @@ +const displayTypeEnum = { + column: 'vertical', + row: 'horizontal', + inline: 'inline', +}; + +const transformProps = (props: any) => { + const { + schema, + beforeFinish, + onMount, + displayType = 'column', + watch, + removeHiddenData = true, + readOnly, + column = 1, + mapping, + debugCss, + locale, + configProvider, + validateMessages, + debug, + id, + labelWidth, + maxWidth, + form, + onFinish, + onFinishFailed, + footer, + operateExtra, + logOnMount, + logOnSubmit, + labelCol, + fieldCol, + disabled, + className, + validateTrigger, + antdVersion, + ...otherProps + } = props; + + const formProps = { + ...otherProps, + }; + + if (displayType) { + formProps.layout = displayTypeEnum[displayType] || 'horizontal'; + } + + return { + formProps, + schema, + displayType, + onFinish, + beforeFinish, // form 没有这个 api, 感觉找不到时机 + onMount, + watch, + readOnly, + disabled, + column, + mapping, + debugCss, // 好像没用了 + locale, + configProvider, + footer, + form, + labelWidth, + validateMessages, + debug, // 换成 form 还有用吗? + id, + onFinishFailed, + removeHiddenData, + operateExtra, + logOnMount, + logOnSubmit, + labelCol, + fieldCol, + maxWidth, + className, + validateTrigger, + antdVersion + }; +}; + +export default transformProps; \ No newline at end of file diff --git a/packages/x-flow/src/models/useForm.ts b/packages/x-flow/src/models/useForm.ts new file mode 100644 index 000000000..d78b36f0b --- /dev/null +++ b/packages/x-flow/src/models/useForm.ts @@ -0,0 +1,360 @@ +import { useRef } from 'react'; +import { Form } from 'antd'; +import { cloneDeep } from 'lodash-es'; + +import { transformFieldsData, getSchemaFullPath } from './formCoreUtils'; +import { parseBindToValues, parseValuesToBind } from './bindValues'; +import { _isMatch, _set, _get, _has, _merge, _mergeWith, isFunction, isObject, isArray, _isUndefined, hasFuncProperty } from '../utils'; +import filterValuesUndefined from './filterValuesUndefined'; +import filterValuesHidden from './filterValuesHidden'; +import { flattenSchema as flatten } from './flattenSchema'; +import type { FormInstance } from '../type'; + +const updateSchemaByPath = (_path: string, _newSchema: any, formSchema: any) => { + const path = getSchemaFullPath(_path, formSchema); + const currSchema = _get(formSchema, path, {}); + const newSchema = isFunction(_newSchema) ? _newSchema(currSchema) : _newSchema; + + const result = { + ...currSchema, + ...newSchema, + } + + if (newSchema.props) { + result.props = { + ...currSchema?.props, + ...newSchema.props + } + } + + _set(formSchema, path, result); +}; + +const getFieldName = (_path: any): any => { + if (!_path) { + return undefined; + } + + if (typeof _path === 'boolean') { + return _path; + } + + let result: any[] = []; + + if (isArray(_path)) { + result = _path.map((item: any) => { + return item.split('.').map((ite: any) => { + if (!isNaN(Number(ite))) { + return ite * 1; + } + return ite; + }); + }); + } + + result = _path.split('.').map((item: any) => { + if (!isNaN(Number(item))) { + return item * 1; + } + return item; + }); + + result = result.map(item => { + if (typeof item === 'string' && item?.indexOf('[') === 0 && item?.indexOf(']') === item?.length -1) { + return Number(item.substring(1, item.length-1)); + } + return item; + }); + + return result; +}; + +const useForm = () => { + const [form] = Form.useForm(); + + const flattenSchemaRef = useRef({}); + const storeRef: any = useRef(); + const schemaRef = useRef({}); + const fieldRefs = useRef({}); + + const { + getFieldError, + getFieldsError, + getFieldInstance, + setFieldsValue, + setFields, + scrollToField, + isFieldsTouched, + isFieldTouched, + isFieldValidating, + resetFields, + validateFields, + ...otherForm + } = form; + + const xform: any = otherForm; + + const setStoreData = (data: any) => { + const { setState } = storeRef.current; + + if (!setState) { + setTimeout(() => { + setState({ schema: schemaRef.current, flattenSchema: flattenSchemaRef.current }); + }, 0); + } + setState(data); + }; + + // 更新协议 + const handleSchemaUpdate = (newSchema: any) => { + // form.__schema = Object.freeze(newSchema); + flattenSchemaRef.current = flatten(newSchema) || {}; + schemaRef.current = newSchema; + setStoreData({ schema: newSchema, flattenSchema: flattenSchemaRef.current }); + }; + + // 设置协议 + xform.setSchema = (obj: any, cover = false) => { + if (!isObject(obj)) { + return; + } + + if (cover) { + handleSchemaUpdate(obj); + return; + } + + const schema = cloneDeep(schemaRef.current); + Object.keys(obj || {}).forEach(path => { + updateSchemaByPath(path, obj[path], schema); + }); + + handleSchemaUpdate(schema); + } + + // 设置某个字段的协议 + xform.setSchemaByPath = (_path: string, _newSchema: any) => { + // diff 判断是否需要更新,存在函数跳过 + if (!hasFuncProperty(_newSchema) && _isMatch(_newSchema, xform.getSchemaByPath(_path))) { + return; + } + + const schema = cloneDeep(schemaRef.current); + updateSchemaByPath(_path, _newSchema, schema); + handleSchemaUpdate(schema); + } + + // form.setSchemaByFullPath = (path: string, newSchema: any) => { + // const schema = _cloneDeep(schemaRef.current); + // const currSchema = _get(schema, path, {}); + + // const result = _mergeWith(currSchema, newSchema, (objValue, srcValue, key) => { + // return srcValue; + // }); + + // _set(schema, path, result); + // handleSchemaUpdate(schema); + // } + + // 设置表单数据 + xform.setValues = (_values: any) => { + const values = parseBindToValues(_values, flattenSchemaRef.current); + setFieldsValue(values); + } + + // 获取表单数据 + xform.getValues = (nameList?: any, filterFunc?: any) => { + let values = cloneDeep(form.getFieldsValue(getFieldName(nameList), filterFunc)); + const { removeHiddenData } = storeRef.current?.getState() || {}; + if (removeHiddenData) { + values = filterValuesHidden(values, flattenSchemaRef.current); + } + values = filterValuesUndefined(values); + return parseValuesToBind(values, flattenSchemaRef.current); + } + + xform.getValueByPath = (path: string) => { + const name = getFieldName(path); + return form.getFieldValue(name); + } + + // 设置某个字段的值 + xform.setValueByPath = (path: string, value: any) => { + if (!form.setFieldValue) { + const values = form.getFieldsValue(); + _set(values, path, value); + xform.setValues(values); + return; + } + + const name = getFieldName(path); + form.setFieldValue(name, value); + + try { + if (JSON.stringify(form.getFieldValue(name)) !== JSON.stringify(value)) { + form.setFieldValue(name, value); + } + } catch (error) { + + } + } + + // 通过某个字段的 schema + xform.getSchemaByPath = (_path: string) => { + if (typeof _path !== 'string') { + console.warn('请输入正确的路径'); + } + const path = getSchemaFullPath(_path, schemaRef.current); + return _get(schemaRef.current, path); + }; + + // 获取协议 + xform.getSchema = () => { + return schemaRef.current; + }; + + // 设置一组字段错误 + xform.setErrorFields = (fieldsError: any[]) => { + const fieldsData = transformFieldsData(fieldsError, getFieldName); + if (!fieldsData) { + return; + } + + setFields(fieldsData); + }; + + // 清空某个字段的错误 + xform.removeErrorField = (path: any) => { + setFields([{ name: getFieldName(path), errors: [] }]); + }; + + // 获取对应字段名的错误信息 + xform.getFieldError = (path: string) => { + const name = getFieldName(path); + return form.getFieldError(name); + } + + // 获取一组字段名对应的错误信息,返回为数组形式 + xform.getFieldsError = (path: string[]) => { + const name = getFieldName(path); + return getFieldsError(name); + } + + // 获取对应字段实例 + xform.getFieldInstance = (path: string) => { + const name = getFieldName(path); + return getFieldInstance(name); + } + + // 获取隐藏字段数据 + xform.getHiddenValues = () => { + const values = xform.getValues(); + const allValues = xform.getValues(true); + const hiddenValues = {}; + + const recursion = (obj1: any, obj2: any, path: any) => { + Object.keys(obj1).forEach((key: string) => { + const value = obj1[key]; + const _path = path ? `${path}.${key}` : key; + if (!obj2.hasOwnProperty(key)) { + _set(hiddenValues, _path, value); + return; + } + + if (isObject(value)) { + recursion(value, obj2[key], _path); + } + + if (isArray(value)) { + value.map((item: any, index: number) => { + recursion(item, _get(obj2, `${key}[${index}]`, []), `${_path}[${index}]`) + }); + } + }); + }; + + recursion(allValues, values, null); + return hiddenValues; + } + + // 设置一组字段状态 + xform.setFields = (nameList: any[]) => { + const fieldsData = transformFieldsData(nameList, getFieldName); + if (!fieldsData) { + return; + } + setFields(fieldsData); + } + + xform.__initStore = (store: any) => { + storeRef.current = store; + } + + // 滚动到对应字段位置 + xform.scrollToPath = (path: string, ...rest: any[]) => { + const name = getFieldName(path); + scrollToField(name, ...rest); + } + + // 检查一组字段是否被用户操作过,allTouched 为 true 时检查是否所有字段都被操作过 + xform.isFieldsTouched = (pathList?: string[], allTouched?: boolean) => { + const nameList = (pathList || []).map(path => getFieldName(path)); + return isFieldsTouched(nameList, allTouched); + } + + // 检查对应字段是否被用户操作过 + xform.isFieldTouched = (path: string) => { + const name = getFieldName(path); + return isFieldTouched(name); + } + + // 检查对应字段是否被用户操作过 + xform.isFieldValidating = (path: string) => { + const name = getFieldName(path); + return isFieldValidating(name); + } + + xform.resetFields = (pathList?: string[]) => { + const nameList = (pathList || []).map(path => getFieldName(path)); + if (nameList.length > 0) { + resetFields(nameList); + } else { + resetFields(); + } + } + + // 触发表单验证 + xform.validateFields = (pathList?: string[], config?: object) => { + const nameList = (pathList || []).map(path => getFieldName(path)); + if (nameList.length > 0) { + return validateFields(nameList, config); + } + return validateFields(); + }; + + + xform.getFlattenSchema = (path?: string) => { + if (!path) { + return flattenSchemaRef.current; + } + return flattenSchemaRef.current?.[path]; + } + + // 老 API 兼容 + xform.onItemChange = xform.setValueByPath; + + xform.setFieldRef = (path: string, ref: any) => { + if (!path) { + return; + } + fieldRefs.current[path] = ref; + } + + xform.getFieldRef = (path: string) => { + return fieldRefs.current[path]; + } + + return xform as FormInstance; +}; + +export default useForm; diff --git a/packages/x-flow/src/models/validateMessage.ts b/packages/x-flow/src/models/validateMessage.ts new file mode 100644 index 000000000..6c22fdb4f --- /dev/null +++ b/packages/x-flow/src/models/validateMessage.ts @@ -0,0 +1,97 @@ +const typeTemplate = "'${label}' is not a valid ${type}"; +const typeTemplateCN = "数据类型必须是 ${type}"; + +export const validateMessagesEN = { + default: "Validation error on field '${label}'", + required: "'${label}' is required", + enum: "'${label}' must be one of [${enum}]", + whitespace: "'${label}' cannot be empty", + date: { + format: "'${label}' is invalid for format date", + parse: "'${label}' could not be parsed as date", + invalid: "'${label}' is invalid date", + }, + types: { + string: typeTemplate, + method: typeTemplate, + array: typeTemplate, + object: typeTemplate, + number: typeTemplate, + date: typeTemplate, + boolean: typeTemplate, + integer: typeTemplate, + float: typeTemplate, + regexp: typeTemplate, + email: typeTemplate, + url: typeTemplate, + hex: typeTemplate, + }, + string: { + len: "'${label}' must be exactly ${len} characters", + min: "'${label}' must be at least ${min} characters", + max: "'${label}' cannot be longer than ${max} characters", + range: "'${label}' must be between ${min} and ${max} characters", + }, + number: { + len: "'${label}' must equal ${len}", + min: "'${label}' cannot be less than ${min}", + max: "'${label}' cannot be greater than ${max}", + range: "'${label}' must be between ${min} and ${max}", + }, + array: { + len: "'${label}' must be exactly ${len} in length", + min: "'${label}' cannot be less than ${min} in length", + max: "'${label}' cannot be greater than ${max} in length", + range: "'${label}' must be between ${min} and ${max} in length", + }, + pattern: { + mismatch: "'${label}' does not match pattern ${pattern}", + }, +}; + +export const validateMessagesCN = { + default: '${label}未通过校验', + required: '${label}必填', + whitespace: '${label}不能为空', + date: { + format: '${label}的格式错误', + parse: '${label}无法被解析', + invalid: '${label}数据不合法', + }, + types: { + string: typeTemplateCN, + method: typeTemplateCN, + array: typeTemplateCN, + object: typeTemplateCN, + number: typeTemplateCN, + date: typeTemplateCN, + boolean: typeTemplateCN, + integer: typeTemplateCN, + float: typeTemplateCN, + regexp: typeTemplateCN, + email: typeTemplateCN, + url: typeTemplateCN, + hex: typeTemplateCN, + }, + string: { + len: '${label}长度不是${len}', + min: '${label}长度不能小于${min}', + max: '${label}长度不能大于${max}', + range: '${label}长度需在${min}与${max}之间', + }, + number: { + len: '${label}不等于${len}', + min: '${label}不能小于${min}', + max: '${label}不能大于${max}', + range: '${label}需在${min}与${max}之间', + }, + array: { + len: '${label}长度不是${len}', + min: '${label}长度不能小于${min}', + max: '${label}长度不能大于${max}', + range: '${label}长度需在${min}与${max}之间', + }, + pattern: { + mismatch: '${label}未通过正则判断${pattern}', + }, +}; diff --git a/packages/x-flow/src/models/validates.ts b/packages/x-flow/src/models/validates.ts new file mode 100644 index 000000000..7df7ea3a3 --- /dev/null +++ b/packages/x-flow/src/models/validates.ts @@ -0,0 +1,138 @@ +import Color from 'color'; +import { isUrl, isObject, isFunction } from '../utils'; +import { cloneDeep } from 'lodash-es'; + +const insertLengthRule = (schema: any, rules: any[]) => { + const { type, max, min, message } = schema; + + if (max || max === 0) { + rules.push({ type, max, message: message?.max }); + } + + if (min || min === 0) { + rules.push({ type, min, message: message?.min }); + } +}; + +const insertRequiredRule = (schema: any, rules: any[]) => { + let { + type, + format, + required, + message, + widget, + title + } = schema; + + const requiredAlready = schema?.rules?.some((item: any) => item?.required); + + // 未声明 required,或已经存在 required 校验 + if (!required || requiredAlready) { + return; + } + + let rule: any = { required: true, message: message?.required }; + + if (['year','quarter', 'month', 'week', 'date', 'dateTime', 'time'].includes(format) && type === 'range') { + rule = { + type: 'array', + required: true, + len: 2, + fields: { + 0: { type: 'string', required: true }, + 1: { type: 'string', required: true }, + } + }; + } else if (widget === 'checkbox') { + rule = { type, required: true, whitespace: true, message: title + '必填' }; + } else if (type === 'string') { + rule = { type: 'string', required: true, whitespace: true, message: message?.required || (!title ? '内容必填' : undefined) }; + } + + rules.push(rule); +}; + +export const transformRules = (rules = [], methods: any, form: any) => { + return rules.map(((item: any) => { + if (item.validator && !item.transformed) { + const validator = isFunction(item.validator) ? item.validator : methods[item.validator]; + item.validator = async (_: any, value: any) => { + const result = await validator(_, value, { form }); + if (isObject(result)) { + return result?.status ? Promise.resolve() : Promise.reject(new Error(result.message || item.message)); + } + return result ? Promise.resolve() : Promise.reject(new Error(item.message)); + };; + item.transformed = true; + } + return item; + })); +}; + +export default (_schema: any, form: any, methods: any, fieldRef: any) => { + const schema = cloneDeep(_schema); + let { + format, + rules: ruleList = [], + pattern, + message, + } = schema; + + const rules: any = [...ruleList]; + + insertRequiredRule(schema, rules); + insertLengthRule(schema, rules); + + rules.push({ + validator: async (_: any) => { + if (!isFunction(fieldRef?.current?.validator)) { + return true; + } + const res = await fieldRef.current?.validator(); + return res; + } + }); + + if (pattern) { + rules.push({ pattern, message: message?.pattern }); + } + + if (format === 'url') { + rules.push({ type: 'url', message: message?.url }); + } + + if (format === 'email') { + rules.push({ type: 'email', message: message?.email }); + } + + if (format === 'image') { + rules.push({ + validator: (_: any, value: any) => { + if (!value) { + return true; + } + const imagePattern = '([/|.|w|s|-])*.(?:jpg|gif|png|bmp|apng|webp|jpeg|json)'; + const _isUrl = isUrl(value); + const _isImg = new RegExp(imagePattern).test(value); + return _isUrl || _isImg; + }, + message: message?.email ?? '请输入正确的图片格式' + }); + } + + if (format === 'color') { + rules.push({ + validator: (_: any, value: any) => { + try { + Color(value || null); // 空字符串无法解析会报错,出现空的情况传 null + return true; + } catch (e) { + return false; + } + }, + message: message?.color ?? '请填写正确的颜色格式' + }); + } + + return transformRules(rules, methods, form); +} \ No newline at end of file diff --git a/packages/x-flow/src/nodes/index.less b/packages/x-flow/src/nodes/index.less new file mode 100644 index 000000000..b4048db05 --- /dev/null +++ b/packages/x-flow/src/nodes/index.less @@ -0,0 +1,41 @@ +.node-container { + border: 2px solid #fff; + border-radius: 14px; + + .react-flow__edge-path, + .react-flow__connection-path { + stroke: #d0d5dc; + stroke-width: 2px; + } +} + +.node-container-selected { + border: 2px solid #296dff; + + .react-flow__handle::after { + display: none; + } +} + +.react-flow__handle { + width: 32px; + height: 32px; + background: transparent; + border-radius: 0; + border: none; + + :hover { + border: 2px solid #00a952; + transform: scale(1.25); + } +} + +.react-flow__handle::after { + content: ''; + --tw-bg-opacity: 1; + background-color: #2970ff; + width: 8px; + height: 2px; + display: block; + margin: 15px 0 0 12px; +} \ No newline at end of file diff --git a/packages/x-flow/src/nodes/index.tsx b/packages/x-flow/src/nodes/index.tsx new file mode 100644 index 000000000..07ffe5e84 --- /dev/null +++ b/packages/x-flow/src/nodes/index.tsx @@ -0,0 +1,2 @@ +export { default as NodeInput } from './node-input'; +export { default as NodeOutput } from './node-output'; \ No newline at end of file diff --git a/packages/x-flow/src/nodes/node-input/index.less b/packages/x-flow/src/nodes/node-input/index.less new file mode 100644 index 000000000..0377a0f36 --- /dev/null +++ b/packages/x-flow/src/nodes/node-input/index.less @@ -0,0 +1,16 @@ +.custom-node-start { + width: 240px; + padding: 0 12px; + background: #fff; + border-radius: 12px; + + .title { + display: flex; + height: 50px; + align-items: center; + span { + font-weight: bold; + margin-left: 8px; + } + } +} \ No newline at end of file diff --git a/packages/x-flow/src/nodes/node-input/index.tsx b/packages/x-flow/src/nodes/node-input/index.tsx new file mode 100644 index 000000000..2dbe4efb2 --- /dev/null +++ b/packages/x-flow/src/nodes/node-input/index.tsx @@ -0,0 +1,20 @@ +import { memo } from 'react'; +import NodeContainer from '../../FlowEditor/components/NodeContainer'; +import { iconSettingMap } from '../constant'; + +export default memo((props: any) => { + const { onClick } = props; + const { icon } = iconSettingMap['Start']; + + return ( + + ); +}); + + diff --git a/packages/x-flow/src/nodes/node-input/setting/index.tsx b/packages/x-flow/src/nodes/node-input/setting/index.tsx new file mode 100644 index 000000000..4754e202b --- /dev/null +++ b/packages/x-flow/src/nodes/node-input/setting/index.tsx @@ -0,0 +1,106 @@ +import FormRender, { useForm } from 'form-render'; +import ExpandInput from '@/components/ExpandInput'; +import { TYPES } from '../../constant'; + +const schema = { + type: 'object', + displayType: 'row', + properties: { + list: { + type: 'array', + widget: 'tableList', + props: { + hideMove: true, + hideCopy: true, + onRemove: 'onRemove', + size: 'small', + addBtnProps: { + type: 'dashed' + }, + pagination: { + pageSize: 15 + }, + actionColumnProps: { + width: 55 + } + }, + items: { + type: 'object', + properties: { + name: { + title: '参数名称', + type: 'string', + width: 200, + placeholder: '请输入', + disabled: `{{ rootValue.name === 'session_id' }}`, + rules: [ + { + pattern: /^[a-zA-Z_][a-zA-Z0-9_]*$/, + message: '只能包含字母、数字和下划线且以字母或划线开头' + } + ] + }, + dataType: { + title: '参数类型', + type: 'string', + enum: TYPES.map((el) => el.toUpperCase()), + enumNames: TYPES, + widget: 'select', + width: 120, + placeholder: '请选择', + disabled: `{{ rootValue.name === 'session_id' }}` + }, + value: { + title: '参数值', + type: 'string', + widget: 'ExpandInput', + placeholder: '变量:${变量名称}', + disabled: `{{ rootValue.name === 'session_id' }}` + } + } + } + } + } +}; + + + + +/** + * + * 全局输入参数配置 + * + */ +export default (props: any) => { + const { data, onChange, readonly } = props; + const form = useForm(); + + const onRemove = (deleteFn: any, params: any) => { + if (params.data?.name === 'session_id') { + props.setIsChatFlow(false); + } + deleteFn(); + }; + + const watch = { + '#': (allValues: any) => { + onChange({ ...data, ...allValues }); + }, + }; + + return ( +
+ { + form.setValues({list: data?.list || []}); + }} + /> +
+ ); +} diff --git a/packages/x-flow/src/nodes/node-output/index.less b/packages/x-flow/src/nodes/node-output/index.less new file mode 100644 index 000000000..0377a0f36 --- /dev/null +++ b/packages/x-flow/src/nodes/node-output/index.less @@ -0,0 +1,16 @@ +.custom-node-start { + width: 240px; + padding: 0 12px; + background: #fff; + border-radius: 12px; + + .title { + display: flex; + height: 50px; + align-items: center; + span { + font-weight: bold; + margin-left: 8px; + } + } +} \ No newline at end of file diff --git a/packages/x-flow/src/nodes/node-output/index.tsx b/packages/x-flow/src/nodes/node-output/index.tsx new file mode 100644 index 000000000..361b8982d --- /dev/null +++ b/packages/x-flow/src/nodes/node-output/index.tsx @@ -0,0 +1,22 @@ +import { memo } from 'react'; +import NodeContainer from '../../FlowEditor/components/NodeContainer'; + +export default memo((props: any) => { + const { onClick } = props; + + return ( + + ); +}); + + diff --git a/packages/x-flow/src/nodes/node-output/setting/index.tsx b/packages/x-flow/src/nodes/node-output/setting/index.tsx new file mode 100644 index 000000000..f332fa22c --- /dev/null +++ b/packages/x-flow/src/nodes/node-output/setting/index.tsx @@ -0,0 +1,130 @@ +import { useEffect, useRef } from 'react'; +import { AnyObject } from 'antd/lib/_util/type'; +import FormRender, { useForm } from 'form-render'; +import { ICard, TYPES } from '../../constant'; +import FAutoComplete from '../../../FlowEditor/components/FAutoComplete'; + +export interface GlobalOutputProps { + data?: AnyObject; + onChange: (data: AnyObject) => void; + flowList: ICard[]; + inputItem: ICard; + readonly?: boolean; +} + +const getSchema = (request: any) => ({ + type: 'object', + displayType: 'row', + properties: { + list: { + type: 'array', + widget: 'tableList', + props: { + hideMove: true, + hideCopy: true, + size: 'small', + addBtnProps: { + type: 'dashed', + }, + actionColumnProps: { + width: 60, + }, + }, + items: { + type: 'object', + properties: { + name: { + title: '名称', + type: 'string', + width: 200, + placeholder: '请输入', + rules: [ + { + pattern: /^[a-zA-Z_][a-zA-Z0-9_]*$/, + message: '只能包含字母、数字和下划线且以字母或划线开头', + }, + ], + }, + dataType: { + title: '类型', + type: 'string', + enum: TYPES.map((el) => el.toUpperCase()), + enumNames: TYPES, + width: 120, + widget: 'select', + placeholder: '请选择', + }, + value: { + title: '值', + type: 'string', + widget: 'FAutoComplete', + props: { + placeholder: '${组件名.output}', + request, + }, + }, + }, + }, + }, + }, +}); + +export default (props: GlobalOutputProps) => { + const { data, onChange, inputItem, flowList, readonly } = props; + + const form = useForm(); + const flowListRef = useRef(); + const inputRef = useRef(); + + useEffect(() => { + flowListRef.current = flowList; + inputRef.current = inputItem; + }, [flowList, inputItem]); + + const watch = { + '#': (allValues: any) => { + onChange({ ...data, ...allValues }); + } + }; + + const request = (val: string) => { + return new Promise((resolve) => { + setTimeout(() => { + const inputValue = inputRef.current?.data; + const inputText = 'inputs'; + const options = (inputValue?.list || []) + .filter((el: any) => !!el.name) + .map((item: any) => '${#' + `${inputText}.${item.name}` + `}`); + const nodes = (flowListRef?.current || []) + .filter((el: any) => el.code !== 'Output') + .map((item: any) => { + return '${#' + `${item.code}.output` + `}`; + }); + + resolve( + [...options, ...nodes] + .filter((el: string) => val && el.includes(val)) + .map((el: string) => { + return { + value: el, + }; + }), + ); + }, 10); + }); + }; + const schema = getSchema(request); + + return ( + { + form.setValues({ list: data?.list }); + }} + /> + ); +} diff --git a/packages/x-flow/src/nodes/node-switch/index.less b/packages/x-flow/src/nodes/node-switch/index.less new file mode 100644 index 000000000..0377a0f36 --- /dev/null +++ b/packages/x-flow/src/nodes/node-switch/index.less @@ -0,0 +1,16 @@ +.custom-node-start { + width: 240px; + padding: 0 12px; + background: #fff; + border-radius: 12px; + + .title { + display: flex; + height: 50px; + align-items: center; + span { + font-weight: bold; + margin-left: 8px; + } + } +} \ No newline at end of file diff --git a/packages/x-flow/src/nodes/node-switch/index.tsx b/packages/x-flow/src/nodes/node-switch/index.tsx new file mode 100644 index 000000000..7d7f8d434 --- /dev/null +++ b/packages/x-flow/src/nodes/node-switch/index.tsx @@ -0,0 +1,39 @@ +import { memo } from 'react'; +import NodeContainer from '../../FlowEditor/components/NodeContainer'; + + +const getNodeList = (str: string) => { + try { + return JSON.parse(str); + } catch { + return [] + } +} + + + +export default memo((props: any) => { + const { onClick, data } = props; + const nodeList = getNodeList(data?.contentBody); + + return ( + + {nodeList.map((item: any, index: number) => ( +
+ CASE {index+1} + {item.node} +
+ ))} +
+ ); +}) + diff --git a/packages/x-flow/src/nodes/node-switch/setting/index.tsx b/packages/x-flow/src/nodes/node-switch/setting/index.tsx new file mode 100644 index 000000000..f88b9d7bf --- /dev/null +++ b/packages/x-flow/src/nodes/node-switch/setting/index.tsx @@ -0,0 +1,96 @@ +import { forwardRef, useEffect, useMemo } from 'react'; +import { Collapse } from 'antd'; +import FormRender, { useForm } from 'form-render'; +import { useCollapse } from '@/hooks/useCollapse'; +import FAutoComplete from '../../../FlowEditor/components/FAutoComplete'; +import { getNodeSuggestOptions, getSuggestOptions } from '../../constant'; +import '../../index.less'; +import { getSchema, getSwitchSchema } from './schema'; + +export default forwardRef((props: any, ref: any) => { + const { data, inputItem, flowList, isCollapsed, readonly } = props; + const form = useForm(); + const switchForm = useForm(); + const { activeKey, onChange: onCollapseChange } = useCollapse(isCollapsed); + + const request = useMemo(() => { + return getSuggestOptions(inputItem, flowList, data.code); + }, [inputItem, flowList, data.code]); + + const nodeRequest = useMemo(() => { + return getNodeSuggestOptions(flowList, data.code); + }, [flowList, data.code]); + + useEffect(() => { + form.setValues({ list: data.list }); + const switchData = data?.contentBody; + if (switchData) { + switchForm.setValues({ list: [...JSON.parse(switchData)] }); + } + }, [data.list]); + + const watch = { + '#': (allValues: any) => { + // '#': () => {} 等同于 onValuesChange + if (props.onChange) { + props.onChange( + Object.keys(allValues).length ? allValues : { list: [] }, + ); + } + }, + }; + + const switchWatch = { + '#': (allValues: any) => { + if (props.onChange) { + props.onChange({ + contentBody: JSON.stringify(allValues.list), + }); + } + }, + }; + + const schema = getSchema(request); + const switchSchema = getSwitchSchema(nodeRequest); + const items = [ + { + key: '2', + label: '输入变量(Input)', + children: ( + + ), + }, + { + key: '1', + label: '条件组(Switch)', + children: ( +
+ +
+ ), + }, + ]; + + return ( +
+ +
+ ); +}); diff --git a/packages/x-flow/src/nodes/node-switch/setting/schema.ts b/packages/x-flow/src/nodes/node-switch/setting/schema.ts new file mode 100644 index 000000000..48f2624f0 --- /dev/null +++ b/packages/x-flow/src/nodes/node-switch/setting/schema.ts @@ -0,0 +1,102 @@ +import { TYPES } from '../../constant'; + +export const getSchema = (request: any) => ({ + type: 'object', + displayType: 'row', + properties: { + list: { + type: 'array', + widget: 'tableList', + props: { + hideMove: true, + hideCopy: true, + size: 'small', + addBtnProps: { + type: 'dashed', + size: 'small' + }, + actionColumnProps: { + width: 60, + }, + }, + items: { + type: 'object', + properties: { + name: { + title: '变量名称', + type: 'string', + width: 180, + rules: [ + { + pattern: /^[a-zA-Z_][a-zA-Z0-9_]*$/, + message: '只能包含字母、数字和下划线且以字母或划线开头', + }, + ], + }, + dataType: { + title: '变量类型', + type: 'string', + enum: TYPES.map((el) => el.toUpperCase()), + enumNames: TYPES, + width: 110, + widget: 'select', + }, + value: { + title: '变量值', + type: 'string', + widget: 'FAutoComplete', + props: { + placeholder: '${变量名}', + request + } + } + } + } + } + } +}) + +export const getSwitchSchema = (nodeRequest: any) => ({ + type: 'object', + displayType: 'row', + properties: { + list: { + type: 'array', + widget: 'tableList', + props: { + hideMove: true, + hideCopy: true, + size: 'small', + addBtnProps: { + type: 'dashed', + }, + actionColumnProps: { + width: 60, + }, + }, + items: { + type: 'object', + properties: { + node: { + title: '节点名称', + type: 'string', + widget: 'FAutoComplete', + width: 280, + required: true, + props: { + placeholder: '节点名称', + request: nodeRequest + } + }, + expression: { + title: '表达式', + type: 'string', + props: { + placeholder: '${表达式}', + } + } + } + } + } + } +}); \ No newline at end of file diff --git a/packages/x-flow/src/nodes/utils.ts b/packages/x-flow/src/nodes/utils.ts new file mode 100644 index 000000000..da63bab50 --- /dev/null +++ b/packages/x-flow/src/nodes/utils.ts @@ -0,0 +1,264 @@ +import { AnyObject } from 'antd/es/_util/type'; +import { Node } from '@antv/x6'; +import { ICard, colorMap } from './constant'; + +const getItemStatus = (item: ICard) => { + const { debugging, result } = item; + let status = 'default'; + + if (debugging) { + status = 'running'; + } else if (result?.error) { + status = 'failed'; + } else if (result) { + status = 'success'; + } + return status; +}; +interface ICell extends Node { + code: string; +} + +export const getInitGraphData = (inputItem: ICard, outputItem: ICard) => [ + { + id: inputItem._id, + code: inputItem.code, + shape: 'dag-node', + x: 290, + y: 110, + data: { + label: inputItem.code, + status: 'default', + borderColor: '#5e606a', + icon: 'icon-input', + }, + ports: [ + { + id: `${inputItem._id}-bottom-port`, + group: 'bottom', + }, + ], + }, + { + id: outputItem._id, + // code后端需要 + code: outputItem.code, + shape: 'dag-node', + x: 290, + y: 110 + 120, + data: { + label: outputItem.code, + status: 'default', + borderColor: '#5e606a', + icon: 'icon-output', + }, + ports: [ + { + id: `${outputItem._id}-top-port`, + group: 'top', + }, + ], + }, +]; + +export class GraphNode { + // 图数据的增删改查 + cells: ICell[]; + instance: any; + + constructor(cells: ICell[], instance?: any) { + this.cells = cells; + this.instance = instance; + } + addCell(node: ICard, flowList: ICard[]) { + const graphCells = this.cells; + const outputIndex = graphCells.findIndex( + (el: any) => el.id.toLowerCase() === 'output', + ); + const output = graphCells.splice(outputIndex, 1)[0]; + graphCells.push({ + id: node._id || node.code, + code: node.code, + shape: 'dag-node', + x: flowList.length % 2 === 1 ? 300 : 280, + y: 110 + 120 * flowList.length, + data: { + label: node.code, + status: getItemStatus(node), + borderColor: colorMap[node.type]?.borderColor, + icon: colorMap[node.type]?.icon, + }, + ports: { + items: [ + { + id: `${node._id}-top-port`, + group: 'top', + }, + { + id: `${node._id}-bottom-port`, + group: 'bottom', + }, + ], + }, + }); + if (!output) return; + output.y = 110 + 120 * (flowList.length + 1); + graphCells.push(output); + } + addCells(nodes: ICard[], flowList: ICard[]) { + nodes.forEach((node, index) => { + this.addCell( + node, + flowList.slice(0, flowList.length - nodes.length + index + 1), + ); + }); + } + removeCell(node: ICard) { + this.instance.removeCell(node._id); + } + updateCellLabel(node: ICard, val: string) { + const graphCells = this.cells; + const index = graphCells.findIndex((el: any) => el.id === node._id); + graphCells[index].data.label = val; + graphCells[index].code = val; + } + + updateCellStatus(node: ICard, status: string) { + // const graphCells = this.cells; + // const index = graphCells.findIndex( + // (el: any) => el.id === node._id || el.code === node._id, + // ); + // if (index !== -1) { + // graphCells[index].data.status = status; + // } + const curNode = this.instance.getCellById(node._id); + if (!curNode) { + console.log('err,cannot find node'); + return; + } + const data = curNode.getData(); + curNode.setData({ + ...data, + status, + }); + } + + updateAllCellStatus(status: string) { + const graphCells = this.cells; + + graphCells.forEach((el: any) => { + if (el.shape === 'dag-node' && el.id !== 'Input' && el.id !== 'Output') { + el.data.status = status; + } + }); + } + getCell() { + return this.cells; + } +} + +export const generateGraphByNodes = (devVersion: any) => { + // 根据 nodes 生成图表节点 + const graphIns = new GraphNode([]); + const nodes = devVersion.nodes || []; + + nodes.forEach((item: any, index: number) => { + graphIns.addCell(item, new Array(index)); + }); + let graphCells = graphIns.getCell(); + const inputNode = graphCells.find( + (item: any) => item.id.toLowerCase() === 'input', + ) as any; + const outputNode = graphCells.find( + (item: any) => item.id.toLowerCase() === 'output', + ) as any; + if (outputNode) { + outputNode.ports.items = outputNode.ports.items.filter((el: any) => { + return el.group !== 'bottom'; + }); + } + if (inputNode) { + inputNode.y = graphCells?.[0].y - 120; + inputNode.ports.items = inputNode.ports.items.filter((el: any) => { + return el.group !== 'top'; + }); + + graphCells = graphCells.filter( + (item: any) => item.id.toLowerCase() !== 'input', + ); + graphCells.unshift(inputNode); + } + return graphCells; +}; + +type IMap = AnyObject; +const typeMap: IMap = { + string: 'STRING', + number: 'INTEGER', + object: 'OBJECT', + array: 'ARRAY', + boolean: 'BOOLEAN', +}; +const getParamType = (param: any) => { + return Object.prototype.toString.call(param).slice(8, -1).toLowerCase(); +}; +export const formatObj2Arr = (obj: any) => { + return Object.entries(obj).map((item) => { + return { + name: item[0] === 'undefined' ? '' : item[0], + value: item[1] ?? '', + dataType: typeMap[getParamType(item[1])] ?? 'STRING', + }; + }); +}; + +export const formatArr2Obj = (arr: any) => { + return arr.reduce((pre: AnyObject, cur: { name: string; value: string }) => { + pre[cur.name] = cur.value; + return pre; + }, {}); +}; + +export function extractFencedCodeBlock(text: string, language: string) { + const regex = new RegExp(`\`\`\`${language}([^]*?)\`\`\``, 'gi'); + let match; + let results = []; + + while ((match = regex.exec(text)) !== null) { + results.push(match[1].trim()); + } + + return results.length > 0 ? results.join('\n') : text; +} + +export function typewriter( + text: string, + callback: (text: string) => void, + typingSpeed = 30, +) { + let currentIndex = -1; + let currentText = ''; + + const type = () => { + if (currentIndex < text.length - 1) { + currentIndex++; + currentText += text[currentIndex]; + callback(currentText); + setTimeout(type, typingSpeed); + } + }; + + type(); +} + +export function capitalizeFirstLetter(str: string) { + return str.charAt(0).toUpperCase() + str.slice(1); +} + +export function isNodeConnected(edges: any, nodeId: string) { + return edges.some((edge: any) => { + const source = edge.source.cell; + const target = edge.target.cell; + return source === nodeId || target === nodeId; + }); +} diff --git a/packages/x-flow/src/type.ts b/packages/x-flow/src/type.ts new file mode 100644 index 000000000..7a628e22c --- /dev/null +++ b/packages/x-flow/src/type.ts @@ -0,0 +1,475 @@ +import { RuleItem } from 'async-validator'; +import * as React from 'react'; +import type { FormInstance as AntdFormInstance, FormProps as AntdFormProps, ColProps, TooltipProps } from 'antd'; +import type { ConfigProviderProps } from 'antd/es/config-provider'; +import type { FormProps as RcFormProps } from 'rc-field-form/lib/Form'; + +export type { RuleItem } from 'async-validator'; +export type SchemaType = + | 'string' + | 'object' + | 'array' + | 'number' + | 'boolean' + | 'void' + | 'date' + | 'datetime' + | 'block' + | string; + +export type ActionProps = { + submit: { + text?: string; + hide?: boolean; + [key: string]: any; + }, + reset: { + text?: string; + hide?: boolean; + [key: string]: any; + } +} + +export interface SchemaBase { + type?: SchemaType; + title?: string; + description?: string; + descType?: 'text' | 'icon'; + format?: + | 'image' + | 'textarea' + | 'color' + | 'email' + | 'url' + | 'dateTime' + | 'date' + | 'time' + | 'upload' + | (string & {}); + default?: any; + /** 是否必填,支持 `'{{ formData.xxx === "" }}'` 形式的表达式 */ + required?: boolean | string; + placeholder?: string; + bind?: false | string | string[]; + dependencies?: string[]; + /** 最小值,支持表达式 */ + min?: number | string; + /** 最大值,支持表达式 */ + max?: number | string; + /** 是否禁用,支持 `'{{ formData.xxx === "" }}'` 形式的表达式 */ + disabled?: boolean | string; + /** 是否只读,支持 `'{{ formData.xxx === "" }}'` 形式的表达式 */ + readOnly?: boolean | string; + /** 是否隐藏,隐藏的字段不会在 formData 里透出,支持 `'{{ formData.xxx === "" }}'` 形式的表达式 */ + hidden?: boolean | string; + displayType?: 'row' | 'column' | string; + width?: string | number; + labelWidth?: number | string; + maxWidth?: number | string; + column?: number; + className?: string; + widget?: string; + readOnlyWidget?: string; + extra?: string; + properties?: Record; + items?: Schema; + /** 多选,支持表达式 */ + enum?: Array | string; + /** 多选label,支持表达式 */ + enumNames?: Array | string; + rules?: RuleItem | RuleItem[]; + props?: Record; + /**扩展字段 */ + 'add-widget'?: string; + labelCol?: number | ColProps; + fieldCol?: number | ColProps + tooltip?: string | TooltipProps + cellSpan?: number; + span?: number; + validateTrigger?: string | string[] + [key: string]: any; +} + +export type Schema = Partial; + +export interface Error { + /** 错误的数据路径 */ + name: string; + /** 错误的内容 */ + error: string[]; +} + +export interface FormParams { + formData?: any; + onChange?: (data: any) => void; + onValidate?: (valid: any) => void; + showValidate?: boolean; + /** 数据分析接口,表单展示完成渲染时触发 */ + logOnMount?: (stats: any) => void; + /** 数据分析接口,表单提交成功时触发,获得本次表单填写的总时长 */ + logOnSubmit?: (stats: any) => void; +} + +export interface ValidateParams { + formData: any; + schema: Schema; + error: Error[]; + + [k: string]: any; +} + +export interface ResetParams { + formData?: any; + submitData?: any; + errorFields?: Error[]; + touchedKeys?: any[]; + allTouched?: boolean; + + [k: string]: any; +} + +export interface FieldParams { + name: string; + error?: string[]; + touched?: boolean; + validating?: boolean; + value?: any; +} + +export interface ListOperate { + /* 列表表单操作按钮样式 */ + btnType: 'text' | 'icon'; + /* 是否隐藏移动按钮 */ + hideMove: boolean; +} + +export interface GlobalConfig { + /* 列表表单配置 */ + listOperate: ListOperate; + /** 列表校验气泡模式*/ + listValidatePopover: boolean; + /* 是否禁用表达式 */ + mustacheDisabled: boolean; +} + +export interface FormInstance { + /* + * 提交表单 + */ + submit: () => void, + /** + * 根据路径动态设置 Schema + */ + setSchemaByPath: (path: string, schema: any) => any; + /** + * 获取隐藏的表单数据 + */ + getHiddenValues: () => any; + /** + * 设置 Schema + */ + setSchema: (schema: any, cover?: boolean) => void; + /** + * 获取表单的 schema + */ + getSchema: () => any; + /** + * + * 获取 flatten schema + */ + getFlattenSchema: (path?: string) => any; + /** + * 根据路径获取 Schema + */ + getSchemaByPath: (path: string) => any; + /** + * 外部手动修改 errorFields 校验信息 + */ + setErrorFields: (errors: any[]) => void; + /** + * 外部手动删除某一个 path 下所有的校验信息 + */ + removeErrorField: (path: string) => any; + /** + * 校验表单 + */ + validateFields: AntdFormInstance['validateFields']; + /** + * 获取对应字段 field 的错误信息 + */ + getFieldError: AntdFormInstance['getFieldError']; + /** + * 获取一组字段 fields 的错误信息 + */ + getFieldsError: AntdFormInstance['getFieldsError']; + /** + * 检查某个表单项是否被修改过 + */ + isFieldTouched: AntdFormInstance['isFieldTouched']; + /** + * 检查一组表单项是否被修改过 + */ + isFieldsTouched: AntdFormInstance['isFieldsTouched']; + /** + * 检查某个表单项是否在校验中 + */ + isFieldValidating: AntdFormInstance['isFieldValidating']; + /** + * 根据路径获取表单值 + */ + getValueByPath: AntdFormInstance['getFieldValue']; + /** + * 根据路径修改表单值 + */ + setValueByPath: AntdFormInstance['setFieldValue']; + /** + * 获取表单值 + */ + getValues: AntdFormInstance['getFieldsValue']; + /** + * 设置表单值 + */ + setValues: AntdFormInstance['setFieldsValue']; + /** + * 重置表单 + */ + resetFields: AntdFormInstance['resetFields']; + /** + * @deprecated 即将弃用,请勿使用此 api,使用 getFieldsError + */ + errorFields: AntdFormInstance['getFieldsError']; + /** + * @deprecated 即将弃用,请勿使用此 api,使用 form.isFieldsValidating + */ + scrollToPath: AntdFormInstance['scrollToField']; + /** + * @deprecated 即将弃用,请勿使用此 api,使用setValueByPath + */ + onItemChange: AntdFormInstance['setFieldValue']; + /** + * @deprecated 即将弃用,请勿使用此 api + */ + init: any; + /** + * @deprecated 即将弃用,请勿使用此 api,使用 getSchema 代替 + */ + __schema: any; + /** + * @deprecated 内部方法不要使用 + */ + __initStore: (data: any) => any; + /** + * 存储 field 的 ref 对象 + */ + setFieldRef: (path: string, ref: any) => void; + /** + * 获取 field 的 ref 对象 + */ + getFieldRef: (path: string) => any; +} + +export type WatchProperties = { + [path: string]: + | { + handler: (value: any) => void; + immediate?: boolean; + } + | ((value: any) => void); +}; + +interface ExtendedColProps extends ColProps { + // 额外的属性可以放在这里 +} + +export interface FRProps extends Omit { + /** + * 表单顶层的className + */ + className?: string; + /** + * 表单顶层的样式 + */ + style?: React.CSSProperties; + /** + * 表单 schema + */ + schema: Schema; + /** + * form单例 + */ + form: FormInstance; + /** + * 组件和schema的映射规则 + */ + mapping?: Record; + /** + * 自定义组件 + */ + widgets?: Record; + /** + * 标签元素和输入元素的排列方式,column-分两行展示,row-同行展示,inline-自然顺排,默认`column` + */ + displayType?: 'column' | 'row' | 'inline'; + /** + * 表示是否显示 label 后面的冒号 + */ + colon?: boolean; + /** + * label 标签的文本对齐方式 + */ + labelAlign?: 'right' | 'left'; + // labelCol?: number | ExtendedColProps; + fieldCol?: number | ColProps; + /** + * 只读模式 + */ + readOnly?: boolean; + /** + * 禁用模式 + */ + disabled?: boolean; + /** + * 标签宽度 + */ + labelWidth?: string | number; + /** + * antd的全局config + */ + configProvider?: ConfigProviderProps; + /** + * 覆盖默认的校验信息 + */ + validateMessages?: RcFormProps['validateMessages']; + /** + * 显示当前表单内部状态 + */ + debug?: boolean; + /** + * 显示css布局提示线 + */ + debugCss?: boolean; + /** + * 展示语言,目前只支持中文、英文 + */ + locale?: 'zh-CN' | 'en-US'; + /** + * 一行展示的列数 + */ + column?: number; + /** + * 数据会作为 beforeFinish 的第四个参数传入 + */ + config?: any; + /** + * 类似于 vuejs 的 watch 的用法,监控值的变化,触发 callback + */ + watch?: WatchProperties; + /* + * 表单全局配置 + */ + globalConfig?: GlobalConfig; + /** + * 表单的全局共享属性 + */ + globalProps?: any; + /** + * 表单首次加载钩子 + */ + onMount?: () => void; + /** + * 表单提交前钩子 + */ + beforeFinish?: (params: ValidateParams) => Error[] | Promise; + /** + * 表单提交后钩子 + */ + onFinish?: (formData: any) => void; + /** + * 字段值更新时触发回调事件 + */ + onValuesChange?: ( + changedValues: { + dataPath: string; + value: any; + dataIndex: number[] | unknown; + }, + formData: any + ) => void; + /** + * 隐藏的数据是否去掉,默认不去掉 + */ + removeHiddenData?: boolean; + /** + * 配置自定义layout组件 + */ + layoutWidgets?: any; + /** + * 扩展方法 + */ + methods?: Record; + operateExtra?: React.ReactNode; + maxWidth?: number | string; + footer?: boolean | ((dom: React.JSX.Element[]) => React.ReactNode) | Partial ; +} + +export interface SearchProps extends Omit { + debug?: boolean; + searchBtnStyle?: React.CSSProperties; + searchBtnClassName?: string; + displayType?: any; + propsSchema?: any; + className?: string; + style?: React.CSSProperties; + hidden?: boolean; + searchOnMount?: boolean | unknown; + searchWithError?: boolean; + searchBtnRender?: ( + submit: Function, + clearSearch: Function, + other: any + ) => React.ReactNode[]; + searchText?: string; + resetText?: string; + onSearch?: (search: any) => any; + afterSearch?: (params: any) => any; + onReset?: (form: any) => void; + widgets?: any; + form?: any; + [key:string]: any +} + +/** 自定义组件 props */ +export type WidgetProps = { + value: any, + onChange: (value: any) => void, + schema: Schema, + style: React.CSSProperties, + id: string, + addons: WidgetAddonsType, + disabled?: boolean, + readOnly?: boolean, + [other: string]: any, +} + +/** 自定义组件 addons */ +export type WidgetAddonsType = FormInstance & { + globalProps: Record, + dependValues: any[], + dataIndex: string[], + dataPath: string, + schemaPath: string, +} + +declare const FR: React.FC; + +export declare function useForm(params?: FormParams): FormInstance; + +export type ConnectedForm = T & { + form: FormInstance; +}; + +export declare function connectForm( + component: React.ComponentType> +): React.ComponentType; + +export default FR; diff --git a/packages/x-flow/src/utils/index.ts b/packages/x-flow/src/utils/index.ts new file mode 100644 index 000000000..386d1fe46 --- /dev/null +++ b/packages/x-flow/src/utils/index.ts @@ -0,0 +1,125 @@ +import { isMatch, some, set, get, cloneDeep, has as _has, merge, mergeWith, isUndefined, omitBy } from 'lodash-es'; + +export const _set = set; +export const _get = get; +export const _cloneDeep = cloneDeep; +// export const _has = has; +export { _has }; +export const _merge = merge; +export const _mergeWith = mergeWith; +export const _isUndefined = isUndefined; +export const _omitBy = omitBy; +export const _some = some; +export const _isMatch = isMatch; + +export const isObject = (data: any) => { + const str = Object.prototype.toString.call(data); + return str.indexOf('Object') > -1; +} + +export const isArray = (data: any) => { + const str = Object.prototype.toString.call(data); + return str.indexOf('Array') > -1; +} + +export const isFunction = (data: any) => typeof data === 'function'; + +export function isUrl(string: string) { + const protocolRE = /^(?:\w+:)?\/\/(\S+)$/; + // const domainRE = /^[^\s\.]+\.\S{2,}$/; + if (typeof string !== 'string') return false; + return protocolRE.test(string); +} + +export const isNumber = (str: string | number) => !isNaN(Number(str)) + +export const getArray = (arr, defaultValue = []) => { + if (Array.isArray(arr)) return arr; + return defaultValue; +}; + +export function getFormat(format) { + let dateFormat; + switch (format) { + case 'date': + dateFormat = 'YYYY-MM-DD'; + break; + case 'time': + dateFormat = 'HH:mm:ss'; + break; + case 'dateTime': + dateFormat = 'YYYY-MM-DD HH:mm:ss'; + break; + case 'week': + dateFormat = 'YYYY-w'; + break; + case 'year': + dateFormat = 'YYYY'; + break; + case 'quarter': + dateFormat = 'YYYY-Q'; + break; + case 'month': + dateFormat = 'YYYY-MM'; + break; + default: + // dateTime + if (typeof format === 'string') { + dateFormat = format; + } else { + dateFormat = 'YYYY-MM-DD'; + } + } + return dateFormat; +} + +// TODO: to support case that item is not an object +export function isObjType(schema: any) { + //return schema?.type === 'object' && schema.properties && !schema.widget; + return schema?.type === 'object' && schema?.properties && schema?.widgetType !== 'field'; +}; + +export function isListType(schema: any) { + return schema?.type === 'array' && isObjType(schema?.items) && schema?.enum === undefined; +}; + +export function isCheckBoxType(schema: any, readOnly: boolean) { + if (readOnly) return false; + if (schema.widget === 'checkbox') return true; + if (schema && schema.type === 'boolean') { + if (schema.enum) return false; + if (schema.widget === undefined) return true; + return false; + } +} + +export const translation = (configCtx: any) => (key: string) => { + const locale = configCtx?.locale.FormRender; + return locale[key]; +}; + +export const hasFuncProperty = (obj: any) => { + return _some(obj, (value) => { + if (isFunction(value)) { + return true; + } + if (isObject(value)) { + return hasFuncProperty(value); + } + return false; + }); +}; + +/** + * 安全地获取对象的值,如果值为 null 或 undefined,则返回 defaultValue。 + * + * @param {Object} object - 要获取值的对象。 + * @param {string|Array} path - 要获取的路径,可以是字符串或数组。 + * @param {*} [defaultValue] - 如果值为 null 或 undefined,则返回 defaultValue。 + * @returns {*} - 返回获取的值,或者默认值。 + */ +export const safeGet = (object: any, path: string, defaultValue: any) => { + return get(object, path, defaultValue) ?? defaultValue; +}; + + diff --git a/packages/x-flow/src/withProvider.tsx b/packages/x-flow/src/withProvider.tsx new file mode 100644 index 000000000..cdfabaf5c --- /dev/null +++ b/packages/x-flow/src/withProvider.tsx @@ -0,0 +1,85 @@ +import React, { useEffect, useRef } from 'react'; +import { ConfigProvider } from 'antd'; +import dayjs from 'dayjs'; +import { useUnmount } from 'ahooks'; + +import zhCN from 'antd/lib/locale/zh_CN'; +import enUS from 'antd/lib/locale/en_US'; +import locales from './locales'; +import 'dayjs/locale/zh-cn'; + +import { createStore } from './models/store'; +import { FRContext, ConfigContext } from './models/context'; +import { validateMessagesEN, validateMessagesCN } from './models/validateMessage'; + +export default function withProvider(Element: React.ComponentType, defaultWidgets?: any) : React.ComponentType { + return (props: any) => { + const { + configProvider, + locale = 'zh-CN', + widgets, + methods, + form, + validateMessages, + globalProps={}, + globalConfig = {}, + ...otherProps + } = props; + + const storeRef = useRef(createStore()); + const store: any = storeRef.current; + + useEffect(() => { + if (locale === 'en-US') { + dayjs.locale('en'); + return; + } + dayjs.locale('zh-cn'); + }, [locale]); + + useUnmount(() => { + form.resetFields(); + }); + + if (!form) { + console.warn('Please provide a form instance to FormRender'); + return null; + } + + const antdLocale = locale === 'zh-CN' ? zhCN : enUS; + const formValidateMessages = locale === 'zh-CN' ? validateMessagesCN : validateMessagesEN; + const configContext = { + locale, + widgets: { ...defaultWidgets, ...widgets }, + methods, + form, + globalProps, + globalConfig + }; + + const langPack: any = { + ...antdLocale, + 'FormRender': locales[locale], + ...configProvider?.locale + }; + + return ( + + + + + + + + ); + }; +} \ No newline at end of file diff --git a/packages/x-flow/tsconfig.json b/packages/x-flow/tsconfig.json new file mode 100644 index 000000000..3e09e8c01 --- /dev/null +++ b/packages/x-flow/tsconfig.json @@ -0,0 +1,42 @@ +{ + "compilerOptions": { + "target": "ES2015", + "module": "ES2015", + "moduleResolution": "node", + "importHelpers": true, + "jsx": "react", + "esModuleInterop": true, + "sourceMap": true, + "baseUrl": "./", + "skipLibCheck": true, + // "strict": true, + "declaration": true, + "paths": { + "@/*": [ + "src/*" + ], + "@@/*": [ + "src/.umi/*" + ], + "form-render": [ + "packages/form-render/src/*" + ], + }, + "allowJs": true, + "allowSyntheticDefaultImports": true, + "noImplicitAny": false, + "resolveJsonModule": true + }, + "exclude": [ + "node_modules", + "lib", + "es", + "dist", + "typings", + "**/__test__", + "test", + "tests", + "docs", + "**/*.js" + ] +} From 1aa11b6a89997dd8ad4961e96cfe62e088e50e48 Mon Sep 17 00:00:00 2001 From: "zhanbo.lh" Date: Wed, 13 Nov 2024 20:54:07 +0800 Subject: [PATCH 02/38] =?UTF-8?q?feat:=20=E5=89=94=E9=99=A4=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/x-flow/package.json | 3 +- .../components/CandidateNode/index.tsx | 3 +- .../components/CustomEdge/index.tsx | 2 +- .../components/CustomHtml/index.tsx | 3 +- .../components/CustomNode/index.tsx | 2 +- .../FlowEditor/components/IconView/index.tsx | 13 ++++++ .../components/NodeSelectPopover/index.tsx | 4 +- .../src/FlowEditor/operator/Control/index.tsx | 2 +- packages/x-flow/src/FlowEditor/utils/hooks.ts | 43 +++++++++++++++++++ packages/x-flow/src/context/event-emitter.tsx | 29 +++++++++++++ packages/x-flow/src/utils/createIconFont.ts | 7 +++ 11 files changed, 103 insertions(+), 8 deletions(-) create mode 100644 packages/x-flow/src/FlowEditor/components/IconView/index.tsx create mode 100644 packages/x-flow/src/FlowEditor/utils/hooks.ts create mode 100644 packages/x-flow/src/context/event-emitter.tsx create mode 100644 packages/x-flow/src/utils/createIconFont.ts diff --git a/packages/x-flow/package.json b/packages/x-flow/package.json index 5629f7cc5..b62df6421 100644 --- a/packages/x-flow/package.json +++ b/packages/x-flow/package.json @@ -61,7 +61,8 @@ "virtualizedtableforantd4": "^1.1.2", "ahooks": "^3.7.5", "zustand": "^4.5.4", - "@xyflow/react": "^12.3.2" + "@xyflow/react": "^12.3.2", + "@remixicon/react": "^4.2.0" }, "devDependencies": { "deep-equal": "^2.0.3", diff --git a/packages/x-flow/src/FlowEditor/components/CandidateNode/index.tsx b/packages/x-flow/src/FlowEditor/components/CandidateNode/index.tsx index 28e2f506e..636275f7c 100644 --- a/packages/x-flow/src/FlowEditor/components/CandidateNode/index.tsx +++ b/packages/x-flow/src/FlowEditor/components/CandidateNode/index.tsx @@ -1,9 +1,10 @@ +import React from 'react'; import { memo } from 'react'; import produce from 'immer'; import { useShallow } from 'zustand/react/shallow'; import { useReactFlow, useViewport } from '@xyflow/react'; import { useEventListener } from 'ahooks'; -import CustomNode from '../../../nodes'; +import CustomNode from '../CustomNode'; import useStore from '../../store'; const CandidateNode = () => { diff --git a/packages/x-flow/src/FlowEditor/components/CustomEdge/index.tsx b/packages/x-flow/src/FlowEditor/components/CustomEdge/index.tsx index 8a1489984..8a48b3936 100644 --- a/packages/x-flow/src/FlowEditor/components/CustomEdge/index.tsx +++ b/packages/x-flow/src/FlowEditor/components/CustomEdge/index.tsx @@ -1,4 +1,4 @@ -import { memo } from 'react'; +import React, { memo } from 'react'; import { PlusOutlined, CloseOutlined } from '@ant-design/icons'; import { BezierEdge, EdgeLabelRenderer, getBezierPath, useReactFlow } from '@xyflow/react'; import { useShallow } from 'zustand/react/shallow'; diff --git a/packages/x-flow/src/FlowEditor/components/CustomHtml/index.tsx b/packages/x-flow/src/FlowEditor/components/CustomHtml/index.tsx index 1ec473fb3..29d46f421 100644 --- a/packages/x-flow/src/FlowEditor/components/CustomHtml/index.tsx +++ b/packages/x-flow/src/FlowEditor/components/CustomHtml/index.tsx @@ -1,6 +1,7 @@ +import React from 'react'; import { Tooltip, Typography } from 'antd'; import _ from 'lodash'; -import IconView from '@/components/IconView'; +import IconView from '../../components/IconView'; const { Text } = Typography; diff --git a/packages/x-flow/src/FlowEditor/components/CustomNode/index.tsx b/packages/x-flow/src/FlowEditor/components/CustomNode/index.tsx index 29ec46395..bb8a3f81e 100644 --- a/packages/x-flow/src/FlowEditor/components/CustomNode/index.tsx +++ b/packages/x-flow/src/FlowEditor/components/CustomNode/index.tsx @@ -1,4 +1,4 @@ -import { memo } from 'react'; +import React, { memo } from 'react'; import classNames from 'classnames'; import { Handle, Position } from '@xyflow/react'; import './index.less'; diff --git a/packages/x-flow/src/FlowEditor/components/IconView/index.tsx b/packages/x-flow/src/FlowEditor/components/IconView/index.tsx new file mode 100644 index 000000000..eb681076f --- /dev/null +++ b/packages/x-flow/src/FlowEditor/components/IconView/index.tsx @@ -0,0 +1,13 @@ +import { createFromIconfontCN } from '@ant-design/icons'; + +/** + * icon 图标库 + * 图标库资源变动需要更新 scriptUrl 资源路径 + * https://www.iconfont.cn/manage/index?manage_type=myprojects&projectId=4201076 + */ + +const Icon = createFromIconfontCN({ + scriptUrl: '//at.alicdn.com/t/a/font_4201076_frx3c9x07if.js', +}); + +export default Icon; diff --git a/packages/x-flow/src/FlowEditor/components/NodeSelectPopover/index.tsx b/packages/x-flow/src/FlowEditor/components/NodeSelectPopover/index.tsx index a6250d4f4..2fed50839 100644 --- a/packages/x-flow/src/FlowEditor/components/NodeSelectPopover/index.tsx +++ b/packages/x-flow/src/FlowEditor/components/NodeSelectPopover/index.tsx @@ -5,8 +5,8 @@ import { SearchOutlined } from '@ant-design/icons'; import { useEventListener } from 'ahooks'; import { useShallow } from 'zustand/react/shallow'; import { useClickAway } from 'ahooks'; -import { useSet } from '@/utils/hooks'; -import IconView from '@/components/IconView'; +import { useSet } from '../../utils/hooks'; +import IconView from '../../components/IconView'; import useStore from '../../store'; import './index.less'; diff --git a/packages/x-flow/src/FlowEditor/operator/Control/index.tsx b/packages/x-flow/src/FlowEditor/operator/Control/index.tsx index 1d14f91d8..471db840f 100644 --- a/packages/x-flow/src/FlowEditor/operator/Control/index.tsx +++ b/packages/x-flow/src/FlowEditor/operator/Control/index.tsx @@ -7,7 +7,7 @@ import { RiStickyNoteAddLine, } from '@remixicon/react'; import { Tooltip, Button } from 'antd'; -import IconView from '@/components/IconView'; +import IconView from '../../components/IconView'; import { useEventEmitterContextContext } from '../../../context/event-emitter'; import NodeSelectPopover from '../../components/NodeSelectPopover'; import './index.less'; diff --git a/packages/x-flow/src/FlowEditor/utils/hooks.ts b/packages/x-flow/src/FlowEditor/utils/hooks.ts new file mode 100644 index 000000000..0c0f8baaf --- /dev/null +++ b/packages/x-flow/src/FlowEditor/utils/hooks.ts @@ -0,0 +1,43 @@ +import { useReducer, useRef, useEffect, useState } from 'react'; +import { message } from 'antd'; + +export function usePrevious(value: any) { + const ref = useRef(); + useEffect(() => { + ref.current = value; + }, [value]); + return ref.current; +} + +// 类似于class component的setState +export const useSet = (initState: any) => { + return useReducer((state: any, newState: any) => { + return { ...state, ...newState }; + }, initState); +}; + +/** + * 业务中为了防止提交按钮会被连续点击 我们会手动的设置setLoading true 和setLoading false 非常麻烦 + * 所以写了这个hook 省事 + * @returns + */ +type TCallback = () => Promise; +type TRequestLoading = () => [boolean, (callback: TCallback) => void]; + +export const useLoadingRequest: TRequestLoading = () => { + const [loading, setLoading] = useState(false); + + const handleRequest = async (callback: TCallback) => { + try { + setLoading(true); + await callback(); + } catch (error: any) { + message.error(error); + throw error; + } finally { + setLoading(false); + } + }; + + return [loading, handleRequest]; +}; diff --git a/packages/x-flow/src/context/event-emitter.tsx b/packages/x-flow/src/context/event-emitter.tsx new file mode 100644 index 000000000..a881bc0fb --- /dev/null +++ b/packages/x-flow/src/context/event-emitter.tsx @@ -0,0 +1,29 @@ +'use client' + +import { useEventEmitter } from 'ahooks'; +import type { EventEmitter } from 'ahooks/lib/useEventEmitter'; +import { createContext, useContext } from 'use-context-selector'; + +const EventEmitterContext = createContext<{ eventEmitter: EventEmitter | null }>({ + eventEmitter: null, +}) + +export const useEventEmitterContextContext = () => useContext(EventEmitterContext) + +type EventEmitterContextProviderProps = { + children: React.ReactNode +} + +export const EventEmitterContextProvider = ({ + children, +}: EventEmitterContextProviderProps) => { + const eventEmitter = useEventEmitter() + + return ( + + {children} + + ) +} + +export default EventEmitterContext diff --git a/packages/x-flow/src/utils/createIconFont.ts b/packages/x-flow/src/utils/createIconFont.ts new file mode 100644 index 000000000..6285d207e --- /dev/null +++ b/packages/x-flow/src/utils/createIconFont.ts @@ -0,0 +1,7 @@ +import { createFromIconfontCN } from '@ant-design/icons'; + +export default (url?: string) => { + return createFromIconfontCN({ + scriptUrl: url || '//at.alicdn.com/t/a/font_2750617_sax751jyfjl.js', + }); +}; From 0e868f01fb95813ec3c5401e214e1d847205ec35 Mon Sep 17 00:00:00 2001 From: "zhanbo.lh" Date: Thu, 14 Nov 2024 10:31:04 +0800 Subject: [PATCH 03/38] =?UTF-8?q?feat:=20xflow=20=E8=BF=AD=E4=BB=A3?= =?UTF-8?q?=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .dumirc.ts | 2 +- docs/xflow/index.md | 39 ++++- packages/x-flow/package.json | 6 +- .../components/NodeContainer/index.tsx | 4 +- .../components/NodeDebugDrawer/index.less | 114 ------------ .../components/NodeDebugDrawer/index.tsx | 163 ------------------ .../components/NodeSelectPopover/index.tsx | 2 +- .../components/PanelContainer/index.tsx | 22 +-- packages/x-flow/src/FlowEditor/index.tsx | 2 +- packages/x-flow/src/FlowEditor/main.tsx | 24 +-- .../src/FlowEditor/operator/Control/index.tsx | 2 +- .../FlowEditor/operator/UndoRedo/index.tsx | 4 +- .../FlowEditor/operator/ZoomInOut/index.tsx | 4 +- .../x-flow/src/FlowEditor/operator/index.tsx | 2 +- packages/x-flow/src/index.ts | 4 +- .../x-flow/src/nodes/node-input/index.tsx | 7 +- packages/x-flow/src/withProvider.tsx | 23 +-- 17 files changed, 73 insertions(+), 351 deletions(-) delete mode 100644 packages/x-flow/src/FlowEditor/components/NodeDebugDrawer/index.less delete mode 100644 packages/x-flow/src/FlowEditor/components/NodeDebugDrawer/index.tsx diff --git a/.dumirc.ts b/.dumirc.ts index d903ccc66..84bdc3999 100644 --- a/.dumirc.ts +++ b/.dumirc.ts @@ -76,7 +76,7 @@ export default defineConfig({ 'form-render-mobile': path.resolve(__dirname, 'packages/form-render-mobile/src'), '@xrenders/schema-builder': path.resolve(__dirname, 'tools/schema-builder/src'), '@xrenders/data-render': path.resolve(__dirname, 'packages/data-render/src'), - '@xrenders/x-flow': path.resolve(__dirname, 'packages/x-flow/src'), + '@xrenders/xflow': path.resolve(__dirname, 'packages/x-flow/src'), }, codeSplitting: { jsStrategy: 'granularChunks' }, //...(process.env.NODE_ENV === 'development' ? {} : { ssr: {} }), diff --git a/docs/xflow/index.md b/docs/xflow/index.md index 6fc00c8c2..9362a473d 100644 --- a/docs/xflow/index.md +++ b/docs/xflow/index.md @@ -6,7 +6,7 @@ mobile: false
logo - DataView + xflow

@@ -23,12 +23,12 @@ mobile: false

-中后台详情页解决方案,通过 schema 协议渲染页面 +画布流程编排解决方案 ## 安装 ```shell -npm i @xrenders/data-render --save +npm i @xrenders/xflow --save ``` ## 使用方式 @@ -41,13 +41,42 @@ npm i @xrenders/data-render --save * defaultShowCode: true */ import React from 'react'; -import DataView from '@xrenders/x-flow'; +import XFlow from '@xrenders/xflow'; import schema from './schema/basic'; import data from './data/basic'; export default () => { + const nodes = [ + { + id: '1', // 节点 ID + type: 'custom', // 节点类型 + data: {}, // 节点配置数据 + position: { // 节点画布坐标位置 + x: 100, + y: 300, + } + }, + { + id: '2', + type: 'custom', // 节点类型 + data: {}, // 节点配置数据 + position: { // 节点画布坐标位置 + x: 300, + y: 600, + } + } + ] + + const nodeMenus = []; + return ( - +
+ +
); } ``` \ No newline at end of file diff --git a/packages/x-flow/package.json b/packages/x-flow/package.json index b62df6421..ce907dbef 100644 --- a/packages/x-flow/package.json +++ b/packages/x-flow/package.json @@ -62,7 +62,11 @@ "ahooks": "^3.7.5", "zustand": "^4.5.4", "@xyflow/react": "^12.3.2", - "@remixicon/react": "^4.2.0" + "@remixicon/react": "^4.2.0", + "@dagrejs/dagre": "^1.1.3", + "zundo": "^2.0.0-beta.18", + "use-context-selector": "^1.4.1", + "form-render": "^2.3.4" }, "devDependencies": { "deep-equal": "^2.0.3", diff --git a/packages/x-flow/src/FlowEditor/components/NodeContainer/index.tsx b/packages/x-flow/src/FlowEditor/components/NodeContainer/index.tsx index 75673ef88..fc30ebff4 100644 --- a/packages/x-flow/src/FlowEditor/components/NodeContainer/index.tsx +++ b/packages/x-flow/src/FlowEditor/components/NodeContainer/index.tsx @@ -1,5 +1,5 @@ -import { memo } from 'react'; -import IconView from '@/components/IconView'; +import React, { memo } from 'react'; +import IconView from '../../components/IconView'; import classNames from 'classnames'; import './index.less'; diff --git a/packages/x-flow/src/FlowEditor/components/NodeDebugDrawer/index.less b/packages/x-flow/src/FlowEditor/components/NodeDebugDrawer/index.less deleted file mode 100644 index 894a25329..000000000 --- a/packages/x-flow/src/FlowEditor/components/NodeDebugDrawer/index.less +++ /dev/null @@ -1,114 +0,0 @@ -.debug-node-panel { - .ant-drawer-content-wrapper { - top: 54px; - bottom: 14px; - right: 12px; - border-radius: 20px; - } - - .ant-drawer-close { - display: none; - } - - .ant-drawer-header { - padding: 16px; - } - - .ant-drawer-content { - border-radius: 20px; - } - - .ant-drawer-body { - padding: 8px 16px; - } - - .title-box { - display: flex; - align-items: center; - justify-content: space-between; - - .ant-input-outlined { - border-color: #fff; - font-weight: 600; - } - } - - .icon-box { - width: 24px; - height: 24px; - border-radius: 6px; - display: flex; - align-items: center; - justify-content: center; - margin-right: 5px; - } - - .title-actions { - display: flex; - align-items: center; - margin-left: 24px; - } - - .desc-box { - font-size: 12px; - line-height: 32px; - font-weight: normal; - - .ant-input-outlined { - border-color: #fff; - } - - textarea { - margin: 12px 0; - } - } - - .ant-input-outlined:focus-within { - border-color: #3b82f6 !important; - } - - .fr-table-cell-content { - .ant-col { - padding: 0 !important; - } - } - - .ant-collapse-content-box { - padding: 0 !important; - } - - .item-collapse { - border: none; - background-color: #fff; - - .ant-collapse-header { - background: none; - padding: 6px 0; - } - - .ant-collapse-item { - border: none !important; - padding: 0; - } - - .ant-collapse-content { - border: none; - } - } - - .ant-collapse-header-text { - color: #354052; - font-weight: 600; - } - - .ant-table-thead>tr>th { - font-size: 12px; - font-weight: normal; - } - - input, - select, - .ant-select-selector { - font-size: 13px !important; - } -} \ No newline at end of file diff --git a/packages/x-flow/src/FlowEditor/components/NodeDebugDrawer/index.tsx b/packages/x-flow/src/FlowEditor/components/NodeDebugDrawer/index.tsx deleted file mode 100644 index 6a1d10ce8..000000000 --- a/packages/x-flow/src/FlowEditor/components/NodeDebugDrawer/index.tsx +++ /dev/null @@ -1,163 +0,0 @@ -import { useEffect, useState } from 'react'; -import ReactJson from 'react-json-view'; -import { Alert, Button, Drawer, Flex } from 'antd'; -import FormRender, { useForm } from 'form-render'; -import { cloneDeep } from 'lodash'; -import ExpandInput from '@/components/ExpandInput'; -import IconView from '@/components/IconView'; -import { useFlow } from '../../../../../hooks/useWorkFlow'; -import CustomHtml from '../CustomHtml'; -import './index.less'; - -interface IDebugDrawerProp { - visible: boolean; - onClose: () => void; - title: string; - node: any; -} -const schema = { - type: 'object', - displayType: 'row', - properties: { - list: { - type: 'array', - widget: 'tableList', - readOnly: true, - - items: { - type: 'object', - properties: { - name: { - title: '名称', - type: 'string', - widget: 'html', - width: 140, - props: { - width: 100, - }, - }, - dataType: { - title: '类型', - type: 'string', - widget: 'html', - width: 80, - }, - value: { - title: '值', - type: 'string', - widget: 'ExpandInput', - placeholder: '请输入常量', - // required: true, - }, - }, - }, - }, - }, -}; -const NodeDebugDrawer = (props: IDebugDrawerProp) => { - const { visible, onClose, title, node } = props; - const form = useForm(); - const [debugging, setDebugging] = useState(false); - const [debugData, setDebugData] = useState(); - - const { handleDebugOk } = useFlow(); - useEffect(() => { - const inputData = cloneDeep(node); - - if (inputData?.list) { - inputData.list = inputData.list.map((item: any) => { - return { - ...item, - value: '', - }; - }); - } - form.setValues({ - list: inputData?.list || [], - }); - return () => { - form.resetFields(); - }; - }, [node]); - - const handleOk = async () => { - const formData = await form.validateFields(); - - setDebugging(true); - await handleDebugOk({ ...formData, code: node.code }, false, { - successCb: (res: any) => { - if (res.taskCode === node.code) { - setDebugData(res?.output || {}); - } - setDebugging(false); - }, - errorCb: (err: any) => { - setDebugData(err); - setDebugging(false); - }, - }); - }; - return ( - -
{`测试 ${title}`}
- - - } - > - - , - ExpandInput, - }} - /> - - - - {!debugging && debugData && ( - -

输出结果:

- -
- )} -
- ); -}; -export default NodeDebugDrawer; diff --git a/packages/x-flow/src/FlowEditor/components/NodeSelectPopover/index.tsx b/packages/x-flow/src/FlowEditor/components/NodeSelectPopover/index.tsx index 2fed50839..278bcf78b 100644 --- a/packages/x-flow/src/FlowEditor/components/NodeSelectPopover/index.tsx +++ b/packages/x-flow/src/FlowEditor/components/NodeSelectPopover/index.tsx @@ -1,5 +1,5 @@ -import { useCallback, useState, useRef } from 'react'; +import React, { useCallback, useState, useRef } from 'react'; import { Popover, Input, Tabs } from 'antd'; import { SearchOutlined } from '@ant-design/icons'; import { useEventListener } from 'ahooks'; diff --git a/packages/x-flow/src/FlowEditor/components/PanelContainer/index.tsx b/packages/x-flow/src/FlowEditor/components/PanelContainer/index.tsx index 664c46ff5..685d35409 100644 --- a/packages/x-flow/src/FlowEditor/components/PanelContainer/index.tsx +++ b/packages/x-flow/src/FlowEditor/components/PanelContainer/index.tsx @@ -1,8 +1,6 @@ import React from 'react'; import { Divider, Drawer, Input, Space } from 'antd'; -import IconView from '@/components/IconView'; -import { useFlow } from '@/hooks/useWorkFlow'; -import NodeDebugDrawer from '../NodeDebugDrawer'; +import IconView from '../../components/IconView'; import './index.less'; const getDescription = (nodeType: string, description: string) => { @@ -17,20 +15,10 @@ const getDescription = (nodeType: string, description: string) => { const Panel = (props: any) => { const { onClose, children, title, icon, nodeType, disabled, node } = props; - const { handleItemDebug } = useFlow(); - - const [nodeDebugVisible, setNodeDebugVisible] = React.useState(false); const isDisabled = ['Input', 'Output'].includes(nodeType) || disabled; const description = getDescription(nodeType, props.description); - const handleNodeDebug = async () => { - const res = await handleItemDebug(node); - if (!res) { - return; - } - setNodeDebugVisible(true); - }; return ( { @@ -91,12 +79,6 @@ const Panel = (props: any) => { } > {children} - setNodeDebugVisible(false)} - /> ); }; diff --git a/packages/x-flow/src/FlowEditor/index.tsx b/packages/x-flow/src/FlowEditor/index.tsx index 6251c049d..e46db7f6d 100644 --- a/packages/x-flow/src/FlowEditor/index.tsx +++ b/packages/x-flow/src/FlowEditor/index.tsx @@ -1,4 +1,4 @@ -import { memo } from 'react'; +import React, { memo } from 'react'; import { ReactFlowProvider } from '@xyflow/react'; import { WorkflowContextProvider } from './context'; import FlowEditor from './main'; diff --git a/packages/x-flow/src/FlowEditor/main.tsx b/packages/x-flow/src/FlowEditor/main.tsx index 77dbcff3d..62d9ffb84 100644 --- a/packages/x-flow/src/FlowEditor/main.tsx +++ b/packages/x-flow/src/FlowEditor/main.tsx @@ -1,5 +1,5 @@ import type { FC } from 'react'; -import { memo, useEffect, useMemo, useRef, useState } from 'react'; +import React, { memo, useEffect, useMemo, useRef, useState } from 'react'; import { useEventListener, useMemoizedFn } from 'ahooks'; import produce, { setAutoFreeze } from 'immer'; import { debounce } from 'lodash'; @@ -18,8 +18,7 @@ import CandidateNode from './components/CandidateNode'; import CustomEdge from './components/CustomEdge'; import PanelContainer from './components/PanelContainer'; import './index.less'; -import CustomNodeComponent from '../nodes'; -import { PanelComponentMap } from '../nodes/nodes'; +import CustomNodeComponent from './components/CustomNode'; import Operator from './operator'; import useStore, { useUndoRedo } from './store'; import { FlowEditorProps } from './types'; @@ -28,13 +27,20 @@ import autoLayoutNodes from './utils/autoLayoutNodes'; const edgeTypes = { buttonedge: memo(CustomEdge) }; const CustomNode = memo(CustomNodeComponent); + + + /*** * * ReactFlow 入口 * */ -const FlowEditor: FC = memo( - ({ nodeMenus, nodes: originalNodes, edges: originalEdges }) => { +const FlowEditor: FC = memo((props) => { + const { nodeMenus, nodes: originalNodes, edges: originalEdges } = props; + + + + const workflowContainerRef = useRef(null); const store = useStoreApi(); const { updateEdge, addNodes, addEdges, zoomTo } = useReactFlow(); @@ -77,8 +83,7 @@ const FlowEditor: FC = memo( useEffect(() => { setNodeMenus(nodeMenus); - const _nodes: any = autoLayoutNodes(originalNodes, originalEdges); - setNodes(_nodes); + setNodes(originalNodes); setEdges(originalEdges); }, [JSON.stringify(originalNodes)]); @@ -202,8 +207,7 @@ const FlowEditor: FC = memo( (item) => item.type?.toLowerCase() === activeNode?.node?.toLowerCase(), ) || {}; - const NodeEditor = - PanelComponentMap[capitalize(`${activeNode?.node}Setting`)]; + // const NodeEditor = PanelComponentMap[capitalize(`${activeNode?.node}Setting`)]; return (
@@ -272,7 +276,7 @@ const FlowEditor: FC = memo( onClose={() => setActiveNode(null)} node={activeNode} > - + {/* */} )}
diff --git a/packages/x-flow/src/FlowEditor/operator/Control/index.tsx b/packages/x-flow/src/FlowEditor/operator/Control/index.tsx index 471db840f..b55d2a552 100644 --- a/packages/x-flow/src/FlowEditor/operator/Control/index.tsx +++ b/packages/x-flow/src/FlowEditor/operator/Control/index.tsx @@ -1,5 +1,5 @@ import type { MouseEvent } from 'react'; -import { memo } from 'react'; +import React, { memo } from 'react'; import { RiCursorLine, RiFunctionAddLine, diff --git a/packages/x-flow/src/FlowEditor/operator/UndoRedo/index.tsx b/packages/x-flow/src/FlowEditor/operator/UndoRedo/index.tsx index 3bc629df4..10ad5a173 100644 --- a/packages/x-flow/src/FlowEditor/operator/UndoRedo/index.tsx +++ b/packages/x-flow/src/FlowEditor/operator/UndoRedo/index.tsx @@ -1,7 +1,7 @@ -import { memo } from 'react'; +import React, { memo } from 'react'; import { RiArrowGoBackLine, RiArrowGoForwardFill } from '@remixicon/react' import { Button, Tooltip } from 'antd'; -import IconView from '@/components/IconView'; +import IconView from '../../components/IconView'; import './index.less'; export type UndoRedoProps = { diff --git a/packages/x-flow/src/FlowEditor/operator/ZoomInOut/index.tsx b/packages/x-flow/src/FlowEditor/operator/ZoomInOut/index.tsx index 809c76488..4c56c2d54 100644 --- a/packages/x-flow/src/FlowEditor/operator/ZoomInOut/index.tsx +++ b/packages/x-flow/src/FlowEditor/operator/ZoomInOut/index.tsx @@ -1,10 +1,10 @@ import type { FC } from 'react'; -import { Fragment, memo } from 'react'; +import React, { Fragment, memo } from 'react'; import { Button, Popover, Tooltip } from 'antd'; import { useReactFlow, useViewport } from '@xyflow/react'; import { getKeyboardKeyNameBySystem } from '../../utils'; import ShortcutsName from './shortcuts-name'; -import IconView from '@/components/IconView'; +import IconView from '../../components/IconView'; import './index.less'; enum ZoomType { diff --git a/packages/x-flow/src/FlowEditor/operator/index.tsx b/packages/x-flow/src/FlowEditor/operator/index.tsx index 31c753d1b..88301db8f 100644 --- a/packages/x-flow/src/FlowEditor/operator/index.tsx +++ b/packages/x-flow/src/FlowEditor/operator/index.tsx @@ -1,4 +1,4 @@ -import { memo } from 'react'; +import React, { memo } from 'react'; // import UndoRedo from '../header/undo-redo' import ZoomInOut from './ZoomInOut'; import UndoRedo from './UndoRedo'; diff --git a/packages/x-flow/src/index.ts b/packages/x-flow/src/index.ts index 267339946..768e79625 100644 --- a/packages/x-flow/src/index.ts +++ b/packages/x-flow/src/index.ts @@ -1,6 +1,6 @@ import FlowEditor from './FlowEditor'; import withProvider from './withProvider'; -import * as defaultWidgets from './widgets'; +import * as defaultNodes from './nodes'; export { default as useForm } from './models/useForm'; @@ -20,4 +20,4 @@ export type { WidgetProps, } from './type'; -export default withProvider(FlowEditor, defaultWidgets); +export default withProvider(FlowEditor, defaultNodes); diff --git a/packages/x-flow/src/nodes/node-input/index.tsx b/packages/x-flow/src/nodes/node-input/index.tsx index 2dbe4efb2..08e78d9d0 100644 --- a/packages/x-flow/src/nodes/node-input/index.tsx +++ b/packages/x-flow/src/nodes/node-input/index.tsx @@ -1,16 +1,15 @@ -import { memo } from 'react'; +import React, { memo } from 'react'; import NodeContainer from '../../FlowEditor/components/NodeContainer'; -import { iconSettingMap } from '../constant'; export default memo((props: any) => { const { onClick } = props; - const { icon } = iconSettingMap['Start']; + return ( diff --git a/packages/x-flow/src/withProvider.tsx b/packages/x-flow/src/withProvider.tsx index cdfabaf5c..fe01710fa 100644 --- a/packages/x-flow/src/withProvider.tsx +++ b/packages/x-flow/src/withProvider.tsx @@ -19,10 +19,6 @@ export default function withProvider(Element: React.ComponentType, default locale = 'zh-CN', widgets, methods, - form, - validateMessages, - globalProps={}, - globalConfig = {}, ...otherProps } = props; @@ -37,14 +33,8 @@ export default function withProvider(Element: React.ComponentType, default dayjs.locale('zh-cn'); }, [locale]); - useUnmount(() => { - form.resetFields(); - }); - if (!form) { - console.warn('Please provide a form instance to FormRender'); - return null; - } + const antdLocale = locale === 'zh-CN' ? zhCN : enUS; const formValidateMessages = locale === 'zh-CN' ? validateMessagesCN : validateMessagesEN; @@ -52,9 +42,6 @@ export default function withProvider(Element: React.ComponentType, default locale, widgets: { ...defaultWidgets, ...widgets }, methods, - form, - globalProps, - globalConfig }; const langPack: any = { @@ -67,16 +54,10 @@ export default function withProvider(Element: React.ComponentType, default - + From 882d44f87a015566654a99a1376735e74d2f02db Mon Sep 17 00:00:00 2001 From: "zhanbo.lh" Date: Thu, 14 Nov 2024 11:02:30 +0800 Subject: [PATCH 04/38] =?UTF-8?q?feat:=20xflow=20=E8=BF=AD=E4=BB=A3?= =?UTF-8?q?=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/x-flow/__tests__/core-utils.spec.ts | 28 -- packages/x-flow/__tests__/demo.tsx | 61 --- packages/x-flow/__tests__/form-demo.tsx | 112 ----- .../x-flow/__tests__/form-fields.spec.tsx | 129 ----- packages/x-flow/__tests__/form.spec.tsx | 28 -- .../x-flow/__tests__/get-descriptor.spec.ts | 144 ------ packages/x-flow/__tests__/schema.ts | 310 ------------ packages/x-flow/__tests__/utils.spec.ts | 38 -- packages/x-flow/src/FlowEditor/index.tsx | 22 - packages/x-flow/src/FlowEditor/types.ts | 5 - .../components/CandidateNode/index.tsx | 0 .../components/CustomEdge/index.less | 0 .../components/CustomEdge/index.tsx | 0 .../components/CustomHtml/index.tsx | 2 +- .../components/CustomNode/index.less | 0 .../components/CustomNode/index.tsx | 0 .../components/CustomNode/utils.ts | 0 .../components/FAutoComplete/index.tsx | 0 .../components/FlowDebugDrawer/index.less | 0 .../components/FlowDebugDrawer/index.tsx | 0 .../components/IconView/index.tsx | 0 .../components/NodeContainer/index.less | 0 .../components/NodeContainer/index.tsx | 2 +- .../components/NodeSelectPopover/index.less | 0 .../components/NodeSelectPopover/index.tsx | 2 +- .../components/PanelContainer/index.less | 0 .../components/PanelContainer/index.tsx | 2 +- .../src/{FlowEditor => core}/constants.ts | 0 .../src/{FlowEditor => core}/context.tsx | 2 +- .../src/{FlowEditor => core}/index.less | 0 .../{FlowEditor/main.tsx => core/index.tsx} | 14 +- .../operator/Control/index.less | 0 .../operator/Control/index.tsx | 0 .../operator/UndoRedo/index.less | 0 .../operator/UndoRedo/index.tsx | 0 .../operator/ZoomInOut/index.less | 0 .../operator/ZoomInOut/index.tsx | 0 .../operator/ZoomInOut/shortcuts-name.tsx | 0 .../{FlowEditor => core}/operator/index.less | 0 .../{FlowEditor => core}/operator/index.tsx | 0 .../x-flow/src/{FlowEditor => core}/store.ts | 0 packages/x-flow/src/core/types.ts | 7 + .../utils/autoLayoutNodes.ts | 0 .../src/{FlowEditor => core}/utils/hooks.ts | 0 .../src/{FlowEditor => core}/utils/index.ts | 0 packages/x-flow/src/index.ts | 21 +- packages/x-flow/src/locales/en_US.ts | 27 - packages/x-flow/src/locales/index.ts | 7 - packages/x-flow/src/locales/zh_CN.ts | 27 - packages/x-flow/src/models/context.ts | 2 +- packages/x-flow/src/models/store.ts | 4 +- .../x-flow/src/nodes/node-input/index.tsx | 2 +- .../x-flow/src/nodes/node-output/index.tsx | 2 +- .../src/nodes/node-output/setting/index.tsx | 2 +- .../x-flow/src/nodes/node-switch/index.tsx | 2 +- .../src/nodes/node-switch/setting/index.tsx | 2 +- packages/x-flow/src/type.ts | 475 ------------------ packages/x-flow/src/withProvider.tsx | 23 +- 58 files changed, 40 insertions(+), 1464 deletions(-) delete mode 100644 packages/x-flow/__tests__/core-utils.spec.ts delete mode 100644 packages/x-flow/__tests__/demo.tsx delete mode 100644 packages/x-flow/__tests__/form-demo.tsx delete mode 100644 packages/x-flow/__tests__/form-fields.spec.tsx delete mode 100644 packages/x-flow/__tests__/form.spec.tsx delete mode 100644 packages/x-flow/__tests__/get-descriptor.spec.ts delete mode 100644 packages/x-flow/__tests__/schema.ts delete mode 100644 packages/x-flow/__tests__/utils.spec.ts delete mode 100644 packages/x-flow/src/FlowEditor/index.tsx delete mode 100644 packages/x-flow/src/FlowEditor/types.ts rename packages/x-flow/src/{FlowEditor => core}/components/CandidateNode/index.tsx (100%) rename packages/x-flow/src/{FlowEditor => core}/components/CustomEdge/index.less (100%) rename packages/x-flow/src/{FlowEditor => core}/components/CustomEdge/index.tsx (100%) rename packages/x-flow/src/{FlowEditor => core}/components/CustomHtml/index.tsx (97%) rename packages/x-flow/src/{FlowEditor => core}/components/CustomNode/index.less (100%) rename packages/x-flow/src/{FlowEditor => core}/components/CustomNode/index.tsx (100%) rename packages/x-flow/src/{FlowEditor => core}/components/CustomNode/utils.ts (100%) rename packages/x-flow/src/{FlowEditor => core}/components/FAutoComplete/index.tsx (100%) rename packages/x-flow/src/{FlowEditor => core}/components/FlowDebugDrawer/index.less (100%) rename packages/x-flow/src/{FlowEditor => core}/components/FlowDebugDrawer/index.tsx (100%) rename packages/x-flow/src/{FlowEditor => core}/components/IconView/index.tsx (100%) rename packages/x-flow/src/{FlowEditor => core}/components/NodeContainer/index.less (100%) rename packages/x-flow/src/{FlowEditor => core}/components/NodeContainer/index.tsx (93%) rename packages/x-flow/src/{FlowEditor => core}/components/NodeSelectPopover/index.less (100%) rename packages/x-flow/src/{FlowEditor => core}/components/NodeSelectPopover/index.tsx (99%) rename packages/x-flow/src/{FlowEditor => core}/components/PanelContainer/index.less (100%) rename packages/x-flow/src/{FlowEditor => core}/components/PanelContainer/index.tsx (98%) rename packages/x-flow/src/{FlowEditor => core}/constants.ts (100%) rename packages/x-flow/src/{FlowEditor => core}/context.tsx (94%) rename packages/x-flow/src/{FlowEditor => core}/index.less (100%) rename packages/x-flow/src/{FlowEditor/main.tsx => core/index.tsx} (96%) rename packages/x-flow/src/{FlowEditor => core}/operator/Control/index.less (100%) rename packages/x-flow/src/{FlowEditor => core}/operator/Control/index.tsx (100%) rename packages/x-flow/src/{FlowEditor => core}/operator/UndoRedo/index.less (100%) rename packages/x-flow/src/{FlowEditor => core}/operator/UndoRedo/index.tsx (100%) rename packages/x-flow/src/{FlowEditor => core}/operator/ZoomInOut/index.less (100%) rename packages/x-flow/src/{FlowEditor => core}/operator/ZoomInOut/index.tsx (100%) rename packages/x-flow/src/{FlowEditor => core}/operator/ZoomInOut/shortcuts-name.tsx (100%) rename packages/x-flow/src/{FlowEditor => core}/operator/index.less (100%) rename packages/x-flow/src/{FlowEditor => core}/operator/index.tsx (100%) rename packages/x-flow/src/{FlowEditor => core}/store.ts (100%) create mode 100644 packages/x-flow/src/core/types.ts rename packages/x-flow/src/{FlowEditor => core}/utils/autoLayoutNodes.ts (100%) rename packages/x-flow/src/{FlowEditor => core}/utils/hooks.ts (100%) rename packages/x-flow/src/{FlowEditor => core}/utils/index.ts (100%) delete mode 100644 packages/x-flow/src/locales/en_US.ts delete mode 100644 packages/x-flow/src/locales/index.ts delete mode 100644 packages/x-flow/src/locales/zh_CN.ts delete mode 100644 packages/x-flow/src/type.ts diff --git a/packages/x-flow/__tests__/core-utils.spec.ts b/packages/x-flow/__tests__/core-utils.spec.ts deleted file mode 100644 index e9a04621f..000000000 --- a/packages/x-flow/__tests__/core-utils.spec.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { flattenSchema } from '../src/form-render-core/src/utils'; - -describe('Test FormRender Utils', () => { - it('Test flattenSchema', () => { - const schema = { - type: 'object', - properties: { - input1: { - title: '简单输入框', - type: 'string', - order: 2, - required: true, - }, - select1: { - title: '单选', - type: 'string', - order: 1, - enum: ['a', 'b', 'c'], - enumNames: ['早', '中', '晚'], - }, - }, - }; - - const _schema = flattenSchema(schema); - expect(Object.keys(_schema)).toEqual(['select1', 'input1', '#']); - }); -}); diff --git a/packages/x-flow/__tests__/demo.tsx b/packages/x-flow/__tests__/demo.tsx deleted file mode 100644 index eef173d2f..000000000 --- a/packages/x-flow/__tests__/demo.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import * as React from 'react'; -import { useState } from 'react'; -import FormRender, { useForm } from '../src/index'; -import { normalSchema } from './schema'; - -const SimpleForm = () => { - const form = useForm(); - const [state, setState] = useState({ - input1: 'fr', - select1: 'd', - }); - const onFinish = (formData, errors) => { - setState(formData); - }; - - const watch = { - // # 为全局 - '#': val => { - console.log('表单的实时数据为:', val); - }, - input1: { - handler: val => { - console.log(val); - }, - immediate: true, - }, - onSearch: val => {}, - }; - - const onMount = () => { - form.setValueByPath('link', 'www.baidu.com'); - }; - - const onClick = () => { - form.setValueByPath('link', 'www.baidu.com'); - }; - - return ( -
- -
-
{state?.input1}
-
{state?.select1}
-
- - -
- ); -}; - -export default SimpleForm; diff --git a/packages/x-flow/__tests__/form-demo.tsx b/packages/x-flow/__tests__/form-demo.tsx deleted file mode 100644 index 4d368dc7b..000000000 --- a/packages/x-flow/__tests__/form-demo.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import * as React from 'react'; -import { useState } from 'react'; -import FormRender, { useForm } from '../src/index'; - -const schema = { - type: 'object', - properties: { - input1: { - type: 'object', - properties: { - test: { - title: '简单输入框', - type: 'string', - }, - }, - }, - select1: { - title: '单选', - type: 'string', - enum: ['a', 'b', 'c'], - enumNames: ['早', '中', '晚'], - }, - }, -}; - -const SimpleForm = () => { - const form = useForm(); - const [state, setState] = useState(); - const setRes = data => { - let res; - if (typeof data === 'object') { - res = JSON.stringify(data); - } - res = (res || data) + ''; - setState(res); - }; - - const handleIsFieldTouched = () => { - const res = form.isFieldTouched('input1.test'); - setRes(res); - }; - - const handleIsFieldsTouched = () => { - const res = form.isFieldsTouched(['input1.test', 'select1'], true); - setRes(res); - }; - - const handleIsFieldValidating = () => { - const res = form.isFieldValidating('select1'); - setRes(res); - }; - - const handleSetFields = () => { - form.setFields([ - { - name: 'input1.test', - touched: true, - error: ['set input1.test error'], - value: 'input1.test value', - }, - { - name: 'select1', - validating: true, - value: 'select1 value', - }, - ]); - }; - - const handleGetFieldError = () => { - const res = form.getFieldError('input1.test'); - setRes(res); - }; - - const handleValidateFields = () => { - form.validateFields().then(data => { - // data: - // { - // input1: { - // test: 'input1.test value', - // }, - // select1: 'select1 value, - // } - setRes(data); - }); - }; - - const handleGetValues = () => { - const res = form.getValues(['input1.test', 'select1'], ({ touched }) => { - return touched; - }); - setRes(res); - }; - - return ( -
- -
-
-
-
-
-
-
-
{state}
-
- ); -}; - -export default SimpleForm; diff --git a/packages/x-flow/__tests__/form-fields.spec.tsx b/packages/x-flow/__tests__/form-fields.spec.tsx deleted file mode 100644 index 03fc4448d..000000000 --- a/packages/x-flow/__tests__/form-fields.spec.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import { describe, test, afterAll, expect } from 'vitest'; -import * as React from 'react'; -import '@testing-library/jest-dom'; -import { render, act, cleanup } from '@testing-library/react'; -import Demo from './form-demo'; - -function sleep(ms): Promise { - return new Promise(resolve => setTimeout(resolve, ms)); -} - -afterAll(cleanup); - -describe('FormRender API', () => { - test('📦 api test setFields and getFieldError success', async () => { - const { getByTestId, unmount } = render(); - // 测试 setFields + getFieldError - act(() => { - getByTestId('setFields').click(); - }); - await act(() => sleep(500)); - act(() => { - getByTestId('getFieldError').click(); - }); - await act(() => sleep(500)); - expect(getByTestId('result')).toHaveTextContent( - JSON.stringify(['set input1.test error']) - ); - act(() => { - unmount(); - }); - }); - test('📦 api test validateFields success', async () => { - const { getByTestId, unmount } = render(); - act(() => { - getByTestId('setFields').click(); - }); - await act(() => sleep(500)); - act(() => { - getByTestId('validateFields').click(); - }); - await act(() => sleep(500)); - expect(getByTestId('result')).toHaveTextContent( - JSON.stringify({ - input1: { - test: 'input1.test value', - }, - select1: 'select1 value', - }) - ); - act(() => { - unmount(); - }); - }); - test('📦 api test isFieldValidating success', async () => { - const { getByTestId, unmount } = render(); - // 测试 isFieldValidating - act(() => { - getByTestId('setFields').click(); - }); - await act(() => sleep(500)); - act(() => { - getByTestId('fieldValidating').click(); - }); - await act(() => sleep(500)); - expect(getByTestId('result')).toHaveTextContent('true'); - act(() => { - unmount(); - }); - }); - - test('📦 api test isFieldTouched success', async () => { - const { getByTestId, unmount } = render(); - // 测试 isFieldTouched - act(() => { - getByTestId('setFields').click(); - }); - await act(() => sleep(500)); - act(() => { - getByTestId('fieldTouched').click(); - }); - await act(() => sleep(500)); - expect(getByTestId('result')).toHaveTextContent('true'); - act(() => { - unmount(); - }); - }); - - test('📦 api test isFieldsTouched success', async () => { - const { getByTestId, unmount } = render(); - act(() => { - getByTestId('setFields').click(); - }); - await act(() => sleep(500)); - // 测试 isFieldsTouched - act(() => { - getByTestId('fieldsTouched').click(); - }); - await act(() => sleep(500)); - expect(getByTestId('result')).toHaveTextContent('false'); - act(() => { - unmount(); - }); - }); - - test('📦 api test getValues success', async () => { - const { getByTestId, unmount } = render(); - - // 测试 getValues - act(() => { - getByTestId('setFields').click(); - }); - await act(() => sleep(500)); - // 测试 getValues - act(() => { - getByTestId('getValues').click(); - }); - await act(() => sleep(500)); - expect(getByTestId('result')).toHaveTextContent( - JSON.stringify({ - input1: { - test: 'input1.test value', - }, - }) - ); - act(() => { - unmount(); - }); - }); -}); diff --git a/packages/x-flow/__tests__/form.spec.tsx b/packages/x-flow/__tests__/form.spec.tsx deleted file mode 100644 index b81a1e34f..000000000 --- a/packages/x-flow/__tests__/form.spec.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { describe, it, afterAll, expect } from 'vitest'; -import * as React from 'react'; -import { render, act, cleanup } from '@testing-library/react'; -import '@testing-library/jest-dom'; -import Demo from './demo'; - -function sleep(ms): Promise { - return new Promise(resolve => setTimeout(resolve, ms)); -} - -afterAll(cleanup); - -describe('FormRender', () => { - it('📦 Render FR Success', async () => { - const { getByTestId, unmount } = render(); - act(() => { - getByTestId('submit').click(); - getByTestId('test').click(); - }); - await act(() => sleep(500)); - expect(getByTestId('input')).toHaveTextContent('简单输入框'); - expect(getByTestId('select')).toHaveTextContent('a'); - - act(() => { - unmount(); - }); - }); -}); diff --git a/packages/x-flow/__tests__/get-descriptor.spec.ts b/packages/x-flow/__tests__/get-descriptor.spec.ts deleted file mode 100644 index 0055c96b3..000000000 --- a/packages/x-flow/__tests__/get-descriptor.spec.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { getDescriptorSimple } from '../src/form-render-core/src/getDescriptorSimple'; - -// 主要对转换格式进行单元测试 -describe('get-descriptor utils', () => { - it('transform test 1', () => { - const schema = { - title: '代号', - type: 'string', - required: true, - rules: [{ pattern: '^[a-z]+$', message: 'incorrect province' }], - }; - - const res = getDescriptorSimple(schema, 'count'); - const expectData = { - count: [ - { required: true, type: 'string' }, - { pattern: /^[a-z]+$/, message: 'incorrect province' }, - ], - }; - - expect(res).toEqual(expectData); - }); - - it('transform test 2', () => { - const schema = { - title: '代号', - type: 'string', - rules: [ - { required: true, message: '必填' }, - { pattern: '^[a-z]+$', message: 'incorrect province' }, - ], - }; - - const res = getDescriptorSimple(schema, 'count'); - const expectData = { - count: [ - { required: true, message: '必填' }, - { pattern: /^[a-z]+$/, message: 'incorrect province' }, - { type: 'string' }, - ], - }; - - expect(res).toEqual(expectData); - }); - - it('transform test 3', () => { - const schema = { - title: '代号', - type: 'string', - rules: [{ pattern: '^[a-z]+$', message: 'incorrect province' }], - }; - - const res = getDescriptorSimple(schema, 'count'); - - const expectData = { - count: [ - { type: 'string' }, - { pattern: /^[a-z]+$/, message: 'incorrect province' }, - ], - }; - - expect(res).toEqual(expectData); - }); - - it('transform test 4', () => { - const schema = { - title: '代号', - type: 'string', - required: true, - }; - - const res = getDescriptorSimple(schema, 'count'); - - const expectData = { - count: [{ type: 'string', required: true }], - }; - - expect(res).toEqual(expectData); - }); - - it('transform test 5', () => { - const schema = { - title: '代号', - type: 'string', - }; - - const res = getDescriptorSimple(schema, 'count'); - - const expectData = { - count: [{ type: 'string' }], - }; - - expect(res).toEqual(expectData); - }); - - it('transform test 6', () => { - const schema = { - title: '时间选择', - type: 'string', - widget: 'site', - format: 'time', - required: true, - }; - - const res = getDescriptorSimple(schema, 'time'); - - const expectData = - '{"time":[{"required":true},{"type":"string","message":"${title}的格式错误"}]}'; - - expect(JSON.stringify(res)).toEqual(expectData); - }); - - it('transform test 7', () => { - const schema = { - title: '时间选择', - type: 'string', - widget: 'site', - format: 'time', - }; - - const res = getDescriptorSimple(schema, 'time'); - const expectData = - '{"time":[{"type":"string","message":"${title}的格式错误"}]}'; - - expect(JSON.stringify(res)).toEqual(expectData); - }); - - it('transform test 9', () => { - const schema = { - title: '时间选择', - type: 'string', - widget: 'site', - format: 'time', - required: true, - rules: [{ pattern: '^[a-z]+$', message: 'incorrect province' }], - }; - - const res = getDescriptorSimple(schema, 'count'); - const expectData = - '{"count":[{"required":true},{"type":"string","message":"${title}的格式错误"},{"pattern":{},"message":"incorrect province"}]}'; - expect(JSON.stringify(res)).toEqual(expectData); - }); -}); diff --git a/packages/x-flow/__tests__/schema.ts b/packages/x-flow/__tests__/schema.ts deleted file mode 100644 index 33e454d24..000000000 --- a/packages/x-flow/__tests__/schema.ts +++ /dev/null @@ -1,310 +0,0 @@ -export const normalSchema = { - type: 'object', - properties: { - input1: { - title: '简单输入框', - type: 'string', - required: true, - default: '简单输入框', - placeholder: '尝试在此输入', - className: 'input-with-px', - props: { - addonAfter: 'px', - }, - }, - numberDemo: { - title: '数字', - description: '数字输入框', - type: 'number', - min: 10, - max: 100, - step: 10, - }, - textareaDemo: { - title: '输入框', - type: 'string', - widget: 'textarea', - default: 'FormRender\nHello World!', - required: true, - }, - imgDemo: { - title: '图片', - type: 'string', - format: 'image', - default: - 'https://img.alicdn.com/tfs/TB1P8p2uQyWBuNjy0FpXXassXXa-750-1334.png', - }, - uploadDemo: { - title: '文件上传', - type: 'string', - default: - 'https://img.alicdn.com/tfs/TB1P8p2uQyWBuNjy0FpXXassXXa-750-1334.png', - widget: 'upload', - props: { - action: 'https://www.mocky.io/v2/5cc8019d300000980a055e76', - }, - }, - disabledDemo: { - title: '不可用', - type: 'string', - default: '我是一个被 disabled 的值', - disabled: true, - }, - select: { - title: '单选', - type: 'string', - enum: ['a', 'b', 'c'], - enumNames: ['', '中', '晚'], - default: 'a', - props: { - showSearch: true, - onSearch: 'onSearch', - }, - }, - select1: { - title: '单选', - type: 'string', - default: 'a', - props: { - options: [ - { label: '早', value: 'a' }, - { label: '晚', value: 'b' }, - ], - }, - }, - select2: { - title: '复选', - type: 'array', - enum: ['a', 'b', 'c'], - enumNames: ['早', '中', '晚'], - widgets: 'checkboxes', - default: 'a', - }, - select3: { - title: '多选', - type: 'array', - enum: ['a', 'b', 'c', 'd', 'e', 'f', 'g'], - enumNames: ['早', '中', '晚', 'd', 'e', 'f', 'g'], - default: 'a', - }, - radio: { - title: '单选radio', - type: 'string', - enum: ['a', 'b'], - enumNames: ['早', '中'], - default: 'a', - }, - fontSize: { - title: '字体大小', - readOnly: false, - required: false, - type: 'number', - widget: 'slider', - default: 24, - min: 12, - max: 64, - }, - width: { - title: '宽度', - readOnly: false, - required: false, - type: 'number', - widget: 'slider', - default: 280, - min: 100, - max: 560, - props: { - hideInput: true, - }, - }, - time: { - title: '时间', - type: 'string', - format: 'time', - }, - time2: { - title: '时间范围', - type: 'range', - format: 'time', - }, - link: { - title: '链接', - type: 'string', - format: 'url', - props: { - prefix: 'https://', - suffix: '.com', - }, - }, - dateDemo: { - title: '时间', - format: 'dateTime', - type: 'string', - widget: 'date', - width: '50%', - default: '2018-11-22', - required: true, - }, - dateRange: { - title: '时间范围', - format: 'dateTime', - type: 'range', - width: '50%', - }, - objDemo: { - title: '单个对象', - description: '这是一个对象类型', - type: 'object', - properties: { - isLike: { - title: '是否显示颜色选择', - type: 'boolean', - default: true, - }, - background: { - title: '颜色选择', - description: '特殊面板', - format: 'color', - type: 'string', - hidden: '{{rootValue.isLike === false}}', - default: '#ffff00', - }, - wayToTravel: { - title: '旅行方式', - type: 'string', - enum: ['self', 'group'], - enumNames: ['自驾', '跟团'], - widget: 'radio', - }, - canDrive: { - title: '是否拥有驾照', - type: 'boolean', - default: false, - hidden: "{{rootValue.wayToTravel !== 'self'}}", - }, - }, - required: ['background'], - }, - html1: { - title: '纯字符串', - type: 'html', - default: 'hello world', - }, - list: { - title: 'list', - type: 'array', - }, - objectName: { - type: 'object', - description: '这是一个对象类型', - collapsed: false, - properties: { - input1: { - title: '简单输入框', - type: 'string', - required: true, - }, - }, - }, - }, -}; - -export const listSchema = { - type: 'object', - properties: { - listName2: { - title: '对象数组', - description: '对象数组嵌套功能', - type: 'array', - // widget: 'cardList', - items: { - type: 'object', - properties: { - input1: { - title: '简单输入框', - type: 'string', - required: true, - }, - select1: { - title: '单选', - type: 'string', - enum: ['a', 'b', 'c'], - enumNames: ['早', '中', '晚'], - default: 'a', - }, - obj: { - title: '对象', - type: 'object', - properties: { - input1: { - title: '简单输入框', - type: 'string', - required: true, - default: '卡片列表', - }, - select1: { - title: '单选', - type: 'string', - enum: ['a', 'b', 'c'], - enumNames: ['早', '中', '晚'], - }, - }, - }, - }, - }, - }, - listName3: { - title: '对象数组', - description: '对象数组嵌套功能', - type: 'array', - widget: 'simpleList', - items: { - type: 'object', - properties: { - input1: { - title: '简单输入框', - type: 'string', - required: true, - }, - select1: { - title: '单选', - type: 'string', - enum: ['a', 'b', 'c'], - enumNames: ['早', '中', '晚'], - }, - }, - }, - }, - listName4: { - title: '对象数组', - description: '对象数组嵌套功能', - type: 'array', - widget: 'tableList', - items: { - type: 'object', - properties: { - input1: { - title: '简单输入框', - type: 'string', - required: true, - }, - input2: { - title: '简单输入框2', - type: 'string', - }, - input3: { - title: '简单输入框3', - type: 'string', - }, - select1: { - title: '单选', - type: 'string', - enum: ['a', 'b', 'c'], - enumNames: ['早', '中', '晚'], - widget: 'select', - }, - }, - }, - }, - }, -}; diff --git a/packages/x-flow/__tests__/utils.spec.ts b/packages/x-flow/__tests__/utils.spec.ts deleted file mode 100644 index 10d3577c2..000000000 --- a/packages/x-flow/__tests__/utils.spec.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { describe, test, expect } from 'vitest'; -import { getWidgetName } from '../src/form-render-core/src/mapping'; -import { getArray, getFormat, isUrl } from '../src/utils'; - -describe('Test Utils', () => { - test('Test getFormat', () => { - expect(getFormat('date')).toBe('YYYY-MM-DD'); - expect(getFormat('time')).toBe('HH:mm:ss'); - expect(getFormat('dateTime')).toBe('YYYY-MM-DD HH:mm:ss'); - expect(getFormat('week')).toBe('YYYY-w'); - expect(getFormat('year')).toBe('YYYY'); - expect(getFormat('quarter')).toBe('YYYY-Q'); - expect(getFormat('month')).toBe('YYYY-MM'); - expect(getFormat('YYYY-MM-DD')).toBe('YYYY-MM-DD'); - expect(getFormat(123)).toBe('YYYY-MM-DD'); - }); - - test('Test isUrl', () => { - expect(isUrl('https://github.com/alibaba/x-render')).toBe(true); - expect(isUrl('http://github.com/alibaba/x-render')).toBe(true); - expect(isUrl('github.com/alibaba/x-render')).toBe(false); - expect(isUrl(123)).toBe(false); - }); - - test('Test getArray', () => { - expect(getArray(['hangzhou', 'nanjing'])).toEqual(['hangzhou', 'nanjing']); - expect(getArray('test')).toEqual([]); - }); - - test('Test getWidgetName', () => { - expect( - getWidgetName({ - type: 'string', - format: 'date', - }) - ).toEqual('date'); - }); -}); diff --git a/packages/x-flow/src/FlowEditor/index.tsx b/packages/x-flow/src/FlowEditor/index.tsx deleted file mode 100644 index e46db7f6d..000000000 --- a/packages/x-flow/src/FlowEditor/index.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import React, { memo } from 'react'; -import { ReactFlowProvider } from '@xyflow/react'; -import { WorkflowContextProvider } from './context'; -import FlowEditor from './main'; - -const WorkflowContainer = (props: any) => { - const { initialState, nodeMenus } = props; - - return ( - - - - - - ); -}; - -export default memo(WorkflowContainer) diff --git a/packages/x-flow/src/FlowEditor/types.ts b/packages/x-flow/src/FlowEditor/types.ts deleted file mode 100644 index 52263e357..000000000 --- a/packages/x-flow/src/FlowEditor/types.ts +++ /dev/null @@ -1,5 +0,0 @@ -export type FlowEditorProps = { - nodes: any[] - edges: any[] - nodeMenus: any[] -} \ No newline at end of file diff --git a/packages/x-flow/src/FlowEditor/components/CandidateNode/index.tsx b/packages/x-flow/src/core/components/CandidateNode/index.tsx similarity index 100% rename from packages/x-flow/src/FlowEditor/components/CandidateNode/index.tsx rename to packages/x-flow/src/core/components/CandidateNode/index.tsx diff --git a/packages/x-flow/src/FlowEditor/components/CustomEdge/index.less b/packages/x-flow/src/core/components/CustomEdge/index.less similarity index 100% rename from packages/x-flow/src/FlowEditor/components/CustomEdge/index.less rename to packages/x-flow/src/core/components/CustomEdge/index.less diff --git a/packages/x-flow/src/FlowEditor/components/CustomEdge/index.tsx b/packages/x-flow/src/core/components/CustomEdge/index.tsx similarity index 100% rename from packages/x-flow/src/FlowEditor/components/CustomEdge/index.tsx rename to packages/x-flow/src/core/components/CustomEdge/index.tsx diff --git a/packages/x-flow/src/FlowEditor/components/CustomHtml/index.tsx b/packages/x-flow/src/core/components/CustomHtml/index.tsx similarity index 97% rename from packages/x-flow/src/FlowEditor/components/CustomHtml/index.tsx rename to packages/x-flow/src/core/components/CustomHtml/index.tsx index 29d46f421..64eda1582 100644 --- a/packages/x-flow/src/FlowEditor/components/CustomHtml/index.tsx +++ b/packages/x-flow/src/core/components/CustomHtml/index.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { Tooltip, Typography } from 'antd'; import _ from 'lodash'; -import IconView from '../../components/IconView'; +import IconView from '../IconView'; const { Text } = Typography; diff --git a/packages/x-flow/src/FlowEditor/components/CustomNode/index.less b/packages/x-flow/src/core/components/CustomNode/index.less similarity index 100% rename from packages/x-flow/src/FlowEditor/components/CustomNode/index.less rename to packages/x-flow/src/core/components/CustomNode/index.less diff --git a/packages/x-flow/src/FlowEditor/components/CustomNode/index.tsx b/packages/x-flow/src/core/components/CustomNode/index.tsx similarity index 100% rename from packages/x-flow/src/FlowEditor/components/CustomNode/index.tsx rename to packages/x-flow/src/core/components/CustomNode/index.tsx diff --git a/packages/x-flow/src/FlowEditor/components/CustomNode/utils.ts b/packages/x-flow/src/core/components/CustomNode/utils.ts similarity index 100% rename from packages/x-flow/src/FlowEditor/components/CustomNode/utils.ts rename to packages/x-flow/src/core/components/CustomNode/utils.ts diff --git a/packages/x-flow/src/FlowEditor/components/FAutoComplete/index.tsx b/packages/x-flow/src/core/components/FAutoComplete/index.tsx similarity index 100% rename from packages/x-flow/src/FlowEditor/components/FAutoComplete/index.tsx rename to packages/x-flow/src/core/components/FAutoComplete/index.tsx diff --git a/packages/x-flow/src/FlowEditor/components/FlowDebugDrawer/index.less b/packages/x-flow/src/core/components/FlowDebugDrawer/index.less similarity index 100% rename from packages/x-flow/src/FlowEditor/components/FlowDebugDrawer/index.less rename to packages/x-flow/src/core/components/FlowDebugDrawer/index.less diff --git a/packages/x-flow/src/FlowEditor/components/FlowDebugDrawer/index.tsx b/packages/x-flow/src/core/components/FlowDebugDrawer/index.tsx similarity index 100% rename from packages/x-flow/src/FlowEditor/components/FlowDebugDrawer/index.tsx rename to packages/x-flow/src/core/components/FlowDebugDrawer/index.tsx diff --git a/packages/x-flow/src/FlowEditor/components/IconView/index.tsx b/packages/x-flow/src/core/components/IconView/index.tsx similarity index 100% rename from packages/x-flow/src/FlowEditor/components/IconView/index.tsx rename to packages/x-flow/src/core/components/IconView/index.tsx diff --git a/packages/x-flow/src/FlowEditor/components/NodeContainer/index.less b/packages/x-flow/src/core/components/NodeContainer/index.less similarity index 100% rename from packages/x-flow/src/FlowEditor/components/NodeContainer/index.less rename to packages/x-flow/src/core/components/NodeContainer/index.less diff --git a/packages/x-flow/src/FlowEditor/components/NodeContainer/index.tsx b/packages/x-flow/src/core/components/NodeContainer/index.tsx similarity index 93% rename from packages/x-flow/src/FlowEditor/components/NodeContainer/index.tsx rename to packages/x-flow/src/core/components/NodeContainer/index.tsx index fc30ebff4..4a671775a 100644 --- a/packages/x-flow/src/FlowEditor/components/NodeContainer/index.tsx +++ b/packages/x-flow/src/core/components/NodeContainer/index.tsx @@ -1,5 +1,5 @@ import React, { memo } from 'react'; -import IconView from '../../components/IconView'; +import IconView from '../IconView'; import classNames from 'classnames'; import './index.less'; diff --git a/packages/x-flow/src/FlowEditor/components/NodeSelectPopover/index.less b/packages/x-flow/src/core/components/NodeSelectPopover/index.less similarity index 100% rename from packages/x-flow/src/FlowEditor/components/NodeSelectPopover/index.less rename to packages/x-flow/src/core/components/NodeSelectPopover/index.less diff --git a/packages/x-flow/src/FlowEditor/components/NodeSelectPopover/index.tsx b/packages/x-flow/src/core/components/NodeSelectPopover/index.tsx similarity index 99% rename from packages/x-flow/src/FlowEditor/components/NodeSelectPopover/index.tsx rename to packages/x-flow/src/core/components/NodeSelectPopover/index.tsx index 278bcf78b..f239c9bdf 100644 --- a/packages/x-flow/src/FlowEditor/components/NodeSelectPopover/index.tsx +++ b/packages/x-flow/src/core/components/NodeSelectPopover/index.tsx @@ -6,7 +6,7 @@ import { useEventListener } from 'ahooks'; import { useShallow } from 'zustand/react/shallow'; import { useClickAway } from 'ahooks'; import { useSet } from '../../utils/hooks'; -import IconView from '../../components/IconView'; +import IconView from '../IconView'; import useStore from '../../store'; import './index.less'; diff --git a/packages/x-flow/src/FlowEditor/components/PanelContainer/index.less b/packages/x-flow/src/core/components/PanelContainer/index.less similarity index 100% rename from packages/x-flow/src/FlowEditor/components/PanelContainer/index.less rename to packages/x-flow/src/core/components/PanelContainer/index.less diff --git a/packages/x-flow/src/FlowEditor/components/PanelContainer/index.tsx b/packages/x-flow/src/core/components/PanelContainer/index.tsx similarity index 98% rename from packages/x-flow/src/FlowEditor/components/PanelContainer/index.tsx rename to packages/x-flow/src/core/components/PanelContainer/index.tsx index 685d35409..21c0a9f1c 100644 --- a/packages/x-flow/src/FlowEditor/components/PanelContainer/index.tsx +++ b/packages/x-flow/src/core/components/PanelContainer/index.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { Divider, Drawer, Input, Space } from 'antd'; -import IconView from '../../components/IconView'; +import IconView from '../IconView'; import './index.less'; const getDescription = (nodeType: string, description: string) => { diff --git a/packages/x-flow/src/FlowEditor/constants.ts b/packages/x-flow/src/core/constants.ts similarity index 100% rename from packages/x-flow/src/FlowEditor/constants.ts rename to packages/x-flow/src/core/constants.ts diff --git a/packages/x-flow/src/FlowEditor/context.tsx b/packages/x-flow/src/core/context.tsx similarity index 94% rename from packages/x-flow/src/FlowEditor/context.tsx rename to packages/x-flow/src/core/context.tsx index 4a320c434..d0ce0520e 100644 --- a/packages/x-flow/src/FlowEditor/context.tsx +++ b/packages/x-flow/src/core/context.tsx @@ -1,5 +1,5 @@ import { createContext, useRef, useContext } from 'react'; -import { createStore } from 'zustand/vanilla' +import { createStore } from 'zustand/vanilla'; type Shape = { appId: string diff --git a/packages/x-flow/src/FlowEditor/index.less b/packages/x-flow/src/core/index.less similarity index 100% rename from packages/x-flow/src/FlowEditor/index.less rename to packages/x-flow/src/core/index.less diff --git a/packages/x-flow/src/FlowEditor/main.tsx b/packages/x-flow/src/core/index.tsx similarity index 96% rename from packages/x-flow/src/FlowEditor/main.tsx rename to packages/x-flow/src/core/index.tsx index 62d9ffb84..bc47f2f98 100644 --- a/packages/x-flow/src/FlowEditor/main.tsx +++ b/packages/x-flow/src/core/index.tsx @@ -4,6 +4,8 @@ import { useEventListener, useMemoizedFn } from 'ahooks'; import produce, { setAutoFreeze } from 'immer'; import { debounce } from 'lodash'; import { useShallow } from 'zustand/react/shallow'; +import { ReactFlowProvider } from '@xyflow/react'; + import { Background, BackgroundVariant, @@ -21,7 +23,7 @@ import './index.less'; import CustomNodeComponent from './components/CustomNode'; import Operator from './operator'; import useStore, { useUndoRedo } from './store'; -import { FlowEditorProps } from './types'; +import XFlowProps from './types'; import { capitalize, uuid } from './utils'; import autoLayoutNodes from './utils/autoLayoutNodes'; @@ -29,13 +31,12 @@ const edgeTypes = { buttonedge: memo(CustomEdge) }; const CustomNode = memo(CustomNodeComponent); - /*** * - * ReactFlow 入口 + * XFlow 入口 * */ -const FlowEditor: FC = memo((props) => { +const FlowEditor: FC = memo((props) => { const { nodeMenus, nodes: originalNodes, edges: originalEdges } = props; @@ -210,7 +211,8 @@ const FlowEditor: FC = memo((props) => { // const NodeEditor = PanelComponentMap[capitalize(`${activeNode?.node}Setting`)]; return ( -
+ +
= memo((props) => { )}
+ +
); }, ); diff --git a/packages/x-flow/src/FlowEditor/operator/Control/index.less b/packages/x-flow/src/core/operator/Control/index.less similarity index 100% rename from packages/x-flow/src/FlowEditor/operator/Control/index.less rename to packages/x-flow/src/core/operator/Control/index.less diff --git a/packages/x-flow/src/FlowEditor/operator/Control/index.tsx b/packages/x-flow/src/core/operator/Control/index.tsx similarity index 100% rename from packages/x-flow/src/FlowEditor/operator/Control/index.tsx rename to packages/x-flow/src/core/operator/Control/index.tsx diff --git a/packages/x-flow/src/FlowEditor/operator/UndoRedo/index.less b/packages/x-flow/src/core/operator/UndoRedo/index.less similarity index 100% rename from packages/x-flow/src/FlowEditor/operator/UndoRedo/index.less rename to packages/x-flow/src/core/operator/UndoRedo/index.less diff --git a/packages/x-flow/src/FlowEditor/operator/UndoRedo/index.tsx b/packages/x-flow/src/core/operator/UndoRedo/index.tsx similarity index 100% rename from packages/x-flow/src/FlowEditor/operator/UndoRedo/index.tsx rename to packages/x-flow/src/core/operator/UndoRedo/index.tsx diff --git a/packages/x-flow/src/FlowEditor/operator/ZoomInOut/index.less b/packages/x-flow/src/core/operator/ZoomInOut/index.less similarity index 100% rename from packages/x-flow/src/FlowEditor/operator/ZoomInOut/index.less rename to packages/x-flow/src/core/operator/ZoomInOut/index.less diff --git a/packages/x-flow/src/FlowEditor/operator/ZoomInOut/index.tsx b/packages/x-flow/src/core/operator/ZoomInOut/index.tsx similarity index 100% rename from packages/x-flow/src/FlowEditor/operator/ZoomInOut/index.tsx rename to packages/x-flow/src/core/operator/ZoomInOut/index.tsx diff --git a/packages/x-flow/src/FlowEditor/operator/ZoomInOut/shortcuts-name.tsx b/packages/x-flow/src/core/operator/ZoomInOut/shortcuts-name.tsx similarity index 100% rename from packages/x-flow/src/FlowEditor/operator/ZoomInOut/shortcuts-name.tsx rename to packages/x-flow/src/core/operator/ZoomInOut/shortcuts-name.tsx diff --git a/packages/x-flow/src/FlowEditor/operator/index.less b/packages/x-flow/src/core/operator/index.less similarity index 100% rename from packages/x-flow/src/FlowEditor/operator/index.less rename to packages/x-flow/src/core/operator/index.less diff --git a/packages/x-flow/src/FlowEditor/operator/index.tsx b/packages/x-flow/src/core/operator/index.tsx similarity index 100% rename from packages/x-flow/src/FlowEditor/operator/index.tsx rename to packages/x-flow/src/core/operator/index.tsx diff --git a/packages/x-flow/src/FlowEditor/store.ts b/packages/x-flow/src/core/store.ts similarity index 100% rename from packages/x-flow/src/FlowEditor/store.ts rename to packages/x-flow/src/core/store.ts diff --git a/packages/x-flow/src/core/types.ts b/packages/x-flow/src/core/types.ts new file mode 100644 index 000000000..ac3d87dc4 --- /dev/null +++ b/packages/x-flow/src/core/types.ts @@ -0,0 +1,7 @@ +export interface XFlowProps { + nodes: any[] + edges: any[] + nodeMenus: any[] +} + +export default XFlowProps; diff --git a/packages/x-flow/src/FlowEditor/utils/autoLayoutNodes.ts b/packages/x-flow/src/core/utils/autoLayoutNodes.ts similarity index 100% rename from packages/x-flow/src/FlowEditor/utils/autoLayoutNodes.ts rename to packages/x-flow/src/core/utils/autoLayoutNodes.ts diff --git a/packages/x-flow/src/FlowEditor/utils/hooks.ts b/packages/x-flow/src/core/utils/hooks.ts similarity index 100% rename from packages/x-flow/src/FlowEditor/utils/hooks.ts rename to packages/x-flow/src/core/utils/hooks.ts diff --git a/packages/x-flow/src/FlowEditor/utils/index.ts b/packages/x-flow/src/core/utils/index.ts similarity index 100% rename from packages/x-flow/src/FlowEditor/utils/index.ts rename to packages/x-flow/src/core/utils/index.ts diff --git a/packages/x-flow/src/index.ts b/packages/x-flow/src/index.ts index 768e79625..0323a4943 100644 --- a/packages/x-flow/src/index.ts +++ b/packages/x-flow/src/index.ts @@ -1,23 +1,10 @@ -import FlowEditor from './FlowEditor'; +import FlowCore from './core'; import withProvider from './withProvider'; -import * as defaultNodes from './nodes'; +import * as nodes from './nodes'; export { default as useForm } from './models/useForm'; - export type { default as FR, - Schema, - FRProps, - FormInstance, - FormParams, - FieldParams, - WatchProperties, - SchemaType, - SchemaBase, - ValidateParams, - ResetParams, - RuleItem, - WidgetProps, -} from './type'; +} from './core/types'; -export default withProvider(FlowEditor, defaultNodes); +export default withProvider(FlowCore, nodes); diff --git a/packages/x-flow/src/locales/en_US.ts b/packages/x-flow/src/locales/en_US.ts deleted file mode 100644 index 42e529c14..000000000 --- a/packages/x-flow/src/locales/en_US.ts +++ /dev/null @@ -1,27 +0,0 @@ -export default { - "copy_max_tip": "The maximum number of table items has been reached and cannot be copied", - "copy": "Copy", - "add_item": "Add a new line", - "confirm_delete": "Are you sure to delete?", - "confirm": "Yes", - "cancel": "No", - "operate": "Operate", - "delete": "Delete", - "edit": "Edit", - "img_src_error": "Image address error", - "upload": "Upload", - "upload_success": "upload success", - "upload_fail": "upload failed", - "uploaded_address": "Uploaded address", - "test_src": "Test address", - "schema_not_match": "Schema does not match the display component:", - "item": "Item", - "search": "Search", - "reset": "Reset", - "expand": "Expand", - "fold": "Fold", - "submit": "Submit", - "save": "Save", - "moveDown": "Move Down", - "moveUp": "Move Up" -} \ No newline at end of file diff --git a/packages/x-flow/src/locales/index.ts b/packages/x-flow/src/locales/index.ts deleted file mode 100644 index 0e44ddda6..000000000 --- a/packages/x-flow/src/locales/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import enUS from './en_US'; -import zhCN from './zh_CN'; - -export default { - 'en-US': enUS, - 'zh-CN': zhCN, -} \ No newline at end of file diff --git a/packages/x-flow/src/locales/zh_CN.ts b/packages/x-flow/src/locales/zh_CN.ts deleted file mode 100644 index e7dfc3082..000000000 --- a/packages/x-flow/src/locales/zh_CN.ts +++ /dev/null @@ -1,27 +0,0 @@ -export default { - "copy_max_tip": "已达表单项数量上限,无法复制!", - "copy": "复制", - "add_item": "新增一条", - "confirm_delete": "确定删除?", - "confirm": "确定", - "cancel": "取消", - "operate": "操作", - "delete": "删除", - "edit": "编辑", - "img_src_error": "图片地址错误", - "upload": "上传", - "upload_success": "上传成功", - "upload_fail": "上传失败", - "uploaded_address": "已上传地址", - "test_src": "测试链接", - "schema_not_match": "schema未匹配到展示组件:", - "item": "项目", - "search": "查询", - "reset": "重置", - "expand": "展开", - "fold": "收起", - "submit": "提交", - "save": "保存", - "moveDown": "下移", - "moveUp": "上移" -}; \ No newline at end of file diff --git a/packages/x-flow/src/models/context.ts b/packages/x-flow/src/models/context.ts index 1bd2251ff..418906d74 100644 --- a/packages/x-flow/src/models/context.ts +++ b/packages/x-flow/src/models/context.ts @@ -1,5 +1,5 @@ import { createContext } from 'react'; -export const FRContext = createContext(null); +export const FlowContext = createContext(null); export const ConfigContext = createContext(null); \ No newline at end of file diff --git a/packages/x-flow/src/models/store.ts b/packages/x-flow/src/models/store.ts index 814bb7490..602daea2e 100644 --- a/packages/x-flow/src/models/store.ts +++ b/packages/x-flow/src/models/store.ts @@ -1,4 +1,4 @@ -import { createStore as createx } from 'zustand'; +import { createStore as create } from 'zustand'; type FormStore = { schema?: any; @@ -10,7 +10,7 @@ type FormStore = { }; // 将 useStore 改为 createStore, 并把它改为 create 方法 -export const createStore = () => createx((setState: any, get: any) => ({ +export const createStore = () => create((setState: any, get: any) => ({ initialized: false, schema: {}, flattenSchema: {}, diff --git a/packages/x-flow/src/nodes/node-input/index.tsx b/packages/x-flow/src/nodes/node-input/index.tsx index 08e78d9d0..7ed08a8b5 100644 --- a/packages/x-flow/src/nodes/node-input/index.tsx +++ b/packages/x-flow/src/nodes/node-input/index.tsx @@ -1,5 +1,5 @@ import React, { memo } from 'react'; -import NodeContainer from '../../FlowEditor/components/NodeContainer'; +import NodeContainer from '../../core/components/NodeContainer'; export default memo((props: any) => { const { onClick } = props; diff --git a/packages/x-flow/src/nodes/node-output/index.tsx b/packages/x-flow/src/nodes/node-output/index.tsx index 361b8982d..268ae4e4d 100644 --- a/packages/x-flow/src/nodes/node-output/index.tsx +++ b/packages/x-flow/src/nodes/node-output/index.tsx @@ -1,5 +1,5 @@ import { memo } from 'react'; -import NodeContainer from '../../FlowEditor/components/NodeContainer'; +import NodeContainer from '../../core/components/NodeContainer'; export default memo((props: any) => { const { onClick } = props; diff --git a/packages/x-flow/src/nodes/node-output/setting/index.tsx b/packages/x-flow/src/nodes/node-output/setting/index.tsx index f332fa22c..f9fe2da37 100644 --- a/packages/x-flow/src/nodes/node-output/setting/index.tsx +++ b/packages/x-flow/src/nodes/node-output/setting/index.tsx @@ -2,7 +2,7 @@ import { useEffect, useRef } from 'react'; import { AnyObject } from 'antd/lib/_util/type'; import FormRender, { useForm } from 'form-render'; import { ICard, TYPES } from '../../constant'; -import FAutoComplete from '../../../FlowEditor/components/FAutoComplete'; +import FAutoComplete from '../../../core/components/FAutoComplete'; export interface GlobalOutputProps { data?: AnyObject; diff --git a/packages/x-flow/src/nodes/node-switch/index.tsx b/packages/x-flow/src/nodes/node-switch/index.tsx index 7d7f8d434..b99437f44 100644 --- a/packages/x-flow/src/nodes/node-switch/index.tsx +++ b/packages/x-flow/src/nodes/node-switch/index.tsx @@ -1,5 +1,5 @@ import { memo } from 'react'; -import NodeContainer from '../../FlowEditor/components/NodeContainer'; +import NodeContainer from '../../core/components/NodeContainer'; const getNodeList = (str: string) => { diff --git a/packages/x-flow/src/nodes/node-switch/setting/index.tsx b/packages/x-flow/src/nodes/node-switch/setting/index.tsx index f88b9d7bf..408482124 100644 --- a/packages/x-flow/src/nodes/node-switch/setting/index.tsx +++ b/packages/x-flow/src/nodes/node-switch/setting/index.tsx @@ -2,7 +2,7 @@ import { forwardRef, useEffect, useMemo } from 'react'; import { Collapse } from 'antd'; import FormRender, { useForm } from 'form-render'; import { useCollapse } from '@/hooks/useCollapse'; -import FAutoComplete from '../../../FlowEditor/components/FAutoComplete'; +import FAutoComplete from '../../../core/components/FAutoComplete'; import { getNodeSuggestOptions, getSuggestOptions } from '../../constant'; import '../../index.less'; import { getSchema, getSwitchSchema } from './schema'; diff --git a/packages/x-flow/src/type.ts b/packages/x-flow/src/type.ts deleted file mode 100644 index 7a628e22c..000000000 --- a/packages/x-flow/src/type.ts +++ /dev/null @@ -1,475 +0,0 @@ -import { RuleItem } from 'async-validator'; -import * as React from 'react'; -import type { FormInstance as AntdFormInstance, FormProps as AntdFormProps, ColProps, TooltipProps } from 'antd'; -import type { ConfigProviderProps } from 'antd/es/config-provider'; -import type { FormProps as RcFormProps } from 'rc-field-form/lib/Form'; - -export type { RuleItem } from 'async-validator'; -export type SchemaType = - | 'string' - | 'object' - | 'array' - | 'number' - | 'boolean' - | 'void' - | 'date' - | 'datetime' - | 'block' - | string; - -export type ActionProps = { - submit: { - text?: string; - hide?: boolean; - [key: string]: any; - }, - reset: { - text?: string; - hide?: boolean; - [key: string]: any; - } -} - -export interface SchemaBase { - type?: SchemaType; - title?: string; - description?: string; - descType?: 'text' | 'icon'; - format?: - | 'image' - | 'textarea' - | 'color' - | 'email' - | 'url' - | 'dateTime' - | 'date' - | 'time' - | 'upload' - | (string & {}); - default?: any; - /** 是否必填,支持 `'{{ formData.xxx === "" }}'` 形式的表达式 */ - required?: boolean | string; - placeholder?: string; - bind?: false | string | string[]; - dependencies?: string[]; - /** 最小值,支持表达式 */ - min?: number | string; - /** 最大值,支持表达式 */ - max?: number | string; - /** 是否禁用,支持 `'{{ formData.xxx === "" }}'` 形式的表达式 */ - disabled?: boolean | string; - /** 是否只读,支持 `'{{ formData.xxx === "" }}'` 形式的表达式 */ - readOnly?: boolean | string; - /** 是否隐藏,隐藏的字段不会在 formData 里透出,支持 `'{{ formData.xxx === "" }}'` 形式的表达式 */ - hidden?: boolean | string; - displayType?: 'row' | 'column' | string; - width?: string | number; - labelWidth?: number | string; - maxWidth?: number | string; - column?: number; - className?: string; - widget?: string; - readOnlyWidget?: string; - extra?: string; - properties?: Record; - items?: Schema; - /** 多选,支持表达式 */ - enum?: Array | string; - /** 多选label,支持表达式 */ - enumNames?: Array | string; - rules?: RuleItem | RuleItem[]; - props?: Record; - /**扩展字段 */ - 'add-widget'?: string; - labelCol?: number | ColProps; - fieldCol?: number | ColProps - tooltip?: string | TooltipProps - cellSpan?: number; - span?: number; - validateTrigger?: string | string[] - [key: string]: any; -} - -export type Schema = Partial; - -export interface Error { - /** 错误的数据路径 */ - name: string; - /** 错误的内容 */ - error: string[]; -} - -export interface FormParams { - formData?: any; - onChange?: (data: any) => void; - onValidate?: (valid: any) => void; - showValidate?: boolean; - /** 数据分析接口,表单展示完成渲染时触发 */ - logOnMount?: (stats: any) => void; - /** 数据分析接口,表单提交成功时触发,获得本次表单填写的总时长 */ - logOnSubmit?: (stats: any) => void; -} - -export interface ValidateParams { - formData: any; - schema: Schema; - error: Error[]; - - [k: string]: any; -} - -export interface ResetParams { - formData?: any; - submitData?: any; - errorFields?: Error[]; - touchedKeys?: any[]; - allTouched?: boolean; - - [k: string]: any; -} - -export interface FieldParams { - name: string; - error?: string[]; - touched?: boolean; - validating?: boolean; - value?: any; -} - -export interface ListOperate { - /* 列表表单操作按钮样式 */ - btnType: 'text' | 'icon'; - /* 是否隐藏移动按钮 */ - hideMove: boolean; -} - -export interface GlobalConfig { - /* 列表表单配置 */ - listOperate: ListOperate; - /** 列表校验气泡模式*/ - listValidatePopover: boolean; - /* 是否禁用表达式 */ - mustacheDisabled: boolean; -} - -export interface FormInstance { - /* - * 提交表单 - */ - submit: () => void, - /** - * 根据路径动态设置 Schema - */ - setSchemaByPath: (path: string, schema: any) => any; - /** - * 获取隐藏的表单数据 - */ - getHiddenValues: () => any; - /** - * 设置 Schema - */ - setSchema: (schema: any, cover?: boolean) => void; - /** - * 获取表单的 schema - */ - getSchema: () => any; - /** - * - * 获取 flatten schema - */ - getFlattenSchema: (path?: string) => any; - /** - * 根据路径获取 Schema - */ - getSchemaByPath: (path: string) => any; - /** - * 外部手动修改 errorFields 校验信息 - */ - setErrorFields: (errors: any[]) => void; - /** - * 外部手动删除某一个 path 下所有的校验信息 - */ - removeErrorField: (path: string) => any; - /** - * 校验表单 - */ - validateFields: AntdFormInstance['validateFields']; - /** - * 获取对应字段 field 的错误信息 - */ - getFieldError: AntdFormInstance['getFieldError']; - /** - * 获取一组字段 fields 的错误信息 - */ - getFieldsError: AntdFormInstance['getFieldsError']; - /** - * 检查某个表单项是否被修改过 - */ - isFieldTouched: AntdFormInstance['isFieldTouched']; - /** - * 检查一组表单项是否被修改过 - */ - isFieldsTouched: AntdFormInstance['isFieldsTouched']; - /** - * 检查某个表单项是否在校验中 - */ - isFieldValidating: AntdFormInstance['isFieldValidating']; - /** - * 根据路径获取表单值 - */ - getValueByPath: AntdFormInstance['getFieldValue']; - /** - * 根据路径修改表单值 - */ - setValueByPath: AntdFormInstance['setFieldValue']; - /** - * 获取表单值 - */ - getValues: AntdFormInstance['getFieldsValue']; - /** - * 设置表单值 - */ - setValues: AntdFormInstance['setFieldsValue']; - /** - * 重置表单 - */ - resetFields: AntdFormInstance['resetFields']; - /** - * @deprecated 即将弃用,请勿使用此 api,使用 getFieldsError - */ - errorFields: AntdFormInstance['getFieldsError']; - /** - * @deprecated 即将弃用,请勿使用此 api,使用 form.isFieldsValidating - */ - scrollToPath: AntdFormInstance['scrollToField']; - /** - * @deprecated 即将弃用,请勿使用此 api,使用setValueByPath - */ - onItemChange: AntdFormInstance['setFieldValue']; - /** - * @deprecated 即将弃用,请勿使用此 api - */ - init: any; - /** - * @deprecated 即将弃用,请勿使用此 api,使用 getSchema 代替 - */ - __schema: any; - /** - * @deprecated 内部方法不要使用 - */ - __initStore: (data: any) => any; - /** - * 存储 field 的 ref 对象 - */ - setFieldRef: (path: string, ref: any) => void; - /** - * 获取 field 的 ref 对象 - */ - getFieldRef: (path: string) => any; -} - -export type WatchProperties = { - [path: string]: - | { - handler: (value: any) => void; - immediate?: boolean; - } - | ((value: any) => void); -}; - -interface ExtendedColProps extends ColProps { - // 额外的属性可以放在这里 -} - -export interface FRProps extends Omit { - /** - * 表单顶层的className - */ - className?: string; - /** - * 表单顶层的样式 - */ - style?: React.CSSProperties; - /** - * 表单 schema - */ - schema: Schema; - /** - * form单例 - */ - form: FormInstance; - /** - * 组件和schema的映射规则 - */ - mapping?: Record; - /** - * 自定义组件 - */ - widgets?: Record; - /** - * 标签元素和输入元素的排列方式,column-分两行展示,row-同行展示,inline-自然顺排,默认`column` - */ - displayType?: 'column' | 'row' | 'inline'; - /** - * 表示是否显示 label 后面的冒号 - */ - colon?: boolean; - /** - * label 标签的文本对齐方式 - */ - labelAlign?: 'right' | 'left'; - // labelCol?: number | ExtendedColProps; - fieldCol?: number | ColProps; - /** - * 只读模式 - */ - readOnly?: boolean; - /** - * 禁用模式 - */ - disabled?: boolean; - /** - * 标签宽度 - */ - labelWidth?: string | number; - /** - * antd的全局config - */ - configProvider?: ConfigProviderProps; - /** - * 覆盖默认的校验信息 - */ - validateMessages?: RcFormProps['validateMessages']; - /** - * 显示当前表单内部状态 - */ - debug?: boolean; - /** - * 显示css布局提示线 - */ - debugCss?: boolean; - /** - * 展示语言,目前只支持中文、英文 - */ - locale?: 'zh-CN' | 'en-US'; - /** - * 一行展示的列数 - */ - column?: number; - /** - * 数据会作为 beforeFinish 的第四个参数传入 - */ - config?: any; - /** - * 类似于 vuejs 的 watch 的用法,监控值的变化,触发 callback - */ - watch?: WatchProperties; - /* - * 表单全局配置 - */ - globalConfig?: GlobalConfig; - /** - * 表单的全局共享属性 - */ - globalProps?: any; - /** - * 表单首次加载钩子 - */ - onMount?: () => void; - /** - * 表单提交前钩子 - */ - beforeFinish?: (params: ValidateParams) => Error[] | Promise; - /** - * 表单提交后钩子 - */ - onFinish?: (formData: any) => void; - /** - * 字段值更新时触发回调事件 - */ - onValuesChange?: ( - changedValues: { - dataPath: string; - value: any; - dataIndex: number[] | unknown; - }, - formData: any - ) => void; - /** - * 隐藏的数据是否去掉,默认不去掉 - */ - removeHiddenData?: boolean; - /** - * 配置自定义layout组件 - */ - layoutWidgets?: any; - /** - * 扩展方法 - */ - methods?: Record; - operateExtra?: React.ReactNode; - maxWidth?: number | string; - footer?: boolean | ((dom: React.JSX.Element[]) => React.ReactNode) | Partial ; -} - -export interface SearchProps extends Omit { - debug?: boolean; - searchBtnStyle?: React.CSSProperties; - searchBtnClassName?: string; - displayType?: any; - propsSchema?: any; - className?: string; - style?: React.CSSProperties; - hidden?: boolean; - searchOnMount?: boolean | unknown; - searchWithError?: boolean; - searchBtnRender?: ( - submit: Function, - clearSearch: Function, - other: any - ) => React.ReactNode[]; - searchText?: string; - resetText?: string; - onSearch?: (search: any) => any; - afterSearch?: (params: any) => any; - onReset?: (form: any) => void; - widgets?: any; - form?: any; - [key:string]: any -} - -/** 自定义组件 props */ -export type WidgetProps = { - value: any, - onChange: (value: any) => void, - schema: Schema, - style: React.CSSProperties, - id: string, - addons: WidgetAddonsType, - disabled?: boolean, - readOnly?: boolean, - [other: string]: any, -} - -/** 自定义组件 addons */ -export type WidgetAddonsType = FormInstance & { - globalProps: Record, - dependValues: any[], - dataIndex: string[], - dataPath: string, - schemaPath: string, -} - -declare const FR: React.FC; - -export declare function useForm(params?: FormParams): FormInstance; - -export type ConnectedForm = T & { - form: FormInstance; -}; - -export declare function connectForm( - component: React.ComponentType> -): React.ComponentType; - -export default FR; diff --git a/packages/x-flow/src/withProvider.tsx b/packages/x-flow/src/withProvider.tsx index fe01710fa..5a832b85b 100644 --- a/packages/x-flow/src/withProvider.tsx +++ b/packages/x-flow/src/withProvider.tsx @@ -1,23 +1,19 @@ import React, { useEffect, useRef } from 'react'; import { ConfigProvider } from 'antd'; -import dayjs from 'dayjs'; -import { useUnmount } from 'ahooks'; - import zhCN from 'antd/lib/locale/zh_CN'; import enUS from 'antd/lib/locale/en_US'; -import locales from './locales'; +import dayjs from 'dayjs'; import 'dayjs/locale/zh-cn'; import { createStore } from './models/store'; -import { FRContext, ConfigContext } from './models/context'; -import { validateMessagesEN, validateMessagesCN } from './models/validateMessage'; +import { FlowContext, ConfigContext } from './models/context'; -export default function withProvider(Element: React.ComponentType, defaultWidgets?: any) : React.ComponentType { +export default function withProvider(Element: React.ComponentType, defaultNodes?: any) : React.ComponentType { return (props: any) => { const { configProvider, locale = 'zh-CN', - widgets, + nodeWidges, methods, ...otherProps } = props; @@ -33,20 +29,15 @@ export default function withProvider(Element: React.ComponentType, default dayjs.locale('zh-cn'); }, [locale]); - - - const antdLocale = locale === 'zh-CN' ? zhCN : enUS; - const formValidateMessages = locale === 'zh-CN' ? validateMessagesCN : validateMessagesEN; const configContext = { locale, - widgets: { ...defaultWidgets, ...widgets }, methods, + widgets: { ...defaultNodes, ...nodeWidges }, }; const langPack: any = { ...antdLocale, - 'FormRender': locales[locale], ...configProvider?.locale }; @@ -56,9 +47,9 @@ export default function withProvider(Element: React.ComponentType, default locale={langPack} > - + - + ); From a3cf7453af50f99ecee7df01637053ab0a8aa26d Mon Sep 17 00:00:00 2001 From: "zhanbo.lh" Date: Thu, 14 Nov 2024 13:29:46 +0800 Subject: [PATCH 05/38] =?UTF-8?q?feat:=20=E8=BF=AD=E4=BB=A3=E5=BC=80?= =?UTF-8?q?=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/xflow/index.md | 4 +- .../components/CandidateNode/index.tsx | 2 +- .../components/CustomEdge/index.less | 0 .../components/CustomEdge/index.tsx | 4 +- .../components/CustomHtml/index.tsx | 0 .../src/components/CustomNode/index.less | 54 +++++++++++++++ .../src/components/CustomNode/index.tsx | 51 +++++++++++++++ .../{core => }/components/CustomNode/utils.ts | 0 .../components/FAutoComplete/index.tsx | 0 .../components/FlowDebugDrawer/index.less | 0 .../components/FlowDebugDrawer/index.tsx | 0 .../{core => }/components/IconView/index.tsx | 0 .../components/NodeContainer/index.less | 0 .../components/NodeContainer/index.tsx | 2 +- .../components/NodeSelectPopover/index.less | 0 .../components/NodeSelectPopover/index.tsx | 8 +-- .../components/PanelContainer/index.less | 0 .../components/PanelContainer/index.tsx | 0 .../src/core/components/CustomNode/index.less | 41 ------------ .../src/core/components/CustomNode/index.tsx | 41 ------------ packages/x-flow/src/core/index.less | 5 +- packages/x-flow/src/core/index.tsx | 65 +++++++++++++------ .../src/core/operator/Control/index.tsx | 4 +- .../src/core/operator/UndoRedo/index.tsx | 2 +- .../src/core/operator/ZoomInOut/index.tsx | 2 +- packages/x-flow/src/core/store.ts | 2 + packages/x-flow/src/core/types.ts | 12 +++- packages/x-flow/src/core/utils/index.ts | 18 ++++- packages/x-flow/src/nodes/index.tsx | 4 +- .../x-flow/src/nodes/node-input/index.tsx | 2 +- .../x-flow/src/nodes/node-output/index.tsx | 2 +- .../src/nodes/node-output/setting/index.tsx | 2 +- .../x-flow/src/nodes/node-switch/index.tsx | 2 +- .../src/nodes/node-switch/setting/index.tsx | 2 +- packages/x-flow/src/withProvider.tsx | 25 ++++--- 35 files changed, 212 insertions(+), 144 deletions(-) rename packages/x-flow/src/{core => }/components/CandidateNode/index.tsx (98%) rename packages/x-flow/src/{core => }/components/CustomEdge/index.less (100%) rename packages/x-flow/src/{core => }/components/CustomEdge/index.tsx (96%) rename packages/x-flow/src/{core => }/components/CustomHtml/index.tsx (100%) create mode 100644 packages/x-flow/src/components/CustomNode/index.less create mode 100644 packages/x-flow/src/components/CustomNode/index.tsx rename packages/x-flow/src/{core => }/components/CustomNode/utils.ts (100%) rename packages/x-flow/src/{core => }/components/FAutoComplete/index.tsx (100%) rename packages/x-flow/src/{core => }/components/FlowDebugDrawer/index.less (100%) rename packages/x-flow/src/{core => }/components/FlowDebugDrawer/index.tsx (100%) rename packages/x-flow/src/{core => }/components/IconView/index.tsx (100%) rename packages/x-flow/src/{core => }/components/NodeContainer/index.less (100%) rename packages/x-flow/src/{core => }/components/NodeContainer/index.tsx (86%) rename packages/x-flow/src/{core => }/components/NodeSelectPopover/index.less (100%) rename packages/x-flow/src/{core => }/components/NodeSelectPopover/index.tsx (95%) rename packages/x-flow/src/{core => }/components/PanelContainer/index.less (100%) rename packages/x-flow/src/{core => }/components/PanelContainer/index.tsx (100%) delete mode 100644 packages/x-flow/src/core/components/CustomNode/index.less delete mode 100644 packages/x-flow/src/core/components/CustomNode/index.tsx diff --git a/docs/xflow/index.md b/docs/xflow/index.md index 9362a473d..79d70d8c3 100644 --- a/docs/xflow/index.md +++ b/docs/xflow/index.md @@ -49,7 +49,7 @@ export default () => { const nodes = [ { id: '1', // 节点 ID - type: 'custom', // 节点类型 + type: 'Input', // 节点类型 data: {}, // 节点配置数据 position: { // 节点画布坐标位置 x: 100, @@ -58,7 +58,7 @@ export default () => { }, { id: '2', - type: 'custom', // 节点类型 + type: 'Output', // 节点类型 data: {}, // 节点配置数据 position: { // 节点画布坐标位置 x: 300, diff --git a/packages/x-flow/src/core/components/CandidateNode/index.tsx b/packages/x-flow/src/components/CandidateNode/index.tsx similarity index 98% rename from packages/x-flow/src/core/components/CandidateNode/index.tsx rename to packages/x-flow/src/components/CandidateNode/index.tsx index 636275f7c..056ed5dc6 100644 --- a/packages/x-flow/src/core/components/CandidateNode/index.tsx +++ b/packages/x-flow/src/components/CandidateNode/index.tsx @@ -5,7 +5,7 @@ import { useShallow } from 'zustand/react/shallow'; import { useReactFlow, useViewport } from '@xyflow/react'; import { useEventListener } from 'ahooks'; import CustomNode from '../CustomNode'; -import useStore from '../../store'; +import useStore from '../../core/store'; const CandidateNode = () => { const reactflow = useReactFlow(); diff --git a/packages/x-flow/src/core/components/CustomEdge/index.less b/packages/x-flow/src/components/CustomEdge/index.less similarity index 100% rename from packages/x-flow/src/core/components/CustomEdge/index.less rename to packages/x-flow/src/components/CustomEdge/index.less diff --git a/packages/x-flow/src/core/components/CustomEdge/index.tsx b/packages/x-flow/src/components/CustomEdge/index.tsx similarity index 96% rename from packages/x-flow/src/core/components/CustomEdge/index.tsx rename to packages/x-flow/src/components/CustomEdge/index.tsx index 8a48b3936..65516c8bc 100644 --- a/packages/x-flow/src/core/components/CustomEdge/index.tsx +++ b/packages/x-flow/src/components/CustomEdge/index.tsx @@ -3,8 +3,8 @@ import { PlusOutlined, CloseOutlined } from '@ant-design/icons'; import { BezierEdge, EdgeLabelRenderer, getBezierPath, useReactFlow } from '@xyflow/react'; import { useShallow } from 'zustand/react/shallow'; import produce from 'immer'; -import { uuid } from '../../utils'; -import useStore from '../../store'; +import { uuid } from '../../core/utils'; +import useStore from '../../core/store'; import NodeSelectPopover from '../NodeSelectPopover'; import './index.less'; diff --git a/packages/x-flow/src/core/components/CustomHtml/index.tsx b/packages/x-flow/src/components/CustomHtml/index.tsx similarity index 100% rename from packages/x-flow/src/core/components/CustomHtml/index.tsx rename to packages/x-flow/src/components/CustomHtml/index.tsx diff --git a/packages/x-flow/src/components/CustomNode/index.less b/packages/x-flow/src/components/CustomNode/index.less new file mode 100644 index 000000000..3265c31ec --- /dev/null +++ b/packages/x-flow/src/components/CustomNode/index.less @@ -0,0 +1,54 @@ +.xflow-node-container { + border: 2px solid #fff; + border-radius: 14px; + + .react-flow__edge-path, + .react-flow__connection-path { + stroke: #d0d5dc; + stroke-width: 2px; + } + + .react-flow__handle { + width: 32px; + height: 32px; + background: transparent; + border-radius: 0; + border: none; + + :hover { + border: 2px solid #00a952; + transform: scale(1.25); + } + } + + .react-flow__handle::after { + content: ''; + --tw-bg-opacity: 1; + background-color: #2970ff; + width: 2px; + height: 8px; + display: block; + margin: 14px 0 8px 15px; + } +} + +.xflow-node-container-tb { + .react-flow__handle::after { + content: ''; + --tw-bg-opacity: 1; + background-color: #2970ff; + width: 8px; + height: 2px; + display: block; + margin: 15px 0 0 12px; + } +} + +.xflow-node-container-selected { + border: 2px solid #296dff; + + .react-flow__handle::after { + display: none; + } +} + diff --git a/packages/x-flow/src/components/CustomNode/index.tsx b/packages/x-flow/src/components/CustomNode/index.tsx new file mode 100644 index 000000000..b5384c68d --- /dev/null +++ b/packages/x-flow/src/components/CustomNode/index.tsx @@ -0,0 +1,51 @@ +import React, { memo, useContext } from 'react'; +import classNames from 'classnames'; +import { Handle, Position } from '@xyflow/react'; +import { capitalize } from '../../core/utils'; + +import { ConfigContext } from '../../models/context'; +import './index.less'; + +export default memo((props: any) => { + const { id, type, data, layout, isConnectable, selected, onClick } = props; + const configCtx: any = useContext(ConfigContext); + + const NodeWidget = configCtx?.nodeWidgets[`${capitalize(type)}Node`]; + + let targetPosition = Position.Left; + let sourcePosition = Position.Right; + if (layout === 'TB') { + targetPosition = Position.Top; + sourcePosition = Position.Bottom; + } + + return ( +
+ {capitalize(type)!== 'Input' && ( + + )} + onClick(data)} + /> + {capitalize(type) !== 'Output' && ( + + )} +
+ ); +}) diff --git a/packages/x-flow/src/core/components/CustomNode/utils.ts b/packages/x-flow/src/components/CustomNode/utils.ts similarity index 100% rename from packages/x-flow/src/core/components/CustomNode/utils.ts rename to packages/x-flow/src/components/CustomNode/utils.ts diff --git a/packages/x-flow/src/core/components/FAutoComplete/index.tsx b/packages/x-flow/src/components/FAutoComplete/index.tsx similarity index 100% rename from packages/x-flow/src/core/components/FAutoComplete/index.tsx rename to packages/x-flow/src/components/FAutoComplete/index.tsx diff --git a/packages/x-flow/src/core/components/FlowDebugDrawer/index.less b/packages/x-flow/src/components/FlowDebugDrawer/index.less similarity index 100% rename from packages/x-flow/src/core/components/FlowDebugDrawer/index.less rename to packages/x-flow/src/components/FlowDebugDrawer/index.less diff --git a/packages/x-flow/src/core/components/FlowDebugDrawer/index.tsx b/packages/x-flow/src/components/FlowDebugDrawer/index.tsx similarity index 100% rename from packages/x-flow/src/core/components/FlowDebugDrawer/index.tsx rename to packages/x-flow/src/components/FlowDebugDrawer/index.tsx diff --git a/packages/x-flow/src/core/components/IconView/index.tsx b/packages/x-flow/src/components/IconView/index.tsx similarity index 100% rename from packages/x-flow/src/core/components/IconView/index.tsx rename to packages/x-flow/src/components/IconView/index.tsx diff --git a/packages/x-flow/src/core/components/NodeContainer/index.less b/packages/x-flow/src/components/NodeContainer/index.less similarity index 100% rename from packages/x-flow/src/core/components/NodeContainer/index.less rename to packages/x-flow/src/components/NodeContainer/index.less diff --git a/packages/x-flow/src/core/components/NodeContainer/index.tsx b/packages/x-flow/src/components/NodeContainer/index.tsx similarity index 86% rename from packages/x-flow/src/core/components/NodeContainer/index.tsx rename to packages/x-flow/src/components/NodeContainer/index.tsx index 4a671775a..89a81d71e 100644 --- a/packages/x-flow/src/core/components/NodeContainer/index.tsx +++ b/packages/x-flow/src/components/NodeContainer/index.tsx @@ -9,7 +9,7 @@ export default memo((props: any) => { return (
- + {title}
{children}
diff --git a/packages/x-flow/src/core/components/NodeSelectPopover/index.less b/packages/x-flow/src/components/NodeSelectPopover/index.less similarity index 100% rename from packages/x-flow/src/core/components/NodeSelectPopover/index.less rename to packages/x-flow/src/components/NodeSelectPopover/index.less diff --git a/packages/x-flow/src/core/components/NodeSelectPopover/index.tsx b/packages/x-flow/src/components/NodeSelectPopover/index.tsx similarity index 95% rename from packages/x-flow/src/core/components/NodeSelectPopover/index.tsx rename to packages/x-flow/src/components/NodeSelectPopover/index.tsx index f239c9bdf..2b6201bff 100644 --- a/packages/x-flow/src/core/components/NodeSelectPopover/index.tsx +++ b/packages/x-flow/src/components/NodeSelectPopover/index.tsx @@ -5,9 +5,9 @@ import { SearchOutlined } from '@ant-design/icons'; import { useEventListener } from 'ahooks'; import { useShallow } from 'zustand/react/shallow'; import { useClickAway } from 'ahooks'; -import { useSet } from '../../utils/hooks'; +import { useSet } from '../../core/utils/hooks'; import IconView from '../IconView'; -import useStore from '../../store'; +import useStore from '../../core/store'; import './index.less'; const items: any['items'] = [ @@ -53,7 +53,7 @@ const filterNodeList = (query: string, _nodeList: any[]) => { const NodeInfo = ({ icon, title, description }: any) => { return (
-
+
@@ -115,7 +115,7 @@ const SelectNodeView = ({ onCreate, nodeMenus, containerRef }: any) => { ) : ( } placement='right' arrow={false} key={item.type}>
onCreate(ev, item.type)}> - + {item.title} diff --git a/packages/x-flow/src/core/components/PanelContainer/index.less b/packages/x-flow/src/components/PanelContainer/index.less similarity index 100% rename from packages/x-flow/src/core/components/PanelContainer/index.less rename to packages/x-flow/src/components/PanelContainer/index.less diff --git a/packages/x-flow/src/core/components/PanelContainer/index.tsx b/packages/x-flow/src/components/PanelContainer/index.tsx similarity index 100% rename from packages/x-flow/src/core/components/PanelContainer/index.tsx rename to packages/x-flow/src/components/PanelContainer/index.tsx diff --git a/packages/x-flow/src/core/components/CustomNode/index.less b/packages/x-flow/src/core/components/CustomNode/index.less deleted file mode 100644 index b4048db05..000000000 --- a/packages/x-flow/src/core/components/CustomNode/index.less +++ /dev/null @@ -1,41 +0,0 @@ -.node-container { - border: 2px solid #fff; - border-radius: 14px; - - .react-flow__edge-path, - .react-flow__connection-path { - stroke: #d0d5dc; - stroke-width: 2px; - } -} - -.node-container-selected { - border: 2px solid #296dff; - - .react-flow__handle::after { - display: none; - } -} - -.react-flow__handle { - width: 32px; - height: 32px; - background: transparent; - border-radius: 0; - border: none; - - :hover { - border: 2px solid #00a952; - transform: scale(1.25); - } -} - -.react-flow__handle::after { - content: ''; - --tw-bg-opacity: 1; - background-color: #2970ff; - width: 8px; - height: 2px; - display: block; - margin: 15px 0 0 12px; -} \ No newline at end of file diff --git a/packages/x-flow/src/core/components/CustomNode/index.tsx b/packages/x-flow/src/core/components/CustomNode/index.tsx deleted file mode 100644 index bb8a3f81e..000000000 --- a/packages/x-flow/src/core/components/CustomNode/index.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import React, { memo } from 'react'; -import classNames from 'classnames'; -import { Handle, Position } from '@xyflow/react'; -import './index.less'; - -function capitalize(string: string) { - if (typeof string !== 'string' || string.length === 0) { - return string; - } - return string.charAt(0).toUpperCase() + string.slice(1); -} - -export default memo((props: any) => { - const { data, isConnectable, selected, onClick } = props; - - const NodeComponent = NodeComponentMap[`${capitalize(data?.node)}Node`]; - - return ( -
- {data?.node !== 'Input' && ( - - )} - onClick(data)} /> - {data?.node !== 'Output' && ( - - )} -
- ); -}); diff --git a/packages/x-flow/src/core/index.less b/packages/x-flow/src/core/index.less index 45dc2df69..c25b2c6a5 100644 --- a/packages/x-flow/src/core/index.less +++ b/packages/x-flow/src/core/index.less @@ -1,5 +1,6 @@ -#workflow-container { +#xflow-container { + height: 100%; width: 100%; background: #F0F2F7; - flex: 1; + position: relative; } \ No newline at end of file diff --git a/packages/x-flow/src/core/index.tsx b/packages/x-flow/src/core/index.tsx index bc47f2f98..d8246b8f5 100644 --- a/packages/x-flow/src/core/index.tsx +++ b/packages/x-flow/src/core/index.tsx @@ -1,11 +1,9 @@ import type { FC } from 'react'; -import React, { memo, useEffect, useMemo, useRef, useState } from 'react'; +import React, { memo, useEffect, useMemo, useRef, useState, useContext } from 'react'; import { useEventListener, useMemoizedFn } from 'ahooks'; import produce, { setAutoFreeze } from 'immer'; import { debounce } from 'lodash'; import { useShallow } from 'zustand/react/shallow'; -import { ReactFlowProvider } from '@xyflow/react'; - import { Background, BackgroundVariant, @@ -16,16 +14,17 @@ import { } from '@xyflow/react'; import '@xyflow/react/dist/style.css'; import { useEventEmitterContextContext } from '../context/event-emitter'; -import CandidateNode from './components/CandidateNode'; -import CustomEdge from './components/CustomEdge'; -import PanelContainer from './components/PanelContainer'; +import CandidateNode from '../components/CandidateNode'; +import CustomEdge from '../components/CustomEdge'; +import PanelContainer from '../components/PanelContainer'; import './index.less'; -import CustomNodeComponent from './components/CustomNode'; +import CustomNodeComponent from '../components/CustomNode'; import Operator from './operator'; import useStore, { useUndoRedo } from './store'; import XFlowProps from './types'; -import { capitalize, uuid } from './utils'; +import { capitalize, uuid, transformNodes } from './utils'; import autoLayoutNodes from './utils/autoLayoutNodes'; +import { ConfigContext } from '../models/context'; const edgeTypes = { buttonedge: memo(CustomEdge) }; const CustomNode = memo(CustomNodeComponent); @@ -38,6 +37,8 @@ const CustomNode = memo(CustomNodeComponent); */ const FlowEditor: FC = memo((props) => { const { nodeMenus, nodes: originalNodes, edges: originalEdges } = props; + const configCtx: any = useContext(ConfigContext); + @@ -47,6 +48,7 @@ const FlowEditor: FC = memo((props) => { const { updateEdge, addNodes, addEdges, zoomTo } = useReactFlow(); const { undo, redo, record } = useUndoRedo(false); const { + layout, nodes, edges, onNodesChange, @@ -61,6 +63,7 @@ const FlowEditor: FC = memo((props) => { useShallow((state) => ({ nodes: state.nodes, edges: state.edges, + layout: state.layout, setNodes: state.setNodes, setEdges: state.setEdges, setNodeMenus: state.setNodeMenus, @@ -84,7 +87,7 @@ const FlowEditor: FC = memo((props) => { useEffect(() => { setNodeMenus(nodeMenus); - setNodes(originalNodes); + setNodes(transformNodes(originalNodes)); setEdges(originalEdges); }, [JSON.stringify(originalNodes)]); @@ -196,23 +199,41 @@ const FlowEditor: FC = memo((props) => { setNodes([...nodes]); }, 200); + const nodeTypes = useMemo(() => { return { - custom: (props: any) => ( - - ), + custom: (props: any) => { + const { data, ...rest } = props; + const { _nodeType, ...restData } = data || {}; + return ( + + ); + } }; }, []); - const { icon, description } = - nodeMenus.find( + + + + + + + + const { icon, description } = nodeMenus.find( (item) => item.type?.toLowerCase() === activeNode?.node?.toLowerCase(), ) || {}; - // const NodeEditor = PanelComponentMap[capitalize(`${activeNode?.node}Setting`)]; + // const NodeEditor = useMemo(() => { + // return configCtx.nodeWidgets[capitalize(`${activeNode?.type}Panel`)] ||
1
; + // }, [activeNode?.id]); return ( - -
+
= memo((props) => { + {activeNode && ( = memo((props) => { onClose={() => setActiveNode(null)} node={activeNode} > - {/* */} + {/* */} )}
- - ); }, ); diff --git a/packages/x-flow/src/core/operator/Control/index.tsx b/packages/x-flow/src/core/operator/Control/index.tsx index b55d2a552..7c33839d5 100644 --- a/packages/x-flow/src/core/operator/Control/index.tsx +++ b/packages/x-flow/src/core/operator/Control/index.tsx @@ -7,9 +7,9 @@ import { RiStickyNoteAddLine, } from '@remixicon/react'; import { Tooltip, Button } from 'antd'; -import IconView from '../../components/IconView'; +import IconView from '../../../components/IconView'; import { useEventEmitterContextContext } from '../../../context/event-emitter'; -import NodeSelectPopover from '../../components/NodeSelectPopover'; +import NodeSelectPopover from '../../../components/NodeSelectPopover'; import './index.less'; const Control = (props: any) => { diff --git a/packages/x-flow/src/core/operator/UndoRedo/index.tsx b/packages/x-flow/src/core/operator/UndoRedo/index.tsx index 10ad5a173..c8349c63b 100644 --- a/packages/x-flow/src/core/operator/UndoRedo/index.tsx +++ b/packages/x-flow/src/core/operator/UndoRedo/index.tsx @@ -1,7 +1,7 @@ import React, { memo } from 'react'; import { RiArrowGoBackLine, RiArrowGoForwardFill } from '@remixicon/react' import { Button, Tooltip } from 'antd'; -import IconView from '../../components/IconView'; +import IconView from '../../../components/IconView'; import './index.less'; export type UndoRedoProps = { diff --git a/packages/x-flow/src/core/operator/ZoomInOut/index.tsx b/packages/x-flow/src/core/operator/ZoomInOut/index.tsx index 4c56c2d54..77fa759ad 100644 --- a/packages/x-flow/src/core/operator/ZoomInOut/index.tsx +++ b/packages/x-flow/src/core/operator/ZoomInOut/index.tsx @@ -4,7 +4,7 @@ import { Button, Popover, Tooltip } from 'antd'; import { useReactFlow, useViewport } from '@xyflow/react'; import { getKeyboardKeyNameBySystem } from '../../utils'; import ShortcutsName from './shortcuts-name'; -import IconView from '../../components/IconView'; +import IconView from '../../../components/IconView'; import './index.less'; enum ZoomType { diff --git a/packages/x-flow/src/core/store.ts b/packages/x-flow/src/core/store.ts index 13cb75e44..ee2f907a0 100644 --- a/packages/x-flow/src/core/store.ts +++ b/packages/x-flow/src/core/store.ts @@ -16,6 +16,7 @@ import _ from "lodash"; export type AppNode = Node; export type AppState = { + layout: 'LR' | 'TB', nodes: AppNode[]; edges: Edge[]; nodeMenus: any[]; @@ -37,6 +38,7 @@ const useStore = create()( immer( temporal( (set, get) => ({ + layout: 'LR', nodes: [], edges: [], candidateNode: null, diff --git a/packages/x-flow/src/core/types.ts b/packages/x-flow/src/core/types.ts index ac3d87dc4..db6a4e3d9 100644 --- a/packages/x-flow/src/core/types.ts +++ b/packages/x-flow/src/core/types.ts @@ -1,7 +1,13 @@ +import React from 'react'; +export interface ConfigCtxProps { + nodeWidges: React.ComponentType +} + export interface XFlowProps { - nodes: any[] - edges: any[] - nodeMenus: any[] + nodes: any[]; + edges: any[]; + nodeMenus: any[]; + layout: 'LR' | 'TB' } export default XFlowProps; diff --git a/packages/x-flow/src/core/utils/index.ts b/packages/x-flow/src/core/utils/index.ts index b203ee0bd..c7809ed8c 100644 --- a/packages/x-flow/src/core/utils/index.ts +++ b/packages/x-flow/src/core/utils/index.ts @@ -21,7 +21,21 @@ export const getKeyboardKeyNameBySystem = (key: string) => { export const capitalize = (string: string) => { if (typeof string !== 'string' || string.length === 0) { - return string; + return string; } - return string.charAt(0).toUpperCase() + string.slice(1); + return `${string.charAt(0).toUpperCase()}${string.slice(1)}`; +} + +export const transformNodes = (nodes: any[]) => { + return nodes?.map(item => { + const { type, data, ...rest } = item; + return { + type: 'custom', + data: { + ...data, + _nodeType: type, + }, + ...rest + } + }) } \ No newline at end of file diff --git a/packages/x-flow/src/nodes/index.tsx b/packages/x-flow/src/nodes/index.tsx index 07ffe5e84..36f8c1f56 100644 --- a/packages/x-flow/src/nodes/index.tsx +++ b/packages/x-flow/src/nodes/index.tsx @@ -1,2 +1,2 @@ -export { default as NodeInput } from './node-input'; -export { default as NodeOutput } from './node-output'; \ No newline at end of file +export { default as InputNode } from './node-input'; +export { default as OutputNode } from './node-output'; \ No newline at end of file diff --git a/packages/x-flow/src/nodes/node-input/index.tsx b/packages/x-flow/src/nodes/node-input/index.tsx index 7ed08a8b5..1216b9491 100644 --- a/packages/x-flow/src/nodes/node-input/index.tsx +++ b/packages/x-flow/src/nodes/node-input/index.tsx @@ -1,5 +1,5 @@ import React, { memo } from 'react'; -import NodeContainer from '../../core/components/NodeContainer'; +import NodeContainer from '../../components/NodeContainer'; export default memo((props: any) => { const { onClick } = props; diff --git a/packages/x-flow/src/nodes/node-output/index.tsx b/packages/x-flow/src/nodes/node-output/index.tsx index 268ae4e4d..fb232230f 100644 --- a/packages/x-flow/src/nodes/node-output/index.tsx +++ b/packages/x-flow/src/nodes/node-output/index.tsx @@ -1,5 +1,5 @@ import { memo } from 'react'; -import NodeContainer from '../../core/components/NodeContainer'; +import NodeContainer from '../../components/NodeContainer'; export default memo((props: any) => { const { onClick } = props; diff --git a/packages/x-flow/src/nodes/node-output/setting/index.tsx b/packages/x-flow/src/nodes/node-output/setting/index.tsx index f9fe2da37..ab584a305 100644 --- a/packages/x-flow/src/nodes/node-output/setting/index.tsx +++ b/packages/x-flow/src/nodes/node-output/setting/index.tsx @@ -2,7 +2,7 @@ import { useEffect, useRef } from 'react'; import { AnyObject } from 'antd/lib/_util/type'; import FormRender, { useForm } from 'form-render'; import { ICard, TYPES } from '../../constant'; -import FAutoComplete from '../../../core/components/FAutoComplete'; +import FAutoComplete from '../../../components/FAutoComplete'; export interface GlobalOutputProps { data?: AnyObject; diff --git a/packages/x-flow/src/nodes/node-switch/index.tsx b/packages/x-flow/src/nodes/node-switch/index.tsx index b99437f44..85f045e6f 100644 --- a/packages/x-flow/src/nodes/node-switch/index.tsx +++ b/packages/x-flow/src/nodes/node-switch/index.tsx @@ -1,5 +1,5 @@ import { memo } from 'react'; -import NodeContainer from '../../core/components/NodeContainer'; +import NodeContainer from '../../components/NodeContainer'; const getNodeList = (str: string) => { diff --git a/packages/x-flow/src/nodes/node-switch/setting/index.tsx b/packages/x-flow/src/nodes/node-switch/setting/index.tsx index 408482124..b7a37488f 100644 --- a/packages/x-flow/src/nodes/node-switch/setting/index.tsx +++ b/packages/x-flow/src/nodes/node-switch/setting/index.tsx @@ -2,7 +2,7 @@ import { forwardRef, useEffect, useMemo } from 'react'; import { Collapse } from 'antd'; import FormRender, { useForm } from 'form-render'; import { useCollapse } from '@/hooks/useCollapse'; -import FAutoComplete from '../../../core/components/FAutoComplete'; +import FAutoComplete from '../../../components/FAutoComplete'; import { getNodeSuggestOptions, getSuggestOptions } from '../../constant'; import '../../index.less'; import { getSchema, getSwitchSchema } from './schema'; diff --git a/packages/x-flow/src/withProvider.tsx b/packages/x-flow/src/withProvider.tsx index 5a832b85b..87dc129ba 100644 --- a/packages/x-flow/src/withProvider.tsx +++ b/packages/x-flow/src/withProvider.tsx @@ -1,4 +1,5 @@ import React, { useEffect, useRef } from 'react'; +import { ReactFlowProvider } from '@xyflow/react'; import { ConfigProvider } from 'antd'; import zhCN from 'antd/lib/locale/zh_CN'; import enUS from 'antd/lib/locale/en_US'; @@ -8,35 +9,31 @@ import 'dayjs/locale/zh-cn'; import { createStore } from './models/store'; import { FlowContext, ConfigContext } from './models/context'; -export default function withProvider(Element: React.ComponentType, defaultNodes?: any) : React.ComponentType { +export default function withProvider(Element: React.ComponentType, defaultNodeWidgets?: any) : React.ComponentType { return (props: any) => { const { configProvider, locale = 'zh-CN', - nodeWidges, + nodeWidgets, methods, - ...otherProps + ...restProps } = props; const storeRef = useRef(createStore()); const store: any = storeRef.current; useEffect(() => { - if (locale === 'en-US') { - dayjs.locale('en'); - return; - } - dayjs.locale('zh-cn'); + dayjs.locale(locale === 'en-US' ? 'en': 'zh-cn'); }, [locale]); const antdLocale = locale === 'zh-CN' ? zhCN : enUS; const configContext = { locale, methods, - widgets: { ...defaultNodes, ...nodeWidges }, + nodeWidgets: { ...defaultNodeWidgets, ...nodeWidgets }, }; - const langPack: any = { + const languagePackage = { ...antdLocale, ...configProvider?.locale }; @@ -44,14 +41,16 @@ export default function withProvider(Element: React.ComponentType, default return ( - + + + ); - }; + } } \ No newline at end of file From 72e658be72c45caa45f1171d5dee1d0eb379182c Mon Sep 17 00:00:00 2001 From: "zhanbo.lh" Date: Thu, 14 Nov 2024 13:36:46 +0800 Subject: [PATCH 06/38] =?UTF-8?q?feat:=20=E8=BF=AD=E4=BB=A3=E5=BC=80?= =?UTF-8?q?=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/xflow/index.md | 4 +- .../src/components/CustomNode/index.tsx | 4 +- packages/x-flow/src/nodes/index.tsx | 4 +- .../nodes/{node-input => node-end}/index.less | 0 .../nodes/{node-output => node-end}/index.tsx | 2 +- .../setting/index.tsx | 0 .../{node-output => node-start}/index.less | 0 .../{node-input => node-start}/index.tsx | 0 .../setting/index.tsx | 0 .../x-flow/src/nodes/node-switch/index.less | 16 --- .../x-flow/src/nodes/node-switch/index.tsx | 39 ------- .../src/nodes/node-switch/setting/index.tsx | 96 ----------------- .../src/nodes/node-switch/setting/schema.ts | 102 ------------------ 13 files changed, 7 insertions(+), 260 deletions(-) rename packages/x-flow/src/nodes/{node-input => node-end}/index.less (100%) rename packages/x-flow/src/nodes/{node-output => node-end}/index.tsx (91%) rename packages/x-flow/src/nodes/{node-output => node-end}/setting/index.tsx (100%) rename packages/x-flow/src/nodes/{node-output => node-start}/index.less (100%) rename packages/x-flow/src/nodes/{node-input => node-start}/index.tsx (100%) rename packages/x-flow/src/nodes/{node-input => node-start}/setting/index.tsx (100%) delete mode 100644 packages/x-flow/src/nodes/node-switch/index.less delete mode 100644 packages/x-flow/src/nodes/node-switch/index.tsx delete mode 100644 packages/x-flow/src/nodes/node-switch/setting/index.tsx delete mode 100644 packages/x-flow/src/nodes/node-switch/setting/schema.ts diff --git a/docs/xflow/index.md b/docs/xflow/index.md index 79d70d8c3..27c62d5c1 100644 --- a/docs/xflow/index.md +++ b/docs/xflow/index.md @@ -49,7 +49,7 @@ export default () => { const nodes = [ { id: '1', // 节点 ID - type: 'Input', // 节点类型 + type: 'Start', // 节点类型 data: {}, // 节点配置数据 position: { // 节点画布坐标位置 x: 100, @@ -58,7 +58,7 @@ export default () => { }, { id: '2', - type: 'Output', // 节点类型 + type: 'End', // 节点类型 data: {}, // 节点配置数据 position: { // 节点画布坐标位置 x: 300, diff --git a/packages/x-flow/src/components/CustomNode/index.tsx b/packages/x-flow/src/components/CustomNode/index.tsx index b5384c68d..c5022907c 100644 --- a/packages/x-flow/src/components/CustomNode/index.tsx +++ b/packages/x-flow/src/components/CustomNode/index.tsx @@ -26,7 +26,7 @@ export default memo((props: any) => { ['xflow-node-container-tb']: layout === 'TB' })} > - {capitalize(type)!== 'Input' && ( + {capitalize(type)!== 'Start' && ( { data={data} onClick={() => onClick(data)} /> - {capitalize(type) !== 'Output' && ( + {capitalize(type) !== 'End' && ( { diff --git a/packages/x-flow/src/nodes/node-output/setting/index.tsx b/packages/x-flow/src/nodes/node-end/setting/index.tsx similarity index 100% rename from packages/x-flow/src/nodes/node-output/setting/index.tsx rename to packages/x-flow/src/nodes/node-end/setting/index.tsx diff --git a/packages/x-flow/src/nodes/node-output/index.less b/packages/x-flow/src/nodes/node-start/index.less similarity index 100% rename from packages/x-flow/src/nodes/node-output/index.less rename to packages/x-flow/src/nodes/node-start/index.less diff --git a/packages/x-flow/src/nodes/node-input/index.tsx b/packages/x-flow/src/nodes/node-start/index.tsx similarity index 100% rename from packages/x-flow/src/nodes/node-input/index.tsx rename to packages/x-flow/src/nodes/node-start/index.tsx diff --git a/packages/x-flow/src/nodes/node-input/setting/index.tsx b/packages/x-flow/src/nodes/node-start/setting/index.tsx similarity index 100% rename from packages/x-flow/src/nodes/node-input/setting/index.tsx rename to packages/x-flow/src/nodes/node-start/setting/index.tsx diff --git a/packages/x-flow/src/nodes/node-switch/index.less b/packages/x-flow/src/nodes/node-switch/index.less deleted file mode 100644 index 0377a0f36..000000000 --- a/packages/x-flow/src/nodes/node-switch/index.less +++ /dev/null @@ -1,16 +0,0 @@ -.custom-node-start { - width: 240px; - padding: 0 12px; - background: #fff; - border-radius: 12px; - - .title { - display: flex; - height: 50px; - align-items: center; - span { - font-weight: bold; - margin-left: 8px; - } - } -} \ No newline at end of file diff --git a/packages/x-flow/src/nodes/node-switch/index.tsx b/packages/x-flow/src/nodes/node-switch/index.tsx deleted file mode 100644 index 85f045e6f..000000000 --- a/packages/x-flow/src/nodes/node-switch/index.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { memo } from 'react'; -import NodeContainer from '../../components/NodeContainer'; - - -const getNodeList = (str: string) => { - try { - return JSON.parse(str); - } catch { - return [] - } -} - - - -export default memo((props: any) => { - const { onClick, data } = props; - const nodeList = getNodeList(data?.contentBody); - - return ( - - {nodeList.map((item: any, index: number) => ( -
- CASE {index+1} - {item.node} -
- ))} -
- ); -}) - diff --git a/packages/x-flow/src/nodes/node-switch/setting/index.tsx b/packages/x-flow/src/nodes/node-switch/setting/index.tsx deleted file mode 100644 index b7a37488f..000000000 --- a/packages/x-flow/src/nodes/node-switch/setting/index.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import { forwardRef, useEffect, useMemo } from 'react'; -import { Collapse } from 'antd'; -import FormRender, { useForm } from 'form-render'; -import { useCollapse } from '@/hooks/useCollapse'; -import FAutoComplete from '../../../components/FAutoComplete'; -import { getNodeSuggestOptions, getSuggestOptions } from '../../constant'; -import '../../index.less'; -import { getSchema, getSwitchSchema } from './schema'; - -export default forwardRef((props: any, ref: any) => { - const { data, inputItem, flowList, isCollapsed, readonly } = props; - const form = useForm(); - const switchForm = useForm(); - const { activeKey, onChange: onCollapseChange } = useCollapse(isCollapsed); - - const request = useMemo(() => { - return getSuggestOptions(inputItem, flowList, data.code); - }, [inputItem, flowList, data.code]); - - const nodeRequest = useMemo(() => { - return getNodeSuggestOptions(flowList, data.code); - }, [flowList, data.code]); - - useEffect(() => { - form.setValues({ list: data.list }); - const switchData = data?.contentBody; - if (switchData) { - switchForm.setValues({ list: [...JSON.parse(switchData)] }); - } - }, [data.list]); - - const watch = { - '#': (allValues: any) => { - // '#': () => {} 等同于 onValuesChange - if (props.onChange) { - props.onChange( - Object.keys(allValues).length ? allValues : { list: [] }, - ); - } - }, - }; - - const switchWatch = { - '#': (allValues: any) => { - if (props.onChange) { - props.onChange({ - contentBody: JSON.stringify(allValues.list), - }); - } - }, - }; - - const schema = getSchema(request); - const switchSchema = getSwitchSchema(nodeRequest); - const items = [ - { - key: '2', - label: '输入变量(Input)', - children: ( - - ), - }, - { - key: '1', - label: '条件组(Switch)', - children: ( -
- -
- ), - }, - ]; - - return ( -
- -
- ); -}); diff --git a/packages/x-flow/src/nodes/node-switch/setting/schema.ts b/packages/x-flow/src/nodes/node-switch/setting/schema.ts deleted file mode 100644 index 48f2624f0..000000000 --- a/packages/x-flow/src/nodes/node-switch/setting/schema.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { TYPES } from '../../constant'; - -export const getSchema = (request: any) => ({ - type: 'object', - displayType: 'row', - properties: { - list: { - type: 'array', - widget: 'tableList', - props: { - hideMove: true, - hideCopy: true, - size: 'small', - addBtnProps: { - type: 'dashed', - size: 'small' - }, - actionColumnProps: { - width: 60, - }, - }, - items: { - type: 'object', - properties: { - name: { - title: '变量名称', - type: 'string', - width: 180, - rules: [ - { - pattern: /^[a-zA-Z_][a-zA-Z0-9_]*$/, - message: '只能包含字母、数字和下划线且以字母或划线开头', - }, - ], - }, - dataType: { - title: '变量类型', - type: 'string', - enum: TYPES.map((el) => el.toUpperCase()), - enumNames: TYPES, - width: 110, - widget: 'select', - }, - value: { - title: '变量值', - type: 'string', - widget: 'FAutoComplete', - props: { - placeholder: '${变量名}', - request - } - } - } - } - } - } -}) - -export const getSwitchSchema = (nodeRequest: any) => ({ - type: 'object', - displayType: 'row', - properties: { - list: { - type: 'array', - widget: 'tableList', - props: { - hideMove: true, - hideCopy: true, - size: 'small', - addBtnProps: { - type: 'dashed', - }, - actionColumnProps: { - width: 60, - }, - }, - items: { - type: 'object', - properties: { - node: { - title: '节点名称', - type: 'string', - widget: 'FAutoComplete', - width: 280, - required: true, - props: { - placeholder: '节点名称', - request: nodeRequest - } - }, - expression: { - title: '表达式', - type: 'string', - props: { - placeholder: '${表达式}', - } - } - } - } - } - } -}); \ No newline at end of file From cac76560e30380653571369703e5d03d6e37800b Mon Sep 17 00:00:00 2001 From: "zhanbo.lh" Date: Mon, 18 Nov 2024 00:37:55 +0800 Subject: [PATCH 07/38] =?UTF-8?q?feat:=20=E7=94=BB=E5=B8=83=E4=BC=98?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/xflow/index.md | 106 ++++++++++++++++++++++++++-- packages/x-flow/src/core/index.less | 4 ++ 2 files changed, 103 insertions(+), 7 deletions(-) diff --git a/docs/xflow/index.md b/docs/xflow/index.md index 27c62d5c1..7e2297548 100644 --- a/docs/xflow/index.md +++ b/docs/xflow/index.md @@ -45,6 +45,99 @@ import XFlow from '@xrenders/xflow'; import schema from './schema/basic'; import data from './data/basic'; + + +const nodeMenus = [ + { + title: 'Input', + type: 'Input', + icon: { + type: 'icon-start', + bgColor: '#17B26A', + } + }, + { + title: 'Output', + type: 'Output', + icon: { + type: 'icon-end', + bgColor: '#F79009', + } + }, + { + title: 'LLM', + type: 'LLM', + description: '调用大语言模型回答问题或者对自然语言进行处理', + icon: { + type: 'icon-model', + bgColor: '#6172F3', + } + }, + { + title: 'Prompt', + type: 'Prompt', + description: '通过精心设计提示词,提升大语言模型回答效果', + icon: { + type: 'icon-prompt', + bgColor: '#17B26A', + } + }, + { + title: '知识库', + type: 'knowledge', + description: '允许你从知识库中查询与用户问题相关的文本内容', + icon: { + type: 'icon-knowledge', + bgColor: '#6172F3' + } + }, + { + title: 'Switch', + type: 'Switch', + description: '允许你根据 if/else 条件将 workflow 拆分成两个分支', + icon: { + type: 'icon-switch', + bgColor: '#06AED4', + } + }, + { + title: 'HSF', + type: 'hsf', + description: '允许通过 HSF 协议发送服务器请求', + icon: { + type: 'icon-hsf', + bgColor: '#875BF7' + } + }, + { + title: 'Http', + type: 'http', + description: '允许通过 HTTP 协议发送服务器请求', + icon: { + type: 'icon-http', + bgColor: '#875BF7' + } + }, + { + title: '代码执行', + type: 'Code', + description: '执行一段 Groovy 或 Python 或 NodeJS 代码实现自定义逻辑', + icon: { + type: 'icon-code', + bgColor: '#2E90FA' + } + }, + { + title: '工具', + type: 'tool', + description: '允许使用工具能力', + icon: { + type: 'icon-gongju', + bgColor: '#2E90FA' + } + } +]; + export default () => { const nodes = [ { @@ -52,8 +145,8 @@ export default () => { type: 'Start', // 节点类型 data: {}, // 节点配置数据 position: { // 节点画布坐标位置 - x: 100, - y: 300, + x: 40, + y: 240, } }, { @@ -61,18 +154,17 @@ export default () => { type: 'End', // 节点类型 data: {}, // 节点配置数据 position: { // 节点画布坐标位置 - x: 300, - y: 600, + x: 500, + y: 240, } } - ] - - const nodeMenus = []; + ]; return (
diff --git a/packages/x-flow/src/core/index.less b/packages/x-flow/src/core/index.less index c25b2c6a5..540913a0b 100644 --- a/packages/x-flow/src/core/index.less +++ b/packages/x-flow/src/core/index.less @@ -3,4 +3,8 @@ width: 100%; background: #F0F2F7; position: relative; + + .react-flow__attribution { + display: none; + } } \ No newline at end of file From 1a7db4ccc9422c44758692bb7099f04ebe82b234 Mon Sep 17 00:00:00 2001 From: "zhanbo.lh" Date: Mon, 18 Nov 2024 01:06:14 +0800 Subject: [PATCH 08/38] =?UTF-8?q?feat:=20=E7=94=BB=E5=B8=83=E8=BF=AD?= =?UTF-8?q?=E4=BB=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/xflow/index.md | 2 +- .../src/components/CustomEdge/index.tsx | 22 +++++++++++++++---- .../src/components/CustomNode/index.less | 4 ++-- .../src/components/CustomNode/index.tsx | 1 - packages/x-flow/src/core/index.tsx | 7 ++++-- packages/x-flow/src/core/store.ts | 7 ++++++ 6 files changed, 33 insertions(+), 10 deletions(-) diff --git a/docs/xflow/index.md b/docs/xflow/index.md index 7e2297548..7eab87743 100644 --- a/docs/xflow/index.md +++ b/docs/xflow/index.md @@ -166,7 +166,7 @@ export default () => { nodes={nodes} edges={[]} nodeMenus={nodeMenus} - + layout='TB' />
); diff --git a/packages/x-flow/src/components/CustomEdge/index.tsx b/packages/x-flow/src/components/CustomEdge/index.tsx index 65516c8bc..fbf0520d9 100644 --- a/packages/x-flow/src/components/CustomEdge/index.tsx +++ b/packages/x-flow/src/components/CustomEdge/index.tsx @@ -1,10 +1,11 @@ -import React, { memo } from 'react'; +import React, { memo, useContext } from 'react'; import { PlusOutlined, CloseOutlined } from '@ant-design/icons'; import { BezierEdge, EdgeLabelRenderer, getBezierPath, useReactFlow } from '@xyflow/react'; import { useShallow } from 'zustand/react/shallow'; import produce from 'immer'; import { uuid } from '../../core/utils'; import useStore from '../../core/store'; +import { ConfigContext } from '../../models/context'; import NodeSelectPopover from '../NodeSelectPopover'; import './index.less'; @@ -20,8 +21,11 @@ export default memo((edge: any) => { selected, source, target, + layout, } = edge; + const configCtx: any = useContext(ConfigContext); + const reactflow = useReactFlow(); const [edgePath, labelX, labelY] = getBezierPath({ sourceX, @@ -56,12 +60,22 @@ export default memo((edge: any) => { }); setNodes(newNodes); }; + + let edgeExtra: any = { + sourceX: edge.sourceX - 15, + targetX: edge.targetX + 15 + } + if (layout === 'TB') { + edgeExtra = { + sourceY: edge.sourceY - 15, + targetY: edge.targetY + 13 + } + } return ( { } /> ); -}); \ No newline at end of file +}) \ No newline at end of file diff --git a/packages/x-flow/src/components/CustomNode/index.less b/packages/x-flow/src/components/CustomNode/index.less index 3265c31ec..f67c9971d 100644 --- a/packages/x-flow/src/components/CustomNode/index.less +++ b/packages/x-flow/src/components/CustomNode/index.less @@ -26,9 +26,9 @@ --tw-bg-opacity: 1; background-color: #2970ff; width: 2px; - height: 8px; + height: 10px; display: block; - margin: 14px 0 8px 15px; + margin: 11px 0 8px 15px; } } diff --git a/packages/x-flow/src/components/CustomNode/index.tsx b/packages/x-flow/src/components/CustomNode/index.tsx index c5022907c..e90100f09 100644 --- a/packages/x-flow/src/components/CustomNode/index.tsx +++ b/packages/x-flow/src/components/CustomNode/index.tsx @@ -2,7 +2,6 @@ import React, { memo, useContext } from 'react'; import classNames from 'classnames'; import { Handle, Position } from '@xyflow/react'; import { capitalize } from '../../core/utils'; - import { ConfigContext } from '../../models/context'; import './index.less'; diff --git a/packages/x-flow/src/core/index.tsx b/packages/x-flow/src/core/index.tsx index d8246b8f5..c74a01f51 100644 --- a/packages/x-flow/src/core/index.tsx +++ b/packages/x-flow/src/core/index.tsx @@ -26,7 +26,6 @@ import { capitalize, uuid, transformNodes } from './utils'; import autoLayoutNodes from './utils/autoLayoutNodes'; import { ConfigContext } from '../models/context'; -const edgeTypes = { buttonedge: memo(CustomEdge) }; const CustomNode = memo(CustomNodeComponent); @@ -56,6 +55,7 @@ const FlowEditor: FC = memo((props) => { onConnect, setNodes, setEdges, + setLayout, setNodeMenus, setCandidateNode, setMousePosition, @@ -64,6 +64,7 @@ const FlowEditor: FC = memo((props) => { nodes: state.nodes, edges: state.edges, layout: state.layout, + setLayout: state.setLayout, setNodes: state.setNodes, setEdges: state.setEdges, setNodeMenus: state.setNodeMenus, @@ -86,6 +87,7 @@ const FlowEditor: FC = memo((props) => { }, []); useEffect(() => { + setLayout(props.layout); setNodeMenus(nodeMenus); setNodes(transformNodes(originalNodes)); setEdges(originalEdges); @@ -216,8 +218,9 @@ const FlowEditor: FC = memo((props) => { ); } }; - }, []); + }, [layout]); + const edgeTypes = { buttonedge: (edgeProps: any) => }; diff --git a/packages/x-flow/src/core/store.ts b/packages/x-flow/src/core/store.ts index ee2f907a0..afbadd048 100644 --- a/packages/x-flow/src/core/store.ts +++ b/packages/x-flow/src/core/store.ts @@ -27,6 +27,7 @@ export type AppState = { onConnect: OnConnect; setNodes: (nodes: AppNode[]) => void; setEdges: (edges: Edge[]) => void; + setLayout: (layout: 'LR' | 'TB') => void; setNodeMenus: (nodeMenus: any[]) => void; setCandidateNode: (candidateNode: any) => void; setMousePosition: (mousePosition: any) => void; @@ -73,6 +74,12 @@ const useStore = create()( }, setMousePosition: (mousePosition: any) => { set({ mousePosition }); + }, + setLayout: (layout: 'LR' | 'TB') => { + if (!layout) { + return; + } + set({ layout }); } }), { From 715accbc0e510021e558409c42125a2201195130 Mon Sep 17 00:00:00 2001 From: "zhanbo.lh" Date: Mon, 18 Nov 2024 01:39:45 +0800 Subject: [PATCH 09/38] =?UTF-8?q?feat:=20=E8=BF=AD=E4=BB=A3=E4=BC=98?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/xflow/index.md | 1 - .../src/components/CustomNode/index.less | 18 +++++++++---- .../src/components/CustomNode/index.tsx | 27 ++++++++++++++++--- packages/x-flow/src/core/index.tsx | 2 +- 4 files changed, 38 insertions(+), 10 deletions(-) diff --git a/docs/xflow/index.md b/docs/xflow/index.md index 7eab87743..47125029f 100644 --- a/docs/xflow/index.md +++ b/docs/xflow/index.md @@ -166,7 +166,6 @@ export default () => { nodes={nodes} edges={[]} nodeMenus={nodeMenus} - layout='TB' />
); diff --git a/packages/x-flow/src/components/CustomNode/index.less b/packages/x-flow/src/components/CustomNode/index.less index f67c9971d..2e3ee847b 100644 --- a/packages/x-flow/src/components/CustomNode/index.less +++ b/packages/x-flow/src/components/CustomNode/index.less @@ -14,11 +14,6 @@ background: transparent; border-radius: 0; border: none; - - :hover { - border: 2px solid #00a952; - transform: scale(1.25); - } } .react-flow__handle::after { @@ -30,6 +25,19 @@ display: block; margin: 11px 0 8px 15px; } + + .xflow-node-add-box { + position: absolute; + top: 8px; + right: 9px; + width: 16px; + height: 16px; + border-radius: 16px; + display: flex; + align-items: center; + justify-content: center; + background-color: #2970ff; + } } .xflow-node-container-tb { diff --git a/packages/x-flow/src/components/CustomNode/index.tsx b/packages/x-flow/src/components/CustomNode/index.tsx index e90100f09..4f7c5cc02 100644 --- a/packages/x-flow/src/components/CustomNode/index.tsx +++ b/packages/x-flow/src/components/CustomNode/index.tsx @@ -1,4 +1,6 @@ -import React, { memo, useContext } from 'react'; +import React, { memo, useContext, useState } from 'react'; +import { Tooltip } from 'antd'; +import { PlusOutlined } from '@ant-design/icons'; import classNames from 'classnames'; import { Handle, Position } from '@xyflow/react'; import { capitalize } from '../../core/utils'; @@ -8,9 +10,10 @@ import './index.less'; export default memo((props: any) => { const { id, type, data, layout, isConnectable, selected, onClick } = props; const configCtx: any = useContext(ConfigContext); - const NodeWidget = configCtx?.nodeWidgets[`${capitalize(type)}Node`]; + const [isHovered, setIsHovered] = useState(false); + let targetPosition = Position.Left; let sourcePosition = Position.Right; if (layout === 'TB') { @@ -24,6 +27,8 @@ export default memo((props: any) => { ['xflow-node-container-selected']: !!selected, ['xflow-node-container-tb']: layout === 'TB' })} + onMouseEnter={() => setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} > {capitalize(type)!== 'Start' && ( { type='source' position={sourcePosition} isConnectable={isConnectable} - /> + > + {(selected || isHovered) && ( +
+ + + +
+ )} +
)}
); diff --git a/packages/x-flow/src/core/index.tsx b/packages/x-flow/src/core/index.tsx index c74a01f51..91c2303da 100644 --- a/packages/x-flow/src/core/index.tsx +++ b/packages/x-flow/src/core/index.tsx @@ -248,7 +248,7 @@ const FlowEditor: FC = memo((props) => { defaultEdgeOptions={{ type: 'buttonedge', style: { - strokeWidth: 1.2, // 线粗细 + strokeWidth: 1.5, // 线粗细 }, markerEnd: { type: MarkerType.ArrowClosed, // 箭头 From 7c0beb155a92216a415bed47c9e316511bb88fe0 Mon Sep 17 00:00:00 2001 From: "zhanbo.lh" Date: Mon, 18 Nov 2024 03:19:34 +0800 Subject: [PATCH 10/38] =?UTF-8?q?feat:=20=E8=BF=AD=E4=BB=A3=E5=BC=80?= =?UTF-8?q?=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/xflow/index.md | 10 +- .../src/components/CustomEdge/index.less | 18 +- .../src/components/CustomEdge/index.tsx | 98 +++-- .../src/components/CustomNode/index.tsx | 79 +++- .../components/NodeSelectPopover/index.tsx | 5 +- packages/x-flow/src/core/index.tsx | 366 +++++++++--------- 6 files changed, 333 insertions(+), 243 deletions(-) diff --git a/docs/xflow/index.md b/docs/xflow/index.md index 47125029f..44f75f27f 100644 --- a/docs/xflow/index.md +++ b/docs/xflow/index.md @@ -50,7 +50,7 @@ import data from './data/basic'; const nodeMenus = [ { title: 'Input', - type: 'Input', + type: 'Start', icon: { type: 'icon-start', bgColor: '#17B26A', @@ -58,7 +58,7 @@ const nodeMenus = [ }, { title: 'Output', - type: 'Output', + type: 'End', icon: { type: 'icon-end', bgColor: '#F79009', @@ -160,11 +160,15 @@ export default () => { } ]; + const edges = [ + { source: '1', target: '2', id: '234123' } + ] + return (
diff --git a/packages/x-flow/src/components/CustomEdge/index.less b/packages/x-flow/src/components/CustomEdge/index.less index 1920d5ccd..9b41ecff7 100644 --- a/packages/x-flow/src/components/CustomEdge/index.less +++ b/packages/x-flow/src/components/CustomEdge/index.less @@ -11,24 +11,14 @@ align-items: center; } - .icon-box { - width: 20px; - height: 20px; - border-radius: 20px; + .line-icon-box { + width: 16px; + height: 16px; + border-radius: 16px; display: flex; align-items: center; justify-content: center; - background: #c6c6c6; - visibility: hidden; - } - - .icon-box:hover { background: #296dff; - } -} - -.custom-edge-line:hover { - .icon-box { visibility: visible; } } \ No newline at end of file diff --git a/packages/x-flow/src/components/CustomEdge/index.tsx b/packages/x-flow/src/components/CustomEdge/index.tsx index fbf0520d9..3cab13790 100644 --- a/packages/x-flow/src/components/CustomEdge/index.tsx +++ b/packages/x-flow/src/components/CustomEdge/index.tsx @@ -1,32 +1,27 @@ -import React, { memo, useContext } from 'react'; +import React, { memo, useState } from 'react'; import { PlusOutlined, CloseOutlined } from '@ant-design/icons'; import { BezierEdge, EdgeLabelRenderer, getBezierPath, useReactFlow } from '@xyflow/react'; import { useShallow } from 'zustand/react/shallow'; import produce from 'immer'; import { uuid } from '../../core/utils'; import useStore from '../../core/store'; -import { ConfigContext } from '../../models/context'; import NodeSelectPopover from '../NodeSelectPopover'; import './index.less'; export default memo((edge: any) => { const { - label, id, + selected, sourceX, sourceY, targetX, targetY, - data, - selected, source, target, - layout, } = edge; - const configCtx: any = useContext(ConfigContext); - const reactflow = useReactFlow(); + const [isHovered, setIsHovered] = useState(false); const [edgePath, labelX, labelY] = getBezierPath({ sourceX, sourceY, @@ -36,13 +31,21 @@ export default memo((edge: any) => { const { nodes, + edges, setNodes, + setEdges, mousePosition, + onEdgesChange, + layout, } = useStore( useShallow((state: any) => ({ + layout: state.layout, nodes: state.nodes, + edges: state.edges, mousePosition: state.mousePosition, - setNodes: state.setNodes + setNodes: state.setNodes, + setEdges: state.setEdges, + onEdgesChange: state.onEdgesChange })) ); @@ -50,15 +53,36 @@ export default memo((edge: any) => { const { screenToFlowPosition } = reactflow; const { x, y } = screenToFlowPosition({ x: mousePosition.pageX, y: mousePosition.pageY }); + + const targetId = uuid(); const newNodes = produce(nodes, (draft: any) => { draft.push({ - id: uuid(), + id: targetId, type: 'custom', data, position: { x, y } }); }); + + const newEdges = produce(edges, (draft: any) => { + draft.push(...[ + { + id: uuid(), + source, + target: targetId, + }, + { + id: uuid(), + source: targetId, + target, + } + ]) + + }); + setNodes(newNodes); + setEdges(newEdges); + onEdgesChange([{ id, type: 'remove' }]); }; let edgeExtra: any = { @@ -73,32 +97,36 @@ export default memo((edge: any) => { } return ( - -
-
-
- + setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > + +
+
+
onEdgesChange([{ id, type: 'remove' }])}> + +
+ +
+ +
+
- -
- -
-
-
- - } - /> + + } + /> + ); }) \ No newline at end of file diff --git a/packages/x-flow/src/components/CustomNode/index.tsx b/packages/x-flow/src/components/CustomNode/index.tsx index 4f7c5cc02..9ad044964 100644 --- a/packages/x-flow/src/components/CustomNode/index.tsx +++ b/packages/x-flow/src/components/CustomNode/index.tsx @@ -2,9 +2,13 @@ import React, { memo, useContext, useState } from 'react'; import { Tooltip } from 'antd'; import { PlusOutlined } from '@ant-design/icons'; import classNames from 'classnames'; -import { Handle, Position } from '@xyflow/react'; -import { capitalize } from '../../core/utils'; +import produce from 'immer'; +import { useShallow } from 'zustand/react/shallow'; +import { Handle, Position, useReactFlow } from '@xyflow/react'; +import useStore from '../../core/store'; +import { capitalize, uuid } from '../../core/utils'; import { ConfigContext } from '../../models/context'; +import NodeSelectPopover from '../NodeSelectPopover'; import './index.less'; export default memo((props: any) => { @@ -13,6 +17,49 @@ export default memo((props: any) => { const NodeWidget = configCtx?.nodeWidgets[`${capitalize(type)}Node`]; const [isHovered, setIsHovered] = useState(false); + const reactflow = useReactFlow(); + const { + edges, + nodes, + setNodes, + setEdges, + mousePosition, + } = useStore( + useShallow((state: any) => ({ + nodes: state.nodes, + edges: state.edges, + mousePosition: state.mousePosition, + setNodes: state.setNodes, + setEdges: state.setEdges, + onEdgesChange: state.onEdgesChange + })) + ); + + // 增加节点并进行联系 + const handleAddNode = (data: any) => { + const { screenToFlowPosition } = reactflow; + const { x, y } = screenToFlowPosition({ x: mousePosition.pageX + 100, y: mousePosition.pageY + 100 }); + const targetId = uuid(); + + const newNodes = produce(nodes, (draft: any) => { + draft.push({ + id: targetId, + type: 'custom', + data, + position: { x, y } + }); + }); + const newEdges = produce(edges, (draft: any) => { + draft.push({ + id: uuid(), + source: id, + target: targetId, + }) + }); + setNodes(newNodes); + setEdges(newEdges); + }; + let targetPosition = Position.Left; let sourcePosition = Position.Right; @@ -30,7 +77,7 @@ export default memo((props: any) => { onMouseEnter={() => setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} > - {capitalize(type)!== 'Start' && ( + { ( { data={data} onClick={() => onClick(data)} /> - {capitalize(type) !== 'End' && ( + {( { > {(selected || isHovered) && (
- - - + + + + +
)}
diff --git a/packages/x-flow/src/components/NodeSelectPopover/index.tsx b/packages/x-flow/src/components/NodeSelectPopover/index.tsx index 2b6201bff..d7471c61d 100644 --- a/packages/x-flow/src/components/NodeSelectPopover/index.tsx +++ b/packages/x-flow/src/components/NodeSelectPopover/index.tsx @@ -2,7 +2,6 @@ import React, { useCallback, useState, useRef } from 'react'; import { Popover, Input, Tabs } from 'antd'; import { SearchOutlined } from '@ant-design/icons'; -import { useEventListener } from 'ahooks'; import { useShallow } from 'zustand/react/shallow'; import { useClickAway } from 'ahooks'; import { useSet } from '../../core/utils/hooks'; @@ -154,14 +153,14 @@ export default (props: any) => { const handAddNode = useCallback((ev: any, type: any) => { ev.stopPropagation(); // 阻止事件冒泡 - addNode({ node: type }); + addNode({ _nodeType: type }); setOpen(false); }, []); return ( } - zIndex={1000} + zIndex={2000} trigger='click' arrow={false} open={open} diff --git a/packages/x-flow/src/core/index.tsx b/packages/x-flow/src/core/index.tsx index 91c2303da..ac295db42 100644 --- a/packages/x-flow/src/core/index.tsx +++ b/packages/x-flow/src/core/index.tsx @@ -24,10 +24,9 @@ import useStore, { useUndoRedo } from './store'; import XFlowProps from './types'; import { capitalize, uuid, transformNodes } from './utils'; import autoLayoutNodes from './utils/autoLayoutNodes'; -import { ConfigContext } from '../models/context'; const CustomNode = memo(CustomNodeComponent); - +const edgeTypes = { buttonedge: memo(CustomEdge) }; /*** * @@ -36,197 +35,190 @@ const CustomNode = memo(CustomNodeComponent); */ const FlowEditor: FC = memo((props) => { const { nodeMenus, nodes: originalNodes, edges: originalEdges } = props; - const configCtx: any = useContext(ConfigContext); - - + const workflowContainerRef = useRef(null); + const store = useStoreApi(); + const { updateEdge, addNodes, addEdges, zoomTo } = useReactFlow(); + const { undo, redo, record } = useUndoRedo(false); + const { + layout, + nodes, + edges, + onNodesChange, + onEdgesChange, + onConnect, + setNodes, + setEdges, + setLayout, + setNodeMenus, + setCandidateNode, + setMousePosition, + } = useStore( + useShallow((state) => ({ + nodes: state.nodes, + edges: state.edges, + layout: state.layout, + setLayout: state.setLayout, + setNodes: state.setNodes, + setEdges: state.setEdges, + setNodeMenus: state.setNodeMenus, + setMousePosition: state.setMousePosition, + setCandidateNode: state.setCandidateNode, + onNodesChange: state.onNodesChange, + onEdgesChange: state.onEdgesChange, + onConnect: state.onConnect, + })), + ); + const [activeNode, setActiveNode] = useState(null); + useEffect(() => { + zoomTo(0.8); + setAutoFreeze(false); + return () => { + setAutoFreeze(true); + }; + }, []); - const workflowContainerRef = useRef(null); - const store = useStoreApi(); - const { updateEdge, addNodes, addEdges, zoomTo } = useReactFlow(); - const { undo, redo, record } = useUndoRedo(false); - const { - layout, - nodes, - edges, - onNodesChange, - onEdgesChange, - onConnect, - setNodes, - setEdges, - setLayout, - setNodeMenus, - setCandidateNode, - setMousePosition, - } = useStore( - useShallow((state) => ({ - nodes: state.nodes, - edges: state.edges, - layout: state.layout, - setLayout: state.setLayout, - setNodes: state.setNodes, - setEdges: state.setEdges, - setNodeMenus: state.setNodeMenus, - setMousePosition: state.setMousePosition, - setCandidateNode: state.setCandidateNode, - onNodesChange: state.onNodesChange, - onEdgesChange: state.onEdgesChange, - onConnect: state.onConnect, - })), - ); + useEffect(() => { + setLayout(props.layout); + setNodeMenus(nodeMenus); + setNodes(transformNodes(originalNodes)); + setEdges(originalEdges); + }, [JSON.stringify(originalNodes)]); - const [activeNode, setActiveNode] = useState(null); + useEventListener('keydown', (e) => { + if ((e.key === 'd' || e.key === 'D') && (e.ctrlKey || e.metaKey)) + e.preventDefault(); + if ((e.key === 'z' || e.key === 'Z') && (e.ctrlKey || e.metaKey)) + e.preventDefault(); + if ((e.key === 'y' || e.key === 'Y') && (e.ctrlKey || e.metaKey)) + e.preventDefault(); + if ((e.key === 's' || e.key === 'S') && (e.ctrlKey || e.metaKey)) + e.preventDefault(); + }); - useEffect(() => { - zoomTo(0.8); - setAutoFreeze(false); - return () => { - setAutoFreeze(true); - }; - }, []); + useEventListener('mousemove', (e) => { + const containerClientRect = + workflowContainerRef.current?.getBoundingClientRect(); + if (containerClientRect) { + setMousePosition({ + pageX: e.clientX, + pageY: e.clientY, + elementX: e.clientX - containerClientRect.left, + elementY: e.clientY - containerClientRect.top, + }); + } + }); - useEffect(() => { - setLayout(props.layout); - setNodeMenus(nodeMenus); - setNodes(transformNodes(originalNodes)); - setEdges(originalEdges); - }, [JSON.stringify(originalNodes)]); + const { eventEmitter } = useEventEmitterContextContext(); + eventEmitter?.useSubscription((v: any) => { + // 整理节点 + if (v.type === 'auto-layout-nodes') { + const newNodes: any = autoLayoutNodes(store.getState().nodes, edges); + setNodes(newNodes); + } - useEventListener('keydown', (e) => { - if ((e.key === 'd' || e.key === 'D') && (e.ctrlKey || e.metaKey)) - e.preventDefault(); - if ((e.key === 'z' || e.key === 'Z') && (e.ctrlKey || e.metaKey)) - e.preventDefault(); - if ((e.key === 'y' || e.key === 'Y') && (e.ctrlKey || e.metaKey)) - e.preventDefault(); - if ((e.key === 's' || e.key === 'S') && (e.ctrlKey || e.metaKey)) - e.preventDefault(); - }); + if (v.type === 'deleteNode') { + setActiveNode(null); + } + }); - useEventListener('mousemove', (e) => { - const containerClientRect = - workflowContainerRef.current?.getBoundingClientRect(); - if (containerClientRect) { - setMousePosition({ - pageX: e.clientX, - pageY: e.clientY, - elementX: e.clientX - containerClientRect.left, - elementY: e.clientY - containerClientRect.top, - }); - } - }); - const { eventEmitter } = useEventEmitterContextContext(); - eventEmitter?.useSubscription((v: any) => { - // 整理节点 - if (v.type === 'auto-layout-nodes') { - const newNodes: any = autoLayoutNodes(store.getState().nodes, edges); - setNodes(newNodes); - } - }); - - // 新增节点 - const handleAddNode = (data: any) => { - const newNode = { - id: uuid(), - type: 'custom', - data, - position: { - x: 0, - y: 0, - }, - }; - setCandidateNode(newNode); - // record(() => { - // addNodes(newNode); - // addEdges({ - // id: uuid(), - // source: '1', - // target: newNode.id, - // }); - // }); + // 新增节点 + const handleAddNode = (data: any) => { + const newNode = { + id: uuid(), + type: 'custom', + data, + position: { + x: 0, + y: 0, + }, }; + setCandidateNode(newNode); + // record(() => { + // addNodes(newNode); + // addEdges({ + // id: uuid(), + // source: '1', + // target: newNode.id, + // }); + // }); + }; - // 插入节点 - const handleInsertNode = () => { - const newNode = { + // 插入节点 + const handleInsertNode = () => { + const newNode = { + id: uuid(), + data: { label: 'new node' }, + position: { + x: 0, + y: 0, + }, + }; + record(() => { + addNodes(newNode); + addEdges({ id: uuid(), - data: { label: 'new node' }, - position: { - x: 0, - y: 0, - }, - }; - record(() => { - 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, - }); + source: '2', + target: newNode.id, }); - }; - - // edge 移入/移出效果 - const getUpdateEdgeConfig = useMemoizedFn((edge: any, color: string) => { - const newEdges = produce(edges, (draft) => { - const currEdge: any = draft.find((e) => e.id === edge.id); - currEdge.style = { - ...edge.style, - stroke: color, - }; - currEdge.markerEnd = { - ...edge?.markerEnd, - color, - }; + const targetEdge = edges.find((edge) => edge.source === '2'); + updateEdge(targetEdge?.id as string, { + source: newNode.id, }); - setEdges(newEdges); }); + }; - const handleNodeValueChange = debounce((data: any) => { - for (let node of nodes) { - if (node.id === activeNode.name) { - node.data = { - ...node?.data, - ...data, - }; - break; - } - } - setNodes([...nodes]); - }, 200); - - - const nodeTypes = useMemo(() => { - return { - custom: (props: any) => { - const { data, ...rest } = props; - const { _nodeType, ...restData } = data || {}; - return ( - - ); - } + // edge 移入/移出效果 + const getUpdateEdgeConfig = useMemoizedFn((edge: any, color: string) => { + const newEdges = produce(edges, (draft) => { + const currEdge: any = draft.find((e) => e.id === edge.id); + currEdge.style = { + ...edge.style, + stroke: color, }; - }, [layout]); - - const edgeTypes = { buttonedge: (edgeProps: any) => }; - - - + currEdge.markerEnd = { + ...edge?.markerEnd, + color, + }; + }); + setEdges(newEdges); + }); + const handleNodeValueChange = debounce((data: any) => { + for (let node of nodes) { + if (node.id === activeNode.name) { + node.data = { + ...node?.data, + ...data, + }; + break; + } + } + setNodes([...nodes]); + }, 200); + const nodeTypes = useMemo(() => { + return { + custom: (props: any) => { + const { data, ...rest } = props; + const { _nodeType, ...restData } = data || {}; + return ( + + ); + } + }; + }, [layout]); + + // const edgeTypes = { buttonedge: (edgeProps: any) => }; const { icon, description } = nodeMenus.find( (item) => item.type?.toLowerCase() === activeNode?.node?.toLowerCase(), ) || {}; @@ -235,6 +227,8 @@ const FlowEditor: FC = memo((props) => { // return configCtx.nodeWidgets[capitalize(`${activeNode?.type}Panel`)] ||
1
; // }, [activeNode?.id]); + console.log(nodes, '23123123nodes', edges) + return (
@@ -255,9 +249,35 @@ const FlowEditor: FC = memo((props) => { }, }} onConnect={onConnect} + // onNodesChange={(changes) => { + // const recordTypes = new Set(['add', 'remove']); + // changes.forEach((change) => { + // if (recordTypes.has(change.type)) { + // record(() => { + // onNodesChange([change]); + // }); + // } else { + // onNodesChange([change]); + // } + // }); + // }} onNodesChange={(changes) => { const recordTypes = new Set(['add', 'remove']); changes.forEach((change) => { + console.log( + '🚀 ~ file: main.tsx:226 ~ changes.forEach ~ change:', + change, + ); + + const removeChanges = changes.filter( + (change) => change.type === 'remove', + ); + + if (removeChanges.length > 0) { + removeChanges.forEach((change) => { + eventEmitter?.emit({ type: 'deleteNode', payload: change }); + }); + } if (recordTypes.has(change.type)) { record(() => { onNodesChange([change]); From 3b36f5531eeaf1c328b8840458e5c338dac120b6 Mon Sep 17 00:00:00 2001 From: "zhanbo.lh" Date: Tue, 19 Nov 2024 16:30:24 +0800 Subject: [PATCH 11/38] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=20xflow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/x-flow/package.json | 16 +- .../src/components/CandidateNode/index.tsx | 2 +- .../src/components/CustomEdge/index.tsx | 4 +- .../src/components/CustomNode/index.tsx | 4 +- .../src/components/FlowDebugDrawer/index.less | 114 ----- .../src/components/FlowDebugDrawer/index.tsx | 411 ------------------ .../components/NodeSelectPopover/index.tsx | 4 +- .../{core/constants.ts => constants/index.ts} | 4 +- packages/x-flow/src/core/context.tsx | 41 -- packages/x-flow/src/core/store.ts | 117 ----- packages/x-flow/src/core/utils/index.ts | 41 -- packages/x-flow/src/{core => }/index.less | 0 packages/x-flow/src/index.ts | 9 +- .../x-flow/src/{core/index.tsx => main.tsx} | 12 +- packages/x-flow/src/models/bindValues.ts | 175 -------- .../src/{context => models}/event-emitter.tsx | 3 +- packages/x-flow/src/models/expression.ts | 163 ------- .../x-flow/src/models/fieldShouldUpdate.ts | 80 ---- .../x-flow/src/models/filterValuesHidden.ts | 74 ---- .../src/models/filterValuesUndefined.ts | 51 --- packages/x-flow/src/models/flattenSchema.ts | 87 ---- packages/x-flow/src/models/formCoreUtils.ts | 188 -------- .../x-flow/src/models/formDataSkeleton.ts | 31 -- packages/x-flow/src/models/layout.ts | 73 ---- packages/x-flow/src/models/mapping.tsx | 134 ------ packages/x-flow/src/models/sortProperties.ts | 23 - packages/x-flow/src/models/store.ts | 136 +++++- packages/x-flow/src/models/transformProps.ts | 85 ---- packages/x-flow/src/models/useForm.ts | 360 --------------- packages/x-flow/src/models/validateMessage.ts | 97 ----- packages/x-flow/src/models/validates.ts | 138 ------ .../{core => }/operator/Control/index.less | 0 .../src/{core => }/operator/Control/index.tsx | 6 +- .../{core => }/operator/UndoRedo/index.less | 0 .../{core => }/operator/UndoRedo/index.tsx | 2 +- .../{core => }/operator/ZoomInOut/index.less | 0 .../{core => }/operator/ZoomInOut/index.tsx | 2 +- .../operator/ZoomInOut/shortcuts-name.tsx | 2 +- .../x-flow/src/{core => }/operator/index.less | 0 .../x-flow/src/{core => }/operator/index.tsx | 0 packages/x-flow/src/{core => }/types.ts | 0 .../src/{core => }/utils/autoLayoutNodes.ts | 0 packages/x-flow/src/utils/createIconFont.ts | 2 +- packages/x-flow/src/{core => }/utils/hooks.ts | 0 packages/x-flow/src/utils/index.ts | 45 ++ packages/x-flow/src/withProvider.tsx | 37 +- 46 files changed, 196 insertions(+), 2577 deletions(-) delete mode 100644 packages/x-flow/src/components/FlowDebugDrawer/index.less delete mode 100644 packages/x-flow/src/components/FlowDebugDrawer/index.tsx rename packages/x-flow/src/{core/constants.ts => constants/index.ts} (98%) delete mode 100644 packages/x-flow/src/core/context.tsx delete mode 100644 packages/x-flow/src/core/store.ts delete mode 100644 packages/x-flow/src/core/utils/index.ts rename packages/x-flow/src/{core => }/index.less (100%) rename packages/x-flow/src/{core/index.tsx => main.tsx} (96%) delete mode 100644 packages/x-flow/src/models/bindValues.ts rename packages/x-flow/src/{context => models}/event-emitter.tsx (96%) delete mode 100644 packages/x-flow/src/models/expression.ts delete mode 100644 packages/x-flow/src/models/fieldShouldUpdate.ts delete mode 100644 packages/x-flow/src/models/filterValuesHidden.ts delete mode 100644 packages/x-flow/src/models/filterValuesUndefined.ts delete mode 100644 packages/x-flow/src/models/flattenSchema.ts delete mode 100644 packages/x-flow/src/models/formCoreUtils.ts delete mode 100644 packages/x-flow/src/models/formDataSkeleton.ts delete mode 100644 packages/x-flow/src/models/layout.ts delete mode 100644 packages/x-flow/src/models/mapping.tsx delete mode 100644 packages/x-flow/src/models/sortProperties.ts delete mode 100644 packages/x-flow/src/models/transformProps.ts delete mode 100644 packages/x-flow/src/models/useForm.ts delete mode 100644 packages/x-flow/src/models/validateMessage.ts delete mode 100644 packages/x-flow/src/models/validates.ts rename packages/x-flow/src/{core => }/operator/Control/index.less (100%) rename packages/x-flow/src/{core => }/operator/Control/index.tsx (89%) rename packages/x-flow/src/{core => }/operator/UndoRedo/index.less (100%) rename packages/x-flow/src/{core => }/operator/UndoRedo/index.tsx (94%) rename packages/x-flow/src/{core => }/operator/ZoomInOut/index.less (100%) rename packages/x-flow/src/{core => }/operator/ZoomInOut/index.tsx (98%) rename packages/x-flow/src/{core => }/operator/ZoomInOut/shortcuts-name.tsx (94%) rename packages/x-flow/src/{core => }/operator/index.less (100%) rename packages/x-flow/src/{core => }/operator/index.tsx (100%) rename packages/x-flow/src/{core => }/types.ts (100%) rename packages/x-flow/src/{core => }/utils/autoLayoutNodes.ts (100%) rename packages/x-flow/src/{core => }/utils/hooks.ts (100%) diff --git a/packages/x-flow/package.json b/packages/x-flow/package.json index ce907dbef..4a7f5d7d9 100644 --- a/packages/x-flow/package.json +++ b/packages/x-flow/package.json @@ -1,17 +1,11 @@ { "name": "@xrenders/xflow", "version": "0.0.1", - "description": "通过 JSON Schema 生成标准 Form,常用于自定义搭建配置界面生成", + "description": "一款功能强大、易用灵活的流程编辑器框架,帮助你轻松构建复杂的工作流和流程产品", "keywords": [ - "Form", - "FormRender", - "Render", - "XRender", - "React", - "Json Schema", - "Ant Design" + "xflow" ], - "homepage": "https://xrender.fun/form-render", + "homepage": "https://xrender.fun/xflow", "bugs": { "url": "https://github.com/alibaba/x-render/issues" }, @@ -52,13 +46,9 @@ }, "dependencies": { "@ant-design/icons": "^4.0.2", - "async-validator": "^3.5.1", "classnames": "^2.3.1", - "color": "^3.1.2", "lodash-es": "^4.17.21", "dayjs": "^1.11.7", - "rc-color-picker": "^1.2.6", - "virtualizedtableforantd4": "^1.1.2", "ahooks": "^3.7.5", "zustand": "^4.5.4", "@xyflow/react": "^12.3.2", diff --git a/packages/x-flow/src/components/CandidateNode/index.tsx b/packages/x-flow/src/components/CandidateNode/index.tsx index 056ed5dc6..d312727fd 100644 --- a/packages/x-flow/src/components/CandidateNode/index.tsx +++ b/packages/x-flow/src/components/CandidateNode/index.tsx @@ -5,7 +5,7 @@ import { useShallow } from 'zustand/react/shallow'; import { useReactFlow, useViewport } from '@xyflow/react'; import { useEventListener } from 'ahooks'; import CustomNode from '../CustomNode'; -import useStore from '../../core/store'; +import useStore from '../../models/store'; const CandidateNode = () => { const reactflow = useReactFlow(); diff --git a/packages/x-flow/src/components/CustomEdge/index.tsx b/packages/x-flow/src/components/CustomEdge/index.tsx index 3cab13790..f09dd251c 100644 --- a/packages/x-flow/src/components/CustomEdge/index.tsx +++ b/packages/x-flow/src/components/CustomEdge/index.tsx @@ -3,8 +3,8 @@ import { PlusOutlined, CloseOutlined } from '@ant-design/icons'; import { BezierEdge, EdgeLabelRenderer, getBezierPath, useReactFlow } from '@xyflow/react'; import { useShallow } from 'zustand/react/shallow'; import produce from 'immer'; -import { uuid } from '../../core/utils'; -import useStore from '../../core/store'; +import { uuid } from '../../utils'; +import useStore from '../../models/store'; import NodeSelectPopover from '../NodeSelectPopover'; import './index.less'; diff --git a/packages/x-flow/src/components/CustomNode/index.tsx b/packages/x-flow/src/components/CustomNode/index.tsx index 9ad044964..6a3e4cb91 100644 --- a/packages/x-flow/src/components/CustomNode/index.tsx +++ b/packages/x-flow/src/components/CustomNode/index.tsx @@ -5,8 +5,8 @@ import classNames from 'classnames'; import produce from 'immer'; import { useShallow } from 'zustand/react/shallow'; import { Handle, Position, useReactFlow } from '@xyflow/react'; -import useStore from '../../core/store'; -import { capitalize, uuid } from '../../core/utils'; +import useStore from '../../models/store'; +import { capitalize, uuid } from '../../utils'; import { ConfigContext } from '../../models/context'; import NodeSelectPopover from '../NodeSelectPopover'; import './index.less'; diff --git a/packages/x-flow/src/components/FlowDebugDrawer/index.less b/packages/x-flow/src/components/FlowDebugDrawer/index.less deleted file mode 100644 index 1745f21a4..000000000 --- a/packages/x-flow/src/components/FlowDebugDrawer/index.less +++ /dev/null @@ -1,114 +0,0 @@ -.debug-flow-panel { - .ant-drawer-content-wrapper { - top: 54px; - bottom: 14px; - right: 12px; - border-radius: 20px; - } - - .ant-drawer-close { - display: none; - } - - .ant-drawer-header { - padding: 16px; - } - - .ant-drawer-content { - border-radius: 20px; - } - - .ant-drawer-body { - padding: 8px 16px; - } - - .title-box { - display: flex; - align-items: center; - justify-content: space-between; - - .ant-input-outlined { - border-color: #fff; - font-weight: 600; - } - } - - .icon-box { - width: 24px; - height: 24px; - border-radius: 6px; - display: flex; - align-items: center; - justify-content: center; - margin-right: 5px; - } - - .title-actions { - display: flex; - align-items: center; - margin-left: 24px; - } - - .desc-box { - font-size: 12px; - line-height: 32px; - font-weight: normal; - - .ant-input-outlined { - border-color: #fff; - } - - textarea { - margin: 12px 0; - } - } - - .ant-input-outlined:focus-within { - border-color: #3b82f6 !important; - } - - .fr-table-cell-content { - .ant-col { - padding: 0 !important; - } - } - - .ant-collapse-content-box { - padding: 0 !important; - } - - .item-collapse { - border: none; - background-color: #fff; - - .ant-collapse-header { - background: none; - padding: 6px 0; - } - - .ant-collapse-item { - border: none !important; - padding: 0; - } - - .ant-collapse-content { - border: none; - } - } - - .ant-collapse-header-text { - color: #354052; - font-weight: 600; - } - - .ant-table-thead>tr>th { - font-size: 12px; - font-weight: normal; - } - - input, - select, - .ant-select-selector { - font-size: 13px !important; - } -} \ No newline at end of file diff --git a/packages/x-flow/src/components/FlowDebugDrawer/index.tsx b/packages/x-flow/src/components/FlowDebugDrawer/index.tsx deleted file mode 100644 index c6207b1fa..000000000 --- a/packages/x-flow/src/components/FlowDebugDrawer/index.tsx +++ /dev/null @@ -1,411 +0,0 @@ -import { useEffect, useRef, useState } from 'react'; -import ReactJson from 'react-json-view'; -import { - Button, - Collapse, - Drawer, - Flex, - message, - Select, - Space, - Tabs, - Upload, - UploadFile, - UploadProps, -} from 'antd'; -import { DownloadOutlined, UploadOutlined } from '@ant-design/icons'; -import saveAs from 'file-saver'; -import FormRender, { useForm } from 'form-render'; -import * as XLSX from 'xlsx'; -import { getUrlParams } from '@/utils'; -import api from '@/apis'; -import ExpandInput from '@/components/ExpandInput'; -import IconView from '@/components/IconView'; -import { useFlow } from '@/hooks/useWorkFlow'; -import { transformData } from '@/pages/WorkflowDetail/DebugModal/util'; -import CustomHtml from '../CustomHtml'; -import './index.less'; - -interface IDebugDrawerProp { - visible: boolean; - onClose: () => void; - item: any; -} -const schema = { - type: 'object', - displayType: 'row', - properties: { - list: { - type: 'array', - widget: 'tableList', - readOnly: true, - - items: { - type: 'object', - properties: { - name: { - title: '名称', - type: 'string', - widget: 'html', - width: 140, - props: { - width: 100, - }, - }, - dataType: { - title: '类型', - type: 'string', - widget: 'html', - width: 80, - }, - value: { - title: '值', - type: 'string', - widget: 'ExpandInput', - placeholder: '请输入常量', - // required: true, - }, - }, - }, - }, - }, -}; -const NodeDebugDrawer = (props: IDebugDrawerProp) => { - const { onClose, visible, item } = props; - const [activeKey, setActiveKey] = useState('single'); - const [historyItem, setHistoryItem] = useState(null); - const [historyOptions, setHistoryOptions] = useState([]); - const form = useForm(); - const { id: detailId } = getUrlParams(); - const [activeTab, setActiveTab] = useState('output'); - const { handleDebugOk, handleBatchDebugOk, flowList } = useFlow(); - const [outputResult, setOutputResult] = useState(); - const [flowsResult, setFlowsResult] = useState([]); - - const fileData = useRef(null); - const [uploading, setUploading] = useState(false); - const [loading, setLoading] = useState(false); - const [fileList, setFileList] = useState([]); - const initInputList = (item?.data?.list || []).map((item: any) => { - return { - ...item, - value: '', - }; - }); - - useEffect(() => { - form.setValues({ - list: initInputList || [], - }); - - return () => { - form.resetFields(); - }; - }, [item]); - useEffect(() => { - if (visible) { - // eslint-disable-next-line @typescript-eslint/no-use-before-define - getHistory(); - } - }, [visible]); - useEffect(() => { - const currentHistory = historyOptions.find( - (item: any) => item.value === historyItem, - ); - // input字段修改,如果有历史记录,需要将历史记录的值填充到input中 - const list = initInputList.map((item: any) => { - const current = currentHistory?.inputs?.find( - (i: any) => i.name === item.name, - ); - return { - ...item, - value: current?.value, - }; - }); - - form.setValues({ - list, - }); - }, [historyOptions, historyItem]); - const getHistory = async () => { - if (!detailId) return; - const data = await api.workFlow.getDebugHistoryRecord({ - flowId: detailId, - pageable: { - current: 1, - pageSize: 10, - }, - }); - if (data.success) { - setHistoryOptions( - data.data.list.map((item: any, index: number) => { - return { - label: `调试记录${index + 1}: ${item.gmtCreate}`, - value: item.id, - ...item, - }; - }), - ); - setHistoryItem(data.data.list?.[0]?.id); - } - }; - const handleHistoryChange = (value: any) => { - setHistoryItem(value); - }; - // 批量 - const handleDownload = async () => { - const inputs = item?.data.list; - let data = [inputs.map((i) => `${i.name}|${i.dataType}`)]; - let ws = XLSX.utils.aoa_to_sheet(data); - let wb = XLSX.utils.book_new(); - XLSX.utils.book_append_sheet(wb, ws, 'SheetJS'); - let wbOut = XLSX.write(wb, { - bookType: 'xlsx', - bookSST: true, - type: 'array', - }); - saveAs( - new Blob([wbOut], { type: 'application/octet-stream' }), - 'workflow_batch_test.xlsx', - ); - }; - const handleBeforeUpload: UploadProps['beforeUpload'] = async (item) => { - let newFileList = [...fileList, item]; - if (newFileList.length > 1) { - message.error('一次最多上传一个文件'); - return false; - } - - try { - setUploading(true); - const uploadPromises = newFileList.map(async (file) => { - const reader = new FileReader(); - reader.onload = function (event) { - const arrayBuffer = event.target?.result; - if (!arrayBuffer) return; - const data = new Uint8Array(arrayBuffer as ArrayBuffer); - const workbook = XLSX.read(data, { - type: 'array', - }); - - // 默认处理第一个工作表 - const firstSheetName = workbook.SheetNames[0]; - const worksheet = workbook.Sheets[firstSheetName]; - const json = XLSX.utils.sheet_to_json(worksheet); - fileData.current = transformData(json as any); - setUploading(false); - console.log(fileData.current, json); - }; - reader.readAsArrayBuffer(file); - return { - file, - }; - }); - - const uploadResults = await Promise.allSettled(uploadPromises); - uploadResults.forEach(({ status, value }: any) => { - if (status === 'fulfilled') { - const { file, url } = value; - file.status = 'done'; - file.url = url; - } else if (status === 'rejected') { - message.error('Failed to upload file:', value.reason); - } - }); - } catch (error) { - console.error('Failed to upload files:', error); - } - - setFileList(newFileList); - return false; - }; - const uploadProps = { - accept: '.xlsx', - onRemove: (file: any) => { - const index = fileList.indexOf(file); - const newFileList = fileList.slice(); - newFileList.splice(index, 1); - setFileList(newFileList); - }, - beforeUpload: handleBeforeUpload, - }; - - const handleOk = async () => { - if (activeKey === 'single') { - const formData = await form.validateFields(); - setLoading(true); - await handleDebugOk(formData, true, { - successCb: (res: any) => { - console.log(323, res, flowList); - if (res.taskCode === 'Output') { - setOutputResult(res.output); - } - flowList.forEach((item: any) => { - if (item.code === res.taskCode) { - item.result = res.output; - } - }); - setLoading(false); - }, - errorCb: (err: any) => { - console.log(31221 + 'rr', err); - setLoading(false); - }, - }); - } else { - setLoading(true); - await handleBatchDebugOk(fileData.current); - setLoading(false); - } - }; - const traceItems = flowList.map((item: any) => { - return { - key: item.code, - label: item.name, - children: ( - -

输出结果:

- -
- ), - }; - }); - const singleOutItems = [ - { - key: 'output', - label: '结果', - children: ( - - ), - }, - { - key: 'log', - label: '追踪', - children: ( - <> - - - ), - }, - ]; - const items = [ - { - key: 'single', - label: '单次验证', - children: ( - <> - -
- -
- , - ExpandInput, - }} - /> - - ), - }, - { - key: 'batch', - label: '批量测试', - children: ( - - - - - - - - - ), - }, - ]; - const handleModalClose = () => { - if (onClose) { - form.resetFields(); - fileData.current = null; - setFileList([]); - onClose(); - } - }; - - return ( - -
流程调试
- - - } - > - - - - - {activeKey === 'single' && !loading && outputResult && ( - - )} -
- ); -}; -export default NodeDebugDrawer; diff --git a/packages/x-flow/src/components/NodeSelectPopover/index.tsx b/packages/x-flow/src/components/NodeSelectPopover/index.tsx index d7471c61d..471a30009 100644 --- a/packages/x-flow/src/components/NodeSelectPopover/index.tsx +++ b/packages/x-flow/src/components/NodeSelectPopover/index.tsx @@ -4,9 +4,9 @@ import { Popover, Input, Tabs } from 'antd'; import { SearchOutlined } from '@ant-design/icons'; import { useShallow } from 'zustand/react/shallow'; import { useClickAway } from 'ahooks'; -import { useSet } from '../../core/utils/hooks'; +import { useSet } from '../../utils/hooks'; import IconView from '../IconView'; -import useStore from '../../core/store'; +import useStore from '../../models/store'; import './index.less'; const items: any['items'] = [ diff --git a/packages/x-flow/src/core/constants.ts b/packages/x-flow/src/constants/index.ts similarity index 98% rename from packages/x-flow/src/core/constants.ts rename to packages/x-flow/src/constants/index.ts index 72eb85411..c33d923f7 100644 --- a/packages/x-flow/src/core/constants.ts +++ b/packages/x-flow/src/constants/index.ts @@ -1,5 +1,5 @@ -import type { Var } from './types' -import { BlockEnum, VarType } from './types'; +import type { Var } from '../types' +import { BlockEnum, VarType } from '../types'; export const CUSTOM_ITERATION_START_NODE = 'custom-iteration-start' diff --git a/packages/x-flow/src/core/context.tsx b/packages/x-flow/src/core/context.tsx deleted file mode 100644 index d0ce0520e..000000000 --- a/packages/x-flow/src/core/context.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { createContext, useRef, useContext } from 'react'; -import { createStore } from 'zustand/vanilla'; - -type Shape = { - appId: string -} - -export const createWorkflowStore = () => { - return createStore(set => ({ - appId: '', - candidateNode: {} - })) -} - - - -type WorkflowStore = ReturnType -export const WorkflowContext = createContext(null) - -type WorkflowProviderProps = { - children: React.ReactNode -} - - -export const WorkflowContextProvider = ({ children }: WorkflowProviderProps) => { - const storeRef = useRef(); - - if (!storeRef.current) { - storeRef.current = createWorkflowStore(); - } - - return ( - - {children} - - ); -} - -export const useWorkflowStore = () => { - return useContext(WorkflowContext)! -} diff --git a/packages/x-flow/src/core/store.ts b/packages/x-flow/src/core/store.ts deleted file mode 100644 index afbadd048..000000000 --- a/packages/x-flow/src/core/store.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { create } from 'zustand'; -import { immer } from 'zustand/middleware/immer'; -import { temporal } from 'zundo'; -import { - addEdge, - applyNodeChanges, - applyEdgeChanges, - Edge, - Node, - OnNodesChange, - OnEdgesChange, - OnConnect, -} from '@xyflow/react'; -import _ from "lodash"; - -export type AppNode = Node; - -export type AppState = { - layout: 'LR' | 'TB', - nodes: AppNode[]; - edges: Edge[]; - nodeMenus: any[]; - candidateNode: any; - mousePosition: any; - onNodesChange: OnNodesChange; - onEdgesChange: OnEdgesChange; - onConnect: OnConnect; - setNodes: (nodes: AppNode[]) => void; - setEdges: (edges: Edge[]) => void; - setLayout: (layout: 'LR' | 'TB') => void; - setNodeMenus: (nodeMenus: any[]) => void; - setCandidateNode: (candidateNode: any) => void; - setMousePosition: (mousePosition: any) => void; -}; - -// 这是我们的 useStore hook,我们可以在我们的组件中使用它来获取 store 并调用动作 -// 注意:immer 使用方式是 create()(immer(() => ({}))) -const useStore = create()( - immer( - temporal( - (set, get) => ({ - layout: 'LR', - nodes: [], - edges: [], - candidateNode: null, - nodeMenus: [], - mousePosition: { pageX: 0, pageY: 0, elementX: 0, elementY: 0 }, - onNodesChange: (changes) => { - set({ - nodes: applyNodeChanges(changes, get().nodes), - }); - }, - onEdgesChange: (changes) => { - set({ - edges: applyEdgeChanges(changes, get().edges), - }); - }, - onConnect: (connection) => { - set({ - edges: addEdge(connection, get().edges), - }); - }, - setNodes: (nodes) => { - set({ nodes }); - }, - setEdges: (edges) => { - set({ edges }); - }, - setNodeMenus: (nodeMenus: any) => { - set({ nodeMenus }); - }, - setCandidateNode: (candidateNode) => { - set({ candidateNode }); - }, - setMousePosition: (mousePosition: any) => { - set({ mousePosition }); - }, - setLayout: (layout: 'LR' | 'TB') => { - if (!layout) { - return; - } - set({ layout }); - } - }), - { - // 偏函数 - partialize: (state) => { - const { nodes, edges } = state; - return { - edges, - nodes, - }; - }, - }, - ), - ), -); - - -export const useUndoRedo = (isTracking = true) => { - const temporalStore = useStore.temporal.getState(); - if (temporalStore.isTracking) { - // 暂停时间旅行机器, - temporalStore.pause(); - } - - return { - ...temporalStore, - record: (callback: () => void) => { - temporalStore.resume(); - callback(); - temporalStore.pause(); - } - } -}; - -export default useStore; \ No newline at end of file diff --git a/packages/x-flow/src/core/utils/index.ts b/packages/x-flow/src/core/utils/index.ts deleted file mode 100644 index c7809ed8c..000000000 --- a/packages/x-flow/src/core/utils/index.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { customAlphabet } from 'nanoid'; -export const uuid = customAlphabet('0123456789abcdefghijklmnopqrstuvwxyz', 16); - - -export const isMac = () => { - return navigator.userAgent.toUpperCase().includes('MAC') -} - -const specialKeysNameMap: Record = { - ctrl: '⌘', - alt: '⌥', -} - -export const getKeyboardKeyNameBySystem = (key: string) => { - if (isMac()) - return specialKeysNameMap[key] || key - - return key -} - - -export const capitalize = (string: string) => { - if (typeof string !== 'string' || string.length === 0) { - return string; - } - return `${string.charAt(0).toUpperCase()}${string.slice(1)}`; -} - -export const transformNodes = (nodes: any[]) => { - return nodes?.map(item => { - const { type, data, ...rest } = item; - return { - type: 'custom', - data: { - ...data, - _nodeType: type, - }, - ...rest - } - }) -} \ No newline at end of file diff --git a/packages/x-flow/src/core/index.less b/packages/x-flow/src/index.less similarity index 100% rename from packages/x-flow/src/core/index.less rename to packages/x-flow/src/index.less diff --git a/packages/x-flow/src/index.ts b/packages/x-flow/src/index.ts index 0323a4943..bc8459c28 100644 --- a/packages/x-flow/src/index.ts +++ b/packages/x-flow/src/index.ts @@ -1,10 +1,9 @@ -import FlowCore from './core'; +import Main from './main'; import withProvider from './withProvider'; import * as nodes from './nodes'; -export { default as useForm } from './models/useForm'; export type { - default as FR, -} from './core/types'; +default as FR, +} from './types'; -export default withProvider(FlowCore, nodes); +export default withProvider(Main, nodes); diff --git a/packages/x-flow/src/core/index.tsx b/packages/x-flow/src/main.tsx similarity index 96% rename from packages/x-flow/src/core/index.tsx rename to packages/x-flow/src/main.tsx index ac295db42..511f0f971 100644 --- a/packages/x-flow/src/core/index.tsx +++ b/packages/x-flow/src/main.tsx @@ -13,14 +13,14 @@ import { useStoreApi, } from '@xyflow/react'; import '@xyflow/react/dist/style.css'; -import { useEventEmitterContextContext } from '../context/event-emitter'; -import CandidateNode from '../components/CandidateNode'; -import CustomEdge from '../components/CustomEdge'; -import PanelContainer from '../components/PanelContainer'; +import { useEventEmitterContextContext } from './models/event-emitter'; +import CandidateNode from './components/CandidateNode'; +import CustomEdge from './components/CustomEdge'; +import PanelContainer from './components/PanelContainer'; import './index.less'; -import CustomNodeComponent from '../components/CustomNode'; +import CustomNodeComponent from './components/CustomNode'; import Operator from './operator'; -import useStore, { useUndoRedo } from './store'; +import useStore, { useUndoRedo } from './models/store'; import XFlowProps from './types'; import { capitalize, uuid, transformNodes } from './utils'; import autoLayoutNodes from './utils/autoLayoutNodes'; diff --git a/packages/x-flow/src/models/bindValues.ts b/packages/x-flow/src/models/bindValues.ts deleted file mode 100644 index aef74294d..000000000 --- a/packages/x-flow/src/models/bindValues.ts +++ /dev/null @@ -1,175 +0,0 @@ -import { get, set, unset } from 'lodash-es'; -import { - _cloneDeep, - isArray, - isObject, - safeGet -} from '../utils/index'; - -const isMultiBind = (array: string[]) => isArray(array) && array.every(item => typeof item === 'string'); - -// Need to consider list nested controls -const transformPath = (path: string) => { - const result: string[] = []; - - const recursion = (str: string) => { - const index = str.indexOf('[]'); - if (index === -1) { - result.push(str); - return; - } - result.push(str.substring(0, index)); - recursion(str.substring(index+3)) - }; - - recursion(path); - - if (result.length === 1) { - return result[0]; - } - return result; -}; - -const transformValueToBind = (data: any, path: any, bind: false | string | string[]) => { - if (bind === false) { - unset(data, path); - return; - } - - if (typeof bind === 'string') { - let value = get(data, path); - const preValue = get(data, bind); - if (isObject(preValue)) { - value = { ...preValue, ...value }; - } - set(data, bind, value); - unset(data, path); - return; - } - - // The array is converted to multiple fields. - if (isMultiBind(bind)) { - const value = get(data, path); - unset(data, path); - - if (Array.isArray(value)) { - value.forEach((item, index) => { - const bindPath = bind[index]; - bindPath && set(data, bindPath, item); - }); - } - } -} - -const transformBindToValue = (data: any, path: any, bind: any) => { - if (typeof bind === 'string') { - let value = get(data, bind); - const preValue = get(data, path); - if (isObject(preValue)) { - value = { ...preValue, ...value }; - } - set(data, path, value); - unset(data, bind); - return; - } - - // The array is converted to multiple fields. - if (isMultiBind(bind)) { - const value = []; - bind.forEach(key => { - const bindValue = get(data, key); - // if (bindValue != undefined) { - // value.push(bindValue); - // } - value.push(bindValue); - unset(data, key); - }); - - if (value.length > 0) { - set(data, path, value); - } - } -} - - -export const parseValuesToBind = (values: any, flatten: any) => { - // No bind field exists, no processing - if (!JSON.stringify(flatten).includes('bind')) { - return values; - } - - const data = _cloneDeep(values); - - const dealFieldList = (obj: any, [path, ...rest]: any, bind: any) => { - if (rest.length === 1) { - const list = get(obj, path, [])||[]; - list.forEach((item: any, index: number) => { - const value = get(item, rest[0]); - if (bind === 'root') { - list[index] = value; - return; - } - transformValueToBind(item, rest[0], bind); - }); - } - - if (isArray(obj)) { - obj.forEach((item: any) => dealFieldList(item, [path, ...rest], bind)); - } else if (isObject(obj)) { - const value = get(obj, path); - dealFieldList(value, rest, bind); - } - }; - - Object.keys(flatten).forEach(key => { - const bind = flatten[key]?.schema?.bind; - if (bind === undefined) { - return; - } - const path = transformPath(key); - isArray(path) ? dealFieldList(data, path, bind) : transformValueToBind(data, path, bind); - }); - - return data; -}; - -export const parseBindToValues = (values: any, flatten: any) => { - if (!JSON.stringify(flatten).includes('bind')) { - return values; - } - - const data = _cloneDeep(values); - const dealFieldList = (obj: any, [path, ...rest]: any, bind: any) => { - if (rest.length === 1) { - const list = safeGet(obj, path, []); - list.forEach((item: any, index: number) => { - if (bind === 'root') { - list[index] = { [rest[0]] : item }; - return; - } - transformBindToValue(item, rest[0], bind); - }); - } - - if (isArray(obj)) { - obj.forEach((item: any) => dealFieldList(item, [path, ...rest], bind)); - } else if (isObject(obj)) { - const value = get(obj, path); - dealFieldList(value, rest, bind); - } - }; - - Object.keys(flatten).forEach(key => { - const bind = flatten[key]?.schema?.bind; - if (bind === undefined) { - return; - } - const path = transformPath(key); - - isArray(path) ? dealFieldList(data, path, bind) : transformBindToValue(data, path, bind); - }); - - return data; -}; - - diff --git a/packages/x-flow/src/context/event-emitter.tsx b/packages/x-flow/src/models/event-emitter.tsx similarity index 96% rename from packages/x-flow/src/context/event-emitter.tsx rename to packages/x-flow/src/models/event-emitter.tsx index a881bc0fb..a62cf68cf 100644 --- a/packages/x-flow/src/context/event-emitter.tsx +++ b/packages/x-flow/src/models/event-emitter.tsx @@ -1,5 +1,4 @@ -'use client' - +import React from 'react'; import { useEventEmitter } from 'ahooks'; import type { EventEmitter } from 'ahooks/lib/useEventEmitter'; import { createContext, useContext } from 'use-context-selector'; diff --git a/packages/x-flow/src/models/expression.ts b/packages/x-flow/src/models/expression.ts deleted file mode 100644 index d4987f513..000000000 --- a/packages/x-flow/src/models/expression.ts +++ /dev/null @@ -1,163 +0,0 @@ -import { get } from 'lodash-es'; -import { isObject, _cloneDeep, isArray } from '../utils/index'; -import { createDataSkeleton } from './formDataSkeleton'; - -export const isExpression = (str: string) => { - if (typeof str !== 'string') { - return false; - } - - const pattern = /^{\s*{(.+)}\s*}$/s; - const reg1 = /^{\s*{function\(.+}\s*}$/; - return str.match(pattern) && !str.match(reg1); -} - -export const isHasExpression = (schema: any) => { - const result = Object.keys(schema).some((key: string) => { - const item = schema[key]; - - // 子协议不做递归确认 - if (key === 'properties') { - return false; - } - - const recursionArray = (list: any[]) => { - const result = list.some(ite => { - if (isArray(ite)) { - return recursionArray(ite); - } - - if (isObject(ite)) { - return isHasExpression(ite); - } - return isExpression(ite); - }); - return result; - }; - - if (isArray(item)) { - return recursionArray(item); - } - - if (isObject(item)) { - return isHasExpression(item); - } - - return isExpression(item); - }); - - return result; -}; - -const parseFunc = (funcBody: string) => { - const funcBodyTemp = funcBody.replace(/(\.|\?\.)/g, '?.'); // 将. 和 ?. 统一替换为?. - const funcBodyStr = funcBodyTemp.replace(/(\d+)\?\.(\d+)/g, '$1.$2'); // 排除数字中的?. - const result = [...funcBodyStr].reduce((acc, char, index) => { - if (char === '[') { - if (index > 0 && funcBodyStr[index - 1] !== '\n') { - // 排除开头[] - return `${acc}?.${char}`; - } - } - return `${acc}${char}`; - }, ''); - return result; -}; - -export const parseExpression = ( - func: any, - formData = {}, - parentPath: string | [] -) => { - const parentData = get(formData, parentPath) || {}; - - if (typeof func === 'string') { - const funcBody = func - .replace(/^{\s*{/g, '') - .replace(/}\s*}$/g, '') - .trim(); - let isHandleData = - funcBody?.startsWith('formData') || funcBody?.startsWith('rootValue'); - - let funcBodyStr = isHandleData ? parseFunc(funcBody) : funcBody; - - const funcStr = ` - return ${funcBodyStr - .replace(/formData/g, JSON.stringify(formData)) - .replace(/rootValue/g, JSON.stringify(parentData))} - `; - try { - const result = Function(funcStr)(); - return result; - } catch (error) { - console.log(error, funcStr, parentPath); - return null; // 如果计算有错误,return null 最合适 - } - } - - return func; -} - -export function getRealDataPath(path) { - if (typeof path !== 'string') { - throw Error(`id ${path} is not a string!!! Something wrong here`); - } - - if (path.match(/[$]void_[^.]+$/)) { - return undefined; - } - - return path.replace(/[$]void_[^.]+./g, ''); -} - -export function getValueByPath(formData, path) { - if (path === '#' || !path) { - return formData || {}; - } else if (typeof path === 'string') { - const realPath = getRealDataPath(path); - return realPath && get(formData, realPath); - } else { - console.error('path has to be a string'); - } -} - -export const parseAllExpression = (_schema: any, _formData: any, dataPath: string, formSchema?: any) => { - const schema = _cloneDeep(_schema); - let formData = _formData; - if (formSchema) { - formData = createDataSkeleton(formSchema, formData); - } - - const recursionArray = (list: any[]) => { - const result = list.map(item => { - if (isArray(item)) { - return recursionArray(item); - } - if (isObject(item)) { - return parseAllExpression(item, formData, dataPath); - } - - if (isExpression(item)) { - return parseExpression(item, formData, dataPath); - } - return item; - }); - - return result; - } - - Object.keys(schema).forEach(key => { - const value = schema[key]; - - if (isArray(value)) { - schema[key] = recursionArray(value); - } if (isObject(value) && (value.mustacheParse ?? true)) { - schema[key] = parseAllExpression(value, formData, dataPath); - } else if (isExpression(value)) { - schema[key] = parseExpression(value, formData, dataPath); - } - }); - - return schema; -}; - diff --git a/packages/x-flow/src/models/fieldShouldUpdate.ts b/packages/x-flow/src/models/fieldShouldUpdate.ts deleted file mode 100644 index 2d403740d..000000000 --- a/packages/x-flow/src/models/fieldShouldUpdate.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { parseExpression } from './expression'; - -// 提取 formData. 开头的字符串 -const extractFormDataStrings = (list: string[]) => { - let result = []; - list.forEach(str => { - // TODO: 为啥要拆开来获取? - // const regex = /formData.\w+(.\w+)*(\(.*\))?/g; // 匹配formData.后面跟着字母、数字、下划线间隔的组合 - const regex = /formData(\.\w+|\[\w+\])(\.\w+|\[\w+\])*/g; // 1.同时匹配两种格式 - const matches = str.match(regex); - if (matches) { - result = result.concat( - matches - ); - } - }); - - return result; -}; - -// 提取 rootValue. 开头的字符串 -const extractRootValueStrings = (list: string[]) => { - let result = []; - list.forEach(str => { - // const regex = /rootValue.\w+(.\w+)*(\(.*\))?/g; // 匹配formData.后面跟着字母、数字、下划线间隔的组合 - const regex = /rootValue(\.\w+|\[\w+\])(\.\w+|\[\w+\])*/g; // 1.同时匹配两种格式 - const matches = str.match(regex); - if (matches) { - result = result.concat( - matches - ); - } - }); - return result; -}; - -// 提取 {{ }} 里面的内容 -const findStrList = (str: any, type: string) => { - const regex = /{{(.*?)}}/g; - const matches = []; - let match; - while ((match = regex.exec(str)) !== null) { - matches.push(match[1]); - }; - - if (type === 'formData') { - return extractFormDataStrings(matches); - } - - if (type === 'rootValue') { - return extractRootValueStrings(matches); - } - return []; -}; - -const getListEveryResult = (list: string[], preValue: any, nextValue: any, dataPath: string) => { - return list.every(item => { - const pre = parseExpression(item, preValue, dataPath); - const curr = parseExpression(item, nextValue, dataPath); - return pre === curr; - }); -}; - -export default (str: string, dataPath: string, dependencies: any[], shouldUpdateOpen: boolean) => (preValue: any, nextValue: any) => { - // dependencies 先不处理 - if (dependencies) { - return true; - } - - const formDataList = findStrList(str, 'formData'); - const rootValueList = findStrList(str, 'rootValue'); - const formDataRes = getListEveryResult(formDataList, preValue, nextValue, dataPath); - const rootValueRes = getListEveryResult(rootValueList, preValue, nextValue, dataPath); - - if (formDataRes && rootValueRes) { - return false; - } - - return true; - }; diff --git a/packages/x-flow/src/models/filterValuesHidden.ts b/packages/x-flow/src/models/filterValuesHidden.ts deleted file mode 100644 index 1ca7c62c7..000000000 --- a/packages/x-flow/src/models/filterValuesHidden.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { isObject, isArray } from '../utils'; - -const transformHidden = (str: any, formData = {}, parentData = {}) => { - if (typeof str !== 'string') { - return !!str; - } - - const funcBody = str.replace(/^{\s*{/g, '').replace(/}\s*}$/g, '').trim(); - const funcStr = ` - return ${funcBody - .replace(/formData/g, JSON.stringify(formData)) - .replace(/rootValue/g, JSON.stringify(parentData))} - `; - try { - const result = Function(funcStr)(); - return result; - } catch (error) { - return false; - } -}; - -/** - * 过滤 field.schema.hidden = true,的值 - */ -export default (_values: any, flattenSchema: object) => { - - const recursiveArray = (list: any[], _path: string) => { - return list.map(item => { - if (isObject(item)) { - return recursiveObj(item, _path, item); - } - return item; - }); - }; - - const recursiveObj = (obj: any, prePath?: string, parentData?: any) => { - - for (let key of Object.keys(obj)) { - const item = obj[key]; - let path = prePath ? `${prePath}.${key}` : key; - let schema = flattenSchema[path]?.schema; - - if (isArray(item) && !schema) { - path = prePath ? `${prePath}.${key}[]` : `${key}[]`; - schema = flattenSchema[path]?.schema; - } - - // 剔除隐藏数据 - if (schema?.hidden) { - const hidden = transformHidden(schema.hidden, _values, parentData); - if (hidden) { - obj[key] = undefined; - continue; - } - } - - if (isObject(item)) { - obj[key] = recursiveObj(item, path, parentData); - continue; - } - - if (isArray(item) && schema?.items) { - obj[key] = recursiveArray(item, path) || []; - continue; - } - - obj[key] = item; - } - - return obj; - }; - - return recursiveObj(_values) || {}; -} \ No newline at end of file diff --git a/packages/x-flow/src/models/filterValuesUndefined.ts b/packages/x-flow/src/models/filterValuesUndefined.ts deleted file mode 100644 index 735fb179b..000000000 --- a/packages/x-flow/src/models/filterValuesUndefined.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { isUndefined, omitBy } from 'lodash-es'; -import { isObject, isArray } from '../utils'; - -export default (values: any, notFilter?: boolean) => { - const recursiveArray = (list: any[]) => { - let result = list.map(item => { - if (isObject(item)) { - return recursiveObj(item, false); - } - if (isArray(item)) { - return recursiveArray(item); - } - return item; - }); - if (Object.keys(result).length === 0) { - return undefined; - } - return result; - }; - - const recursiveObj = (_obj: any, filter = true) => { - if (_obj._isAMomentObject) { - return _obj; - } - - let obj = omitBy(_obj, isUndefined); - Object.keys(obj).forEach(key => { - const item = obj[key]; - - if (isObject(item)) { - obj[key] = recursiveObj(item); - } - - if (isArray(item)) { - const data = recursiveArray(item); - obj[key] = data; - if (!notFilter && data) { - obj[key] = data.filter((item: any) => item !== undefined); - } - } - }); - - obj = omitBy(obj, isUndefined); - if (Object.keys(obj).length === 0 && filter) { - return undefined; - } - return obj; - }; - - return recursiveObj(values) || {}; -}; \ No newline at end of file diff --git a/packages/x-flow/src/models/flattenSchema.ts b/packages/x-flow/src/models/flattenSchema.ts deleted file mode 100644 index 411b90e27..000000000 --- a/packages/x-flow/src/models/flattenSchema.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { _cloneDeep, isObjType, isListType } from '../utils/index'; -import sortProperties from './sortProperties'; - -export const getKeyFromPath = (path = '#') => { - try { - const arr = path.split('.'); - const last = arr.slice(-1)[0]; - const result = last.replace('[]', ''); - return result; - } catch (error) { - console.error(error, 'getKeyFromPath'); - return ''; - } -}; - -export function getSchemaFromFlatten(flatten: any, path = '#') { - let schema: any = {}; - const item = _cloneDeep(flatten[path]); - - if (!item) { - return schema; - } - - schema = item.schema; - // schema.$id && delete schema.$id; - if (item.children.length > 0) { - item.children.forEach((child: any) => { - if (!flatten[child]) return; - const key = getKeyFromPath(child); - if (isObjType(schema)) { - schema.properties[key] = getSchemaFromFlatten(flatten, child); - } - if (isListType(schema)) { - schema.items.properties[key] = getSchemaFromFlatten(flatten, child); - } - }); - } - - return schema; -} - -// TODO: more tests to make sure weird & wrong schema won't crush -export function flattenSchema(_schema = {}, name?: any, parent?: any, _result?: any) { - // 排序 - // _schema = orderBy(_schema, item => item.order, ['asc']); - - const result = _result || {}; - - const schema: any = _cloneDeep(_schema) || {}; - let _name = name || '#'; - if (!schema.$id) { - schema.$id = _name; // path as $id, for easy access to path in schema - } - const children: any[] = []; - if (isObjType(schema)) { - sortProperties(Object.entries(schema.properties)).forEach( - ([key, value]) => { - const _key = isListType(value) ? key + '[]' : key; - const uniqueName = _name === '#' ? _key : _name + '.' + _key; - children.push(uniqueName); - - flattenSchema(value, uniqueName, _name, result); - } - ); - - schema.properties = {}; - } - if (isListType(schema)) { - sortProperties(Object.entries(schema.items.properties)).forEach( - ([key, value]) => { - const _key = isListType(value) ? key + '[]' : key; - const uniqueName = _name === '#' ? _key : _name + '.' + _key; - children.push(uniqueName); - flattenSchema(value, uniqueName, _name, result); - } - ); - - schema.items.properties = {}; - } - - if (schema.type) { - result[_name] = { parent, schema, children }; - } - - return result; -} - diff --git a/packages/x-flow/src/models/formCoreUtils.ts b/packages/x-flow/src/models/formCoreUtils.ts deleted file mode 100644 index fc42ba022..000000000 --- a/packages/x-flow/src/models/formCoreUtils.ts +++ /dev/null @@ -1,188 +0,0 @@ -import { isObject, isArray, _get, _has, isFunction, isObjType } from '../utils'; - -const executeCallBack = (watchItem: any, value: any, path: string, index?: any) => { - if (isFunction(watchItem)) { - try { - watchItem(value, index); - } catch (error) { - console.log(`${path}对应的watch函数执行报错:`, error); - } - } - - if (isFunction(watchItem?.handler)) { - try { - watchItem.handler(value, index); - } catch (error) { - console.log(`${path}对应的watch函数执行报错:`, error); - } - } -}; - -const traverseValues = ({ changedValues, allValues, flatValues }) => { - - const traverseArray = (list: any[], fullList: any, path: string, index: number[]) => { - if (!list.length) { - return - } - - const _path = path += '[]'; - const filterLength = list.filter(item => (item || item === undefined)).length; - - let flag = filterLength !== fullList.length || list.length === 1; - let isRemove = false; - if (filterLength > 1 && filterLength < fullList.length) { - flag = false; - isRemove = true; - } - - list.forEach((item: any, idx: number) => { - if (!isRemove) { - flatValues[_path] = { value: fullList[idx], index }; - } - if (isObject(item)) { - traverseObj(item, fullList[idx], _path, [...index, idx], !flag); - } - if (isArray(item)) { - traverseArray(item, fullList[idx], _path, [...index, idx]); - } - }); - }; - - const traverseObj = (obj: any, fullObj: any, path: string, index: number[], flag?: boolean) => { - Object.keys(obj).forEach((key: string) => { - const item = obj[key]; - const fullItem = fullObj?.[key]; - let value = item; - - const _path = path ? (path + '.' + key) : key; - - let last = true; - - if (isArray(item)) { - value = fullItem ? [...fullItem] : fullItem; - last = false; - traverseArray(item, fullItem, _path, index); - } - - if (isObject(item)) { - last = false; - traverseObj(item, fullItem, _path, index, flag); - } - - if (!last || !flag) { - flatValues[_path] = { value, index }; - } - }); - }; - - traverseObj(changedValues, allValues, null, []); -}; - -export const valuesWatch = (changedValues: any, allValues: any, watch: any) => { - if (Object.keys(watch || {})?.length === 0) { - return; - } - - const flatValues = { - '#': { value: allValues, index: changedValues } - }; - - traverseValues({ changedValues, allValues, flatValues }); - - Object.keys(watch).forEach(path => { - if (!_has(flatValues, path)) { - return; - } - const { value, index } = _get(flatValues, path) as { value: any; index: any; }; - const item = watch[path]; - executeCallBack(item, value, path, index) - }); -}; - -export const transformFieldsData = (_fieldsError: any, getFieldName: any) => { - let fieldsError = _fieldsError; - if (isObject(fieldsError)) { - fieldsError = [fieldsError]; - } - - if (!(isArray(fieldsError) && fieldsError.length > 0)) { - return; - } - - return fieldsError.map((field: any) => ({ errors: field.error, ...field, name: getFieldName(field.name) })); -}; - -export const immediateWatch = (watch: any, values: any) => { - if (Object.keys(watch || {})?.length === 0) { - return; - } - - const watchObj = {}; - Object.keys(watch).forEach(key => { - const watchItem = watch[key]; - if (watchItem?.immediate && isFunction(watchItem?.handler)) { - watchObj[key] = watchItem; - } - }); - - valuesWatch(values, values, watchObj); -}; - -export const getSchemaFullPath = (path: string, schema: any) => { - if (!path || !path.includes('.')) { - return 'properties.' + path; - } - - // 补全 list 类型 path 路径 - while(path.includes('[]')) { - const index = path.indexOf('[]'); - path = path.substring(0, index) + '.items' + path.substring(index + 2); - } - - // 补全 object 类型 path 路径 - let result = 'properties'; - const pathList = path.split('.'); - pathList.forEach((item, index) => { - const key = result + '.' + item; - const itemSchema = _get(schema, key, {}); - if (isObjType(itemSchema) && index !== pathList.length-1) { - result = key + '.properties'; - return ; - } - result = key; - }); - - return result; -}; - -export function yymmdd(timeStamp) { - const date_ob = new Date(Number(timeStamp)); - const adjustZero = num => ('0' + num).slice(-2); - let day = adjustZero(date_ob.getDate()); - let month = adjustZero(date_ob.getMonth()); - let year = date_ob.getFullYear(); - let hours = adjustZero(date_ob.getHours()); - let minutes = adjustZero(date_ob.getMinutes()); - let seconds = adjustZero(date_ob.getSeconds()); - return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; -} - -export function msToTime(duration) { - let seconds: any = Math.floor((duration / 1000) % 60); - let minutes: any = Math.floor((duration / (1000 * 60)) % 60); - let hours: any = Math.floor((duration / (1000 * 60 * 60)) % 24); - - hours = hours < 10 ? '0' + hours : hours; - minutes = minutes < 10 ? '0' + minutes : minutes; - seconds = seconds < 10 ? '0' + seconds : seconds; - return hours + ':' + minutes + ':' + seconds; -} - -export const getSessionItem = (key: string) => { - return Number(sessionStorage.getItem(key) || 0); -} - -export const setSessionItem = (key: string, data: any) => { - sessionStorage.setItem(key, data +''); -} - diff --git a/packages/x-flow/src/models/formDataSkeleton.ts b/packages/x-flow/src/models/formDataSkeleton.ts deleted file mode 100644 index 30ffdc4fd..000000000 --- a/packages/x-flow/src/models/formDataSkeleton.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { _cloneDeep, isObjType, isListType } from '../utils/index'; - -export const createDataSkeleton = (schema: any, formData?: any) => { - let _formData = _cloneDeep(formData); - let result = _formData; - - if (isObjType(schema)) { - if (_formData === undefined || typeof _formData !== 'object') { - _formData = {}; - result = {}; - } - Object.keys(schema.properties).forEach(key => { - const childSchema = schema.properties[key]; - const childData = _formData[key]; - const childResult = createDataSkeleton(childSchema, childData); - result[key] = childResult; - }); - } else if (_formData !== undefined) { - // result = _formData; - } else if (schema.default !== undefined) { - result = _cloneDeep(schema.default); - } else if (isListType(schema)) { - result = [createDataSkeleton(schema.items)]; - } else if (schema.type === 'boolean' && !schema.widget) { - // result = false; - result = undefined; - } else { - result = undefined; - } - return result; -}; \ No newline at end of file diff --git a/packages/x-flow/src/models/layout.ts b/packages/x-flow/src/models/layout.ts deleted file mode 100644 index 5f8d8322b..000000000 --- a/packages/x-flow/src/models/layout.ts +++ /dev/null @@ -1,73 +0,0 @@ -export const getFormItemLayout = (column: number, schema: any, { labelWidth, displayType, _labelCol, _fieldCol }: any) => { - let labelCol: any = { span: 5 }; - let fieldCol: any = { span: 9 }; - - if (column === 2) { - labelCol = { span: 6 }; - fieldCol = { span: 14 } - } - - if (column > 2) { - labelCol = { span: 7 }; - fieldCol = { span: 16 } - } - - if (displayType === 'column') { - // labelCol = { xl: 9, xxl: 6 }; - // if (column > 1) { - // labelCol = {}; - // fieldCol = {}; - // } - labelCol = {}; - fieldCol = {}; - } - - if (_labelCol) { - labelCol = _labelCol; - if (displayType === 'column') { - labelCol = {}; - } - } - - if (_fieldCol) { - fieldCol = _fieldCol; - if (typeof _fieldCol === 'number') { - fieldCol = { span: _fieldCol } - } - } - - if (displayType === 'inline') { - labelCol = {}; - fieldCol = {}; - } - - // 兼容一下 1.0 版本 - if ((labelWidth || labelWidth === 0) && displayType !== 'column') { - labelCol = { flex : labelWidth + 'px' }; - fieldCol = { flex: 'auto' }; - } - - // 自定义进行覆盖 - if (schema.cellSpan) { - fieldCol = {}; - } - - - if (schema.labelCol || schema.labelCol === 0) { - labelCol = schema.labelCol; - } - - if (schema.fieldCol || schema.fieldCol === 0) { - fieldCol = schema.fieldCol; - } - - if (typeof labelCol === 'number') { - labelCol = { span: labelCol } - } - - if (typeof fieldCol === 'number') { - fieldCol = { span: fieldCol } - } - - return { labelCol, fieldCol } -} \ No newline at end of file diff --git a/packages/x-flow/src/models/mapping.tsx b/packages/x-flow/src/models/mapping.tsx deleted file mode 100644 index 24905c4dc..000000000 --- a/packages/x-flow/src/models/mapping.tsx +++ /dev/null @@ -1,134 +0,0 @@ -export const mapping = { - default: 'input', - string: 'input', - array: 'list', - boolean: 'checkbox', - integer: 'number', - number: 'inputNumber', - object: 'map', - html: 'html', - card: 'card', - collapse: 'collapse', - lineTitle: 'lineTitle', - line: 'line', - subItem: 'subItem', - panel: 'panel', - 'string:upload': 'upload', - 'string:url': 'urlInput', - 'string:dateTime': 'datePicker', - 'string:date': 'datePicker', - 'string:year': 'datePicker', - 'string:month': 'datePicker', - 'string:week': 'datePicker', - 'string:quarter': 'datePicker', - 'string:time': 'timePicker', - 'string:textarea': 'textArea', - 'string:color': 'color', - 'string:image': 'imageInput', - 'range:time': 'timeRange', - 'range:dateTime': 'dateRange', - 'range:date': 'dateRange', - 'range:year': 'dateRange', - 'range:month': 'dateRange', - 'range:week': 'dateRange', - 'range:quarter': 'dateRange', - '*?enum': 'radio', - '*?enum_long': 'select', - 'array?enum': 'checkboxes', - 'array?enum_long': 'multiSelect', - '*?readOnly': 'html', // TODO: html widgets for list / object -}; - -export function getWidgetName(schema, _mapping = mapping) { - const { type, format, enum: enums, readOnly, widget, props } = schema; - - //如果已经注明了渲染widget,那最好 - if (schema['ui:widget'] || schema.widget) { - return schema['ui:widget'] || schema.widget; - } - - const list: string[] = []; - if (readOnly) { - list.push(`${type}?readOnly`); - list.push('*?readOnly'); - } - - if (enums) { - // 根据 enum 长度来智能选择控件 - if ( - Array.isArray(enums) && - ((type === 'array' && enums.length > 6) || - (type !== 'array' && enums.length > 2)) - ) { - list.push(`${type}?enum_long`); - list.push('*?enum_long'); - } else { - list.push(`${type}?enum`); - // array 默认使用 list,array?enum 默认使用 checkboxes,*?enum 默认使用select - list.push('*?enum'); - } - } - - if (props?.options) { - if ((type === 'array' && props.options.length > 6) || (type !== 'array' && props.options.length > 2)) { - - list.push(`${type}?enum_long`); - list.push('*?enum_long'); - } else { - list.push(`${type}?enum`); - // array 默认使用 list,array?enum 默认使用 checkboxes,*?enum 默认使用select - list.push('*?enum'); - } - } - - const _widget = format; - if (_widget) { - list.push(`${type}:${_widget}`); - } - - if (type === 'object') { - list.push((schema.theme === 'tile' ? 'lineTitle' : schema.theme) || 'collapse'); - } else { - list.push(type); // 放在最后兜底,其他都不match时使用type默认的组件 - } - - let widgetName = ''; - list.some(item => { - widgetName = _mapping[item]; - return !!widgetName; - }); - - return widgetName; -} - - -function capitalizeFirstLetter(str: any) { - if (!str) { - return str; - } - return str.charAt(0).toUpperCase() + str.slice(1); -} - -export const getWidget = (name: string, widgets: any) => { - let widget = widgets[name]; - - // name 转成首字母大写 - if (!widget) { - widget = widgets[capitalizeFirstLetter(name)]; - } - - if (!widget) { - widget = widgets['Html'] || null; - } - - return widget; -} - -export const extraSchemaList = { - checkbox: { - valuePropName: 'checked', - }, - switch: { - valuePropName: 'checked', - }, -}; diff --git a/packages/x-flow/src/models/sortProperties.ts b/packages/x-flow/src/models/sortProperties.ts deleted file mode 100644 index 1c0263d87..000000000 --- a/packages/x-flow/src/models/sortProperties.ts +++ /dev/null @@ -1,23 +0,0 @@ -export default (properties, orderKey = 'order') => { - const orderHash = new Map(); - // order不为数字的数据 - const unsortedList: any[] = []; - const insert = (item: any) => { - const [, value] = item; - if (typeof value[orderKey] !== 'number') { - unsortedList.push(item); - return; - } - if (orderHash.has(value[orderKey])) { - orderHash.get(value[orderKey]).push(item); - } else { - orderHash.set(value[orderKey], [item]); - } - }; - - properties.forEach(item => insert(item)); - const sortedList = Array.from(orderHash.entries()) - .sort(([order1], [order2]) => order1 - order2) // order值越小越靠前 - .flatMap(([, items]) => items); - return sortedList.concat(unsortedList); -} \ No newline at end of file diff --git a/packages/x-flow/src/models/store.ts b/packages/x-flow/src/models/store.ts index 602daea2e..afbadd048 100644 --- a/packages/x-flow/src/models/store.ts +++ b/packages/x-flow/src/models/store.ts @@ -1,27 +1,117 @@ -import { createStore as create } from 'zustand'; +import { create } from 'zustand'; +import { immer } from 'zustand/middleware/immer'; +import { temporal } from 'zundo'; +import { + addEdge, + applyNodeChanges, + applyEdgeChanges, + Edge, + Node, + OnNodesChange, + OnEdgesChange, + OnConnect, +} from '@xyflow/react'; +import _ from "lodash"; -type FormStore = { - schema?: any; - flattenSchema: any; - context?: any; - initialized: boolean, - init?: (schema: FormStore['schema']) => any; - setContext: (context: any) => any; +export type AppNode = Node; + +export type AppState = { + layout: 'LR' | 'TB', + nodes: AppNode[]; + edges: Edge[]; + nodeMenus: any[]; + candidateNode: any; + mousePosition: any; + onNodesChange: OnNodesChange; + onEdgesChange: OnEdgesChange; + onConnect: OnConnect; + setNodes: (nodes: AppNode[]) => void; + setEdges: (edges: Edge[]) => void; + setLayout: (layout: 'LR' | 'TB') => void; + setNodeMenus: (nodeMenus: any[]) => void; + setCandidateNode: (candidateNode: any) => void; + setMousePosition: (mousePosition: any) => void; }; -// 将 useStore 改为 createStore, 并把它改为 create 方法 -export const createStore = () => create((setState: any, get: any) => ({ - initialized: false, - schema: {}, - flattenSchema: {}, - context: {}, - init: data => { - return setState({ - initialized: true, - ...data - }); - }, - setContext: context => { - return setState({ context }); +// 这是我们的 useStore hook,我们可以在我们的组件中使用它来获取 store 并调用动作 +// 注意:immer 使用方式是 create()(immer(() => ({}))) +const useStore = create()( + immer( + temporal( + (set, get) => ({ + layout: 'LR', + nodes: [], + edges: [], + candidateNode: null, + nodeMenus: [], + mousePosition: { pageX: 0, pageY: 0, elementX: 0, elementY: 0 }, + onNodesChange: (changes) => { + set({ + nodes: applyNodeChanges(changes, get().nodes), + }); + }, + onEdgesChange: (changes) => { + set({ + edges: applyEdgeChanges(changes, get().edges), + }); + }, + onConnect: (connection) => { + set({ + edges: addEdge(connection, get().edges), + }); + }, + setNodes: (nodes) => { + set({ nodes }); + }, + setEdges: (edges) => { + set({ edges }); + }, + setNodeMenus: (nodeMenus: any) => { + set({ nodeMenus }); + }, + setCandidateNode: (candidateNode) => { + set({ candidateNode }); + }, + setMousePosition: (mousePosition: any) => { + set({ mousePosition }); + }, + setLayout: (layout: 'LR' | 'TB') => { + if (!layout) { + return; + } + set({ layout }); + } + }), + { + // 偏函数 + partialize: (state) => { + const { nodes, edges } = state; + return { + edges, + nodes, + }; + }, + }, + ), + ), +); + + +export const useUndoRedo = (isTracking = true) => { + const temporalStore = useStore.temporal.getState(); + if (temporalStore.isTracking) { + // 暂停时间旅行机器, + temporalStore.pause(); } -})); \ No newline at end of file + + return { + ...temporalStore, + record: (callback: () => void) => { + temporalStore.resume(); + callback(); + temporalStore.pause(); + } + } +}; + +export default useStore; \ No newline at end of file diff --git a/packages/x-flow/src/models/transformProps.ts b/packages/x-flow/src/models/transformProps.ts deleted file mode 100644 index 4c9bd956a..000000000 --- a/packages/x-flow/src/models/transformProps.ts +++ /dev/null @@ -1,85 +0,0 @@ -const displayTypeEnum = { - column: 'vertical', - row: 'horizontal', - inline: 'inline', -}; - -const transformProps = (props: any) => { - const { - schema, - beforeFinish, - onMount, - displayType = 'column', - watch, - removeHiddenData = true, - readOnly, - column = 1, - mapping, - debugCss, - locale, - configProvider, - validateMessages, - debug, - id, - labelWidth, - maxWidth, - form, - onFinish, - onFinishFailed, - footer, - operateExtra, - logOnMount, - logOnSubmit, - labelCol, - fieldCol, - disabled, - className, - validateTrigger, - antdVersion, - ...otherProps - } = props; - - const formProps = { - ...otherProps, - }; - - if (displayType) { - formProps.layout = displayTypeEnum[displayType] || 'horizontal'; - } - - return { - formProps, - schema, - displayType, - onFinish, - beforeFinish, // form 没有这个 api, 感觉找不到时机 - onMount, - watch, - readOnly, - disabled, - column, - mapping, - debugCss, // 好像没用了 - locale, - configProvider, - footer, - form, - labelWidth, - validateMessages, - debug, // 换成 form 还有用吗? - id, - onFinishFailed, - removeHiddenData, - operateExtra, - logOnMount, - logOnSubmit, - labelCol, - fieldCol, - maxWidth, - className, - validateTrigger, - antdVersion - }; -}; - -export default transformProps; \ No newline at end of file diff --git a/packages/x-flow/src/models/useForm.ts b/packages/x-flow/src/models/useForm.ts deleted file mode 100644 index d78b36f0b..000000000 --- a/packages/x-flow/src/models/useForm.ts +++ /dev/null @@ -1,360 +0,0 @@ -import { useRef } from 'react'; -import { Form } from 'antd'; -import { cloneDeep } from 'lodash-es'; - -import { transformFieldsData, getSchemaFullPath } from './formCoreUtils'; -import { parseBindToValues, parseValuesToBind } from './bindValues'; -import { _isMatch, _set, _get, _has, _merge, _mergeWith, isFunction, isObject, isArray, _isUndefined, hasFuncProperty } from '../utils'; -import filterValuesUndefined from './filterValuesUndefined'; -import filterValuesHidden from './filterValuesHidden'; -import { flattenSchema as flatten } from './flattenSchema'; -import type { FormInstance } from '../type'; - -const updateSchemaByPath = (_path: string, _newSchema: any, formSchema: any) => { - const path = getSchemaFullPath(_path, formSchema); - const currSchema = _get(formSchema, path, {}); - const newSchema = isFunction(_newSchema) ? _newSchema(currSchema) : _newSchema; - - const result = { - ...currSchema, - ...newSchema, - } - - if (newSchema.props) { - result.props = { - ...currSchema?.props, - ...newSchema.props - } - } - - _set(formSchema, path, result); -}; - -const getFieldName = (_path: any): any => { - if (!_path) { - return undefined; - } - - if (typeof _path === 'boolean') { - return _path; - } - - let result: any[] = []; - - if (isArray(_path)) { - result = _path.map((item: any) => { - return item.split('.').map((ite: any) => { - if (!isNaN(Number(ite))) { - return ite * 1; - } - return ite; - }); - }); - } - - result = _path.split('.').map((item: any) => { - if (!isNaN(Number(item))) { - return item * 1; - } - return item; - }); - - result = result.map(item => { - if (typeof item === 'string' && item?.indexOf('[') === 0 && item?.indexOf(']') === item?.length -1) { - return Number(item.substring(1, item.length-1)); - } - return item; - }); - - return result; -}; - -const useForm = () => { - const [form] = Form.useForm(); - - const flattenSchemaRef = useRef({}); - const storeRef: any = useRef(); - const schemaRef = useRef({}); - const fieldRefs = useRef({}); - - const { - getFieldError, - getFieldsError, - getFieldInstance, - setFieldsValue, - setFields, - scrollToField, - isFieldsTouched, - isFieldTouched, - isFieldValidating, - resetFields, - validateFields, - ...otherForm - } = form; - - const xform: any = otherForm; - - const setStoreData = (data: any) => { - const { setState } = storeRef.current; - - if (!setState) { - setTimeout(() => { - setState({ schema: schemaRef.current, flattenSchema: flattenSchemaRef.current }); - }, 0); - } - setState(data); - }; - - // 更新协议 - const handleSchemaUpdate = (newSchema: any) => { - // form.__schema = Object.freeze(newSchema); - flattenSchemaRef.current = flatten(newSchema) || {}; - schemaRef.current = newSchema; - setStoreData({ schema: newSchema, flattenSchema: flattenSchemaRef.current }); - }; - - // 设置协议 - xform.setSchema = (obj: any, cover = false) => { - if (!isObject(obj)) { - return; - } - - if (cover) { - handleSchemaUpdate(obj); - return; - } - - const schema = cloneDeep(schemaRef.current); - Object.keys(obj || {}).forEach(path => { - updateSchemaByPath(path, obj[path], schema); - }); - - handleSchemaUpdate(schema); - } - - // 设置某个字段的协议 - xform.setSchemaByPath = (_path: string, _newSchema: any) => { - // diff 判断是否需要更新,存在函数跳过 - if (!hasFuncProperty(_newSchema) && _isMatch(_newSchema, xform.getSchemaByPath(_path))) { - return; - } - - const schema = cloneDeep(schemaRef.current); - updateSchemaByPath(_path, _newSchema, schema); - handleSchemaUpdate(schema); - } - - // form.setSchemaByFullPath = (path: string, newSchema: any) => { - // const schema = _cloneDeep(schemaRef.current); - // const currSchema = _get(schema, path, {}); - - // const result = _mergeWith(currSchema, newSchema, (objValue, srcValue, key) => { - // return srcValue; - // }); - - // _set(schema, path, result); - // handleSchemaUpdate(schema); - // } - - // 设置表单数据 - xform.setValues = (_values: any) => { - const values = parseBindToValues(_values, flattenSchemaRef.current); - setFieldsValue(values); - } - - // 获取表单数据 - xform.getValues = (nameList?: any, filterFunc?: any) => { - let values = cloneDeep(form.getFieldsValue(getFieldName(nameList), filterFunc)); - const { removeHiddenData } = storeRef.current?.getState() || {}; - if (removeHiddenData) { - values = filterValuesHidden(values, flattenSchemaRef.current); - } - values = filterValuesUndefined(values); - return parseValuesToBind(values, flattenSchemaRef.current); - } - - xform.getValueByPath = (path: string) => { - const name = getFieldName(path); - return form.getFieldValue(name); - } - - // 设置某个字段的值 - xform.setValueByPath = (path: string, value: any) => { - if (!form.setFieldValue) { - const values = form.getFieldsValue(); - _set(values, path, value); - xform.setValues(values); - return; - } - - const name = getFieldName(path); - form.setFieldValue(name, value); - - try { - if (JSON.stringify(form.getFieldValue(name)) !== JSON.stringify(value)) { - form.setFieldValue(name, value); - } - } catch (error) { - - } - } - - // 通过某个字段的 schema - xform.getSchemaByPath = (_path: string) => { - if (typeof _path !== 'string') { - console.warn('请输入正确的路径'); - } - const path = getSchemaFullPath(_path, schemaRef.current); - return _get(schemaRef.current, path); - }; - - // 获取协议 - xform.getSchema = () => { - return schemaRef.current; - }; - - // 设置一组字段错误 - xform.setErrorFields = (fieldsError: any[]) => { - const fieldsData = transformFieldsData(fieldsError, getFieldName); - if (!fieldsData) { - return; - } - - setFields(fieldsData); - }; - - // 清空某个字段的错误 - xform.removeErrorField = (path: any) => { - setFields([{ name: getFieldName(path), errors: [] }]); - }; - - // 获取对应字段名的错误信息 - xform.getFieldError = (path: string) => { - const name = getFieldName(path); - return form.getFieldError(name); - } - - // 获取一组字段名对应的错误信息,返回为数组形式 - xform.getFieldsError = (path: string[]) => { - const name = getFieldName(path); - return getFieldsError(name); - } - - // 获取对应字段实例 - xform.getFieldInstance = (path: string) => { - const name = getFieldName(path); - return getFieldInstance(name); - } - - // 获取隐藏字段数据 - xform.getHiddenValues = () => { - const values = xform.getValues(); - const allValues = xform.getValues(true); - const hiddenValues = {}; - - const recursion = (obj1: any, obj2: any, path: any) => { - Object.keys(obj1).forEach((key: string) => { - const value = obj1[key]; - const _path = path ? `${path}.${key}` : key; - if (!obj2.hasOwnProperty(key)) { - _set(hiddenValues, _path, value); - return; - } - - if (isObject(value)) { - recursion(value, obj2[key], _path); - } - - if (isArray(value)) { - value.map((item: any, index: number) => { - recursion(item, _get(obj2, `${key}[${index}]`, []), `${_path}[${index}]`) - }); - } - }); - }; - - recursion(allValues, values, null); - return hiddenValues; - } - - // 设置一组字段状态 - xform.setFields = (nameList: any[]) => { - const fieldsData = transformFieldsData(nameList, getFieldName); - if (!fieldsData) { - return; - } - setFields(fieldsData); - } - - xform.__initStore = (store: any) => { - storeRef.current = store; - } - - // 滚动到对应字段位置 - xform.scrollToPath = (path: string, ...rest: any[]) => { - const name = getFieldName(path); - scrollToField(name, ...rest); - } - - // 检查一组字段是否被用户操作过,allTouched 为 true 时检查是否所有字段都被操作过 - xform.isFieldsTouched = (pathList?: string[], allTouched?: boolean) => { - const nameList = (pathList || []).map(path => getFieldName(path)); - return isFieldsTouched(nameList, allTouched); - } - - // 检查对应字段是否被用户操作过 - xform.isFieldTouched = (path: string) => { - const name = getFieldName(path); - return isFieldTouched(name); - } - - // 检查对应字段是否被用户操作过 - xform.isFieldValidating = (path: string) => { - const name = getFieldName(path); - return isFieldValidating(name); - } - - xform.resetFields = (pathList?: string[]) => { - const nameList = (pathList || []).map(path => getFieldName(path)); - if (nameList.length > 0) { - resetFields(nameList); - } else { - resetFields(); - } - } - - // 触发表单验证 - xform.validateFields = (pathList?: string[], config?: object) => { - const nameList = (pathList || []).map(path => getFieldName(path)); - if (nameList.length > 0) { - return validateFields(nameList, config); - } - return validateFields(); - }; - - - xform.getFlattenSchema = (path?: string) => { - if (!path) { - return flattenSchemaRef.current; - } - return flattenSchemaRef.current?.[path]; - } - - // 老 API 兼容 - xform.onItemChange = xform.setValueByPath; - - xform.setFieldRef = (path: string, ref: any) => { - if (!path) { - return; - } - fieldRefs.current[path] = ref; - } - - xform.getFieldRef = (path: string) => { - return fieldRefs.current[path]; - } - - return xform as FormInstance; -}; - -export default useForm; diff --git a/packages/x-flow/src/models/validateMessage.ts b/packages/x-flow/src/models/validateMessage.ts deleted file mode 100644 index 6c22fdb4f..000000000 --- a/packages/x-flow/src/models/validateMessage.ts +++ /dev/null @@ -1,97 +0,0 @@ -const typeTemplate = "'${label}' is not a valid ${type}"; -const typeTemplateCN = "数据类型必须是 ${type}"; - -export const validateMessagesEN = { - default: "Validation error on field '${label}'", - required: "'${label}' is required", - enum: "'${label}' must be one of [${enum}]", - whitespace: "'${label}' cannot be empty", - date: { - format: "'${label}' is invalid for format date", - parse: "'${label}' could not be parsed as date", - invalid: "'${label}' is invalid date", - }, - types: { - string: typeTemplate, - method: typeTemplate, - array: typeTemplate, - object: typeTemplate, - number: typeTemplate, - date: typeTemplate, - boolean: typeTemplate, - integer: typeTemplate, - float: typeTemplate, - regexp: typeTemplate, - email: typeTemplate, - url: typeTemplate, - hex: typeTemplate, - }, - string: { - len: "'${label}' must be exactly ${len} characters", - min: "'${label}' must be at least ${min} characters", - max: "'${label}' cannot be longer than ${max} characters", - range: "'${label}' must be between ${min} and ${max} characters", - }, - number: { - len: "'${label}' must equal ${len}", - min: "'${label}' cannot be less than ${min}", - max: "'${label}' cannot be greater than ${max}", - range: "'${label}' must be between ${min} and ${max}", - }, - array: { - len: "'${label}' must be exactly ${len} in length", - min: "'${label}' cannot be less than ${min} in length", - max: "'${label}' cannot be greater than ${max} in length", - range: "'${label}' must be between ${min} and ${max} in length", - }, - pattern: { - mismatch: "'${label}' does not match pattern ${pattern}", - }, -}; - -export const validateMessagesCN = { - default: '${label}未通过校验', - required: '${label}必填', - whitespace: '${label}不能为空', - date: { - format: '${label}的格式错误', - parse: '${label}无法被解析', - invalid: '${label}数据不合法', - }, - types: { - string: typeTemplateCN, - method: typeTemplateCN, - array: typeTemplateCN, - object: typeTemplateCN, - number: typeTemplateCN, - date: typeTemplateCN, - boolean: typeTemplateCN, - integer: typeTemplateCN, - float: typeTemplateCN, - regexp: typeTemplateCN, - email: typeTemplateCN, - url: typeTemplateCN, - hex: typeTemplateCN, - }, - string: { - len: '${label}长度不是${len}', - min: '${label}长度不能小于${min}', - max: '${label}长度不能大于${max}', - range: '${label}长度需在${min}与${max}之间', - }, - number: { - len: '${label}不等于${len}', - min: '${label}不能小于${min}', - max: '${label}不能大于${max}', - range: '${label}需在${min}与${max}之间', - }, - array: { - len: '${label}长度不是${len}', - min: '${label}长度不能小于${min}', - max: '${label}长度不能大于${max}', - range: '${label}长度需在${min}与${max}之间', - }, - pattern: { - mismatch: '${label}未通过正则判断${pattern}', - }, -}; diff --git a/packages/x-flow/src/models/validates.ts b/packages/x-flow/src/models/validates.ts deleted file mode 100644 index 7df7ea3a3..000000000 --- a/packages/x-flow/src/models/validates.ts +++ /dev/null @@ -1,138 +0,0 @@ -import Color from 'color'; -import { isUrl, isObject, isFunction } from '../utils'; -import { cloneDeep } from 'lodash-es'; - -const insertLengthRule = (schema: any, rules: any[]) => { - const { type, max, min, message } = schema; - - if (max || max === 0) { - rules.push({ type, max, message: message?.max }); - } - - if (min || min === 0) { - rules.push({ type, min, message: message?.min }); - } -}; - -const insertRequiredRule = (schema: any, rules: any[]) => { - let { - type, - format, - required, - message, - widget, - title - } = schema; - - const requiredAlready = schema?.rules?.some((item: any) => item?.required); - - // 未声明 required,或已经存在 required 校验 - if (!required || requiredAlready) { - return; - } - - let rule: any = { required: true, message: message?.required }; - - if (['year','quarter', 'month', 'week', 'date', 'dateTime', 'time'].includes(format) && type === 'range') { - rule = { - type: 'array', - required: true, - len: 2, - fields: { - 0: { type: 'string', required: true }, - 1: { type: 'string', required: true }, - } - }; - } else if (widget === 'checkbox') { - rule = { type, required: true, whitespace: true, message: title + '必填' }; - } else if (type === 'string') { - rule = { type: 'string', required: true, whitespace: true, message: message?.required || (!title ? '内容必填' : undefined) }; - } - - rules.push(rule); -}; - -export const transformRules = (rules = [], methods: any, form: any) => { - return rules.map(((item: any) => { - if (item.validator && !item.transformed) { - const validator = isFunction(item.validator) ? item.validator : methods[item.validator]; - item.validator = async (_: any, value: any) => { - const result = await validator(_, value, { form }); - if (isObject(result)) { - return result?.status ? Promise.resolve() : Promise.reject(new Error(result.message || item.message)); - } - return result ? Promise.resolve() : Promise.reject(new Error(item.message)); - };; - item.transformed = true; - } - return item; - })); -}; - -export default (_schema: any, form: any, methods: any, fieldRef: any) => { - const schema = cloneDeep(_schema); - let { - format, - rules: ruleList = [], - pattern, - message, - } = schema; - - const rules: any = [...ruleList]; - - insertRequiredRule(schema, rules); - insertLengthRule(schema, rules); - - rules.push({ - validator: async (_: any) => { - if (!isFunction(fieldRef?.current?.validator)) { - return true; - } - const res = await fieldRef.current?.validator(); - return res; - } - }); - - if (pattern) { - rules.push({ pattern, message: message?.pattern }); - } - - if (format === 'url') { - rules.push({ type: 'url', message: message?.url }); - } - - if (format === 'email') { - rules.push({ type: 'email', message: message?.email }); - } - - if (format === 'image') { - rules.push({ - validator: (_: any, value: any) => { - if (!value) { - return true; - } - const imagePattern = '([/|.|w|s|-])*.(?:jpg|gif|png|bmp|apng|webp|jpeg|json)'; - const _isUrl = isUrl(value); - const _isImg = new RegExp(imagePattern).test(value); - return _isUrl || _isImg; - }, - message: message?.email ?? '请输入正确的图片格式' - }); - } - - if (format === 'color') { - rules.push({ - validator: (_: any, value: any) => { - try { - Color(value || null); // 空字符串无法解析会报错,出现空的情况传 null - return true; - } catch (e) { - return false; - } - }, - message: message?.color ?? '请填写正确的颜色格式' - }); - } - - return transformRules(rules, methods, form); -} \ No newline at end of file diff --git a/packages/x-flow/src/core/operator/Control/index.less b/packages/x-flow/src/operator/Control/index.less similarity index 100% rename from packages/x-flow/src/core/operator/Control/index.less rename to packages/x-flow/src/operator/Control/index.less diff --git a/packages/x-flow/src/core/operator/Control/index.tsx b/packages/x-flow/src/operator/Control/index.tsx similarity index 89% rename from packages/x-flow/src/core/operator/Control/index.tsx rename to packages/x-flow/src/operator/Control/index.tsx index 7c33839d5..878716621 100644 --- a/packages/x-flow/src/core/operator/Control/index.tsx +++ b/packages/x-flow/src/operator/Control/index.tsx @@ -7,9 +7,9 @@ import { RiStickyNoteAddLine, } from '@remixicon/react'; import { Tooltip, Button } from 'antd'; -import IconView from '../../../components/IconView'; -import { useEventEmitterContextContext } from '../../../context/event-emitter'; -import NodeSelectPopover from '../../../components/NodeSelectPopover'; +import IconView from '../../components/IconView'; +import { useEventEmitterContextContext } from '../../models/event-emitter'; +import NodeSelectPopover from '../../components/NodeSelectPopover'; import './index.less'; const Control = (props: any) => { diff --git a/packages/x-flow/src/core/operator/UndoRedo/index.less b/packages/x-flow/src/operator/UndoRedo/index.less similarity index 100% rename from packages/x-flow/src/core/operator/UndoRedo/index.less rename to packages/x-flow/src/operator/UndoRedo/index.less diff --git a/packages/x-flow/src/core/operator/UndoRedo/index.tsx b/packages/x-flow/src/operator/UndoRedo/index.tsx similarity index 94% rename from packages/x-flow/src/core/operator/UndoRedo/index.tsx rename to packages/x-flow/src/operator/UndoRedo/index.tsx index c8349c63b..10ad5a173 100644 --- a/packages/x-flow/src/core/operator/UndoRedo/index.tsx +++ b/packages/x-flow/src/operator/UndoRedo/index.tsx @@ -1,7 +1,7 @@ import React, { memo } from 'react'; import { RiArrowGoBackLine, RiArrowGoForwardFill } from '@remixicon/react' import { Button, Tooltip } from 'antd'; -import IconView from '../../../components/IconView'; +import IconView from '../../components/IconView'; import './index.less'; export type UndoRedoProps = { diff --git a/packages/x-flow/src/core/operator/ZoomInOut/index.less b/packages/x-flow/src/operator/ZoomInOut/index.less similarity index 100% rename from packages/x-flow/src/core/operator/ZoomInOut/index.less rename to packages/x-flow/src/operator/ZoomInOut/index.less diff --git a/packages/x-flow/src/core/operator/ZoomInOut/index.tsx b/packages/x-flow/src/operator/ZoomInOut/index.tsx similarity index 98% rename from packages/x-flow/src/core/operator/ZoomInOut/index.tsx rename to packages/x-flow/src/operator/ZoomInOut/index.tsx index 77fa759ad..4c56c2d54 100644 --- a/packages/x-flow/src/core/operator/ZoomInOut/index.tsx +++ b/packages/x-flow/src/operator/ZoomInOut/index.tsx @@ -4,7 +4,7 @@ import { Button, Popover, Tooltip } from 'antd'; import { useReactFlow, useViewport } from '@xyflow/react'; import { getKeyboardKeyNameBySystem } from '../../utils'; import ShortcutsName from './shortcuts-name'; -import IconView from '../../../components/IconView'; +import IconView from '../../components/IconView'; import './index.less'; enum ZoomType { diff --git a/packages/x-flow/src/core/operator/ZoomInOut/shortcuts-name.tsx b/packages/x-flow/src/operator/ZoomInOut/shortcuts-name.tsx similarity index 94% rename from packages/x-flow/src/core/operator/ZoomInOut/shortcuts-name.tsx rename to packages/x-flow/src/operator/ZoomInOut/shortcuts-name.tsx index ecb291c45..303e43359 100644 --- a/packages/x-flow/src/core/operator/ZoomInOut/shortcuts-name.tsx +++ b/packages/x-flow/src/operator/ZoomInOut/shortcuts-name.tsx @@ -1,4 +1,4 @@ -import { memo } from 'react' +import React, { memo } from 'react' import { getKeyboardKeyNameBySystem } from '../../utils' import cn from 'classnames' diff --git a/packages/x-flow/src/core/operator/index.less b/packages/x-flow/src/operator/index.less similarity index 100% rename from packages/x-flow/src/core/operator/index.less rename to packages/x-flow/src/operator/index.less diff --git a/packages/x-flow/src/core/operator/index.tsx b/packages/x-flow/src/operator/index.tsx similarity index 100% rename from packages/x-flow/src/core/operator/index.tsx rename to packages/x-flow/src/operator/index.tsx diff --git a/packages/x-flow/src/core/types.ts b/packages/x-flow/src/types.ts similarity index 100% rename from packages/x-flow/src/core/types.ts rename to packages/x-flow/src/types.ts diff --git a/packages/x-flow/src/core/utils/autoLayoutNodes.ts b/packages/x-flow/src/utils/autoLayoutNodes.ts similarity index 100% rename from packages/x-flow/src/core/utils/autoLayoutNodes.ts rename to packages/x-flow/src/utils/autoLayoutNodes.ts diff --git a/packages/x-flow/src/utils/createIconFont.ts b/packages/x-flow/src/utils/createIconFont.ts index 6285d207e..f11184a35 100644 --- a/packages/x-flow/src/utils/createIconFont.ts +++ b/packages/x-flow/src/utils/createIconFont.ts @@ -4,4 +4,4 @@ export default (url?: string) => { return createFromIconfontCN({ scriptUrl: url || '//at.alicdn.com/t/a/font_2750617_sax751jyfjl.js', }); -}; +}; \ No newline at end of file diff --git a/packages/x-flow/src/core/utils/hooks.ts b/packages/x-flow/src/utils/hooks.ts similarity index 100% rename from packages/x-flow/src/core/utils/hooks.ts rename to packages/x-flow/src/utils/hooks.ts diff --git a/packages/x-flow/src/utils/index.ts b/packages/x-flow/src/utils/index.ts index 386d1fe46..fa164c798 100644 --- a/packages/x-flow/src/utils/index.ts +++ b/packages/x-flow/src/utils/index.ts @@ -1,3 +1,8 @@ +import { customAlphabet } from 'nanoid'; +export const uuid = customAlphabet('0123456789abcdefghijklmnopqrstuvwxyz', 16); + + + import { isMatch, some, set, get, cloneDeep, has as _has, merge, mergeWith, isUndefined, omitBy } from 'lodash-es'; export const _set = set; @@ -123,3 +128,43 @@ export const safeGet = (object: any, path: string, defaultValue: any) => { }; + + + +export const isMac = () => { + return navigator.userAgent.toUpperCase().includes('MAC') +} + +const specialKeysNameMap: Record = { + ctrl: '⌘', + alt: '⌥', +} + +export const getKeyboardKeyNameBySystem = (key: string) => { + if (isMac()) + return specialKeysNameMap[key] || key + + return key +} + + +export const capitalize = (string: string) => { + if (typeof string !== 'string' || string.length === 0) { + return string; + } + return `${string.charAt(0).toUpperCase()}${string.slice(1)}`; +} + +export const transformNodes = (nodes: any[]) => { + return nodes?.map(item => { + const { type, data, ...rest } = item; + return { + type: 'custom', + data: { + ...data, + _nodeType: type, + }, + ...rest + } + }) +} \ No newline at end of file diff --git a/packages/x-flow/src/withProvider.tsx b/packages/x-flow/src/withProvider.tsx index 87dc129ba..57a076aa5 100644 --- a/packages/x-flow/src/withProvider.tsx +++ b/packages/x-flow/src/withProvider.tsx @@ -1,50 +1,29 @@ -import React, { useEffect, useRef } from 'react'; -import { ReactFlowProvider } from '@xyflow/react'; +import React from 'react'; import { ConfigProvider } from 'antd'; -import zhCN from 'antd/lib/locale/zh_CN'; -import enUS from 'antd/lib/locale/en_US'; -import dayjs from 'dayjs'; -import 'dayjs/locale/zh-cn'; - -import { createStore } from './models/store'; +import { ReactFlowProvider } from '@xyflow/react'; import { FlowContext, ConfigContext } from './models/context'; export default function withProvider(Element: React.ComponentType, defaultNodeWidgets?: any) : React.ComponentType { return (props: any) => { const { configProvider, - locale = 'zh-CN', nodeWidgets, methods, ...restProps } = props; - const storeRef = useRef(createStore()); - const store: any = storeRef.current; - - useEffect(() => { - dayjs.locale(locale === 'en-US' ? 'en': 'zh-cn'); - }, [locale]); - - const antdLocale = locale === 'zh-CN' ? zhCN : enUS; const configContext = { - locale, methods, - nodeWidgets: { ...defaultNodeWidgets, ...nodeWidgets }, + nodeWidgets: { + ...defaultNodeWidgets, + ...nodeWidgets + } }; - const languagePackage = { - ...antdLocale, - ...configProvider?.locale - }; - return ( - + - + From 5ba95a6a831ba17c673cd90a48f671fe0a2a63cf Mon Sep 17 00:00:00 2001 From: "zhanbo.lh" Date: Wed, 20 Nov 2024 16:22:14 +0800 Subject: [PATCH 12/38] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=20schema=20?= =?UTF-8?q?=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/xflow/index.md | 39 +++- .../src/components/CustomEdge/index.tsx | 2 +- .../src/components/CustomNode/index.tsx | 4 +- .../components/NodeSelectPopover/index.tsx | 179 ------------------ .../index.less | 20 +- .../x-flow/src/components/NodesMenu/index.tsx | 138 ++++++++++++++ .../src/components/NodesPopover/index.tsx | 58 ++++++ packages/x-flow/src/main.tsx | 19 +- packages/x-flow/src/models/store.ts | 2 - .../x-flow/src/operator/Control/index.tsx | 2 +- packages/x-flow/src/types.ts | 43 ++++- packages/x-flow/src/withProvider.tsx | 16 +- 12 files changed, 304 insertions(+), 218 deletions(-) delete mode 100644 packages/x-flow/src/components/NodeSelectPopover/index.tsx rename packages/x-flow/src/components/{NodeSelectPopover => NodesMenu}/index.less (72%) create mode 100644 packages/x-flow/src/components/NodesMenu/index.tsx create mode 100644 packages/x-flow/src/components/NodesPopover/index.tsx diff --git a/docs/xflow/index.md b/docs/xflow/index.md index 44f75f27f..1add844bd 100644 --- a/docs/xflow/index.md +++ b/docs/xflow/index.md @@ -46,11 +46,11 @@ import schema from './schema/basic'; import data from './data/basic'; - -const nodeMenus = [ +const settings = [ { title: 'Input', type: 'Start', + hidden: true, icon: { type: 'icon-start', bgColor: '#17B26A', @@ -59,6 +59,7 @@ const nodeMenus = [ { title: 'Output', type: 'End', + hidden: true, icon: { type: 'icon-end', bgColor: '#F79009', @@ -135,6 +136,30 @@ const nodeMenus = [ type: 'icon-gongju', bgColor: '#2E90FA' } + }, + { + title: '工具', + type: '_group', + items: [ + { + title: '代码执行', + type: 'Code', + description: '执行一段 Groovy 或 Python 或 NodeJS 代码实现自定义逻辑', + icon: { + type: 'icon-code', + bgColor: '#2E90FA' + } + }, + { + title: '工具', + type: 'tool', + description: '允许使用工具能力', + icon: { + type: 'icon-gongju', + bgColor: '#2E90FA' + } + }, + ] } ]; @@ -166,10 +191,12 @@ export default () => { return (
-
); diff --git a/packages/x-flow/src/components/CustomEdge/index.tsx b/packages/x-flow/src/components/CustomEdge/index.tsx index f09dd251c..39ea53c67 100644 --- a/packages/x-flow/src/components/CustomEdge/index.tsx +++ b/packages/x-flow/src/components/CustomEdge/index.tsx @@ -5,7 +5,7 @@ import { useShallow } from 'zustand/react/shallow'; import produce from 'immer'; import { uuid } from '../../utils'; import useStore from '../../models/store'; -import NodeSelectPopover from '../NodeSelectPopover'; +import NodeSelectPopover from '../NodesPopover'; import './index.less'; export default memo((edge: any) => { diff --git a/packages/x-flow/src/components/CustomNode/index.tsx b/packages/x-flow/src/components/CustomNode/index.tsx index 6a3e4cb91..2dcfb2fd7 100644 --- a/packages/x-flow/src/components/CustomNode/index.tsx +++ b/packages/x-flow/src/components/CustomNode/index.tsx @@ -8,13 +8,13 @@ import { Handle, Position, useReactFlow } from '@xyflow/react'; import useStore from '../../models/store'; import { capitalize, uuid } from '../../utils'; import { ConfigContext } from '../../models/context'; -import NodeSelectPopover from '../NodeSelectPopover'; +import NodeSelectPopover from '../NodesPopover'; import './index.less'; export default memo((props: any) => { const { id, type, data, layout, isConnectable, selected, onClick } = props; const configCtx: any = useContext(ConfigContext); - const NodeWidget = configCtx?.nodeWidgets[`${capitalize(type)}Node`]; + const NodeWidget = configCtx?.widgets[`${capitalize(type)}Node`]; const [isHovered, setIsHovered] = useState(false); const reactflow = useReactFlow(); diff --git a/packages/x-flow/src/components/NodeSelectPopover/index.tsx b/packages/x-flow/src/components/NodeSelectPopover/index.tsx deleted file mode 100644 index 471a30009..000000000 --- a/packages/x-flow/src/components/NodeSelectPopover/index.tsx +++ /dev/null @@ -1,179 +0,0 @@ - -import React, { useCallback, useState, useRef } from 'react'; -import { Popover, Input, Tabs } from 'antd'; -import { SearchOutlined } from '@ant-design/icons'; -import { useShallow } from 'zustand/react/shallow'; -import { useClickAway } from 'ahooks'; -import { useSet } from '../../utils/hooks'; -import IconView from '../IconView'; -import useStore from '../../models/store'; -import './index.less'; - -const items: any['items'] = [ - { - key: 'node', - label: '节点', - }, - { - key: 'tools', - label: '工具', - } -]; - -const filterNodeList = (query: string, _nodeList: any[]) => { - if (!query) { - return _nodeList; - } - const searchTerm = query.toLowerCase(); - - function searchNodes(nodes: any, results = []) { - if (nodes.length === 0) { - return results; - } - - const [currentNode, ...restNodes] = nodes; - let newResults: any = [...results]; - - if (currentNode.title.toLowerCase().includes(searchTerm)) { - newResults.push(currentNode); - } else if (currentNode.type === 'group' && currentNode.items) { - const matchingItems = searchNodes(currentNode.items); - if (matchingItems.length > 0) { - newResults.push({ ...currentNode, items: matchingItems }); - } - } - - return searchNodes(restNodes, newResults); - } - - return searchNodes(_nodeList); -}; - -const NodeInfo = ({ icon, title, description }: any) => { - return ( -
-
- -
-
- {title} -
-
- {description} -
-
- ) -}; - -const SelectNodeView = ({ onCreate, nodeMenus, containerRef }: any) => { - - const [state, setState] = useSet({ - nodeType: 'node', - nodeList: [...nodeMenus] - }); - const { nodeType, nodeList } = state; - - const handleSearchCange = (ev: any) => { - if (nodeType === 'node') { - setState({ nodeList: filterNodeList(ev.target.value, nodeMenus)}) - } else if (nodeType === 'tools') { - // todo 可能要调用接口查询了 - } - }; - - return ( -
-
- } - style={{ width: '100%' }} - /> -
-
- setState({ nodeType: type })} - style={{ padding: '0 5px' }} - /> - {nodeType === 'node' ? ( -
- {nodeList.map((item: any) => item.type === 'group' ? ( -
-
{item.title}
- {item.items.map(({ icon, title }: any, index: number) => ( -
onCreate(ev, item.type)}> - - {title} -
- ))} -
- ) : ( - } placement='right' arrow={false} key={item.type}> -
onCreate(ev, item.type)}> - - - - {item.title} -
-
- ))} -
- ) : ( -
工具数据
- )} -
-
- ) -}; - -export default (props: any) => { - const { addNode, children, placement='top' } = props; - - const ref = useRef(null); - const closeRef: any = useRef(null); - const [open, setOpen] = useState(false); - const { - nodeMenus, - } = useStore( - useShallow((state) => ({ - nodeMenus: state.nodeMenus, - })) - ); - - useClickAway(() => { - if (closeRef.current) { - setOpen(false); - closeRef.current = false; - } - }, ref); - - const handAddNode = useCallback((ev: any, type: any) => { - ev.stopPropagation(); // 阻止事件冒泡 - addNode({ _nodeType: type }); - setOpen(false); - }, []); - - return ( - } - zIndex={2000} - trigger='click' - arrow={false} - open={open} - overlayInnerStyle={{ padding: '12px 6px' }} - placement={placement} - onOpenChange={() => { - setTimeout(() => { - closeRef.current = true; - setOpen(true); - }, 50) - }} - > - {children} - - ); -} \ No newline at end of file diff --git a/packages/x-flow/src/components/NodeSelectPopover/index.less b/packages/x-flow/src/components/NodesMenu/index.less similarity index 72% rename from packages/x-flow/src/components/NodeSelectPopover/index.less rename to packages/x-flow/src/components/NodesMenu/index.less index 7fecd2365..23131e0c0 100644 --- a/packages/x-flow/src/components/NodeSelectPopover/index.less +++ b/packages/x-flow/src/components/NodesMenu/index.less @@ -1,6 +1,16 @@ -.fai-reactflow-addblock { - min-height: 400px; - .node-item { +.xflow-node-menu{ + min-height: 340px; + min-width: 150px; + + .menu-group-title { + padding: 0 10px; + color: #667085; + font-size: 12px; + font-weight: 500; + line-height: 24px; + } + + .menu-item { height: 32px; color: #101828; padding: 0 10px; @@ -9,7 +19,7 @@ cursor: pointer; } - .node-item:hover { + .menu-item:hover { background-color: #f9fafb; border-radius: 8px; } @@ -24,7 +34,7 @@ } } -.node-info-tooltip { +.xflow-node-menu-tooltip { width: 200px; .title { diff --git a/packages/x-flow/src/components/NodesMenu/index.tsx b/packages/x-flow/src/components/NodesMenu/index.tsx new file mode 100644 index 000000000..afa40bbb5 --- /dev/null +++ b/packages/x-flow/src/components/NodesMenu/index.tsx @@ -0,0 +1,138 @@ + +import React, { forwardRef, Ref } from 'react'; +import { Popover, Input } from 'antd'; +import { SearchOutlined } from '@ant-design/icons'; +import { useSet } from '../../utils/hooks'; +import IconView from '../IconView'; +import { TNodeMenu } from '../../types'; +import './index.less'; + +// 检索节点 +const searchNodeList = (query: string, list: any[]) => { + if (!query) { + return list; + } + const searchTerm = query.toLowerCase(); + + function searchList(nodes: any, preResult = []) { + if (nodes.length === 0) { + return preResult; + } + + const [currentNode, ...restNodes] = nodes; + let result: any = [...preResult]; + + if (currentNode.title.toLowerCase().includes(searchTerm)) { + result.push(currentNode); + } else if (currentNode.type === '_group' && currentNode.items) { + const matchingItems = searchList(currentNode.items); + if (matchingItems.length > 0) { + result.push({ ...currentNode, items: matchingItems }); + } + } + return searchList(restNodes, result); + } + return searchList(list); +}; + +// 悬浮菜单项详细描述 +const MenuTooltip = ({ icon, title, description }: any) => { + return ( +
+
+ +
+
+ {title} +
+
+ {description} +
+
+ ) +}; + +// 节点菜单项 +const MenuItem = (props: any) => { + const { title, type, icon, onClick } = props; + return ( + } + placement='right' + arrow={false} + > +
+ + + + {title} +
+
+ ); +}; + +// 过滤 hidden 节点 +const filterHiddenMenu = (list: any) => { + return (list || []).filter((item: any) => !item.hidden) +} + +/** + * + * 节点菜单List + * + */ +const NodesMenu = (props: TNodeMenu, ref: Ref) => { + const { items, showSearch, onClick } = props; + + const [state, setState] = useSet({ + menuList: [...items] + }); + const { menuList } = state; + + const handleItemClick = (type: string) => (ev: React.MouseEvent) => { + ev.stopPropagation(); + onClick({ type }); + } + + const handleSearch = (ev: any) => { + setState({ menuList: searchNodeList(ev.target.value, items)}) + }; + + return ( +
+ {!!showSearch && ( +
+ } + style={{ width: '100%' }} + /> +
+ )} +
+ {filterHiddenMenu(menuList).map((item: any, index: number) => item.type === '_group' ? ( +
+
{item.title}
+ {filterHiddenMenu(item.items).map((data: any, index: number) => ( + + ))} +
+ ) : ( +
+ +
+ ))} +
+
+ ); +}; + +export default forwardRef(NodesMenu); \ No newline at end of file diff --git a/packages/x-flow/src/components/NodesPopover/index.tsx b/packages/x-flow/src/components/NodesPopover/index.tsx new file mode 100644 index 000000000..2cf684ae8 --- /dev/null +++ b/packages/x-flow/src/components/NodesPopover/index.tsx @@ -0,0 +1,58 @@ + +import React, { useCallback, useState, useRef, useContext } from 'react'; +import { Popover } from 'antd'; +import { useShallow } from 'zustand/react/shallow'; +import { useClickAway } from 'ahooks'; +import useStore from '../../models/store'; +import { ConfigContext } from '../../models/context'; +import NodesMenu from '../NodesMenu'; + +export default (props: any) => { + const { addNode, children } = props; + + const ref = useRef(null); + const closeRef: any = useRef(null); + const [open, setOpen] = useState(false); + + const { settings, nodeSelector } = useContext(ConfigContext); + const { showSearch, popoverProps = { placement: 'top' } } = nodeSelector || {}; + + useClickAway(() => { + if (closeRef.current) { + setOpen(false); + closeRef.current = false; + } + }, ref); + + const handCreateNode = useCallback(({ type }) => { + addNode({ _nodeType: type }); + setOpen(false); + }, []); + + return ( + { + setTimeout(() => { + closeRef.current = true; + setOpen(true); + }, 50) + }} + content={( + + )} + > + {children} + + ); +} \ No newline at end of file diff --git a/packages/x-flow/src/main.tsx b/packages/x-flow/src/main.tsx index 511f0f971..e6687ab62 100644 --- a/packages/x-flow/src/main.tsx +++ b/packages/x-flow/src/main.tsx @@ -17,7 +17,7 @@ import { useEventEmitterContextContext } from './models/event-emitter'; import CandidateNode from './components/CandidateNode'; import CustomEdge from './components/CustomEdge'; import PanelContainer from './components/PanelContainer'; -import './index.less'; + import CustomNodeComponent from './components/CustomNode'; import Operator from './operator'; import useStore, { useUndoRedo } from './models/store'; @@ -25,6 +25,8 @@ import XFlowProps from './types'; import { capitalize, uuid, transformNodes } from './utils'; import autoLayoutNodes from './utils/autoLayoutNodes'; +import './index.less'; + const CustomNode = memo(CustomNodeComponent); const edgeTypes = { buttonedge: memo(CustomEdge) }; @@ -34,7 +36,7 @@ const edgeTypes = { buttonedge: memo(CustomEdge) }; * */ const FlowEditor: FC = memo((props) => { - const { nodeMenus, nodes: originalNodes, edges: originalEdges } = props; + const { initialValues, settings } = props; const workflowContainerRef = useRef(null); const store = useStoreApi(); const { updateEdge, addNodes, addEdges, zoomTo } = useReactFlow(); @@ -49,7 +51,6 @@ const FlowEditor: FC = memo((props) => { setNodes, setEdges, setLayout, - setNodeMenus, setCandidateNode, setMousePosition, } = useStore( @@ -60,7 +61,6 @@ const FlowEditor: FC = memo((props) => { setLayout: state.setLayout, setNodes: state.setNodes, setEdges: state.setEdges, - setNodeMenus: state.setNodeMenus, setMousePosition: state.setMousePosition, setCandidateNode: state.setCandidateNode, onNodesChange: state.onNodesChange, @@ -81,10 +81,9 @@ const FlowEditor: FC = memo((props) => { useEffect(() => { setLayout(props.layout); - setNodeMenus(nodeMenus); - setNodes(transformNodes(originalNodes)); - setEdges(originalEdges); - }, [JSON.stringify(originalNodes)]); + setNodes(transformNodes(initialValues?.nodes)); + setEdges(initialValues?.edges); + }, []); useEventListener('keydown', (e) => { if ((e.key === 'd' || e.key === 'D') && (e.ctrlKey || e.metaKey)) @@ -219,12 +218,12 @@ const FlowEditor: FC = memo((props) => { }, [layout]); // const edgeTypes = { buttonedge: (edgeProps: any) => }; - const { icon, description } = nodeMenus.find( + const { icon, description } = settings.find( (item) => item.type?.toLowerCase() === activeNode?.node?.toLowerCase(), ) || {}; // const NodeEditor = useMemo(() => { - // return configCtx.nodeWidgets[capitalize(`${activeNode?.type}Panel`)] ||
1
; + // return configCtx.widgets[capitalize(`${activeNode?.type}Panel`)] ||
1
; // }, [activeNode?.id]); console.log(nodes, '23123123nodes', edges) diff --git a/packages/x-flow/src/models/store.ts b/packages/x-flow/src/models/store.ts index afbadd048..7a32f14f8 100644 --- a/packages/x-flow/src/models/store.ts +++ b/packages/x-flow/src/models/store.ts @@ -19,7 +19,6 @@ export type AppState = { layout: 'LR' | 'TB', nodes: AppNode[]; edges: Edge[]; - nodeMenus: any[]; candidateNode: any; mousePosition: any; onNodesChange: OnNodesChange; @@ -28,7 +27,6 @@ export type AppState = { setNodes: (nodes: AppNode[]) => void; setEdges: (edges: Edge[]) => void; setLayout: (layout: 'LR' | 'TB') => void; - setNodeMenus: (nodeMenus: any[]) => void; setCandidateNode: (candidateNode: any) => void; setMousePosition: (mousePosition: any) => void; }; diff --git a/packages/x-flow/src/operator/Control/index.tsx b/packages/x-flow/src/operator/Control/index.tsx index 878716621..516aac763 100644 --- a/packages/x-flow/src/operator/Control/index.tsx +++ b/packages/x-flow/src/operator/Control/index.tsx @@ -9,7 +9,7 @@ import { import { Tooltip, Button } from 'antd'; import IconView from '../../components/IconView'; import { useEventEmitterContextContext } from '../../models/event-emitter'; -import NodeSelectPopover from '../../components/NodeSelectPopover'; +import NodeSelectPopover from '../../components/NodesPopover'; import './index.less'; const Control = (props: any) => { diff --git a/packages/x-flow/src/types.ts b/packages/x-flow/src/types.ts index db6a4e3d9..53a8ce119 100644 --- a/packages/x-flow/src/types.ts +++ b/packages/x-flow/src/types.ts @@ -1,13 +1,44 @@ import React from 'react'; -export interface ConfigCtxProps { - nodeWidges: React.ComponentType +interface TNodeItem { + title: string; // 节点 title + type: string; // 节点类型 _group 比较te s + description?: string; // 节点描述 + hidden?: boolean; // 是否可见 + icon: { + type: string; + bgColor: string; + } } +interface TNodeGroup { + title: string; // 节点 title + type: '_group', + items: TNodeItem[] +} + +export interface TNodeSelector { + showSearch: boolean; // 配置是否可搜索 + items: (TNodeGroup | TNodeItem)[] +} + +export interface TNodeMenu { + ref: React.RefObject; // 可选的 ref 属性 + showSearch: boolean; // 配置是否可搜索 + items: (TNodeGroup | TNodeItem)[] + onClick: ({}: { type: string }) => void +} + + export interface XFlowProps { - nodes: any[]; - edges: any[]; - nodeMenus: any[]; - layout: 'LR' | 'TB' + initialValues: { + nodes: any[], + edges: any + }; + layout: 'LR' | 'TB'; + nodeOptions: TNodeSelector; + widges: any; // 自定义组件 + settings: any; // 节点配置 + nodeSelector: TNodeSelector; } export default XFlowProps; diff --git a/packages/x-flow/src/withProvider.tsx b/packages/x-flow/src/withProvider.tsx index 57a076aa5..d73a771fd 100644 --- a/packages/x-flow/src/withProvider.tsx +++ b/packages/x-flow/src/withProvider.tsx @@ -3,20 +3,24 @@ import { ConfigProvider } from 'antd'; import { ReactFlowProvider } from '@xyflow/react'; import { FlowContext, ConfigContext } from './models/context'; -export default function withProvider(Element: React.ComponentType, defaultNodeWidgets?: any) : React.ComponentType { +export default function withProvider(Element: React.ComponentType, defaultWidgets?: any) : React.ComponentType { return (props: any) => { const { configProvider, - nodeWidgets, + widgets, methods, + nodeSelector, + settings, ...restProps } = props; const configContext = { methods, - nodeWidgets: { - ...defaultNodeWidgets, - ...nodeWidgets + nodeSelector, + settings, + widgets: { + ...defaultWidgets, + ...widgets } }; @@ -25,7 +29,7 @@ export default function withProvider(Element: React.ComponentType, default - + From 69a25d7a1ec049ac923c06705d9a91c4e896bebe Mon Sep 17 00:00:00 2001 From: "zhanbo.lh" Date: Wed, 20 Nov 2024 17:02:42 +0800 Subject: [PATCH 13/38] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=20schema=20?= =?UTF-8?q?=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/xflow/index.md | 2 + .../src/components/CustomNode/index.tsx | 14 +- .../x-flow/src/components/CustomNode/utils.ts | 264 ------------------ packages/x-flow/src/nodes/utils.ts | 264 ------------------ packages/x-flow/src/types.ts | 16 +- packages/x-flow/src/withProvider.tsx | 33 ++- 6 files changed, 46 insertions(+), 547 deletions(-) delete mode 100644 packages/x-flow/src/components/CustomNode/utils.ts delete mode 100644 packages/x-flow/src/nodes/utils.ts diff --git a/docs/xflow/index.md b/docs/xflow/index.md index 1add844bd..9ff1de533 100644 --- a/docs/xflow/index.md +++ b/docs/xflow/index.md @@ -51,6 +51,7 @@ const settings = [ title: 'Input', type: 'Start', hidden: true, + targetHandleHidden: true, icon: { type: 'icon-start', bgColor: '#17B26A', @@ -60,6 +61,7 @@ const settings = [ title: 'Output', type: 'End', hidden: true, + sourceHandleHidden: true, icon: { type: 'icon-end', bgColor: '#F79009', diff --git a/packages/x-flow/src/components/CustomNode/index.tsx b/packages/x-flow/src/components/CustomNode/index.tsx index 2dcfb2fd7..1060220b4 100644 --- a/packages/x-flow/src/components/CustomNode/index.tsx +++ b/packages/x-flow/src/components/CustomNode/index.tsx @@ -5,16 +5,16 @@ import classNames from 'classnames'; import produce from 'immer'; import { useShallow } from 'zustand/react/shallow'; import { Handle, Position, useReactFlow } from '@xyflow/react'; -import useStore from '../../models/store'; import { capitalize, uuid } from '../../utils'; +import useStore from '../../models/store'; import { ConfigContext } from '../../models/context'; import NodeSelectPopover from '../NodesPopover'; import './index.less'; export default memo((props: any) => { const { id, type, data, layout, isConnectable, selected, onClick } = props; - const configCtx: any = useContext(ConfigContext); - const NodeWidget = configCtx?.widgets[`${capitalize(type)}Node`]; + const { widgets, settingMap } = useContext(ConfigContext); + const NodeWidget = widgets[`${capitalize(type)}Node`]; const [isHovered, setIsHovered] = useState(false); const reactflow = useReactFlow(); @@ -60,7 +60,6 @@ export default memo((props: any) => { setEdges(newEdges); }; - let targetPosition = Position.Left; let sourcePosition = Position.Right; if (layout === 'TB') { @@ -68,6 +67,9 @@ export default memo((props: any) => { sourcePosition = Position.Bottom; } + console.log(settingMap, 'settingMap=====') + + return (
{ onMouseEnter={() => setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} > - { ( + {!settingMap?.[type]?.targetHandleHidden && ( { data={data} onClick={() => onClick(data)} /> - {( + {!settingMap?.[type]?.sourceHandleHidden && ( { - const { debugging, result } = item; - let status = 'default'; - - if (debugging) { - status = 'running'; - } else if (result?.error) { - status = 'failed'; - } else if (result) { - status = 'success'; - } - return status; -}; -interface ICell extends Node { - code: string; -} - -export const getInitGraphData = (inputItem: ICard, outputItem: ICard) => [ - { - id: inputItem._id, - code: inputItem.code, - shape: 'dag-node', - x: 290, - y: 110, - data: { - label: inputItem.code, - status: 'default', - borderColor: '#5e606a', - icon: 'icon-input', - }, - ports: [ - { - id: `${inputItem._id}-bottom-port`, - group: 'bottom', - }, - ], - }, - { - id: outputItem._id, - // code后端需要 - code: outputItem.code, - shape: 'dag-node', - x: 290, - y: 110 + 120, - data: { - label: outputItem.code, - status: 'default', - borderColor: '#5e606a', - icon: 'icon-output', - }, - ports: [ - { - id: `${outputItem._id}-top-port`, - group: 'top', - }, - ], - }, -]; - -export class GraphNode { - // 图数据的增删改查 - cells: ICell[]; - instance: any; - - constructor(cells: ICell[], instance?: any) { - this.cells = cells; - this.instance = instance; - } - addCell(node: ICard, flowList: ICard[]) { - const graphCells = this.cells; - const outputIndex = graphCells.findIndex( - (el: any) => el.id.toLowerCase() === 'output', - ); - const output = graphCells.splice(outputIndex, 1)[0]; - graphCells.push({ - id: node._id || node.code, - code: node.code, - shape: 'dag-node', - x: flowList.length % 2 === 1 ? 300 : 280, - y: 110 + 120 * flowList.length, - data: { - label: node.code, - status: getItemStatus(node), - borderColor: colorMap[node.type]?.borderColor, - icon: colorMap[node.type]?.icon, - }, - ports: { - items: [ - { - id: `${node._id}-top-port`, - group: 'top', - }, - { - id: `${node._id}-bottom-port`, - group: 'bottom', - }, - ], - }, - }); - if (!output) return; - output.y = 110 + 120 * (flowList.length + 1); - graphCells.push(output); - } - addCells(nodes: ICard[], flowList: ICard[]) { - nodes.forEach((node, index) => { - this.addCell( - node, - flowList.slice(0, flowList.length - nodes.length + index + 1), - ); - }); - } - removeCell(node: ICard) { - this.instance.removeCell(node._id); - } - updateCellLabel(node: ICard, val: string) { - const graphCells = this.cells; - const index = graphCells.findIndex((el: any) => el.id === node._id); - graphCells[index].data.label = val; - graphCells[index].code = val; - } - - updateCellStatus(node: ICard, status: string) { - // const graphCells = this.cells; - // const index = graphCells.findIndex( - // (el: any) => el.id === node._id || el.code === node._id, - // ); - // if (index !== -1) { - // graphCells[index].data.status = status; - // } - const curNode = this.instance.getCellById(node._id); - if (!curNode) { - console.log('err,cannot find node'); - return; - } - const data = curNode.getData(); - curNode.setData({ - ...data, - status, - }); - } - - updateAllCellStatus(status: string) { - const graphCells = this.cells; - - graphCells.forEach((el: any) => { - if (el.shape === 'dag-node' && el.id !== 'Input' && el.id !== 'Output') { - el.data.status = status; - } - }); - } - getCell() { - return this.cells; - } -} - -export const generateGraphByNodes = (devVersion: any) => { - // 根据 nodes 生成图表节点 - const graphIns = new GraphNode([]); - const nodes = devVersion.nodes || []; - - nodes.forEach((item: any, index: number) => { - graphIns.addCell(item, new Array(index)); - }); - let graphCells = graphIns.getCell(); - const inputNode = graphCells.find( - (item: any) => item.id.toLowerCase() === 'input', - ) as any; - const outputNode = graphCells.find( - (item: any) => item.id.toLowerCase() === 'output', - ) as any; - if (outputNode) { - outputNode.ports.items = outputNode.ports.items.filter((el: any) => { - return el.group !== 'bottom'; - }); - } - if (inputNode) { - inputNode.y = graphCells?.[0].y - 120; - inputNode.ports.items = inputNode.ports.items.filter((el: any) => { - return el.group !== 'top'; - }); - - graphCells = graphCells.filter( - (item: any) => item.id.toLowerCase() !== 'input', - ); - graphCells.unshift(inputNode); - } - return graphCells; -}; - -type IMap = AnyObject; -const typeMap: IMap = { - string: 'STRING', - number: 'INTEGER', - object: 'OBJECT', - array: 'ARRAY', - boolean: 'BOOLEAN', -}; -const getParamType = (param: any) => { - return Object.prototype.toString.call(param).slice(8, -1).toLowerCase(); -}; -export const formatObj2Arr = (obj: any) => { - return Object.entries(obj).map((item) => { - return { - name: item[0] === 'undefined' ? '' : item[0], - value: item[1] ?? '', - dataType: typeMap[getParamType(item[1])] ?? 'STRING', - }; - }); -}; - -export const formatArr2Obj = (arr: any) => { - return arr.reduce((pre: AnyObject, cur: { name: string; value: string }) => { - pre[cur.name] = cur.value; - return pre; - }, {}); -}; - -export function extractFencedCodeBlock(text: string, language: string) { - const regex = new RegExp(`\`\`\`${language}([^]*?)\`\`\``, 'gi'); - let match; - let results = []; - - while ((match = regex.exec(text)) !== null) { - results.push(match[1].trim()); - } - - return results.length > 0 ? results.join('\n') : text; -} - -export function typewriter( - text: string, - callback: (text: string) => void, - typingSpeed = 30, -) { - let currentIndex = -1; - let currentText = ''; - - const type = () => { - if (currentIndex < text.length - 1) { - currentIndex++; - currentText += text[currentIndex]; - callback(currentText); - setTimeout(type, typingSpeed); - } - }; - - type(); -} - -export function capitalizeFirstLetter(str: string) { - return str.charAt(0).toUpperCase() + str.slice(1); -} - -export function isNodeConnected(edges: any, nodeId: string) { - return edges.some((edge: any) => { - const source = edge.source.cell; - const target = edge.target.cell; - return source === nodeId || target === nodeId; - }); -} diff --git a/packages/x-flow/src/nodes/utils.ts b/packages/x-flow/src/nodes/utils.ts deleted file mode 100644 index da63bab50..000000000 --- a/packages/x-flow/src/nodes/utils.ts +++ /dev/null @@ -1,264 +0,0 @@ -import { AnyObject } from 'antd/es/_util/type'; -import { Node } from '@antv/x6'; -import { ICard, colorMap } from './constant'; - -const getItemStatus = (item: ICard) => { - const { debugging, result } = item; - let status = 'default'; - - if (debugging) { - status = 'running'; - } else if (result?.error) { - status = 'failed'; - } else if (result) { - status = 'success'; - } - return status; -}; -interface ICell extends Node { - code: string; -} - -export const getInitGraphData = (inputItem: ICard, outputItem: ICard) => [ - { - id: inputItem._id, - code: inputItem.code, - shape: 'dag-node', - x: 290, - y: 110, - data: { - label: inputItem.code, - status: 'default', - borderColor: '#5e606a', - icon: 'icon-input', - }, - ports: [ - { - id: `${inputItem._id}-bottom-port`, - group: 'bottom', - }, - ], - }, - { - id: outputItem._id, - // code后端需要 - code: outputItem.code, - shape: 'dag-node', - x: 290, - y: 110 + 120, - data: { - label: outputItem.code, - status: 'default', - borderColor: '#5e606a', - icon: 'icon-output', - }, - ports: [ - { - id: `${outputItem._id}-top-port`, - group: 'top', - }, - ], - }, -]; - -export class GraphNode { - // 图数据的增删改查 - cells: ICell[]; - instance: any; - - constructor(cells: ICell[], instance?: any) { - this.cells = cells; - this.instance = instance; - } - addCell(node: ICard, flowList: ICard[]) { - const graphCells = this.cells; - const outputIndex = graphCells.findIndex( - (el: any) => el.id.toLowerCase() === 'output', - ); - const output = graphCells.splice(outputIndex, 1)[0]; - graphCells.push({ - id: node._id || node.code, - code: node.code, - shape: 'dag-node', - x: flowList.length % 2 === 1 ? 300 : 280, - y: 110 + 120 * flowList.length, - data: { - label: node.code, - status: getItemStatus(node), - borderColor: colorMap[node.type]?.borderColor, - icon: colorMap[node.type]?.icon, - }, - ports: { - items: [ - { - id: `${node._id}-top-port`, - group: 'top', - }, - { - id: `${node._id}-bottom-port`, - group: 'bottom', - }, - ], - }, - }); - if (!output) return; - output.y = 110 + 120 * (flowList.length + 1); - graphCells.push(output); - } - addCells(nodes: ICard[], flowList: ICard[]) { - nodes.forEach((node, index) => { - this.addCell( - node, - flowList.slice(0, flowList.length - nodes.length + index + 1), - ); - }); - } - removeCell(node: ICard) { - this.instance.removeCell(node._id); - } - updateCellLabel(node: ICard, val: string) { - const graphCells = this.cells; - const index = graphCells.findIndex((el: any) => el.id === node._id); - graphCells[index].data.label = val; - graphCells[index].code = val; - } - - updateCellStatus(node: ICard, status: string) { - // const graphCells = this.cells; - // const index = graphCells.findIndex( - // (el: any) => el.id === node._id || el.code === node._id, - // ); - // if (index !== -1) { - // graphCells[index].data.status = status; - // } - const curNode = this.instance.getCellById(node._id); - if (!curNode) { - console.log('err,cannot find node'); - return; - } - const data = curNode.getData(); - curNode.setData({ - ...data, - status, - }); - } - - updateAllCellStatus(status: string) { - const graphCells = this.cells; - - graphCells.forEach((el: any) => { - if (el.shape === 'dag-node' && el.id !== 'Input' && el.id !== 'Output') { - el.data.status = status; - } - }); - } - getCell() { - return this.cells; - } -} - -export const generateGraphByNodes = (devVersion: any) => { - // 根据 nodes 生成图表节点 - const graphIns = new GraphNode([]); - const nodes = devVersion.nodes || []; - - nodes.forEach((item: any, index: number) => { - graphIns.addCell(item, new Array(index)); - }); - let graphCells = graphIns.getCell(); - const inputNode = graphCells.find( - (item: any) => item.id.toLowerCase() === 'input', - ) as any; - const outputNode = graphCells.find( - (item: any) => item.id.toLowerCase() === 'output', - ) as any; - if (outputNode) { - outputNode.ports.items = outputNode.ports.items.filter((el: any) => { - return el.group !== 'bottom'; - }); - } - if (inputNode) { - inputNode.y = graphCells?.[0].y - 120; - inputNode.ports.items = inputNode.ports.items.filter((el: any) => { - return el.group !== 'top'; - }); - - graphCells = graphCells.filter( - (item: any) => item.id.toLowerCase() !== 'input', - ); - graphCells.unshift(inputNode); - } - return graphCells; -}; - -type IMap = AnyObject; -const typeMap: IMap = { - string: 'STRING', - number: 'INTEGER', - object: 'OBJECT', - array: 'ARRAY', - boolean: 'BOOLEAN', -}; -const getParamType = (param: any) => { - return Object.prototype.toString.call(param).slice(8, -1).toLowerCase(); -}; -export const formatObj2Arr = (obj: any) => { - return Object.entries(obj).map((item) => { - return { - name: item[0] === 'undefined' ? '' : item[0], - value: item[1] ?? '', - dataType: typeMap[getParamType(item[1])] ?? 'STRING', - }; - }); -}; - -export const formatArr2Obj = (arr: any) => { - return arr.reduce((pre: AnyObject, cur: { name: string; value: string }) => { - pre[cur.name] = cur.value; - return pre; - }, {}); -}; - -export function extractFencedCodeBlock(text: string, language: string) { - const regex = new RegExp(`\`\`\`${language}([^]*?)\`\`\``, 'gi'); - let match; - let results = []; - - while ((match = regex.exec(text)) !== null) { - results.push(match[1].trim()); - } - - return results.length > 0 ? results.join('\n') : text; -} - -export function typewriter( - text: string, - callback: (text: string) => void, - typingSpeed = 30, -) { - let currentIndex = -1; - let currentText = ''; - - const type = () => { - if (currentIndex < text.length - 1) { - currentIndex++; - currentText += text[currentIndex]; - callback(currentText); - setTimeout(type, typingSpeed); - } - }; - - type(); -} - -export function capitalizeFirstLetter(str: string) { - return str.charAt(0).toUpperCase() + str.slice(1); -} - -export function isNodeConnected(edges: any, nodeId: string) { - return edges.some((edge: any) => { - const source = edge.source.cell; - const target = edge.target.cell; - return source === nodeId || target === nodeId; - }); -} diff --git a/packages/x-flow/src/types.ts b/packages/x-flow/src/types.ts index 53a8ce119..e8fa9f9bc 100644 --- a/packages/x-flow/src/types.ts +++ b/packages/x-flow/src/types.ts @@ -1,5 +1,5 @@ import React from 'react'; -interface TNodeItem { +export interface TNodeItem { title: string; // 节点 title type: string; // 节点类型 _group 比较te s description?: string; // 节点描述 @@ -10,17 +10,12 @@ interface TNodeItem { } } -interface TNodeGroup { +export interface TNodeGroup { title: string; // 节点 title type: '_group', items: TNodeItem[] } -export interface TNodeSelector { - showSearch: boolean; // 配置是否可搜索 - items: (TNodeGroup | TNodeItem)[] -} - export interface TNodeMenu { ref: React.RefObject; // 可选的 ref 属性 showSearch: boolean; // 配置是否可搜索 @@ -28,6 +23,10 @@ export interface TNodeMenu { onClick: ({}: { type: string }) => void } +export interface TNodeSelector { + showSearch: boolean; // 配置是否可搜索 + items: (TNodeGroup | TNodeItem)[] +} export interface XFlowProps { initialValues: { @@ -35,9 +34,8 @@ export interface XFlowProps { edges: any }; layout: 'LR' | 'TB'; - nodeOptions: TNodeSelector; widges: any; // 自定义组件 - settings: any; // 节点配置 + settings: (TNodeGroup | TNodeItem)[]; // 节点配置 nodeSelector: TNodeSelector; } diff --git a/packages/x-flow/src/withProvider.tsx b/packages/x-flow/src/withProvider.tsx index d73a771fd..db25ccaa0 100644 --- a/packages/x-flow/src/withProvider.tsx +++ b/packages/x-flow/src/withProvider.tsx @@ -1,10 +1,20 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import { ConfigProvider } from 'antd'; import { ReactFlowProvider } from '@xyflow/react'; import { FlowContext, ConfigContext } from './models/context'; +import { TNodeGroup, TNodeItem } from './types'; + +interface ProviderProps { + configProvider?: any; + widgets?: any; + methods?: any; + nodeSelector?: any; + settings?: (TNodeGroup | TNodeItem)[]; + [key: string]: any; +} export default function withProvider(Element: React.ComponentType, defaultWidgets?: any) : React.ComponentType { - return (props: any) => { + return (props: ProviderProps) => { const { configProvider, widgets, @@ -13,11 +23,26 @@ export default function withProvider(Element: React.ComponentType, default settings, ...restProps } = props; - + + const settingMap = useMemo(() => { + const obj: Record = {}; + settings?.forEach((node: any) => { + if (node.type !== '_group') { + obj[node.type] = node; + } else { + node.items.forEach((item: any) => { + obj[item.type] = item; + }); + } + }); + return obj; + }, [settings]); + const configContext = { methods, nodeSelector, settings, + settingMap, widgets: { ...defaultWidgets, ...widgets @@ -36,4 +61,4 @@ export default function withProvider(Element: React.ComponentType, default ); } -} \ No newline at end of file +} From 054a635a8b0c1c9a3d6bced1d00638984dcbb274 Mon Sep 17 00:00:00 2001 From: "zhanbo.lh" Date: Wed, 20 Nov 2024 17:27:02 +0800 Subject: [PATCH 14/38] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E9=80=9A?= =?UTF-8?q?=E7=94=A8=E8=8A=82=E7=82=B9=E6=B8=B2=E6=9F=93=E8=83=BD=E5=8A=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/CandidateNode/index.tsx | 7 +- .../src/components/CustomNode/index.tsx | 5 +- packages/x-flow/src/nodes/index.tsx | 3 +- .../x-flow/src/nodes/node-common/index.less | 16 +++ .../x-flow/src/nodes/node-common/index.tsx | 26 ++++ .../src/nodes/node-common/setting/index.tsx | 130 ++++++++++++++++++ .../x-flow/src/nodes/node-start/index.tsx | 1 - 7 files changed, 180 insertions(+), 8 deletions(-) create mode 100644 packages/x-flow/src/nodes/node-common/index.less create mode 100644 packages/x-flow/src/nodes/node-common/index.tsx create mode 100644 packages/x-flow/src/nodes/node-common/setting/index.tsx diff --git a/packages/x-flow/src/components/CandidateNode/index.tsx b/packages/x-flow/src/components/CandidateNode/index.tsx index d312727fd..9a4fe1927 100644 --- a/packages/x-flow/src/components/CandidateNode/index.tsx +++ b/packages/x-flow/src/components/CandidateNode/index.tsx @@ -8,8 +8,8 @@ import CustomNode from '../CustomNode'; import useStore from '../../models/store'; const CandidateNode = () => { - const reactflow = useReactFlow(); const { zoom } = useViewport(); + const reactflow = useReactFlow(); const { nodes, @@ -31,6 +31,9 @@ const CandidateNode = () => { })) ); + + console.log(candidateNode, 'candidateNode+++') + useEventListener('click', (ev) => { if (!candidateNode) { return; @@ -76,7 +79,7 @@ const CandidateNode = () => { transformOrigin: '0 0', }} > - +
); } diff --git a/packages/x-flow/src/components/CustomNode/index.tsx b/packages/x-flow/src/components/CustomNode/index.tsx index 1060220b4..24bf72aa6 100644 --- a/packages/x-flow/src/components/CustomNode/index.tsx +++ b/packages/x-flow/src/components/CustomNode/index.tsx @@ -14,7 +14,7 @@ import './index.less'; export default memo((props: any) => { const { id, type, data, layout, isConnectable, selected, onClick } = props; const { widgets, settingMap } = useContext(ConfigContext); - const NodeWidget = widgets[`${capitalize(type)}Node`]; + const NodeWidget = widgets[`${capitalize(type)}Node`] || widgets['CommonNode']; const [isHovered, setIsHovered] = useState(false); const reactflow = useReactFlow(); @@ -67,9 +67,6 @@ export default memo((props: any) => { sourcePosition = Position.Bottom; } - console.log(settingMap, 'settingMap=====') - - return (
{ + const { type, onClick } = props; + const { settingMap } = useContext(ConfigContext); + const nodeSetting = settingMap[type] || {}; + + return ( + + ); +}); + + diff --git a/packages/x-flow/src/nodes/node-common/setting/index.tsx b/packages/x-flow/src/nodes/node-common/setting/index.tsx new file mode 100644 index 000000000..ab584a305 --- /dev/null +++ b/packages/x-flow/src/nodes/node-common/setting/index.tsx @@ -0,0 +1,130 @@ +import { useEffect, useRef } from 'react'; +import { AnyObject } from 'antd/lib/_util/type'; +import FormRender, { useForm } from 'form-render'; +import { ICard, TYPES } from '../../constant'; +import FAutoComplete from '../../../components/FAutoComplete'; + +export interface GlobalOutputProps { + data?: AnyObject; + onChange: (data: AnyObject) => void; + flowList: ICard[]; + inputItem: ICard; + readonly?: boolean; +} + +const getSchema = (request: any) => ({ + type: 'object', + displayType: 'row', + properties: { + list: { + type: 'array', + widget: 'tableList', + props: { + hideMove: true, + hideCopy: true, + size: 'small', + addBtnProps: { + type: 'dashed', + }, + actionColumnProps: { + width: 60, + }, + }, + items: { + type: 'object', + properties: { + name: { + title: '名称', + type: 'string', + width: 200, + placeholder: '请输入', + rules: [ + { + pattern: /^[a-zA-Z_][a-zA-Z0-9_]*$/, + message: '只能包含字母、数字和下划线且以字母或划线开头', + }, + ], + }, + dataType: { + title: '类型', + type: 'string', + enum: TYPES.map((el) => el.toUpperCase()), + enumNames: TYPES, + width: 120, + widget: 'select', + placeholder: '请选择', + }, + value: { + title: '值', + type: 'string', + widget: 'FAutoComplete', + props: { + placeholder: '${组件名.output}', + request, + }, + }, + }, + }, + }, + }, +}); + +export default (props: GlobalOutputProps) => { + const { data, onChange, inputItem, flowList, readonly } = props; + + const form = useForm(); + const flowListRef = useRef(); + const inputRef = useRef(); + + useEffect(() => { + flowListRef.current = flowList; + inputRef.current = inputItem; + }, [flowList, inputItem]); + + const watch = { + '#': (allValues: any) => { + onChange({ ...data, ...allValues }); + } + }; + + const request = (val: string) => { + return new Promise((resolve) => { + setTimeout(() => { + const inputValue = inputRef.current?.data; + const inputText = 'inputs'; + const options = (inputValue?.list || []) + .filter((el: any) => !!el.name) + .map((item: any) => '${#' + `${inputText}.${item.name}` + `}`); + const nodes = (flowListRef?.current || []) + .filter((el: any) => el.code !== 'Output') + .map((item: any) => { + return '${#' + `${item.code}.output` + `}`; + }); + + resolve( + [...options, ...nodes] + .filter((el: string) => val && el.includes(val)) + .map((el: string) => { + return { + value: el, + }; + }), + ); + }, 10); + }); + }; + const schema = getSchema(request); + + return ( + { + form.setValues({ list: data?.list }); + }} + /> + ); +} diff --git a/packages/x-flow/src/nodes/node-start/index.tsx b/packages/x-flow/src/nodes/node-start/index.tsx index 1216b9491..33e0e66eb 100644 --- a/packages/x-flow/src/nodes/node-start/index.tsx +++ b/packages/x-flow/src/nodes/node-start/index.tsx @@ -4,7 +4,6 @@ import NodeContainer from '../../components/NodeContainer'; export default memo((props: any) => { const { onClick } = props; - return ( Date: Wed, 20 Nov 2024 17:38:59 +0800 Subject: [PATCH 15/38] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E9=80=9A?= =?UTF-8?q?=E7=94=A8=E8=8A=82=E7=82=B9=E6=B8=B2=E6=9F=93=E8=83=BD=E5=8A=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/CandidateNode/index.tsx | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/packages/x-flow/src/components/CandidateNode/index.tsx b/packages/x-flow/src/components/CandidateNode/index.tsx index 9a4fe1927..80d6c26b1 100644 --- a/packages/x-flow/src/components/CandidateNode/index.tsx +++ b/packages/x-flow/src/components/CandidateNode/index.tsx @@ -31,9 +31,6 @@ const CandidateNode = () => { })) ); - - console.log(candidateNode, 'candidateNode+++') - useEventListener('click', (ev) => { if (!candidateNode) { return; @@ -56,27 +53,21 @@ const CandidateNode = () => { setCandidateNode(null); }); - useEventListener('contextmenu', (e) => { - // const { candidateNode } = workflowStore.getState() - // if (candidateNode) { - // e.preventDefault() - // workflowStore.setState({ candidateNode: undefined }) - // } - }) - if (!candidateNode) { return null } + + console.log(mousePosition, '=======000000') return (
From 005cb0ef44a1ca738849531f3e9217c9e82015cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=98=94=E6=A2=A6?= Date: Wed, 27 Nov 2024 10:59:55 +0800 Subject: [PATCH 16/38] =?UTF-8?q?feat:=E5=A2=9E=E5=8A=A0=E9=85=8D=E7=BD=AE?= =?UTF-8?q?schema=E5=92=8C=E8=87=AA=E5=AE=9A=E4=B9=89=E7=BB=84=E4=BB=B6?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E8=83=BD=E5=8A=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/NodeEditor/index.tsx | 58 ++++ .../src/components/PanelContainer/index.tsx | 57 +++- packages/x-flow/src/main.tsx | 276 +++++++++--------- .../x-flow/src/nodes/node-common/index.tsx | 6 +- .../x-flow/src/nodes/node-start/index.tsx | 21 +- packages/x-flow/src/types.ts | 23 +- 6 files changed, 269 insertions(+), 172 deletions(-) create mode 100644 packages/x-flow/src/components/NodeEditor/index.tsx diff --git a/packages/x-flow/src/components/NodeEditor/index.tsx b/packages/x-flow/src/components/NodeEditor/index.tsx new file mode 100644 index 000000000..33d83ed6f --- /dev/null +++ b/packages/x-flow/src/components/NodeEditor/index.tsx @@ -0,0 +1,58 @@ +import FormRender, { useForm } from 'form-render'; +import React, { FC, useContext, useEffect } from 'react'; +import { ConfigContext } from '../../models/context'; +import { values } from 'lodash'; +interface INodeEditorProps { + data: any; + onChange: (data: any) => void; + nodeType: string; + id: string; +} + +const NodeEditor: FC = (props: any) => { + const { data, onChange, nodeType, id } = props; + const form = useForm(); + // // 1.获取节点配置信息 + const { settingMap, widgets } = useContext(ConfigContext); + const nodeSetting = settingMap[nodeType] || {}; + + useEffect(() => { + form.resetFields(); + form.setValues(data || {}); + }, [JSON.stringify(data), id]); + + const watch = { + '#': (allValues: any) => { + onChange({ id, values: { ...allValues } }); + }, + }; + + if (nodeSetting?.settingWidget) { + const NodeWidget = widgets[nodeSetting?.settingWidget]; + return ( + { + onChange({ id, values: { ...values } }); + }} + /> + ); + } else if (nodeSetting?.schema) { + return ( + { + // form.setValues({ list: data?.list || [] }); + // }} + /> + ); + } else { + return null; + } +}; + +export default NodeEditor; diff --git a/packages/x-flow/src/components/PanelContainer/index.tsx b/packages/x-flow/src/components/PanelContainer/index.tsx index 21c0a9f1c..aa5f47734 100644 --- a/packages/x-flow/src/components/PanelContainer/index.tsx +++ b/packages/x-flow/src/components/PanelContainer/index.tsx @@ -1,8 +1,21 @@ -import React from 'react'; import { Divider, Drawer, Input, Space } from 'antd'; +import React, { FC, useContext } from 'react'; +import { useShallow } from 'zustand/react/shallow'; +import { ConfigContext } from '../../models/context'; +import useStore from '../../models/store'; import IconView from '../IconView'; import './index.less'; +interface IPanelProps { + disabled?: boolean; // 是否禁用 ---to do:确认一下取的地方 + nodeType: string; + onClose: () => void; + node?: { id: string; _isCandidate: boolean; _nodeType: string }; + description?: string; // 业务描述---to do :确认一下取的地方 + children?: any; + id: string; +} + const getDescription = (nodeType: string, description: string) => { if (nodeType === 'Input') { return '工作流的起始节点,用于设定启动工作流入参信息'; @@ -13,45 +26,59 @@ const getDescription = (nodeType: string, description: string) => { return description || ''; }; -const Panel = (props: any) => { - const { onClose, children, title, icon, nodeType, disabled, node } = props; +const Panel: FC = (props: any) => { + // disabled属性取的地方可能不对------to do + const { onClose, children, nodeType, disabled, node, description,id } = props; + // 1.获取节点配置信息 + const { settingMap } = useContext(ConfigContext); + const nodeSetting = settingMap[nodeType] || {}; + const { nodes, setNodes } = useStore( + useShallow((state: any) => ({ + nodes: state.nodes, + setNodes: state.setNodes, + })) + ); const isDisabled = ['Input', 'Output'].includes(nodeType) || disabled; - const description = getDescription(nodeType, props.description); - + // const description = getDescription(nodeType, props.description); return (
- + {isDisabled ? ( - {title} + {nodeSetting?.title} ) : ( - + { + // console.log('名称改变', val); + }} + /> )}
{!isDisabled && ( <> - + )} diff --git a/packages/x-flow/src/main.tsx b/packages/x-flow/src/main.tsx index e6687ab62..9d05ccdbf 100644 --- a/packages/x-flow/src/main.tsx +++ b/packages/x-flow/src/main.tsx @@ -1,9 +1,3 @@ -import type { FC } from 'react'; -import React, { memo, useEffect, useMemo, useRef, useState, useContext } from 'react'; -import { useEventListener, useMemoizedFn } from 'ahooks'; -import produce, { setAutoFreeze } from 'immer'; -import { debounce } from 'lodash'; -import { useShallow } from 'zustand/react/shallow'; import { Background, BackgroundVariant, @@ -13,18 +7,25 @@ import { useStoreApi, } from '@xyflow/react'; import '@xyflow/react/dist/style.css'; -import { useEventEmitterContextContext } from './models/event-emitter'; +import { useEventListener, useMemoizedFn } from 'ahooks'; +import produce, { setAutoFreeze } from 'immer'; +import { debounce } from 'lodash'; +import type { FC } from 'react'; +import React, { memo, useEffect, useMemo, useRef, useState } from 'react'; +import { useShallow } from 'zustand/react/shallow'; import CandidateNode from './components/CandidateNode'; import CustomEdge from './components/CustomEdge'; import PanelContainer from './components/PanelContainer'; +import { useEventEmitterContextContext } from './models/event-emitter'; import CustomNodeComponent from './components/CustomNode'; -import Operator from './operator'; import useStore, { useUndoRedo } from './models/store'; +import Operator from './operator'; import XFlowProps from './types'; -import { capitalize, uuid, transformNodes } from './utils'; +import { transformNodes, uuid } from './utils'; import autoLayoutNodes from './utils/autoLayoutNodes'; +import NodeEditor from './components/NodeEditor'; import './index.less'; const CustomNode = memo(CustomNodeComponent); @@ -35,7 +36,7 @@ const edgeTypes = { buttonedge: memo(CustomEdge) }; * XFlow 入口 * */ -const FlowEditor: FC = memo((props) => { +const FlowEditor: FC = memo(props => { const { initialValues, settings } = props; const workflowContainerRef = useRef(null); const store = useStoreApi(); @@ -54,7 +55,7 @@ const FlowEditor: FC = memo((props) => { setCandidateNode, setMousePosition, } = useStore( - useShallow((state) => ({ + useShallow(state => ({ nodes: state.nodes, edges: state.edges, layout: state.layout, @@ -66,9 +67,8 @@ const FlowEditor: FC = memo((props) => { onNodesChange: state.onNodesChange, onEdgesChange: state.onEdgesChange, onConnect: state.onConnect, - })), + })) ); - const [activeNode, setActiveNode] = useState(null); useEffect(() => { @@ -85,7 +85,7 @@ const FlowEditor: FC = memo((props) => { setEdges(initialValues?.edges); }, []); - useEventListener('keydown', (e) => { + useEventListener('keydown', e => { if ((e.key === 'd' || e.key === 'D') && (e.ctrlKey || e.metaKey)) e.preventDefault(); if ((e.key === 'z' || e.key === 'Z') && (e.ctrlKey || e.metaKey)) @@ -96,7 +96,7 @@ const FlowEditor: FC = memo((props) => { e.preventDefault(); }); - useEventListener('mousemove', (e) => { + useEventListener('mousemove', e => { const containerClientRect = workflowContainerRef.current?.getBoundingClientRect(); if (containerClientRect) { @@ -122,7 +122,6 @@ const FlowEditor: FC = memo((props) => { } }); - // 新增节点 const handleAddNode = (data: any) => { const newNode = { @@ -162,7 +161,7 @@ const FlowEditor: FC = memo((props) => { source: '2', target: newNode.id, }); - const targetEdge = edges.find((edge) => edge.source === '2'); + const targetEdge = edges.find(edge => edge.source === '2'); updateEdge(targetEdge?.id as string, { source: newNode.id, }); @@ -171,8 +170,8 @@ const FlowEditor: FC = memo((props) => { // edge 移入/移出效果 const getUpdateEdgeConfig = useMemoizedFn((edge: any, color: string) => { - const newEdges = produce(edges, (draft) => { - const currEdge: any = draft.find((e) => e.id === edge.id); + const newEdges = produce(edges, draft => { + const currEdge: any = draft.find(e => e.id === edge.id); currEdge.style = { ...edge.style, stroke: color, @@ -187,10 +186,10 @@ const FlowEditor: FC = memo((props) => { const handleNodeValueChange = debounce((data: any) => { for (let node of nodes) { - if (node.id === activeNode.name) { + if (node.id === data.id) { node.data = { ...node?.data, - ...data, + ...data?.values, }; break; } @@ -198,139 +197,144 @@ const FlowEditor: FC = memo((props) => { setNodes([...nodes]); }, 200); - const nodeTypes = useMemo(() => { return { custom: (props: any) => { - const { data, ...rest } = props; + const { data, id, ...rest } = props; const { _nodeType, ...restData } = data || {}; return ( - { + setActiveNode({ id, _nodeType, values: { ...restData } }); + }} /> ); - } + }, }; }, [layout]); - // const edgeTypes = { buttonedge: (edgeProps: any) => }; - const { icon, description } = settings.find( - (item) => item.type?.toLowerCase() === activeNode?.node?.toLowerCase(), - ) || {}; - - // const NodeEditor = useMemo(() => { - // return configCtx.widgets[capitalize(`${activeNode?.type}Panel`)] ||
1
; - // }, [activeNode?.id]); - - console.log(nodes, '23123123nodes', edges) + // const edgeTypes = { buttonedge: (edgeProps: any) => }; + const { icon, description } = + settings.find( + item => item.type?.toLowerCase() === activeNode?.node?.toLowerCase() + ) || {}; + const NodeEditorWrap = useMemo(() => { return ( -
- - - { - // const recordTypes = new Set(['add', 'remove']); - // changes.forEach((change) => { - // if (recordTypes.has(change.type)) { - // record(() => { - // onNodesChange([change]); - // }); - // } else { - // onNodesChange([change]); - // } - // }); - // }} - onNodesChange={(changes) => { - const recordTypes = new Set(['add', 'remove']); - changes.forEach((change) => { - console.log( - '🚀 ~ file: main.tsx:226 ~ changes.forEach ~ change:', - change, - ); + + ); + }, [activeNode?.id]); - const removeChanges = changes.filter( - (change) => change.type === 'remove', - ); + return ( +
+ + + { + // const recordTypes = new Set(['add', 'remove']); + // changes.forEach((change) => { + // if (recordTypes.has(change.type)) { + // record(() => { + // onNodesChange([change]); + // }); + // } else { + // onNodesChange([change]); + // } + // }); + // }} + onNodesChange={changes => { + const recordTypes = new Set(['add', 'remove']); + changes.forEach(change => { + console.log( + '🚀 ~ file: main.tsx:226 ~ changes.forEach ~ change:', + change + ); - if (removeChanges.length > 0) { - removeChanges.forEach((change) => { - eventEmitter?.emit({ type: 'deleteNode', payload: change }); - }); - } - if (recordTypes.has(change.type)) { - record(() => { - onNodesChange([change]); - }); - } else { + const removeChanges = changes.filter( + change => change.type === 'remove' + ); + + if (removeChanges.length > 0) { + removeChanges.forEach(change => { + eventEmitter?.emit({ type: 'deleteNode', payload: change }); + }); + } + if (recordTypes.has(change.type)) { + record(() => { onNodesChange([change]); - } - }); - }} - onEdgesChange={(changes) => { - const recordTypes = new Set(['add', 'remove']); - changes.forEach((change) => { - if (recordTypes.has(change.type)) { - record(() => { - onEdgesChange([change]); - }); - } else { + }); + } else { + onNodesChange([change]); + } + }); + }} + onEdgesChange={changes => { + const recordTypes = new Set(['add', 'remove']); + changes.forEach(change => { + if (recordTypes.has(change.type)) { + record(() => { onEdgesChange([change]); - } - }); - }} - onEdgeMouseEnter={(_, edge: any) => { - getUpdateEdgeConfig(edge, '#2970ff'); - }} - onEdgeMouseLeave={(_, edge) => { - getUpdateEdgeConfig(edge, '#c9c9c9'); - }} - > - - + }); + } else { + onEdgesChange([change]); + } + }); + }} + onEdgeMouseEnter={(_, edge: any) => { + getUpdateEdgeConfig(edge, '#2970ff'); + }} + onEdgeMouseLeave={(_, edge) => { + getUpdateEdgeConfig(edge, '#c9c9c9'); + }} + > + + - {activeNode && ( - setActiveNode(null)} - node={activeNode} - > - {/* */} - - )} -
- ); - }, -); + {activeNode && ( + setActiveNode(null)} + node={activeNode} + // disabled + > + {NodeEditorWrap} + + )} +
+ ); +}); export default FlowEditor; diff --git a/packages/x-flow/src/nodes/node-common/index.tsx b/packages/x-flow/src/nodes/node-common/index.tsx index 88be15860..3814a149c 100644 --- a/packages/x-flow/src/nodes/node-common/index.tsx +++ b/packages/x-flow/src/nodes/node-common/index.tsx @@ -7,15 +7,15 @@ export default memo((props: any) => { const { type, onClick } = props; const { settingMap } = useContext(ConfigContext); const nodeSetting = settingMap[type] || {}; - + return ( { - const { onClick } = props; - + const { onClick, type } = props; + const { settingMap } = useContext(ConfigContext); + const nodeSetting = settingMap[type] || {}; + return ( ); }); - - diff --git a/packages/x-flow/src/types.ts b/packages/x-flow/src/types.ts index e8fa9f9bc..af704297a 100644 --- a/packages/x-flow/src/types.ts +++ b/packages/x-flow/src/types.ts @@ -1,40 +1,43 @@ +import { Schema } from 'form-render'; import React from 'react'; export interface TNodeItem { title: string; // 节点 title - type: string; // 节点类型 _group 比较te s + type: string; // 节点类型 _group 比较te description?: string; // 节点描述 hidden?: boolean; // 是否可见 icon: { type: string; bgColor: string; - } + }; + schema?: Schema; // 节点的配置schema(弹窗) string为自定义组件 + settingWidget?: string; // 自定义组件 } export interface TNodeGroup { title: string; // 节点 title - type: '_group', - items: TNodeItem[] + type: '_group'; + items: TNodeItem[]; } export interface TNodeMenu { ref: React.RefObject; // 可选的 ref 属性 showSearch: boolean; // 配置是否可搜索 - items: (TNodeGroup | TNodeItem)[] - onClick: ({}: { type: string }) => void + items: (TNodeGroup | TNodeItem)[]; + onClick: ({}: { type: string }) => void; } export interface TNodeSelector { showSearch: boolean; // 配置是否可搜索 - items: (TNodeGroup | TNodeItem)[] + items: (TNodeGroup | TNodeItem)[]; } export interface XFlowProps { initialValues: { - nodes: any[], - edges: any + nodes: any[]; + edges: any; }; layout: 'LR' | 'TB'; - widges: any; // 自定义组件 + widgets: any; // 自定义组件 settings: (TNodeGroup | TNodeItem)[]; // 节点配置 nodeSelector: TNodeSelector; } From 31520eb373f4291537ab7afdf4ceecbb18b6868b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=B4=AB=E5=8D=87?= Date: Wed, 27 Nov 2024 15:23:51 +0800 Subject: [PATCH 17/38] =?UTF-8?q?chore:=20zundo=20=E5=8D=87=E7=BA=A7?= =?UTF-8?q?=E5=88=B0=202.1.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .npmrc | 2 +- packages/x-flow/package.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.npmrc b/.npmrc index 214c29d13..5e4086a7f 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1 @@ -registry=https://registry.npmjs.org/ +registry=https://registry.npmmirror.com/ diff --git a/packages/x-flow/package.json b/packages/x-flow/package.json index 4a7f5d7d9..201c4c7a4 100644 --- a/packages/x-flow/package.json +++ b/packages/x-flow/package.json @@ -54,7 +54,7 @@ "@xyflow/react": "^12.3.2", "@remixicon/react": "^4.2.0", "@dagrejs/dagre": "^1.1.3", - "zundo": "^2.0.0-beta.18", + "zundo": "^2.1.0", "use-context-selector": "^1.4.1", "form-render": "^2.3.4" }, @@ -71,4 +71,4 @@ "pre-commit": "lint-staged" }, "sideEffect": false -} \ No newline at end of file +} From 3b3ccef1dd280a77b8deab0c693f8c76480c4521 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=98=94=E6=A2=A6?= Date: Wed, 27 Nov 2024 15:45:02 +0800 Subject: [PATCH 18/38] =?UTF-8?q?fea:=E5=BC=B9=E7=AA=97=E6=9B=B4=E6=94=B9?= =?UTF-8?q?=E6=A0=87=E9=A2=98=E5=92=8C=E6=8F=8F=E8=BF=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/NodeEditor/index.tsx | 3 - .../src/components/PanelContainer/index.tsx | 55 ++++++++++++++++--- packages/x-flow/src/main.tsx | 5 +- .../x-flow/src/nodes/node-common/index.tsx | 16 +++--- packages/x-flow/src/nodes/node-end/index.tsx | 20 ++++--- .../x-flow/src/nodes/node-start/index.tsx | 7 ++- packages/x-flow/src/types.ts | 1 + 7 files changed, 73 insertions(+), 34 deletions(-) diff --git a/packages/x-flow/src/components/NodeEditor/index.tsx b/packages/x-flow/src/components/NodeEditor/index.tsx index 33d83ed6f..7e7f99aee 100644 --- a/packages/x-flow/src/components/NodeEditor/index.tsx +++ b/packages/x-flow/src/components/NodeEditor/index.tsx @@ -45,9 +45,6 @@ const NodeEditor: FC = (props: any) => { // readOnly={readonly} widgets={widgets} watch={watch} - // onMount={() => { - // form.setValues({ list: data?.list || [] }); - // }} /> ); } else { diff --git a/packages/x-flow/src/components/PanelContainer/index.tsx b/packages/x-flow/src/components/PanelContainer/index.tsx index aa5f47734..f8baaf053 100644 --- a/packages/x-flow/src/components/PanelContainer/index.tsx +++ b/packages/x-flow/src/components/PanelContainer/index.tsx @@ -1,5 +1,7 @@ import { Divider, Drawer, Input, Space } from 'antd'; -import React, { FC, useContext } from 'react'; +import produce from 'immer'; +import { debounce } from 'lodash'; +import React, { FC, useContext, useEffect, useState } from 'react'; import { useShallow } from 'zustand/react/shallow'; import { ConfigContext } from '../../models/context'; import useStore from '../../models/store'; @@ -11,9 +13,10 @@ interface IPanelProps { nodeType: string; onClose: () => void; node?: { id: string; _isCandidate: boolean; _nodeType: string }; - description?: string; // 业务描述---to do :确认一下取的地方 + description?: string; // 业务描述---to do :确认一下取的地方---从data里面取? children?: any; id: string; + data: any; // data值 } const getDescription = (nodeType: string, description: string) => { @@ -28,7 +31,16 @@ const getDescription = (nodeType: string, description: string) => { const Panel: FC = (props: any) => { // disabled属性取的地方可能不对------to do - const { onClose, children, nodeType, disabled, node, description,id } = props; + const { + onClose, + children, + nodeType, + disabled, + node, + description, + id, + data, + } = props; // 1.获取节点配置信息 const { settingMap } = useContext(ConfigContext); const nodeSetting = settingMap[nodeType] || {}; @@ -40,8 +52,28 @@ const Panel: FC = (props: any) => { ); const isDisabled = ['Input', 'Output'].includes(nodeType) || disabled; + const [descVal, setDescVal] = useState(data?.desc); + const [titleVal, setTitleVal] = useState(data?.title || nodeSetting?.title); + // const description = getDescription(nodeType, props.description); + const handleNodeValueChange = debounce((data: any) => { + const newNodes = produce(nodes, draft => { + const node = draft.find(n => n.id === id); + if (node) { + // 更新节点的 data + node.data = { ...node.data, ...data }; + } + }); + setNodes(newNodes); + }, 100); + + useEffect(() => { + setDescVal(data?.desc); + setTitleVal(data?.title || nodeSetting?.title); + }, [JSON.stringify(data), id]); + + return ( = (props: any) => { ) : ( { - // console.log('名称改变', val); + // defaultValue={data?.title || nodeSetting?.title} + value={titleVal} // || nodeSetting?.title + onChange={e => { + setTitleVal(e.target.value); + handleNodeValueChange({ title: e.target.value }); + }} /> )} @@ -98,7 +133,13 @@ const Panel: FC = (props: any) => { { + setDescVal(e.target.value); + handleNodeValueChange({ desc: e.target.value }); + }} /> )}
diff --git a/packages/x-flow/src/main.tsx b/packages/x-flow/src/main.tsx index 9d05ccdbf..687db23d5 100644 --- a/packages/x-flow/src/main.tsx +++ b/packages/x-flow/src/main.tsx @@ -202,6 +202,7 @@ const FlowEditor: FC = memo(props => { custom: (props: any) => { const { data, id, ...rest } = props; const { _nodeType, ...restData } = data || {}; + return ( = memo(props => { {activeNode && ( setActiveNode(null)} node={activeNode} + data={activeNode?.values} // disabled > {NodeEditorWrap} diff --git a/packages/x-flow/src/nodes/node-common/index.tsx b/packages/x-flow/src/nodes/node-common/index.tsx index 3814a149c..fe5b9be9f 100644 --- a/packages/x-flow/src/nodes/node-common/index.tsx +++ b/packages/x-flow/src/nodes/node-common/index.tsx @@ -1,26 +1,24 @@ import React, { memo, useContext } from 'react'; -import { ConfigContext } from '../../models/context'; import NodeContainer from '../../components/NodeContainer'; - +import { ConfigContext } from '../../models/context'; export default memo((props: any) => { - const { type, onClick } = props; + const { type, onClick, data } = props; const { settingMap } = useContext(ConfigContext); const nodeSetting = settingMap[type] || {}; return ( ); }); - - diff --git a/packages/x-flow/src/nodes/node-end/index.tsx b/packages/x-flow/src/nodes/node-end/index.tsx index fb207efc0..c31ecca2c 100644 --- a/packages/x-flow/src/nodes/node-end/index.tsx +++ b/packages/x-flow/src/nodes/node-end/index.tsx @@ -1,22 +1,24 @@ -import React, { memo } from 'react'; +import React, { memo, useContext } from 'react'; import NodeContainer from '../../components/NodeContainer'; +import { ConfigContext } from '../../models/context'; export default memo((props: any) => { - const { onClick } = props; + const { onClick, type, data } = props; + const { settingMap } = useContext(ConfigContext); + const nodeSetting = settingMap[type] || {}; return ( ); }); - - diff --git a/packages/x-flow/src/nodes/node-start/index.tsx b/packages/x-flow/src/nodes/node-start/index.tsx index 86bd6fcb2..36c588b85 100644 --- a/packages/x-flow/src/nodes/node-start/index.tsx +++ b/packages/x-flow/src/nodes/node-start/index.tsx @@ -3,21 +3,22 @@ import NodeContainer from '../../components/NodeContainer'; import { ConfigContext } from '../../models/context'; export default memo((props: any) => { - const { onClick, type } = props; + const { onClick, type, data } = props; const { settingMap } = useContext(ConfigContext); const nodeSetting = settingMap[type] || {}; return ( ); }); diff --git a/packages/x-flow/src/types.ts b/packages/x-flow/src/types.ts index af704297a..31dcb08f1 100644 --- a/packages/x-flow/src/types.ts +++ b/packages/x-flow/src/types.ts @@ -11,6 +11,7 @@ export interface TNodeItem { }; schema?: Schema; // 节点的配置schema(弹窗) string为自定义组件 settingWidget?: string; // 自定义组件 + hideDesc?: boolean;// 隐藏业务描述 } export interface TNodeGroup { From f32ff3da02dbe0c451433909a6c6ac5feeef03f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=B4=AB=E5=8D=87?= Date: Wed, 27 Nov 2024 16:34:51 +0800 Subject: [PATCH 19/38] =?UTF-8?q?chore:=20=E6=92=A4=E9=94=80=E5=9B=9E?= =?UTF-8?q?=E9=80=80=E5=88=9D=E7=89=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/CandidateNode/index.tsx | 9 ++- .../src/components/CustomNode/index.tsx | 6 +- packages/x-flow/src/main.tsx | 60 ++++--------------- packages/x-flow/src/models/store.ts | 36 +++++++---- .../x-flow/src/operator/UndoRedo/index.tsx | 23 +++---- packages/x-flow/src/operator/index.less | 3 +- packages/x-flow/src/operator/index.tsx | 11 ++-- 7 files changed, 63 insertions(+), 85 deletions(-) diff --git a/packages/x-flow/src/components/CandidateNode/index.tsx b/packages/x-flow/src/components/CandidateNode/index.tsx index 80d6c26b1..8beb5e565 100644 --- a/packages/x-flow/src/components/CandidateNode/index.tsx +++ b/packages/x-flow/src/components/CandidateNode/index.tsx @@ -13,9 +13,9 @@ const CandidateNode = () => { const { nodes, - setNodes, candidateNode, mousePosition, + addNodes, setCandidateNode } = useStore( useShallow((state: any) => ({ @@ -23,8 +23,7 @@ const CandidateNode = () => { edges: state.edges, candidateNode: state.candidateNode, mousePosition: state.mousePosition, - setNodes: state.setNodes, - setEdges: state.setEdges, + addNodes: state.addNodes, setCandidateNode: state.setCandidateNode, onNodesChange: state.onNodesChange, onEdgesChange: state.onEdgesChange, @@ -49,7 +48,7 @@ const CandidateNode = () => { position: { x, y } }); }); - setNodes(newNodes); + addNodes(newNodes); setCandidateNode(null); }); @@ -58,7 +57,7 @@ const CandidateNode = () => { } console.log(mousePosition, '=======000000') - + return (
{ onClick(data)} /> {!settingMap?.[type]?.sourceHandleHidden && ( @@ -97,8 +97,8 @@ export default memo((props: any) => { > {(selected || isHovered) && (
- - + = memo(props => { const { initialValues, settings } = props; const workflowContainerRef = useRef(null); const store = useStoreApi(); - const { updateEdge, addNodes, addEdges, zoomTo } = useReactFlow(); - const { undo, redo, record } = useUndoRedo(false); + const { updateEdge, zoomTo } = useReactFlow(); + // const { undo, redo, record } = useTemporalStore(); const { layout, nodes, @@ -51,6 +51,8 @@ const FlowEditor: FC = memo(props => { onConnect, setNodes, setEdges, + addNodes, + addEdges, setLayout, setCandidateNode, setMousePosition, @@ -62,6 +64,8 @@ const FlowEditor: FC = memo(props => { setLayout: state.setLayout, setNodes: state.setNodes, setEdges: state.setEdges, + addNodes: state.addNodes, + addEdges: state.addEdges, setMousePosition: state.setMousePosition, setCandidateNode: state.setCandidateNode, onNodesChange: state.onNodesChange, @@ -134,14 +138,6 @@ const FlowEditor: FC = memo(props => { }, }; setCandidateNode(newNode); - // record(() => { - // addNodes(newNode); - // addEdges({ - // id: uuid(), - // source: '1', - // target: newNode.id, - // }); - // }); }; // 插入节点 @@ -154,7 +150,7 @@ const FlowEditor: FC = memo(props => { y: 0, }, }; - record(() => { + // record(() => { addNodes(newNode); addEdges({ id: uuid(), @@ -165,7 +161,7 @@ const FlowEditor: FC = memo(props => { updateEdge(targetEdge?.id as string, { source: newNode.id, }); - }); + // }); }; // edge 移入/移出效果 @@ -236,7 +232,6 @@ const FlowEditor: FC = memo(props => { return (
- = memo(props => { // }); // }} onNodesChange={changes => { - const recordTypes = new Set(['add', 'remove']); - changes.forEach(change => { - console.log( - '🚀 ~ file: main.tsx:226 ~ changes.forEach ~ change:', - change - ); - - const removeChanges = changes.filter( - change => change.type === 'remove' - ); - - if (removeChanges.length > 0) { - removeChanges.forEach(change => { - eventEmitter?.emit({ type: 'deleteNode', payload: change }); - }); - } - if (recordTypes.has(change.type)) { - record(() => { - onNodesChange([change]); - }); - } else { - onNodesChange([change]); - } - }); + onNodesChange(changes) }} onEdgesChange={changes => { - const recordTypes = new Set(['add', 'remove']); - changes.forEach(change => { - if (recordTypes.has(change.type)) { - record(() => { - onEdgesChange([change]); - }); - } else { - onEdgesChange([change]); - } - }); + onEdgesChange(changes); }} onEdgeMouseEnter={(_, edge: any) => { getUpdateEdgeConfig(edge, '#2970ff'); @@ -311,6 +274,7 @@ const FlowEditor: FC = memo(props => { getUpdateEdgeConfig(edge, '#c9c9c9'); }} > + void; setEdges: (edges: Edge[]) => void; + addNodes: (nodes: AppNode[]) => void; + addEdges: (edges: Edge[]) => void; setLayout: (layout: 'LR' | 'TB') => void; setCandidateNode: (candidateNode: any) => void; setMousePosition: (mousePosition: any) => void; @@ -59,11 +62,21 @@ const useStore = create()( }); }, setNodes: (nodes) => { - set({ nodes }); + // 只记录节点变化 + useTemporalStore().record(() => { + set({ nodes }); + }); }, setEdges: (edges) => { set({ edges }); }, + addNodes: payload => { + const newNodes = get().nodes.concat(payload); + set({ nodes: newNodes }); + }, + addEdges: payload => { + set({ edges: get().edges.concat(payload) }); + }, setNodeMenus: (nodeMenus: any) => { set({ nodeMenus }); }, @@ -81,6 +94,8 @@ const useStore = create()( } }), { + // nodes 和 edges 是引用类型,所以使用深比较 + equality: isDeepEqual, // 偏函数 partialize: (state) => { const { nodes, edges } = state; @@ -89,22 +104,20 @@ const useStore = create()( nodes, }; }, + onSave(pastState, currentState) { + console.log('onSave', pastState, currentState); + }, }, ), ), ); -export const useUndoRedo = (isTracking = true) => { - const temporalStore = useStore.temporal.getState(); - if (temporalStore.isTracking) { - // 暂停时间旅行机器, - temporalStore.pause(); - } - +export const useTemporalStore = () => { return { - ...temporalStore, + ...useStore.temporal.getState(), record: (callback: () => void) => { + const temporalStore = useStore.temporal.getState(); temporalStore.resume(); callback(); temporalStore.pause(); @@ -112,4 +125,7 @@ export const useUndoRedo = (isTracking = true) => { } }; -export default useStore; \ No newline at end of file +// 默认关闭时间机器 +useStore.temporal.getState().pause(); + +export default useStore; diff --git a/packages/x-flow/src/operator/UndoRedo/index.tsx b/packages/x-flow/src/operator/UndoRedo/index.tsx index 10ad5a173..2404b01b4 100644 --- a/packages/x-flow/src/operator/UndoRedo/index.tsx +++ b/packages/x-flow/src/operator/UndoRedo/index.tsx @@ -1,31 +1,34 @@ import React, { memo } from 'react'; -import { RiArrowGoBackLine, RiArrowGoForwardFill } from '@remixicon/react' import { Button, Tooltip } from 'antd'; import IconView from '../../components/IconView'; import './index.less'; export type UndoRedoProps = { - handleUndo: () => void; + handleUndo: () => void; handleRedo: () => void; + pastStates: any[]; + futureStates: any[]; }; -export default memo(({ handleUndo, handleRedo }: UndoRedoProps) => { +export default memo(({ handleUndo, handleRedo, pastStates, futureStates }: UndoRedoProps) => { return (
-
); -}) \ No newline at end of file +}) diff --git a/packages/x-flow/src/operator/index.less b/packages/x-flow/src/operator/index.less index a8cb678c3..d2cc387c0 100644 --- a/packages/x-flow/src/operator/index.less +++ b/packages/x-flow/src/operator/index.less @@ -3,7 +3,6 @@ left: 4px; bottom: 14px; height: 50px; - z-index: 1; .mini-map { position: absolute; @@ -24,4 +23,4 @@ bottom: 4px; z-index: 9; } -} \ No newline at end of file +} diff --git a/packages/x-flow/src/operator/index.tsx b/packages/x-flow/src/operator/index.tsx index 88301db8f..2163875cc 100644 --- a/packages/x-flow/src/operator/index.tsx +++ b/packages/x-flow/src/operator/index.tsx @@ -5,22 +5,19 @@ import UndoRedo from './UndoRedo'; import Control from './Control'; import './index.less'; +import { useTemporalStore } from '../models/store'; export type OperatorProps = { - handleUndo: () => void - handleRedo: () => void addNode: any; } -const Operator = ({ handleUndo, handleRedo, addNode }: OperatorProps) => { +const Operator = ({ addNode }: OperatorProps) => { + const { undo, redo, pastStates, futureStates } = useTemporalStore(); return (
- + undo()} handleRedo={() => redo()} pastStates={pastStates} futureStates={futureStates} />
From da874c72cc6826bcd3c48115632889269a526106 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=B4=AB=E5=8D=87?= Date: Wed, 27 Nov 2024 16:45:47 +0800 Subject: [PATCH 20/38] =?UTF-8?q?chore:=20addNodes=20=E5=8A=A0=E5=85=A5?= =?UTF-8?q?=E6=97=B6=E9=97=B4=E6=9C=BA=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/x-flow/src/models/store.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/x-flow/src/models/store.ts b/packages/x-flow/src/models/store.ts index 32a515440..8a82eb3d6 100644 --- a/packages/x-flow/src/models/store.ts +++ b/packages/x-flow/src/models/store.ts @@ -72,7 +72,9 @@ const useStore = create()( }, addNodes: payload => { const newNodes = get().nodes.concat(payload); - set({ nodes: newNodes }); + useTemporalStore().record(() => { + set({ nodes: newNodes }); + }) }, addEdges: payload => { set({ edges: get().edges.concat(payload) }); From 8cf69c8c58eb9778054590ab3e9c742b46e909be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=98=94=E6=A2=A6?= Date: Wed, 27 Nov 2024 17:29:27 +0800 Subject: [PATCH 21/38] =?UTF-8?q?feat:=E6=96=B0=E5=A2=9Exflow=20API=20?= =?UTF-8?q?=E6=96=87=E6=A1=A31?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .dumi/theme/slots/Header/Navigation.tsx | 28 ++- docs/xflow/api.md | 51 +++++ docs/xflow/index.md | 123 +---------- docs/xflow/schema/settings.ts | 199 ++++++++++++++++++ .../src/components/NodeEditor/index.tsx | 1 + 5 files changed, 272 insertions(+), 130 deletions(-) create mode 100644 docs/xflow/api.md create mode 100644 docs/xflow/schema/settings.ts diff --git a/.dumi/theme/slots/Header/Navigation.tsx b/.dumi/theme/slots/Header/Navigation.tsx index 853da4a90..11c01d6ba 100644 --- a/.dumi/theme/slots/Header/Navigation.tsx +++ b/.dumi/theme/slots/Header/Navigation.tsx @@ -1,7 +1,7 @@ -import React from 'react'; +import { CodeOutlined, DownOutlined } from '@ant-design/icons'; import { Menu } from 'antd'; import { Link } from 'dumi'; -import { CodeOutlined, DownOutlined, MobileOutlined, SwapOutlined, ToolOutlined } from '@ant-design/icons'; +import React from 'react'; const Navigation: React.FC = () => { const items: any = [ @@ -13,9 +13,13 @@ const Navigation: React.FC = () => { label: TableRender, key: 'table-render', }, + { + label: XFlow, + key: 'xflow', + }, { label: FRMobile, - key: 'form-render-mobile' + key: 'form-render-mobile', }, // { // label: DataView, @@ -23,7 +27,7 @@ const Navigation: React.FC = () => { // }, { label: Playground, - key: 'playground' + key: 'playground', }, { label: SchemaBuilder, @@ -33,17 +37,23 @@ const Navigation: React.FC = () => { label: (
更多 - +
), children: [ { - label: ChartRender, + label: ( + + ChartRender + + ), key: 'chart-render', icon: , - } - ] - } + }, + ], + }, ]; return ; diff --git a/docs/xflow/api.md b/docs/xflow/api.md new file mode 100644 index 000000000..75a5ea07c --- /dev/null +++ b/docs/xflow/api.md @@ -0,0 +1,51 @@ +--- +order: 1 +toc: content +title: API +--- +# API + +## XFlow + + + +| 属性 | 描述 | 类型 | 默认值 | +| ------------- | ------------------------------------ | --------------------------------------------------------- | ------ | +| initialValues | 初始的节点和边数据 | `{nodes:any[],edges:any}` | - | - | +| layout | 节点布局的方向 | `LR \| TB` | - | - | +| widges | 自定义组件 | `Record` | - | - | +| settings | 节点配置,定义页面中可拖动的节点配置 | ( [TNodeGroup](#tnodegroup) \| [TNodeItem](#tnodeitem))[] | | +| nodeSelector | 节点选择器配置,可控制节点的可搜索性 | `TNodeSelector` | | + +### TNodeGroup + +节点分组配置 + +| 属性 | 描述 | 类型 | 默认值 | +| ----- | ------------ | ------------- | ------ | +| title | 分组名称 | `string` | | +| type | 分组类型 | `_group` | | +| items | 节点配置信息 | `TNodeItem[]` | | + +## TNodeItem + +单个节点配置 + +| 属性 | 描述 | 类型 | 默认值 | +| ------------------ | ------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | +| title | 节点名称 | `string` | | +| type | 节点类型,内置节点: `Start\|End` | `string` | | +| hidden | 是否在配置面板中显示节点 | `boolean` | false | +| targetHandleHidden | 是否隐藏左侧输入连接头 | `boolean` | false | +| sourceHandleHidden | 是否隐藏右侧输出连接头 | `boolean` | false | +| hideDesc | 是否隐藏节点下方的描述信息 | `boolean` | false | +| icon | 节点的图标配置 | `{type:string;bgColor:string}` | | +| schema | 节点的业务配置信息,详见[form-render 文档](/form-render/api-schema) | SchemaBase | | +| settingWidget | 自定义节点的业务配置组件 | `string` | | + +## TNodeSelector + +| 属性 | 描述 | 类型 | 默认值 | +| ---------- | ---------------- | --------------------------------------------------------- | ------ | +| showSearch | 节点是否可被搜索 | `boolean` | false | +| items | 节点配置 | ( [TNodeGroup](#tnodegroup) \| [TNodeItem](#tnodeitem))[] | | diff --git a/docs/xflow/index.md b/docs/xflow/index.md index 9ff1de533..72c9ad7a6 100644 --- a/docs/xflow/index.md +++ b/docs/xflow/index.md @@ -44,126 +44,7 @@ import React from 'react'; import XFlow from '@xrenders/xflow'; import schema from './schema/basic'; import data from './data/basic'; - - -const settings = [ - { - title: 'Input', - type: 'Start', - hidden: true, - targetHandleHidden: true, - icon: { - type: 'icon-start', - bgColor: '#17B26A', - } - }, - { - title: 'Output', - type: 'End', - hidden: true, - sourceHandleHidden: true, - icon: { - type: 'icon-end', - bgColor: '#F79009', - } - }, - { - title: 'LLM', - type: 'LLM', - description: '调用大语言模型回答问题或者对自然语言进行处理', - icon: { - type: 'icon-model', - bgColor: '#6172F3', - } - }, - { - title: 'Prompt', - type: 'Prompt', - description: '通过精心设计提示词,提升大语言模型回答效果', - icon: { - type: 'icon-prompt', - bgColor: '#17B26A', - } - }, - { - title: '知识库', - type: 'knowledge', - description: '允许你从知识库中查询与用户问题相关的文本内容', - icon: { - type: 'icon-knowledge', - bgColor: '#6172F3' - } - }, - { - title: 'Switch', - type: 'Switch', - description: '允许你根据 if/else 条件将 workflow 拆分成两个分支', - icon: { - type: 'icon-switch', - bgColor: '#06AED4', - } - }, - { - title: 'HSF', - type: 'hsf', - description: '允许通过 HSF 协议发送服务器请求', - icon: { - type: 'icon-hsf', - bgColor: '#875BF7' - } - }, - { - title: 'Http', - type: 'http', - description: '允许通过 HTTP 协议发送服务器请求', - icon: { - type: 'icon-http', - bgColor: '#875BF7' - } - }, - { - title: '代码执行', - type: 'Code', - description: '执行一段 Groovy 或 Python 或 NodeJS 代码实现自定义逻辑', - icon: { - type: 'icon-code', - bgColor: '#2E90FA' - } - }, - { - title: '工具', - type: 'tool', - description: '允许使用工具能力', - icon: { - type: 'icon-gongju', - bgColor: '#2E90FA' - } - }, - { - title: '工具', - type: '_group', - items: [ - { - title: '代码执行', - type: 'Code', - description: '执行一段 Groovy 或 Python 或 NodeJS 代码实现自定义逻辑', - icon: { - type: 'icon-code', - bgColor: '#2E90FA' - } - }, - { - title: '工具', - type: 'tool', - description: '允许使用工具能力', - icon: { - type: 'icon-gongju', - bgColor: '#2E90FA' - } - }, - ] - } -]; +import settings from './schema/settings'; export default () => { const nodes = [ @@ -203,4 +84,4 @@ export default () => {
); } -``` \ No newline at end of file +``` diff --git a/docs/xflow/schema/settings.ts b/docs/xflow/schema/settings.ts new file mode 100644 index 000000000..32e942ac1 --- /dev/null +++ b/docs/xflow/schema/settings.ts @@ -0,0 +1,199 @@ +export default [ + { + title: '开始', + type: 'Start', + hidden: true, + targetHandleHidden: true, + icon: { + type: 'icon-start', + bgColor: '#17B26A', + }, + schema: { + type: 'object', + properties: { + input: { + title: '变量一', + type: 'string', + widget: 'input', + }, + select: { + title: '变量二', + type: 'string', + widget: 'select', + props: { + options: [ + { label: 'a', value: 'a' }, + { label: 'b', value: 'b' }, + { label: 'c', value: 'c' }, + ], + }, + }, + radio1: { + title: '点击单选', + type: 'string', + widget: 'radio', + props: { + options: [ + { label: '早', value: 'a' }, + { label: '中', value: 'b' }, + { label: '晚', value: 'c' } + ] + } + }, + textarea1: { + title: '长文本', + type: 'string', + widget: 'textArea' + }, + date1: { + title: '日期选择', + type: 'string', + widget: 'datePicker' + }, + dateRange1: { + title: '日期范围', + type: 'range', + widget: 'dateRange' + }, + time1: { + title: '时间选择', + type: 'string', + widget: 'timePicker' + }, + timeRange1: { + title: '时间范围', + type: 'range', + widget: 'timeRange' + }, + }, + }, + }, + { + title: '结束', + type: 'End', + hidden: true, + sourceHandleHidden: true, + icon: { + type: 'icon-end', + bgColor: '#F79009', + }, + schema: { + type: "object", + properties: { + input: { + title: '变量一', + type: 'string', + widget: 'input', + }, + select: { + title: '变量二', + type: 'string', + widget: 'select', + props: { + options: [ + { label: 'a', value: 'a' }, + { label: 'b', value: 'b' }, + { label: 'c', value: 'c' }, + ], + }, + }, + } + } + }, + { + title: 'LLM', + type: 'LLM', + description: '调用大语言模型回答问题或者对自然语言进行处理', + icon: { + type: 'icon-model', + bgColor: '#6172F3', + }, + }, + { + title: 'Prompt', + type: 'Prompt', + description: '通过精心设计提示词,提升大语言模型回答效果', + icon: { + type: 'icon-prompt', + bgColor: '#17B26A', + }, + }, + { + title: '知识库', + type: 'knowledge', + description: '允许你从知识库中查询与用户问题相关的文本内容', + icon: { + type: 'icon-knowledge', + bgColor: '#6172F3', + }, + }, + { + title: 'Switch', + type: 'Switch', + description: '允许你根据 if/else 条件将 workflow 拆分成两个分支', + icon: { + type: 'icon-switch', + bgColor: '#06AED4', + }, + }, + { + title: 'HSF', + type: 'hsf', + description: '允许通过 HSF 协议发送服务器请求', + icon: { + type: 'icon-hsf', + bgColor: '#875BF7', + }, + }, + { + title: 'Http', + type: 'http', + description: '允许通过 HTTP 协议发送服务器请求', + icon: { + type: 'icon-http', + bgColor: '#875BF7', + }, + }, + { + title: '代码执行', + type: 'Code', + description: '执行一段 Groovy 或 Python 或 NodeJS 代码实现自定义逻辑', + icon: { + type: 'icon-code', + bgColor: '#2E90FA', + }, + }, + { + title: '工具', + type: 'tool', + description: '允许使用工具能力', + icon: { + type: 'icon-gongju', + bgColor: '#2E90FA', + }, + }, + { + title: '工具', + type: '_group', + items: [ + { + title: '代码执行', + type: 'Code', + description: '执行一段 Groovy 或 Python 或 NodeJS 代码实现自定义逻辑', + icon: { + type: 'icon-code', + bgColor: '#2E90FA', + }, + }, + { + title: '工具', + type: 'tool', + description: '允许使用工具能力', + icon: { + type: 'icon-gongju', + bgColor: '#2E90FA', + }, + }, + ], + }, +]; diff --git a/packages/x-flow/src/components/NodeEditor/index.tsx b/packages/x-flow/src/components/NodeEditor/index.tsx index 7e7f99aee..22788c875 100644 --- a/packages/x-flow/src/components/NodeEditor/index.tsx +++ b/packages/x-flow/src/components/NodeEditor/index.tsx @@ -45,6 +45,7 @@ const NodeEditor: FC = (props: any) => { // readOnly={readonly} widgets={widgets} watch={watch} + size={'small'} /> ); } else { From 92a52cb4c0f1cad183ddd32923113a58340153d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=98=94=E6=A2=A6?= Date: Wed, 27 Nov 2024 18:42:18 +0800 Subject: [PATCH 22/38] =?UTF-8?q?feat=EF=BC=9A=E6=96=87=E6=A1=A3=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E8=87=AA=E5=AE=9A=E4=B9=89=E7=BB=84=E4=BB=B6demo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/xflow/basic.md | 11 + docs/xflow/custom-flow.md | 15 ++ docs/xflow/demo/basic/index.tsx | 42 ++++ docs/xflow/demo/basic/setting.tsx | 199 ++++++++++++++++++ docs/xflow/demo/custom-flow/customWidget.tsx | 13 ++ docs/xflow/demo/custom-flow/index.tsx | 46 ++++ docs/xflow/demo/custom-flow/setting.tsx | 141 +++++++++++++ .../src/components/NodeEditor/index.tsx | 48 ++++- 8 files changed, 508 insertions(+), 7 deletions(-) create mode 100644 docs/xflow/basic.md create mode 100644 docs/xflow/custom-flow.md create mode 100644 docs/xflow/demo/basic/index.tsx create mode 100644 docs/xflow/demo/basic/setting.tsx create mode 100644 docs/xflow/demo/custom-flow/customWidget.tsx create mode 100644 docs/xflow/demo/custom-flow/index.tsx create mode 100644 docs/xflow/demo/custom-flow/setting.tsx diff --git a/docs/xflow/basic.md b/docs/xflow/basic.md new file mode 100644 index 000000000..37699522f --- /dev/null +++ b/docs/xflow/basic.md @@ -0,0 +1,11 @@ +--- +order: 2 +title: '基础交互' +mobile: false +group: + title: 最佳展示 + order: 2 +--- +# 基础交互 + + diff --git a/docs/xflow/custom-flow.md b/docs/xflow/custom-flow.md new file mode 100644 index 000000000..26f12b27d --- /dev/null +++ b/docs/xflow/custom-flow.md @@ -0,0 +1,15 @@ +--- +order: 2 +title: '自定义组件' +mobile: false +group: + title: 最佳展示 + order: 2 +--- +# 自定义组件 + +## 自定义配置组件 + +使用`settingWidget`自定义业务配置组件 + + diff --git a/docs/xflow/demo/basic/index.tsx b/docs/xflow/demo/basic/index.tsx new file mode 100644 index 000000000..6ae0ae689 --- /dev/null +++ b/docs/xflow/demo/basic/index.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import XFlow from '@xrenders/xflow'; +import settings from './setting'; + +export default () => { + const nodes = [ + { + id: '1', + type: 'Start', + data: {}, + position: { + x: 40, + y: 240, + } + }, + { + id: '2', + type: 'End', + data: {}, + position: { + x: 500, + y: 240, + } + } + ]; + + const edges = [ + { source: '1', target: '2', id: '234123' } + ] + + return ( +
+ +
+ ); +} diff --git a/docs/xflow/demo/basic/setting.tsx b/docs/xflow/demo/basic/setting.tsx new file mode 100644 index 000000000..32e942ac1 --- /dev/null +++ b/docs/xflow/demo/basic/setting.tsx @@ -0,0 +1,199 @@ +export default [ + { + title: '开始', + type: 'Start', + hidden: true, + targetHandleHidden: true, + icon: { + type: 'icon-start', + bgColor: '#17B26A', + }, + schema: { + type: 'object', + properties: { + input: { + title: '变量一', + type: 'string', + widget: 'input', + }, + select: { + title: '变量二', + type: 'string', + widget: 'select', + props: { + options: [ + { label: 'a', value: 'a' }, + { label: 'b', value: 'b' }, + { label: 'c', value: 'c' }, + ], + }, + }, + radio1: { + title: '点击单选', + type: 'string', + widget: 'radio', + props: { + options: [ + { label: '早', value: 'a' }, + { label: '中', value: 'b' }, + { label: '晚', value: 'c' } + ] + } + }, + textarea1: { + title: '长文本', + type: 'string', + widget: 'textArea' + }, + date1: { + title: '日期选择', + type: 'string', + widget: 'datePicker' + }, + dateRange1: { + title: '日期范围', + type: 'range', + widget: 'dateRange' + }, + time1: { + title: '时间选择', + type: 'string', + widget: 'timePicker' + }, + timeRange1: { + title: '时间范围', + type: 'range', + widget: 'timeRange' + }, + }, + }, + }, + { + title: '结束', + type: 'End', + hidden: true, + sourceHandleHidden: true, + icon: { + type: 'icon-end', + bgColor: '#F79009', + }, + schema: { + type: "object", + properties: { + input: { + title: '变量一', + type: 'string', + widget: 'input', + }, + select: { + title: '变量二', + type: 'string', + widget: 'select', + props: { + options: [ + { label: 'a', value: 'a' }, + { label: 'b', value: 'b' }, + { label: 'c', value: 'c' }, + ], + }, + }, + } + } + }, + { + title: 'LLM', + type: 'LLM', + description: '调用大语言模型回答问题或者对自然语言进行处理', + icon: { + type: 'icon-model', + bgColor: '#6172F3', + }, + }, + { + title: 'Prompt', + type: 'Prompt', + description: '通过精心设计提示词,提升大语言模型回答效果', + icon: { + type: 'icon-prompt', + bgColor: '#17B26A', + }, + }, + { + title: '知识库', + type: 'knowledge', + description: '允许你从知识库中查询与用户问题相关的文本内容', + icon: { + type: 'icon-knowledge', + bgColor: '#6172F3', + }, + }, + { + title: 'Switch', + type: 'Switch', + description: '允许你根据 if/else 条件将 workflow 拆分成两个分支', + icon: { + type: 'icon-switch', + bgColor: '#06AED4', + }, + }, + { + title: 'HSF', + type: 'hsf', + description: '允许通过 HSF 协议发送服务器请求', + icon: { + type: 'icon-hsf', + bgColor: '#875BF7', + }, + }, + { + title: 'Http', + type: 'http', + description: '允许通过 HTTP 协议发送服务器请求', + icon: { + type: 'icon-http', + bgColor: '#875BF7', + }, + }, + { + title: '代码执行', + type: 'Code', + description: '执行一段 Groovy 或 Python 或 NodeJS 代码实现自定义逻辑', + icon: { + type: 'icon-code', + bgColor: '#2E90FA', + }, + }, + { + title: '工具', + type: 'tool', + description: '允许使用工具能力', + icon: { + type: 'icon-gongju', + bgColor: '#2E90FA', + }, + }, + { + title: '工具', + type: '_group', + items: [ + { + title: '代码执行', + type: 'Code', + description: '执行一段 Groovy 或 Python 或 NodeJS 代码实现自定义逻辑', + icon: { + type: 'icon-code', + bgColor: '#2E90FA', + }, + }, + { + title: '工具', + type: 'tool', + description: '允许使用工具能力', + icon: { + type: 'icon-gongju', + bgColor: '#2E90FA', + }, + }, + ], + }, +]; diff --git a/docs/xflow/demo/custom-flow/customWidget.tsx b/docs/xflow/demo/custom-flow/customWidget.tsx new file mode 100644 index 000000000..a5fe73558 --- /dev/null +++ b/docs/xflow/demo/custom-flow/customWidget.tsx @@ -0,0 +1,13 @@ +import { Input } from 'antd'; + +const customWidget = ({ value, onChange }) => { + + return ( + onChange({ inputVal: e.target.value })} + /> + ); +}; + +export default customWidget; diff --git a/docs/xflow/demo/custom-flow/index.tsx b/docs/xflow/demo/custom-flow/index.tsx new file mode 100644 index 000000000..e3c50cf59 --- /dev/null +++ b/docs/xflow/demo/custom-flow/index.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import XFlow from '@xrenders/xflow'; +import settings from './setting'; +import customWidget from './customWidget'; + +export default () => { + const nodes = [ + { + id: '1', + type: 'Start', + data: { + inputVal:'我是自定义组件' + }, + position: { + x: 40, + y: 240, + } + }, + { + id: '2', + type: 'End', + data: {}, + position: { + x: 500, + y: 240, + } + } + ]; + + const edges = [ + { source: '1', target: '2', id: '234123' } + ] + + return ( +
+ +
+ ); +} diff --git a/docs/xflow/demo/custom-flow/setting.tsx b/docs/xflow/demo/custom-flow/setting.tsx new file mode 100644 index 000000000..8412a6c68 --- /dev/null +++ b/docs/xflow/demo/custom-flow/setting.tsx @@ -0,0 +1,141 @@ +export default [ + { + title: '开始', + type: 'Start', + hidden: true, + targetHandleHidden: true, + icon: { + type: 'icon-start', + bgColor: '#17B26A', + }, + settingWidget:"customWidget" + }, + { + title: '结束', + type: 'End', + hidden: true, + sourceHandleHidden: true, + icon: { + type: 'icon-end', + bgColor: '#F79009', + }, + schema: { + type: "object", + properties: { + input: { + title: '变量一', + type: 'string', + widget: 'input', + }, + select: { + title: '变量二', + type: 'string', + widget: 'select', + props: { + options: [ + { label: 'a', value: 'a' }, + { label: 'b', value: 'b' }, + { label: 'c', value: 'c' }, + ], + }, + }, + } + } + }, + { + title: 'LLM', + type: 'LLM', + description: '调用大语言模型回答问题或者对自然语言进行处理', + icon: { + type: 'icon-model', + bgColor: '#6172F3', + }, + }, + { + title: 'Prompt', + type: 'Prompt', + description: '通过精心设计提示词,提升大语言模型回答效果', + icon: { + type: 'icon-prompt', + bgColor: '#17B26A', + }, + }, + { + title: '知识库', + type: 'knowledge', + description: '允许你从知识库中查询与用户问题相关的文本内容', + icon: { + type: 'icon-knowledge', + bgColor: '#6172F3', + }, + }, + { + title: 'Switch', + type: 'Switch', + description: '允许你根据 if/else 条件将 workflow 拆分成两个分支', + icon: { + type: 'icon-switch', + bgColor: '#06AED4', + }, + }, + { + title: 'HSF', + type: 'hsf', + description: '允许通过 HSF 协议发送服务器请求', + icon: { + type: 'icon-hsf', + bgColor: '#875BF7', + }, + }, + { + title: 'Http', + type: 'http', + description: '允许通过 HTTP 协议发送服务器请求', + icon: { + type: 'icon-http', + bgColor: '#875BF7', + }, + }, + { + title: '代码执行', + type: 'Code', + description: '执行一段 Groovy 或 Python 或 NodeJS 代码实现自定义逻辑', + icon: { + type: 'icon-code', + bgColor: '#2E90FA', + }, + }, + { + title: '工具', + type: 'tool', + description: '允许使用工具能力', + icon: { + type: 'icon-gongju', + bgColor: '#2E90FA', + }, + }, + { + title: '工具', + type: '_group', + items: [ + { + title: '代码执行', + type: 'Code', + description: '执行一段 Groovy 或 Python 或 NodeJS 代码实现自定义逻辑', + icon: { + type: 'icon-code', + bgColor: '#2E90FA', + }, + }, + { + title: '工具', + type: 'tool', + description: '允许使用工具能力', + icon: { + type: 'icon-gongju', + bgColor: '#2E90FA', + }, + }, + ], + }, +]; diff --git a/packages/x-flow/src/components/NodeEditor/index.tsx b/packages/x-flow/src/components/NodeEditor/index.tsx index 22788c875..11f7a59ed 100644 --- a/packages/x-flow/src/components/NodeEditor/index.tsx +++ b/packages/x-flow/src/components/NodeEditor/index.tsx @@ -1,7 +1,11 @@ import FormRender, { useForm } from 'form-render'; -import React, { FC, useContext, useEffect } from 'react'; +import produce from 'immer'; +import { debounce } from 'lodash'; +import React, { FC, useContext, useEffect, useState } from 'react'; +import { useShallow } from 'zustand/react/shallow'; import { ConfigContext } from '../../models/context'; -import { values } from 'lodash'; +import useStore from '../../models/store'; + interface INodeEditorProps { data: any; onChange: (data: any) => void; @@ -15,15 +19,42 @@ const NodeEditor: FC = (props: any) => { // // 1.获取节点配置信息 const { settingMap, widgets } = useContext(ConfigContext); const nodeSetting = settingMap[nodeType] || {}; + const [customVal, setCustomVal] = useState(data); + + const { nodes, setNodes } = useStore( + useShallow((state: any) => ({ + nodes: state.nodes, + setNodes: state.setNodes, + })) + ); useEffect(() => { - form.resetFields(); - form.setValues(data || {}); + if (nodeSetting?.schema) { + form.resetFields(); + form.setValues(data || {}); + } else if (nodeSetting?.settingWidget) { + setCustomVal(data); + } else { + } + + console.log('data', data); }, [JSON.stringify(data), id]); + const handleNodeValueChange = debounce((data: any) => { + const newNodes = produce(nodes, draft => { + const node = draft.find(n => n.id === id); + if (node) { + // 更新节点的 data + node.data = { ...node.data, ...data }; + } + }); + setNodes(newNodes); + }, 100); + const watch = { '#': (allValues: any) => { - onChange({ id, values: { ...allValues } }); + handleNodeValueChange({ ...allValues }); + // onChange({ id, values: { ...allValues } }); }, }; @@ -31,9 +62,12 @@ const NodeEditor: FC = (props: any) => { const NodeWidget = widgets[nodeSetting?.settingWidget]; return ( { - onChange({ id, values: { ...values } }); + console.log('onChange000', values); + setCustomVal(values); + // onChange({ id, values: { ...values } }); + handleNodeValueChange({ ...values }); }} /> ); From 4d3e944655031ccd46aba863a69110b2ef9c0779 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=98=94=E6=A2=A6?= Date: Thu, 28 Nov 2024 16:51:04 +0800 Subject: [PATCH 23/38] =?UTF-8?q?fix:1.API=20=E6=96=87=E6=A1=A3=E9=9A=90?= =?UTF-8?q?=E8=97=8Fhidedesc=202.schema=E9=85=8D=E7=BD=AE=E5=90=8D?= =?UTF-8?q?=E7=A7=B0=E6=94=B9=E4=B8=BAsettingSchema?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/xflow/api.md | 9 +- docs/xflow/custom-flow.md | 58 +++++++ docs/xflow/demo/basic/setting.tsx | 4 +- docs/xflow/demo/custom-flow/setting.tsx | 2 +- docs/xflow/schema/custom-settings.ts | 150 ++++++++++++++++++ docs/xflow/schema/settings.ts | 4 +- .../src/components/NodeEditor/index.tsx | 9 +- packages/x-flow/src/types.ts | 2 +- 8 files changed, 221 insertions(+), 17 deletions(-) create mode 100644 docs/xflow/schema/custom-settings.ts diff --git a/docs/xflow/api.md b/docs/xflow/api.md index 75a5ea07c..fdfbb5703 100644 --- a/docs/xflow/api.md +++ b/docs/xflow/api.md @@ -13,7 +13,7 @@ title: API | ------------- | ------------------------------------ | --------------------------------------------------------- | ------ | | initialValues | 初始的节点和边数据 | `{nodes:any[],edges:any}` | - | - | | layout | 节点布局的方向 | `LR \| TB` | - | - | -| widges | 自定义组件 | `Record` | - | - | +| widgets | 自定义组件 | `Record` | - | - | | settings | 节点配置,定义页面中可拖动的节点配置 | ( [TNodeGroup](#tnodegroup) \| [TNodeItem](#tnodeitem))[] | | | nodeSelector | 节点选择器配置,可控制节点的可搜索性 | `TNodeSelector` | | @@ -34,13 +34,12 @@ title: API | 属性 | 描述 | 类型 | 默认值 | | ------------------ | ------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | | title | 节点名称 | `string` | | -| type | 节点类型,内置节点: `Start\|End` | `string` | | +| type | 节点类型 | `string` | | | hidden | 是否在配置面板中显示节点 | `boolean` | false | | targetHandleHidden | 是否隐藏左侧输入连接头 | `boolean` | false | | sourceHandleHidden | 是否隐藏右侧输出连接头 | `boolean` | false | -| hideDesc | 是否隐藏节点下方的描述信息 | `boolean` | false | -| icon | 节点的图标配置 | `{type:string;bgColor:string}` | | -| schema | 节点的业务配置信息,详见[form-render 文档](/form-render/api-schema) | SchemaBase | | +| icon | 节点的图标配置 | `{type:string;bgColor:string}` | | +| settingSchema | 节点的业务配置信息,详见[form-render 文档](/form-render/api-schema) | SchemaBase | | | settingWidget | 自定义节点的业务配置组件 | `string` | | ## TNodeSelector diff --git a/docs/xflow/custom-flow.md b/docs/xflow/custom-flow.md index 26f12b27d..ffcea9090 100644 --- a/docs/xflow/custom-flow.md +++ b/docs/xflow/custom-flow.md @@ -13,3 +13,61 @@ group: 使用`settingWidget`自定义业务配置组件 + + +## 在schema中自定义组件 + +```jsx +import { Input } from 'antd'; +import React from 'react'; +import XFlow from '@xrenders/xflow'; +import settings from './schema/custom-settings.ts'; + + +const customWidget=({value,onChange})=>{ + return +} +export default () => { + const nodes = [ + { + id: '1', + type: 'Start', + data: { + inputVal:'我是自定义组件' + }, + position: { + x: 40, + y: 240, + } + }, + { + id: '2', + type: 'End', + data: {}, + position: { + x: 500, + y: 240, + } + } + ]; + + const edges = [ + { source: '1', target: '2', id: '234123' } + ] + + return ( +
+ +
+ ); +} + + +``` diff --git a/docs/xflow/demo/basic/setting.tsx b/docs/xflow/demo/basic/setting.tsx index 32e942ac1..72f080f69 100644 --- a/docs/xflow/demo/basic/setting.tsx +++ b/docs/xflow/demo/basic/setting.tsx @@ -8,7 +8,7 @@ export default [ type: 'icon-start', bgColor: '#17B26A', }, - schema: { + settingSchema: { type: 'object', properties: { input: { @@ -77,7 +77,7 @@ export default [ type: 'icon-end', bgColor: '#F79009', }, - schema: { + settingSchema: { type: "object", properties: { input: { diff --git a/docs/xflow/demo/custom-flow/setting.tsx b/docs/xflow/demo/custom-flow/setting.tsx index 8412a6c68..282e3dbc7 100644 --- a/docs/xflow/demo/custom-flow/setting.tsx +++ b/docs/xflow/demo/custom-flow/setting.tsx @@ -19,7 +19,7 @@ export default [ type: 'icon-end', bgColor: '#F79009', }, - schema: { + settingSchema: { type: "object", properties: { input: { diff --git a/docs/xflow/schema/custom-settings.ts b/docs/xflow/schema/custom-settings.ts new file mode 100644 index 000000000..58e00b49c --- /dev/null +++ b/docs/xflow/schema/custom-settings.ts @@ -0,0 +1,150 @@ +export default [ + { + title: '开始', + type: 'Start', + hidden: true, + targetHandleHidden: true, + icon: { + type: 'icon-start', + bgColor: '#17B26A', + }, + settingSchema: { + type: 'object', + properties: { + input: { + title: '变量一', + type: 'string', + widget: 'customWidget', + }, + } + }, + }, + { + title: '结束', + type: 'End', + hidden: true, + sourceHandleHidden: true, + icon: { + type: 'icon-end', + bgColor: '#F79009', + }, + settingSchema: { + type: "object", + properties: { + input: { + title: '变量一', + type: 'string', + widget: 'input', + }, + select: { + title: '变量二', + type: 'string', + widget: 'select', + props: { + options: [ + { label: 'a', value: 'a' }, + { label: 'b', value: 'b' }, + { label: 'c', value: 'c' }, + ], + }, + }, + } + } + }, + { + title: 'LLM', + type: 'LLM', + description: '调用大语言模型回答问题或者对自然语言进行处理', + icon: { + type: 'icon-model', + bgColor: '#6172F3', + }, + }, + { + title: 'Prompt', + type: 'Prompt', + description: '通过精心设计提示词,提升大语言模型回答效果', + icon: { + type: 'icon-prompt', + bgColor: '#17B26A', + }, + }, + { + title: '知识库', + type: 'knowledge', + description: '允许你从知识库中查询与用户问题相关的文本内容', + icon: { + type: 'icon-knowledge', + bgColor: '#6172F3', + }, + }, + { + title: 'Switch', + type: 'Switch', + description: '允许你根据 if/else 条件将 workflow 拆分成两个分支', + icon: { + type: 'icon-switch', + bgColor: '#06AED4', + }, + }, + { + title: 'HSF', + type: 'hsf', + description: '允许通过 HSF 协议发送服务器请求', + icon: { + type: 'icon-hsf', + bgColor: '#875BF7', + }, + }, + { + title: 'Http', + type: 'http', + description: '允许通过 HTTP 协议发送服务器请求', + icon: { + type: 'icon-http', + bgColor: '#875BF7', + }, + }, + { + title: '代码执行', + type: 'Code', + description: '执行一段 Groovy 或 Python 或 NodeJS 代码实现自定义逻辑', + icon: { + type: 'icon-code', + bgColor: '#2E90FA', + }, + }, + { + title: '工具', + type: 'tool', + description: '允许使用工具能力', + icon: { + type: 'icon-gongju', + bgColor: '#2E90FA', + }, + }, + { + title: '工具', + type: '_group', + items: [ + { + title: '代码执行', + type: 'Code', + description: '执行一段 Groovy 或 Python 或 NodeJS 代码实现自定义逻辑', + icon: { + type: 'icon-code', + bgColor: '#2E90FA', + }, + }, + { + title: '工具', + type: 'tool', + description: '允许使用工具能力', + icon: { + type: 'icon-gongju', + bgColor: '#2E90FA', + }, + }, + ], + }, +]; diff --git a/docs/xflow/schema/settings.ts b/docs/xflow/schema/settings.ts index 32e942ac1..72f080f69 100644 --- a/docs/xflow/schema/settings.ts +++ b/docs/xflow/schema/settings.ts @@ -8,7 +8,7 @@ export default [ type: 'icon-start', bgColor: '#17B26A', }, - schema: { + settingSchema: { type: 'object', properties: { input: { @@ -77,7 +77,7 @@ export default [ type: 'icon-end', bgColor: '#F79009', }, - schema: { + settingSchema: { type: "object", properties: { input: { diff --git a/packages/x-flow/src/components/NodeEditor/index.tsx b/packages/x-flow/src/components/NodeEditor/index.tsx index 11f7a59ed..910e3e45e 100644 --- a/packages/x-flow/src/components/NodeEditor/index.tsx +++ b/packages/x-flow/src/components/NodeEditor/index.tsx @@ -29,7 +29,7 @@ const NodeEditor: FC = (props: any) => { ); useEffect(() => { - if (nodeSetting?.schema) { + if (nodeSetting?.settingSchema) { form.resetFields(); form.setValues(data || {}); } else if (nodeSetting?.settingWidget) { @@ -37,7 +37,6 @@ const NodeEditor: FC = (props: any) => { } else { } - console.log('data', data); }, [JSON.stringify(data), id]); const handleNodeValueChange = debounce((data: any) => { @@ -64,19 +63,17 @@ const NodeEditor: FC = (props: any) => { { - console.log('onChange000', values); setCustomVal(values); // onChange({ id, values: { ...values } }); handleNodeValueChange({ ...values }); }} /> ); - } else if (nodeSetting?.schema) { + } else if (nodeSetting?.settingSchema) { return ( Date: Fri, 29 Nov 2024 14:27:05 +0800 Subject: [PATCH 24/38] =?UTF-8?q?fix:=E4=BC=98=E5=8C=96=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E8=8A=82=E7=82=B9=E6=8C=89=E9=92=AE=E5=92=8C=E8=BF=9E=E7=BA=BF?= =?UTF-8?q?=E6=8C=89=E9=92=AE=E5=86=B2=E7=AA=81=E7=9A=84=E9=97=AE=E9=A2=98?= =?UTF-8?q?=EF=BC=8C=E8=BF=9E=E7=BA=BF=E6=9B=B4=E4=B8=9D=E6=BB=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/CustomNode/index.less | 15 +++- .../src/components/CustomNode/index.tsx | 89 +++++++++++-------- .../src/components/NodesPopover/index.tsx | 41 ++++++--- 3 files changed, 90 insertions(+), 55 deletions(-) diff --git a/packages/x-flow/src/components/CustomNode/index.less b/packages/x-flow/src/components/CustomNode/index.less index 2e3ee847b..d967276d5 100644 --- a/packages/x-flow/src/components/CustomNode/index.less +++ b/packages/x-flow/src/components/CustomNode/index.less @@ -9,13 +9,13 @@ } .react-flow__handle { - width: 32px; - height: 32px; + width: 30px; + height: 30px; background: transparent; border-radius: 0; border: none; } - + .react-flow__handle::after { content: ''; --tw-bg-opacity: 1; @@ -26,6 +26,12 @@ margin: 11px 0 8px 15px; } + .react-flow__handle:hover { + .xflow-node-add-box { + scale: 130%; + } + } + .xflow-node-add-box { position: absolute; top: 8px; @@ -37,6 +43,8 @@ align-items: center; justify-content: center; background-color: #2970ff; + pointer-events: none; + transition: all 0.3s; } } @@ -59,4 +67,3 @@ display: none; } } - diff --git a/packages/x-flow/src/components/CustomNode/index.tsx b/packages/x-flow/src/components/CustomNode/index.tsx index 779bb6b0a..de9d12534 100644 --- a/packages/x-flow/src/components/CustomNode/index.tsx +++ b/packages/x-flow/src/components/CustomNode/index.tsx @@ -1,44 +1,45 @@ -import React, { memo, useContext, useState } from 'react'; -import { Tooltip } from 'antd'; import { PlusOutlined } from '@ant-design/icons'; +import { Handle, Position, useReactFlow } from '@xyflow/react'; +import { Tooltip } from 'antd'; import classNames from 'classnames'; import produce from 'immer'; +import React, { memo, useContext, useRef, useState } from 'react'; import { useShallow } from 'zustand/react/shallow'; -import { Handle, Position, useReactFlow } from '@xyflow/react'; -import { capitalize, uuid } from '../../utils'; -import useStore from '../../models/store'; import { ConfigContext } from '../../models/context'; +import useStore from '../../models/store'; +import { capitalize, uuid } from '../../utils'; import NodeSelectPopover from '../NodesPopover'; import './index.less'; export default memo((props: any) => { const { id, type, data, layout, isConnectable, selected, onClick } = props; const { widgets, settingMap } = useContext(ConfigContext); - const NodeWidget = widgets[`${capitalize(type)}Node`] || widgets['CommonNode']; - + const NodeWidget = + widgets[`${capitalize(type)}Node`] || widgets['CommonNode']; const [isHovered, setIsHovered] = useState(false); + const [isShowTooltip, setIsShowTooltip] = useState(false); + const popoverRef = useRef(null); + const [openNodeSelectPopover, setOpenNodeSelectPopover] = useState(false); + const reactflow = useReactFlow(); - const { - edges, - nodes, - setNodes, - setEdges, - mousePosition, - } = useStore( + const { edges, nodes, setNodes, setEdges, mousePosition } = useStore( useShallow((state: any) => ({ nodes: state.nodes, edges: state.edges, mousePosition: state.mousePosition, setNodes: state.setNodes, setEdges: state.setEdges, - onEdgesChange: state.onEdgesChange + onEdgesChange: state.onEdgesChange, })) ); // 增加节点并进行联系 const handleAddNode = (data: any) => { const { screenToFlowPosition } = reactflow; - const { x, y } = screenToFlowPosition({ x: mousePosition.pageX + 100, y: mousePosition.pageY + 100 }); + const { x, y } = screenToFlowPosition({ + x: mousePosition.pageX + 100, + y: mousePosition.pageY + 100, + }); const targetId = uuid(); const newNodes = produce(nodes, (draft: any) => { @@ -46,7 +47,7 @@ export default memo((props: any) => { id: targetId, type: 'custom', data, - position: { x, y } + position: { x, y }, }); }); const newEdges = produce(edges, (draft: any) => { @@ -54,7 +55,7 @@ export default memo((props: any) => { id: uuid(), source: id, target: targetId, - }) + }); }); setNodes(newNodes); setEdges(newEdges); @@ -71,14 +72,14 @@ export default memo((props: any) => {
setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} > {!settingMap?.[type]?.targetHandleHidden && ( @@ -91,29 +92,43 @@ export default memo((props: any) => { /> {!settingMap?.[type]?.sourceHandleHidden && ( setIsShowTooltip(true)} + onMouseLeave={() => setIsShowTooltip(false)} + onClick={e => { + e.stopPropagation(); + popoverRef?.current?.changeOpen(true); + setIsShowTooltip(false); + setOpenNodeSelectPopover(true); + }} > - {(selected || isHovered) && ( -
- - - - - + {(selected || isHovered || openNodeSelectPopover) && ( +
+ setOpenNodeSelectPopover(val)} + > + + + +
)} )}
); -}) +}); diff --git a/packages/x-flow/src/components/NodesPopover/index.tsx b/packages/x-flow/src/components/NodesPopover/index.tsx index 2cf684ae8..726ce81f3 100644 --- a/packages/x-flow/src/components/NodesPopover/index.tsx +++ b/packages/x-flow/src/components/NodesPopover/index.tsx @@ -1,25 +1,37 @@ - -import React, { useCallback, useState, useRef, useContext } from 'react'; -import { Popover } from 'antd'; -import { useShallow } from 'zustand/react/shallow'; import { useClickAway } from 'ahooks'; -import useStore from '../../models/store'; +import { Popover } from 'antd'; +import React, { + forwardRef, + useCallback, + useContext, + useImperativeHandle, + useRef, + useState, +} from 'react'; import { ConfigContext } from '../../models/context'; import NodesMenu from '../NodesMenu'; -export default (props: any) => { - const { addNode, children } = props; +export default forwardRef((props: any, popoverRef) => { + const { addNode, children, onNodeSelectPopoverChange } = props; const ref = useRef(null); const closeRef: any = useRef(null); const [open, setOpen] = useState(false); const { settings, nodeSelector } = useContext(ConfigContext); - const { showSearch, popoverProps = { placement: 'top' } } = nodeSelector || {}; + const { showSearch, popoverProps = { placement: 'top' } } = + nodeSelector || {}; + + useImperativeHandle(popoverRef, () => ({ + changeOpen: val => { + setOpen(val); + }, + })); useClickAway(() => { if (closeRef.current) { setOpen(false); + onNodeSelectPopoverChange && onNodeSelectPopoverChange(false); closeRef.current = false; } }, ref); @@ -27,6 +39,7 @@ export default (props: any) => { const handCreateNode = useCallback(({ type }) => { addNode({ _nodeType: type }); setOpen(false); + onNodeSelectPopoverChange && onNodeSelectPopoverChange(false); }, []); return ( @@ -35,24 +48,24 @@ export default (props: any) => { arrow={false} overlayInnerStyle={{ padding: '12px 6px' }} {...popoverProps} - trigger='click' + trigger="click" open={open} onOpenChange={() => { setTimeout(() => { closeRef.current = true; setOpen(true); - }, 50) + }, 50); }} - content={( + content={ - )} + } > {children} ); -} \ No newline at end of file +}); From 61370d9f84aa562adff422675de393af7c8645f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=98=94=E6=A2=A6?= Date: Fri, 29 Nov 2024 16:00:22 +0800 Subject: [PATCH 25/38] =?UTF-8?q?API=E6=96=87=E6=A1=A3=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E5=B8=83=E5=B1=80=E6=96=B9=E5=90=91+=E8=8A=82=E7=82=B9?= =?UTF-8?q?=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/xflow/api.md | 30 ++-- docs/xflow/demo/basic/setting.tsx | 100 +++++++++++ docs/xflow/demo/layout/LR/index.tsx | 65 +++++++ docs/xflow/demo/layout/LR/setting.tsx | 199 +++++++++++++++++++++ docs/xflow/demo/layout/TB/index.tsx | 73 ++++++++ docs/xflow/demo/layout/TB/setting.tsx | 199 +++++++++++++++++++++ docs/xflow/demo/nodeSetting/index.tsx | 219 ++++++++++++++++++++++++ docs/xflow/demo/nodeSetting/setting.tsx | 199 +++++++++++++++++++++ docs/xflow/index.md | 2 +- docs/xflow/layout.md | 19 ++ docs/xflow/nodeSetting.md | 13 ++ 11 files changed, 1101 insertions(+), 17 deletions(-) create mode 100644 docs/xflow/demo/layout/LR/index.tsx create mode 100644 docs/xflow/demo/layout/LR/setting.tsx create mode 100644 docs/xflow/demo/layout/TB/index.tsx create mode 100644 docs/xflow/demo/layout/TB/setting.tsx create mode 100644 docs/xflow/demo/nodeSetting/index.tsx create mode 100644 docs/xflow/demo/nodeSetting/setting.tsx create mode 100644 docs/xflow/layout.md create mode 100644 docs/xflow/nodeSetting.md diff --git a/docs/xflow/api.md b/docs/xflow/api.md index fdfbb5703..92ab7458d 100644 --- a/docs/xflow/api.md +++ b/docs/xflow/api.md @@ -7,15 +7,13 @@ title: API ## XFlow - - -| 属性 | 描述 | 类型 | 默认值 | -| ------------- | ------------------------------------ | --------------------------------------------------------- | ------ | -| initialValues | 初始的节点和边数据 | `{nodes:any[],edges:any}` | - | - | -| layout | 节点布局的方向 | `LR \| TB` | - | - | -| widgets | 自定义组件 | `Record` | - | - | -| settings | 节点配置,定义页面中可拖动的节点配置 | ( [TNodeGroup](#tnodegroup) \| [TNodeItem](#tnodeitem))[] | | -| nodeSelector | 节点选择器配置,可控制节点的可搜索性 | `TNodeSelector` | | +| 属性 | 描述 | 类型 | 默认值 | +| ------------- | ------------------------------------ | ----------------------------------------------------------- | ------ | +| initialValues | 初始的节点和边数据 | `{nodes:any[],edges:any[]}` | - | - | +| layout | 节点布局的方向 | `LR \| TB` | LR | - | +| widgets | 自定义组件 | `Record` | - | - | +| settings | 节点配置,定义页面中可拖动的节点配置 | ( [TNodeGroup](#tnodegroup) \| [TNodeItem](#tnodeitem) )[ ] | | +| nodeSelector | 节点选择器配置,可控制节点的可搜索性 | `TNodeSelector` | | ### TNodeGroup @@ -24,7 +22,7 @@ title: API | 属性 | 描述 | 类型 | 默认值 | | ----- | ------------ | ------------- | ------ | | title | 分组名称 | `string` | | -| type | 分组类型 | `_group` | | +| type | 分组类型 | `_group` | _group | | items | 节点配置信息 | `TNodeItem[]` | | ## TNodeItem @@ -34,17 +32,17 @@ title: API | 属性 | 描述 | 类型 | 默认值 | | ------------------ | ------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | | title | 节点名称 | `string` | | -| type | 节点类型 | `string` | | +| type | 节点类型 | `string` | | | hidden | 是否在配置面板中显示节点 | `boolean` | false | | targetHandleHidden | 是否隐藏左侧输入连接头 | `boolean` | false | | sourceHandleHidden | 是否隐藏右侧输出连接头 | `boolean` | false | -| icon | 节点的图标配置 | `{type:string;bgColor:string}` | | +| icon | 节点的图标配置 | `{type:string;bgColor:string}` | | | settingSchema | 节点的业务配置信息,详见[form-render 文档](/form-render/api-schema) | SchemaBase | | | settingWidget | 自定义节点的业务配置组件 | `string` | | ## TNodeSelector -| 属性 | 描述 | 类型 | 默认值 | -| ---------- | ---------------- | --------------------------------------------------------- | ------ | -| showSearch | 节点是否可被搜索 | `boolean` | false | -| items | 节点配置 | ( [TNodeGroup](#tnodegroup) \| [TNodeItem](#tnodeitem))[] | | +| 属性 | 描述 | 类型 | 默认值 | +| ---------- | ---------------- | ----------------------------------------------------------- | ------ | +| showSearch | 节点是否可被搜索 | `boolean` | false | +| items | 节点配置 | ( [TNodeGroup](#tnodegroup) \| [TNodeItem](#tnodeitem) )[ ] | | diff --git a/docs/xflow/demo/basic/setting.tsx b/docs/xflow/demo/basic/setting.tsx index 72f080f69..937413778 100644 --- a/docs/xflow/demo/basic/setting.tsx +++ b/docs/xflow/demo/basic/setting.tsx @@ -108,6 +108,16 @@ export default [ type: 'icon-model', bgColor: '#6172F3', }, + settingSchema: { + type: "object", + properties: { + input: { + title: '变量一', + type: 'string', + widget: 'input', + }, + } + } }, { title: 'Prompt', @@ -117,6 +127,16 @@ export default [ type: 'icon-prompt', bgColor: '#17B26A', }, + settingSchema: { + type: "object", + properties: { + input: { + title: '变量一', + type: 'string', + widget: 'input', + }, + } + } }, { title: '知识库', @@ -126,6 +146,16 @@ export default [ type: 'icon-knowledge', bgColor: '#6172F3', }, + settingSchema: { + type: "object", + properties: { + input: { + title: '变量一', + type: 'string', + widget: 'input', + }, + } + } }, { title: 'Switch', @@ -135,6 +165,16 @@ export default [ type: 'icon-switch', bgColor: '#06AED4', }, + settingSchema: { + type: "object", + properties: { + input: { + title: '变量一', + type: 'string', + widget: 'input', + }, + } + } }, { title: 'HSF', @@ -144,6 +184,16 @@ export default [ type: 'icon-hsf', bgColor: '#875BF7', }, + settingSchema: { + type: "object", + properties: { + input: { + title: '变量一', + type: 'string', + widget: 'input', + }, + } + } }, { title: 'Http', @@ -153,6 +203,16 @@ export default [ type: 'icon-http', bgColor: '#875BF7', }, + settingSchema: { + type: "object", + properties: { + input: { + title: '变量一', + type: 'string', + widget: 'input', + }, + } + } }, { title: '代码执行', @@ -162,6 +222,16 @@ export default [ type: 'icon-code', bgColor: '#2E90FA', }, + settingSchema: { + type: "object", + properties: { + input: { + title: '变量一', + type: 'string', + widget: 'input', + }, + } + } }, { title: '工具', @@ -171,6 +241,16 @@ export default [ type: 'icon-gongju', bgColor: '#2E90FA', }, + settingSchema: { + type: "object", + properties: { + input: { + title: '变量一', + type: 'string', + widget: 'input', + }, + } + } }, { title: '工具', @@ -184,6 +264,16 @@ export default [ type: 'icon-code', bgColor: '#2E90FA', }, + settingSchema: { + type: "object", + properties: { + input: { + title: '变量一', + type: 'string', + widget: 'input', + }, + } + } }, { title: '工具', @@ -193,6 +283,16 @@ export default [ type: 'icon-gongju', bgColor: '#2E90FA', }, + settingSchema: { + type: "object", + properties: { + input: { + title: '变量一', + type: 'string', + widget: 'input', + }, + } + } }, ], }, diff --git a/docs/xflow/demo/layout/LR/index.tsx b/docs/xflow/demo/layout/LR/index.tsx new file mode 100644 index 000000000..c95655c45 --- /dev/null +++ b/docs/xflow/demo/layout/LR/index.tsx @@ -0,0 +1,65 @@ +import XFlow from '@xrenders/xflow'; +import settings from './setting'; +import React from 'react'; + +export default () => { + const nodes = [ + { + type: 'Start', + id: '1', + position: { x: -35, y: 268 }, + }, + { + type: 'Switch', + id: '2', + position: { x: 277.5, y: 268 }, + }, + { + type: 'Code', + id: '3', + position: { x: 675, y: 123.75 }, + }, + { + type: 'tool', + id: '4', + position: { x: 686.25, y: 495 }, + }, + { + type: 'End', + id: '5', + position: { x: 1176.2499999999998, y: 281.25 }, + }, + ]; + const edges = [ + { source: '1', target: '2', id: 'e1-2' }, + { + source: '2', + target: '3', + id: 'e2-3', + }, + { source: '2', target: '4', id: 'e2-4' }, + { + source: '3', + target: '5', + id: 'e3-5', + }, + { + source: '4', + target: '5', + id: 'e4-5', + }, + ]; + + return ( +
+ +
+ ); +}; diff --git a/docs/xflow/demo/layout/LR/setting.tsx b/docs/xflow/demo/layout/LR/setting.tsx new file mode 100644 index 000000000..72f080f69 --- /dev/null +++ b/docs/xflow/demo/layout/LR/setting.tsx @@ -0,0 +1,199 @@ +export default [ + { + title: '开始', + type: 'Start', + hidden: true, + targetHandleHidden: true, + icon: { + type: 'icon-start', + bgColor: '#17B26A', + }, + settingSchema: { + type: 'object', + properties: { + input: { + title: '变量一', + type: 'string', + widget: 'input', + }, + select: { + title: '变量二', + type: 'string', + widget: 'select', + props: { + options: [ + { label: 'a', value: 'a' }, + { label: 'b', value: 'b' }, + { label: 'c', value: 'c' }, + ], + }, + }, + radio1: { + title: '点击单选', + type: 'string', + widget: 'radio', + props: { + options: [ + { label: '早', value: 'a' }, + { label: '中', value: 'b' }, + { label: '晚', value: 'c' } + ] + } + }, + textarea1: { + title: '长文本', + type: 'string', + widget: 'textArea' + }, + date1: { + title: '日期选择', + type: 'string', + widget: 'datePicker' + }, + dateRange1: { + title: '日期范围', + type: 'range', + widget: 'dateRange' + }, + time1: { + title: '时间选择', + type: 'string', + widget: 'timePicker' + }, + timeRange1: { + title: '时间范围', + type: 'range', + widget: 'timeRange' + }, + }, + }, + }, + { + title: '结束', + type: 'End', + hidden: true, + sourceHandleHidden: true, + icon: { + type: 'icon-end', + bgColor: '#F79009', + }, + settingSchema: { + type: "object", + properties: { + input: { + title: '变量一', + type: 'string', + widget: 'input', + }, + select: { + title: '变量二', + type: 'string', + widget: 'select', + props: { + options: [ + { label: 'a', value: 'a' }, + { label: 'b', value: 'b' }, + { label: 'c', value: 'c' }, + ], + }, + }, + } + } + }, + { + title: 'LLM', + type: 'LLM', + description: '调用大语言模型回答问题或者对自然语言进行处理', + icon: { + type: 'icon-model', + bgColor: '#6172F3', + }, + }, + { + title: 'Prompt', + type: 'Prompt', + description: '通过精心设计提示词,提升大语言模型回答效果', + icon: { + type: 'icon-prompt', + bgColor: '#17B26A', + }, + }, + { + title: '知识库', + type: 'knowledge', + description: '允许你从知识库中查询与用户问题相关的文本内容', + icon: { + type: 'icon-knowledge', + bgColor: '#6172F3', + }, + }, + { + title: 'Switch', + type: 'Switch', + description: '允许你根据 if/else 条件将 workflow 拆分成两个分支', + icon: { + type: 'icon-switch', + bgColor: '#06AED4', + }, + }, + { + title: 'HSF', + type: 'hsf', + description: '允许通过 HSF 协议发送服务器请求', + icon: { + type: 'icon-hsf', + bgColor: '#875BF7', + }, + }, + { + title: 'Http', + type: 'http', + description: '允许通过 HTTP 协议发送服务器请求', + icon: { + type: 'icon-http', + bgColor: '#875BF7', + }, + }, + { + title: '代码执行', + type: 'Code', + description: '执行一段 Groovy 或 Python 或 NodeJS 代码实现自定义逻辑', + icon: { + type: 'icon-code', + bgColor: '#2E90FA', + }, + }, + { + title: '工具', + type: 'tool', + description: '允许使用工具能力', + icon: { + type: 'icon-gongju', + bgColor: '#2E90FA', + }, + }, + { + title: '工具', + type: '_group', + items: [ + { + title: '代码执行', + type: 'Code', + description: '执行一段 Groovy 或 Python 或 NodeJS 代码实现自定义逻辑', + icon: { + type: 'icon-code', + bgColor: '#2E90FA', + }, + }, + { + title: '工具', + type: 'tool', + description: '允许使用工具能力', + icon: { + type: 'icon-gongju', + bgColor: '#2E90FA', + }, + }, + ], + }, +]; diff --git a/docs/xflow/demo/layout/TB/index.tsx b/docs/xflow/demo/layout/TB/index.tsx new file mode 100644 index 000000000..9b717eb3b --- /dev/null +++ b/docs/xflow/demo/layout/TB/index.tsx @@ -0,0 +1,73 @@ +import XFlow from '@xrenders/xflow'; +import settings from './setting'; +import React from 'react'; + +export default () => { + const nodes = [ + { + type: 'Start', + id: '1', + position: { x: 327.5, y: -14.5 }, + }, + { + type: 'Switch', + id: '2', + position: { x: 328.75, y: 108 }, + }, + { + type: 'Code', + id: '3', + position: { x: 638.75, y: 247.5 }, + }, + { + type: 'tool', + id: '4', + position: { x: 75.00000000000003, y: 261.25 }, + }, + { + type: 'End', + id: '5', + position: { x: 360, y: 501.25 }, + }, + ]; + const edges = [ + { + source: '1', + target: '2', + id: 'e1-2', + }, + { + source: '2', + target: '3', + id: 'e2-3', + }, + { + source: '2', + target: '4', + id: 'e2-4', + }, + { + source: '3', + target: '5', + id: 'e3-5', + }, + { + source: '4', + target: '5', + id: 'e4-5', + }, + ]; + + return ( +
+ +
+ ); +}; diff --git a/docs/xflow/demo/layout/TB/setting.tsx b/docs/xflow/demo/layout/TB/setting.tsx new file mode 100644 index 000000000..72f080f69 --- /dev/null +++ b/docs/xflow/demo/layout/TB/setting.tsx @@ -0,0 +1,199 @@ +export default [ + { + title: '开始', + type: 'Start', + hidden: true, + targetHandleHidden: true, + icon: { + type: 'icon-start', + bgColor: '#17B26A', + }, + settingSchema: { + type: 'object', + properties: { + input: { + title: '变量一', + type: 'string', + widget: 'input', + }, + select: { + title: '变量二', + type: 'string', + widget: 'select', + props: { + options: [ + { label: 'a', value: 'a' }, + { label: 'b', value: 'b' }, + { label: 'c', value: 'c' }, + ], + }, + }, + radio1: { + title: '点击单选', + type: 'string', + widget: 'radio', + props: { + options: [ + { label: '早', value: 'a' }, + { label: '中', value: 'b' }, + { label: '晚', value: 'c' } + ] + } + }, + textarea1: { + title: '长文本', + type: 'string', + widget: 'textArea' + }, + date1: { + title: '日期选择', + type: 'string', + widget: 'datePicker' + }, + dateRange1: { + title: '日期范围', + type: 'range', + widget: 'dateRange' + }, + time1: { + title: '时间选择', + type: 'string', + widget: 'timePicker' + }, + timeRange1: { + title: '时间范围', + type: 'range', + widget: 'timeRange' + }, + }, + }, + }, + { + title: '结束', + type: 'End', + hidden: true, + sourceHandleHidden: true, + icon: { + type: 'icon-end', + bgColor: '#F79009', + }, + settingSchema: { + type: "object", + properties: { + input: { + title: '变量一', + type: 'string', + widget: 'input', + }, + select: { + title: '变量二', + type: 'string', + widget: 'select', + props: { + options: [ + { label: 'a', value: 'a' }, + { label: 'b', value: 'b' }, + { label: 'c', value: 'c' }, + ], + }, + }, + } + } + }, + { + title: 'LLM', + type: 'LLM', + description: '调用大语言模型回答问题或者对自然语言进行处理', + icon: { + type: 'icon-model', + bgColor: '#6172F3', + }, + }, + { + title: 'Prompt', + type: 'Prompt', + description: '通过精心设计提示词,提升大语言模型回答效果', + icon: { + type: 'icon-prompt', + bgColor: '#17B26A', + }, + }, + { + title: '知识库', + type: 'knowledge', + description: '允许你从知识库中查询与用户问题相关的文本内容', + icon: { + type: 'icon-knowledge', + bgColor: '#6172F3', + }, + }, + { + title: 'Switch', + type: 'Switch', + description: '允许你根据 if/else 条件将 workflow 拆分成两个分支', + icon: { + type: 'icon-switch', + bgColor: '#06AED4', + }, + }, + { + title: 'HSF', + type: 'hsf', + description: '允许通过 HSF 协议发送服务器请求', + icon: { + type: 'icon-hsf', + bgColor: '#875BF7', + }, + }, + { + title: 'Http', + type: 'http', + description: '允许通过 HTTP 协议发送服务器请求', + icon: { + type: 'icon-http', + bgColor: '#875BF7', + }, + }, + { + title: '代码执行', + type: 'Code', + description: '执行一段 Groovy 或 Python 或 NodeJS 代码实现自定义逻辑', + icon: { + type: 'icon-code', + bgColor: '#2E90FA', + }, + }, + { + title: '工具', + type: 'tool', + description: '允许使用工具能力', + icon: { + type: 'icon-gongju', + bgColor: '#2E90FA', + }, + }, + { + title: '工具', + type: '_group', + items: [ + { + title: '代码执行', + type: 'Code', + description: '执行一段 Groovy 或 Python 或 NodeJS 代码实现自定义逻辑', + icon: { + type: 'icon-code', + bgColor: '#2E90FA', + }, + }, + { + title: '工具', + type: 'tool', + description: '允许使用工具能力', + icon: { + type: 'icon-gongju', + bgColor: '#2E90FA', + }, + }, + ], + }, +]; diff --git a/docs/xflow/demo/nodeSetting/index.tsx b/docs/xflow/demo/nodeSetting/index.tsx new file mode 100644 index 000000000..5deb83592 --- /dev/null +++ b/docs/xflow/demo/nodeSetting/index.tsx @@ -0,0 +1,219 @@ +import XFlow from '@xrenders/xflow'; +// import settings from './setting'; +import React from 'react'; + +const settings = [ + { + title: '开始', // 节点名称 + type: 'Start', + hidden: true, + targetHandleHidden: true, + icon: { // 图标描述 + type: 'icon-start', // icon-font + bgColor: '#17B26A', // 图标背景颜色 + }, + settingSchema: { // 自定义节点配置 + type: 'object', + properties: { + input: { + title: '变量一', + type: 'string', + widget: 'input', + }, + select: { + title: '变量二', + type: 'string', + widget: 'select', + props: { + options: [ + { label: 'a', value: 'a' }, + { label: 'b', value: 'b' }, + { label: 'c', value: 'c' }, + ], + }, + }, + radio1: { + title: '点击单选', + type: 'string', + widget: 'radio', + props: { + options: [ + { label: '早', value: 'a' }, + { label: '中', value: 'b' }, + { label: '晚', value: 'c' } + ] + } + }, + textarea1: { + title: '长文本', + type: 'string', + widget: 'textArea' + }, + date1: { + title: '日期选择', + type: 'string', + widget: 'datePicker' + }, + dateRange1: { + title: '日期范围', + type: 'range', + widget: 'dateRange' + }, + time1: { + title: '时间选择', + type: 'string', + widget: 'timePicker' + }, + timeRange1: { + title: '时间范围', + type: 'range', + widget: 'timeRange' + }, + }, + }, + }, + { + title: '结束', + type: 'End', + hidden: true, + sourceHandleHidden: true, + icon: { + type: 'icon-end', + bgColor: '#F79009', + }, + settingSchema: { + type: "object", + properties: { + input: { + title: '变量一', + type: 'string', + widget: 'input', + }, + select: { + title: '变量二', + type: 'string', + widget: 'select', + props: { + options: [ + { label: 'a', value: 'a' }, + { label: 'b', value: 'b' }, + { label: 'c', value: 'c' }, + ], + }, + }, + } + } + }, + { + title: 'Switch', + type: 'Switch', + description: '允许你根据 if/else 条件将 workflow 拆分成两个分支', + icon: { + type: 'icon-switch', + bgColor: '#06AED4', + }, + }, + { + title: '代码执行', + type: 'Code', + description: '执行一段 Groovy 或 Python 或 NodeJS 代码实现自定义逻辑', + icon: { + type: 'icon-code', + bgColor: '#2E90FA', + }, + }, + { + title: '工具', + type: 'tool', + description: '允许使用工具能力', + icon: { + type: 'icon-gongju', + bgColor: '#2E90FA', + }, + }, + { + title: '工具', + type: '_group', // 节点分组 + items: [ + { + title: '代码执行', + type: 'Code', + description: '执行一段 Groovy 或 Python 或 NodeJS 代码实现自定义逻辑', + icon: { + type: 'icon-code', + bgColor: '#2E90FA', + }, + }, + { + title: '工具', + type: 'tool', + description: '允许使用工具能力', + icon: { + type: 'icon-gongju', + bgColor: '#2E90FA', + }, + }, + ], + }, +] +export default () => { + const nodes = [ + { + type: 'Start', + id: '1', + position: { x: -35, y: 268 }, + }, + { + type: 'Switch', + id: '2', + position: { x: 277.5, y: 268 }, + }, + { + type: 'Code', + id: '3', + position: { x: 675, y: 123.75 }, + }, + { + type: 'tool', + id: '4', + position: { x: 686.25, y: 495 }, + }, + { + type: 'End', + id: '5', + position: { x: 1176.2499999999998, y: 281.25 }, + }, + ]; + const edges = [ + { source: '1', target: '2', id: 'e1-2' }, + { + source: '2', + target: '3', + id: 'e2-3', + }, + { source: '2', target: '4', id: 'e2-4' }, + { + source: '3', + target: '5', + id: 'e3-5', + }, + { + source: '4', + target: '5', + id: 'e4-5', + }, + ]; + + return ( +
+ +
+ ); +}; diff --git a/docs/xflow/demo/nodeSetting/setting.tsx b/docs/xflow/demo/nodeSetting/setting.tsx new file mode 100644 index 000000000..72f080f69 --- /dev/null +++ b/docs/xflow/demo/nodeSetting/setting.tsx @@ -0,0 +1,199 @@ +export default [ + { + title: '开始', + type: 'Start', + hidden: true, + targetHandleHidden: true, + icon: { + type: 'icon-start', + bgColor: '#17B26A', + }, + settingSchema: { + type: 'object', + properties: { + input: { + title: '变量一', + type: 'string', + widget: 'input', + }, + select: { + title: '变量二', + type: 'string', + widget: 'select', + props: { + options: [ + { label: 'a', value: 'a' }, + { label: 'b', value: 'b' }, + { label: 'c', value: 'c' }, + ], + }, + }, + radio1: { + title: '点击单选', + type: 'string', + widget: 'radio', + props: { + options: [ + { label: '早', value: 'a' }, + { label: '中', value: 'b' }, + { label: '晚', value: 'c' } + ] + } + }, + textarea1: { + title: '长文本', + type: 'string', + widget: 'textArea' + }, + date1: { + title: '日期选择', + type: 'string', + widget: 'datePicker' + }, + dateRange1: { + title: '日期范围', + type: 'range', + widget: 'dateRange' + }, + time1: { + title: '时间选择', + type: 'string', + widget: 'timePicker' + }, + timeRange1: { + title: '时间范围', + type: 'range', + widget: 'timeRange' + }, + }, + }, + }, + { + title: '结束', + type: 'End', + hidden: true, + sourceHandleHidden: true, + icon: { + type: 'icon-end', + bgColor: '#F79009', + }, + settingSchema: { + type: "object", + properties: { + input: { + title: '变量一', + type: 'string', + widget: 'input', + }, + select: { + title: '变量二', + type: 'string', + widget: 'select', + props: { + options: [ + { label: 'a', value: 'a' }, + { label: 'b', value: 'b' }, + { label: 'c', value: 'c' }, + ], + }, + }, + } + } + }, + { + title: 'LLM', + type: 'LLM', + description: '调用大语言模型回答问题或者对自然语言进行处理', + icon: { + type: 'icon-model', + bgColor: '#6172F3', + }, + }, + { + title: 'Prompt', + type: 'Prompt', + description: '通过精心设计提示词,提升大语言模型回答效果', + icon: { + type: 'icon-prompt', + bgColor: '#17B26A', + }, + }, + { + title: '知识库', + type: 'knowledge', + description: '允许你从知识库中查询与用户问题相关的文本内容', + icon: { + type: 'icon-knowledge', + bgColor: '#6172F3', + }, + }, + { + title: 'Switch', + type: 'Switch', + description: '允许你根据 if/else 条件将 workflow 拆分成两个分支', + icon: { + type: 'icon-switch', + bgColor: '#06AED4', + }, + }, + { + title: 'HSF', + type: 'hsf', + description: '允许通过 HSF 协议发送服务器请求', + icon: { + type: 'icon-hsf', + bgColor: '#875BF7', + }, + }, + { + title: 'Http', + type: 'http', + description: '允许通过 HTTP 协议发送服务器请求', + icon: { + type: 'icon-http', + bgColor: '#875BF7', + }, + }, + { + title: '代码执行', + type: 'Code', + description: '执行一段 Groovy 或 Python 或 NodeJS 代码实现自定义逻辑', + icon: { + type: 'icon-code', + bgColor: '#2E90FA', + }, + }, + { + title: '工具', + type: 'tool', + description: '允许使用工具能力', + icon: { + type: 'icon-gongju', + bgColor: '#2E90FA', + }, + }, + { + title: '工具', + type: '_group', + items: [ + { + title: '代码执行', + type: 'Code', + description: '执行一段 Groovy 或 Python 或 NodeJS 代码实现自定义逻辑', + icon: { + type: 'icon-code', + bgColor: '#2E90FA', + }, + }, + { + title: '工具', + type: 'tool', + description: '允许使用工具能力', + icon: { + type: 'icon-gongju', + bgColor: '#2E90FA', + }, + }, + ], + }, +]; diff --git a/docs/xflow/index.md b/docs/xflow/index.md index 72c9ad7a6..54be7ec80 100644 --- a/docs/xflow/index.md +++ b/docs/xflow/index.md @@ -6,7 +6,7 @@ mobile: false
logo - xflow + XFlow

diff --git a/docs/xflow/layout.md b/docs/xflow/layout.md new file mode 100644 index 000000000..00100a152 --- /dev/null +++ b/docs/xflow/layout.md @@ -0,0 +1,19 @@ +--- +order: 3 +title: '布局方向' +mobile: false +group: + title: 最佳展示 + order: 2 +--- +# 布局方向 + +## LR布局 +从左到右布局 + + + +## TB布局 +从上到下布局 + + diff --git a/docs/xflow/nodeSetting.md b/docs/xflow/nodeSetting.md new file mode 100644 index 000000000..38e2930e2 --- /dev/null +++ b/docs/xflow/nodeSetting.md @@ -0,0 +1,13 @@ +--- +order: 4 +title: '节点配置' +mobile: false +group: + title: 最佳展示 + order: 2 +--- +# 节点配置 + +在`settings`属性中你可以自定义每个节点的名称、图标、描述以及配置方案。如果是分组节点,type只能为`_group`。 + + From fb22cf1933a7ca0d5d399df6fa3e3b0eb2a14d99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=B4=AB=E5=8D=87?= Date: Fri, 29 Nov 2024 16:49:22 +0800 Subject: [PATCH 26/38] =?UTF-8?q?chore:=20=E6=B7=BB=E5=8A=A0=20XFlowProvid?= =?UTF-8?q?er=20useStore=20useStoreApi=20useXFlow=20api?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/xflow/XFlowProvider.md | 12 ++ docs/xflow/demo/xflow-provider/index.tsx | 48 +++++ docs/xflow/demo/xflow-provider/setting.tsx | 199 ++++++++++++++++++ packages/x-flow/src/Wrapper.tsx | 28 +++ packages/x-flow/src/{main.tsx => XFlow.tsx} | 110 +++++----- .../src/components/CandidateNode/index.tsx | 2 +- .../src/components/CustomEdge/index.tsx | 8 +- .../src/components/CustomNode/index.tsx | 2 +- .../src/components/NodeEditor/index.tsx | 2 +- .../src/components/NodesPopover/index.tsx | 6 +- .../src/components/PanelContainer/index.tsx | 2 +- .../src/components/XFlowProvider/index.tsx | 27 +++ packages/x-flow/src/hooks/useStore.ts | 68 ++++++ packages/x-flow/src/hooks/useXFlow.ts | 13 ++ packages/x-flow/src/index.ts | 8 +- packages/x-flow/src/models/context.ts | 8 +- packages/x-flow/src/models/store.ts | 102 +++++---- packages/x-flow/src/operator/index.tsx | 2 +- packages/x-flow/src/utils/autoLayoutNodes.ts | 2 +- packages/x-flow/src/withProvider.tsx | 30 +-- 20 files changed, 536 insertions(+), 143 deletions(-) create mode 100644 docs/xflow/XFlowProvider.md create mode 100644 docs/xflow/demo/xflow-provider/index.tsx create mode 100644 docs/xflow/demo/xflow-provider/setting.tsx create mode 100644 packages/x-flow/src/Wrapper.tsx rename packages/x-flow/src/{main.tsx => XFlow.tsx} (80%) create mode 100644 packages/x-flow/src/components/XFlowProvider/index.tsx create mode 100644 packages/x-flow/src/hooks/useStore.ts create mode 100644 packages/x-flow/src/hooks/useXFlow.ts diff --git a/docs/xflow/XFlowProvider.md b/docs/xflow/XFlowProvider.md new file mode 100644 index 000000000..60feb2caa --- /dev/null +++ b/docs/xflow/XFlowProvider.md @@ -0,0 +1,12 @@ +--- +order: 2 +title: 'XFlowProvider' +mobile: false +group: + title: 最佳展示 + order: 2 +--- + +# 基础交互 + + diff --git a/docs/xflow/demo/xflow-provider/index.tsx b/docs/xflow/demo/xflow-provider/index.tsx new file mode 100644 index 000000000..61bac2959 --- /dev/null +++ b/docs/xflow/demo/xflow-provider/index.tsx @@ -0,0 +1,48 @@ +import React from "react"; +import XFlow, { XFlowProvider } from '@xrenders/xflow'; +import settings from './setting'; + +const App = () => { + const nodes = [ + { + id: '1', + type: 'Start', + data: {}, + position: { + x: 40, + y: 240, + }, + }, + { + id: '2', + type: 'End', + data: {}, + position: { + x: 500, + y: 240, + }, + }, + ]; + + const edges = [{ source: '1', target: '2', id: '234123' }]; + + return ( +

+ ); +}; + +export default () => { + return ( + + + + ); +}; diff --git a/docs/xflow/demo/xflow-provider/setting.tsx b/docs/xflow/demo/xflow-provider/setting.tsx new file mode 100644 index 000000000..72f080f69 --- /dev/null +++ b/docs/xflow/demo/xflow-provider/setting.tsx @@ -0,0 +1,199 @@ +export default [ + { + title: '开始', + type: 'Start', + hidden: true, + targetHandleHidden: true, + icon: { + type: 'icon-start', + bgColor: '#17B26A', + }, + settingSchema: { + type: 'object', + properties: { + input: { + title: '变量一', + type: 'string', + widget: 'input', + }, + select: { + title: '变量二', + type: 'string', + widget: 'select', + props: { + options: [ + { label: 'a', value: 'a' }, + { label: 'b', value: 'b' }, + { label: 'c', value: 'c' }, + ], + }, + }, + radio1: { + title: '点击单选', + type: 'string', + widget: 'radio', + props: { + options: [ + { label: '早', value: 'a' }, + { label: '中', value: 'b' }, + { label: '晚', value: 'c' } + ] + } + }, + textarea1: { + title: '长文本', + type: 'string', + widget: 'textArea' + }, + date1: { + title: '日期选择', + type: 'string', + widget: 'datePicker' + }, + dateRange1: { + title: '日期范围', + type: 'range', + widget: 'dateRange' + }, + time1: { + title: '时间选择', + type: 'string', + widget: 'timePicker' + }, + timeRange1: { + title: '时间范围', + type: 'range', + widget: 'timeRange' + }, + }, + }, + }, + { + title: '结束', + type: 'End', + hidden: true, + sourceHandleHidden: true, + icon: { + type: 'icon-end', + bgColor: '#F79009', + }, + settingSchema: { + type: "object", + properties: { + input: { + title: '变量一', + type: 'string', + widget: 'input', + }, + select: { + title: '变量二', + type: 'string', + widget: 'select', + props: { + options: [ + { label: 'a', value: 'a' }, + { label: 'b', value: 'b' }, + { label: 'c', value: 'c' }, + ], + }, + }, + } + } + }, + { + title: 'LLM', + type: 'LLM', + description: '调用大语言模型回答问题或者对自然语言进行处理', + icon: { + type: 'icon-model', + bgColor: '#6172F3', + }, + }, + { + title: 'Prompt', + type: 'Prompt', + description: '通过精心设计提示词,提升大语言模型回答效果', + icon: { + type: 'icon-prompt', + bgColor: '#17B26A', + }, + }, + { + title: '知识库', + type: 'knowledge', + description: '允许你从知识库中查询与用户问题相关的文本内容', + icon: { + type: 'icon-knowledge', + bgColor: '#6172F3', + }, + }, + { + title: 'Switch', + type: 'Switch', + description: '允许你根据 if/else 条件将 workflow 拆分成两个分支', + icon: { + type: 'icon-switch', + bgColor: '#06AED4', + }, + }, + { + title: 'HSF', + type: 'hsf', + description: '允许通过 HSF 协议发送服务器请求', + icon: { + type: 'icon-hsf', + bgColor: '#875BF7', + }, + }, + { + title: 'Http', + type: 'http', + description: '允许通过 HTTP 协议发送服务器请求', + icon: { + type: 'icon-http', + bgColor: '#875BF7', + }, + }, + { + title: '代码执行', + type: 'Code', + description: '执行一段 Groovy 或 Python 或 NodeJS 代码实现自定义逻辑', + icon: { + type: 'icon-code', + bgColor: '#2E90FA', + }, + }, + { + title: '工具', + type: 'tool', + description: '允许使用工具能力', + icon: { + type: 'icon-gongju', + bgColor: '#2E90FA', + }, + }, + { + title: '工具', + type: '_group', + items: [ + { + title: '代码执行', + type: 'Code', + description: '执行一段 Groovy 或 Python 或 NodeJS 代码实现自定义逻辑', + icon: { + type: 'icon-code', + bgColor: '#2E90FA', + }, + }, + { + title: '工具', + type: 'tool', + description: '允许使用工具能力', + icon: { + type: 'icon-gongju', + bgColor: '#2E90FA', + }, + }, + ], + }, +]; diff --git a/packages/x-flow/src/Wrapper.tsx b/packages/x-flow/src/Wrapper.tsx new file mode 100644 index 000000000..46ecaa30f --- /dev/null +++ b/packages/x-flow/src/Wrapper.tsx @@ -0,0 +1,28 @@ +import React, { useContext } from 'react'; + +import { XFlowProvider } from './components/XFlowProvider'; +import StoreContext from './models/context'; + +export const Wrapper = ({ + children, + nodes, + edges, +}: { + children: React.ReactNode; + nodes: any[]; + edges: any[]; +}) => { + const isWrapped = useContext(StoreContext); + + if (isWrapped) { + // we need to wrap it with a fragment because it's not allowed for children to be a ReactNode + // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18051 + return <>{children}; + } + + return ( + + {children} + + ); +}; diff --git a/packages/x-flow/src/main.tsx b/packages/x-flow/src/XFlow.tsx similarity index 80% rename from packages/x-flow/src/main.tsx rename to packages/x-flow/src/XFlow.tsx index 57a7acbe7..f7d66e107 100644 --- a/packages/x-flow/src/main.tsx +++ b/packages/x-flow/src/XFlow.tsx @@ -4,7 +4,6 @@ import { MarkerType, ReactFlow, useReactFlow, - useStoreApi, } from '@xyflow/react'; import '@xyflow/react/dist/style.css'; import { useEventListener, useMemoizedFn } from 'ahooks'; @@ -19,7 +18,7 @@ import PanelContainer from './components/PanelContainer'; import { useEventEmitterContextContext } from './models/event-emitter'; import CustomNodeComponent from './components/CustomNode'; -import useStore from './models/store'; +import { useStore, useStoreApi } from './hooks/useStore'; import Operator from './operator'; import XFlowProps from './types'; import { transformNodes, uuid } from './utils'; @@ -27,6 +26,7 @@ import autoLayoutNodes from './utils/autoLayoutNodes'; import NodeEditor from './components/NodeEditor'; import './index.less'; +import { Wrapper } from './Wrapper'; const CustomNode = memo(CustomNodeComponent); const edgeTypes = { buttonedge: memo(CustomEdge) }; @@ -36,7 +36,7 @@ const edgeTypes = { buttonedge: memo(CustomEdge) }; * XFlow 入口 * */ -const FlowEditor: FC = memo(props => { +const App: FC = memo(props => { const { initialValues, settings } = props; const workflowContainerRef = useRef(null); const store = useStoreApi(); @@ -87,6 +87,7 @@ const FlowEditor: FC = memo(props => { setLayout(props.layout); setNodes(transformNodes(initialValues?.nodes)); setEdges(initialValues?.edges); + store.temporal.getState().pause(); }, []); useEventListener('keydown', e => { @@ -141,28 +142,28 @@ const FlowEditor: FC = memo(props => { }; // 插入节点 - const handleInsertNode = () => { - const newNode = { - id: uuid(), - data: { label: 'new node' }, - position: { - x: 0, - y: 0, - }, - }; - // record(() => { - 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, - }); - // }); - }; + // const handleInsertNode = () => { + // const newNode = { + // id: uuid(), + // data: { label: 'new node' }, + // position: { + // x: 0, + // y: 0, + // }, + // }; + // // record(() => { + // 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) => { @@ -215,10 +216,10 @@ const FlowEditor: FC = memo(props => { }, [layout]); // const edgeTypes = { buttonedge: (edgeProps: any) => }; - const { icon, description } = - settings.find( - item => item.type?.toLowerCase() === activeNode?.node?.toLowerCase() - ) || {}; + // const { icon, description } = + // settings.find( + // item => item.type?.toLowerCase() === activeNode?.node?.toLowerCase() + // ) || {}; const NodeEditorWrap = useMemo(() => { return ( @@ -233,7 +234,6 @@ const FlowEditor: FC = memo(props => { return (
- = memo(props => { }, }} onConnect={onConnect} - // onNodesChange={(changes) => { - // const recordTypes = new Set(['add', 'remove']); - // changes.forEach((change) => { - // if (recordTypes.has(change.type)) { - // record(() => { - // onNodesChange([change]); - // }); - // } else { - // onNodesChange([change]); - // } - // }); - // }} onNodesChange={changes => { - onNodesChange(changes) + onNodesChange(changes); }} onEdgesChange={changes => { onEdgesChange(changes); @@ -275,6 +263,7 @@ const FlowEditor: FC = memo(props => { getUpdateEdgeConfig(edge, '#c9c9c9'); }} > + = memo(props => { color="black" variant={BackgroundVariant.Dots} /> + {activeNode && ( + setActiveNode(null)} + node={activeNode} + data={activeNode?.values} + // disabled + > + {NodeEditorWrap} + + )} - - {activeNode && ( - setActiveNode(null)} - node={activeNode} - data={activeNode?.values} - // disabled - > - {NodeEditorWrap} - - )}
); }); -export default FlowEditor; +const XFlow: FC = (props) => { + const { initialValues } = props; + return ( + + + + ); +}; +export default XFlow; diff --git a/packages/x-flow/src/components/CandidateNode/index.tsx b/packages/x-flow/src/components/CandidateNode/index.tsx index 8beb5e565..8bb35d526 100644 --- a/packages/x-flow/src/components/CandidateNode/index.tsx +++ b/packages/x-flow/src/components/CandidateNode/index.tsx @@ -5,7 +5,7 @@ import { useShallow } from 'zustand/react/shallow'; import { useReactFlow, useViewport } from '@xyflow/react'; import { useEventListener } from 'ahooks'; import CustomNode from '../CustomNode'; -import useStore from '../../models/store'; +import { useStore } from '../../hooks/useStore'; const CandidateNode = () => { const { zoom } = useViewport(); diff --git a/packages/x-flow/src/components/CustomEdge/index.tsx b/packages/x-flow/src/components/CustomEdge/index.tsx index 39ea53c67..c07017715 100644 --- a/packages/x-flow/src/components/CustomEdge/index.tsx +++ b/packages/x-flow/src/components/CustomEdge/index.tsx @@ -4,7 +4,7 @@ import { BezierEdge, EdgeLabelRenderer, getBezierPath, useReactFlow } from '@xyf import { useShallow } from 'zustand/react/shallow'; import produce from 'immer'; import { uuid } from '../../utils'; -import useStore from '../../models/store'; +import { useStore } from '../../hooks/useStore'; import NodeSelectPopover from '../NodesPopover'; import './index.less'; @@ -79,7 +79,7 @@ export default memo((edge: any) => { ]) }); - + setNodes(newNodes); setEdges(newEdges); onEdgesChange([{ id, type: 'remove' }]); @@ -95,7 +95,7 @@ export default memo((edge: any) => { targetY: edge.targetY + 13 } } - + return ( setIsHovered(true)} @@ -129,4 +129,4 @@ export default memo((edge: any) => { /> ); -}) \ No newline at end of file +}) diff --git a/packages/x-flow/src/components/CustomNode/index.tsx b/packages/x-flow/src/components/CustomNode/index.tsx index 779bb6b0a..2cbf9517a 100644 --- a/packages/x-flow/src/components/CustomNode/index.tsx +++ b/packages/x-flow/src/components/CustomNode/index.tsx @@ -6,7 +6,7 @@ import produce from 'immer'; import { useShallow } from 'zustand/react/shallow'; import { Handle, Position, useReactFlow } from '@xyflow/react'; import { capitalize, uuid } from '../../utils'; -import useStore from '../../models/store'; +import { useStore } from '../../hooks/useStore'; import { ConfigContext } from '../../models/context'; import NodeSelectPopover from '../NodesPopover'; import './index.less'; diff --git a/packages/x-flow/src/components/NodeEditor/index.tsx b/packages/x-flow/src/components/NodeEditor/index.tsx index 910e3e45e..46e6378a4 100644 --- a/packages/x-flow/src/components/NodeEditor/index.tsx +++ b/packages/x-flow/src/components/NodeEditor/index.tsx @@ -4,7 +4,7 @@ import { debounce } from 'lodash'; import React, { FC, useContext, useEffect, useState } from 'react'; import { useShallow } from 'zustand/react/shallow'; import { ConfigContext } from '../../models/context'; -import useStore from '../../models/store'; +import { useStore } from '../../hooks/useStore'; interface INodeEditorProps { data: any; diff --git a/packages/x-flow/src/components/NodesPopover/index.tsx b/packages/x-flow/src/components/NodesPopover/index.tsx index 2cf684ae8..abb018b43 100644 --- a/packages/x-flow/src/components/NodesPopover/index.tsx +++ b/packages/x-flow/src/components/NodesPopover/index.tsx @@ -1,9 +1,7 @@ import React, { useCallback, useState, useRef, useContext } from 'react'; import { Popover } from 'antd'; -import { useShallow } from 'zustand/react/shallow'; import { useClickAway } from 'ahooks'; -import useStore from '../../models/store'; import { ConfigContext } from '../../models/context'; import NodesMenu from '../NodesMenu'; @@ -48,11 +46,11 @@ export default (props: any) => { ref={ref} items={settings} showSearch={showSearch} - onClick={handCreateNode} + onClick={handCreateNode} /> )} > {children} ); -} \ No newline at end of file +} diff --git a/packages/x-flow/src/components/PanelContainer/index.tsx b/packages/x-flow/src/components/PanelContainer/index.tsx index f8baaf053..d07a72ca3 100644 --- a/packages/x-flow/src/components/PanelContainer/index.tsx +++ b/packages/x-flow/src/components/PanelContainer/index.tsx @@ -4,7 +4,7 @@ import { debounce } from 'lodash'; import React, { FC, useContext, useEffect, useState } from 'react'; import { useShallow } from 'zustand/react/shallow'; import { ConfigContext } from '../../models/context'; -import useStore from '../../models/store'; +import { useStore } from '../../hooks/useStore'; import IconView from '../IconView'; import './index.less'; diff --git a/packages/x-flow/src/components/XFlowProvider/index.tsx b/packages/x-flow/src/components/XFlowProvider/index.tsx new file mode 100644 index 000000000..3daffe9b7 --- /dev/null +++ b/packages/x-flow/src/components/XFlowProvider/index.tsx @@ -0,0 +1,27 @@ +import { Provider } from '../../models/context'; +import { createStore } from '../../models/store'; + +import type { ReactNode } from 'react'; +import React, { memo, useEffect, useState } from 'react'; + +export const XFlowProvider = memo<{ + initialNodes: any[]; + initialEdges: any[]; + children: ReactNode; +}>(({ initialNodes: nodes, initialEdges: edges, children }) => { + const [store] = useState(() => + createStore({ + nodes, + edges, + }) + ); + + useEffect(() => { + // TODO: 默认暂停时间,向 zundo 贡献代码 + console.info("暂停时间") + // store.temporal.getState().pause(); + }, []); + + // TODO: 合并 Wrapper 和 withProvider + return {children}; +}); diff --git a/packages/x-flow/src/hooks/useStore.ts b/packages/x-flow/src/hooks/useStore.ts new file mode 100644 index 000000000..e1fff4d85 --- /dev/null +++ b/packages/x-flow/src/hooks/useStore.ts @@ -0,0 +1,68 @@ +import { useContext, useMemo } from 'react'; +import StoreContext from '../models/context'; +import { XFlowNode, XFlowState } from '../models/store'; + +import { Edge } from '@xyflow/react'; +import { useStoreWithEqualityFn } from 'zustand/traditional'; + +const useStore = ( + selector: (state: XFlowState) => T, + equalityFn?: (a: T, b: T) => boolean +) => { + const store = useContext(StoreContext); + + if (store === null) { + throw new Error( + '[XFlow]: Seems like you have not used zustand provider as an ancestor.' + ); + } + + return useStoreWithEqualityFn(store, selector, equalityFn); +}; + +const useStoreApi = < + NodeType extends XFlowNode = XFlowNode, + EdgeType extends Edge = Edge +>() => { + const store = useContext(StoreContext); + + if (store === null) { + throw new Error( + '[XFlow]: Seems like you have not used zustand provider as an ancestor.' + ); + } + return useMemo( + () => ({ + getState: store.getState, + setState: store.setState, + subscribe: store.subscribe, + temporal: store.temporal, + }), + [store] + ); +}; + +const useTemporalStore = () => { + const store = useContext(StoreContext); + + if (store === null) { + throw new Error( + '[XFlow]: Seems like you have not used zustand provider as an ancestor.' + ); + } + + return { + ...store.temporal.getState(), + record: (callback: () => void) => { + const temporalStore = store.temporal.getState(); + temporalStore.resume(); + callback(); + temporalStore.pause(); + }, + }; +}; + +// 默认关闭时间机器 +// useStoreApi().temporal.getState(); + +export { useStore, useStoreApi, useTemporalStore }; diff --git a/packages/x-flow/src/hooks/useXFlow.ts b/packages/x-flow/src/hooks/useXFlow.ts new file mode 100644 index 000000000..24a1d4cf9 --- /dev/null +++ b/packages/x-flow/src/hooks/useXFlow.ts @@ -0,0 +1,13 @@ +import { useMemo } from 'react'; +import { useStoreApi } from '../hooks/useStore'; + +export const useXFlow = () => { + const store = useStoreApi(); + + const instance = store.getState(); + + return useMemo(() => ({ + ...instance, + // TODO: 扩展 useXFlow 的方法 + }), [instance]); +}; diff --git a/packages/x-flow/src/index.ts b/packages/x-flow/src/index.ts index bc8459c28..dd4ffa59b 100644 --- a/packages/x-flow/src/index.ts +++ b/packages/x-flow/src/index.ts @@ -1,9 +1,13 @@ -import Main from './main'; +import XFlow from './XFlow'; import withProvider from './withProvider'; + import * as nodes from './nodes'; export type { default as FR, } from './types'; -export default withProvider(Main, nodes); +export { XFlowProvider } from './components/XFlowProvider'; +export { useXFlow } from './hooks/useXFlow'; +export { useStore, useStoreApi } from './hooks/useStore'; +export default withProvider(XFlow, nodes); diff --git a/packages/x-flow/src/models/context.ts b/packages/x-flow/src/models/context.ts index 418906d74..b1d20cc7a 100644 --- a/packages/x-flow/src/models/context.ts +++ b/packages/x-flow/src/models/context.ts @@ -1,5 +1,9 @@ import { createContext } from 'react'; +import { XFlowStore } from './store'; -export const FlowContext = createContext(null); +// TODO: 合并到 StoreContext +export const ConfigContext = createContext(null); -export const ConfigContext = createContext(null); \ No newline at end of file +const StoreContext = createContext(null); +export const Provider = StoreContext.Provider; +export default StoreContext; diff --git a/packages/x-flow/src/models/store.ts b/packages/x-flow/src/models/store.ts index 8a82eb3d6..cde8b640c 100644 --- a/packages/x-flow/src/models/store.ts +++ b/packages/x-flow/src/models/store.ts @@ -1,80 +1,92 @@ -import { create } from 'zustand'; -import { immer } from 'zustand/middleware/immer'; -import { temporal } from 'zundo'; -import isDeepEqual from 'fast-deep-equal'; +import { useTemporalStore } from '../hooks/useStore'; import { addEdge, - applyNodeChanges, applyEdgeChanges, + applyNodeChanges, Edge, Node, - OnNodesChange, - OnEdgesChange, OnConnect, + OnEdgesChange, + OnNodesChange, } from '@xyflow/react'; -import _ from "lodash"; +import isDeepEqual from 'fast-deep-equal'; +import { temporal } from 'zundo'; +import { createStore as createZustandStore } from 'zustand'; -export type AppNode = Node; +export type XFlowProps = { + nodes?: Node[]; + edges?: Edge[]; + layout?: 'LR' | 'TB'; +}; + +export type XFlowStore = ReturnType; + +export type XFlowNode = Node; -export type AppState = { - layout: 'LR' | 'TB', - nodes: AppNode[]; - edges: Edge[]; +export type XFlowState = { + layout?: 'LR' | 'TB'; + nodes?: XFlowNode[]; + edges?: Edge[]; candidateNode: any; mousePosition: any; - onNodesChange: OnNodesChange; + onNodesChange: OnNodesChange; onEdgesChange: OnEdgesChange; onConnect: OnConnect; - setNodes: (nodes: AppNode[]) => void; + setNodes: (nodes: XFlowNode[]) => void; setEdges: (edges: Edge[]) => void; - addNodes: (nodes: AppNode[]) => void; + addNodes: (nodes: XFlowNode[]) => void; addEdges: (edges: Edge[]) => void; setLayout: (layout: 'LR' | 'TB') => void; setCandidateNode: (candidateNode: any) => void; setMousePosition: (mousePosition: any) => void; }; -// 这是我们的 useStore hook,我们可以在我们的组件中使用它来获取 store 并调用动作 -// 注意:immer 使用方式是 create()(immer(() => ({}))) -const useStore = create()( - immer( +const createStore = (initProps?: Partial) => { + const DEFAULT_PROPS: XFlowProps = { + layout: 'LR', + nodes: [], + edges: [] + }; + + return createZustandStore()( temporal( (set, get) => ({ - layout: 'LR', + ...DEFAULT_PROPS, + initProps, nodes: [], edges: [], candidateNode: null, nodeMenus: [], mousePosition: { pageX: 0, pageY: 0, elementX: 0, elementY: 0 }, - onNodesChange: (changes) => { + onNodesChange: changes => { set({ nodes: applyNodeChanges(changes, get().nodes), }); }, - onEdgesChange: (changes) => { + onEdgesChange: changes => { set({ edges: applyEdgeChanges(changes, get().edges), }); }, - onConnect: (connection) => { + onConnect: connection => { set({ edges: addEdge(connection, get().edges), }); }, - setNodes: (nodes) => { + setNodes: nodes => { // 只记录节点变化 - useTemporalStore().record(() => { + // useTemporalStore().record(() => { set({ nodes }); - }); + // }); }, - setEdges: (edges) => { + setEdges: edges => { set({ edges }); }, addNodes: payload => { const newNodes = get().nodes.concat(payload); - useTemporalStore().record(() => { + // useTemporalStore().record(() => { set({ nodes: newNodes }); - }) + // }); }, addEdges: payload => { set({ edges: get().edges.concat(payload) }); @@ -82,7 +94,7 @@ const useStore = create()( setNodeMenus: (nodeMenus: any) => { set({ nodeMenus }); }, - setCandidateNode: (candidateNode) => { + setCandidateNode: candidateNode => { set({ candidateNode }); }, setMousePosition: (mousePosition: any) => { @@ -93,13 +105,13 @@ const useStore = create()( return; } set({ layout }); - } + }, }), { // nodes 和 edges 是引用类型,所以使用深比较 equality: isDeepEqual, // 偏函数 - partialize: (state) => { + partialize: state => { const { nodes, edges } = state; return { edges, @@ -109,25 +121,9 @@ const useStore = create()( onSave(pastState, currentState) { console.log('onSave', pastState, currentState); }, - }, - ), - ), -); - - -export const useTemporalStore = () => { - return { - ...useStore.temporal.getState(), - record: (callback: () => void) => { - const temporalStore = useStore.temporal.getState(); - temporalStore.resume(); - callback(); - temporalStore.pause(); - } - } + } + ) + ); }; -// 默认关闭时间机器 -useStore.temporal.getState().pause(); - -export default useStore; +export { createStore }; diff --git a/packages/x-flow/src/operator/index.tsx b/packages/x-flow/src/operator/index.tsx index 2163875cc..300ae95e1 100644 --- a/packages/x-flow/src/operator/index.tsx +++ b/packages/x-flow/src/operator/index.tsx @@ -5,7 +5,7 @@ import UndoRedo from './UndoRedo'; import Control from './Control'; import './index.less'; -import { useTemporalStore } from '../models/store'; +import { useTemporalStore } from '../hooks/useStore'; export type OperatorProps = { addNode: any; diff --git a/packages/x-flow/src/utils/autoLayoutNodes.ts b/packages/x-flow/src/utils/autoLayoutNodes.ts index 289dcb634..ad2eab0f3 100644 --- a/packages/x-flow/src/utils/autoLayoutNodes.ts +++ b/packages/x-flow/src/utils/autoLayoutNodes.ts @@ -63,6 +63,6 @@ export default (nodes: any, edges: any) => { } }) }); - + return newNodes; } diff --git a/packages/x-flow/src/withProvider.tsx b/packages/x-flow/src/withProvider.tsx index db25ccaa0..4cd8ef796 100644 --- a/packages/x-flow/src/withProvider.tsx +++ b/packages/x-flow/src/withProvider.tsx @@ -1,9 +1,8 @@ -import React, { useMemo } from 'react'; -import { ConfigProvider } from 'antd'; import { ReactFlowProvider } from '@xyflow/react'; -import { FlowContext, ConfigContext } from './models/context'; +import { ConfigProvider } from 'antd'; +import React, { useMemo } from 'react'; +import { ConfigContext } from './models/context'; import { TNodeGroup, TNodeItem } from './types'; - interface ProviderProps { configProvider?: any; widgets?: any; @@ -13,7 +12,10 @@ interface ProviderProps { [key: string]: any; } -export default function withProvider(Element: React.ComponentType, defaultWidgets?: any) : React.ComponentType { +export default function withProvider( + Element: React.ComponentType, + defaultWidgets?: any +): React.ComponentType { return (props: ProviderProps) => { const { configProvider, @@ -43,22 +45,20 @@ export default function withProvider(Element: React.ComponentType, default nodeSelector, settings, settingMap, - widgets: { + widgets: { ...defaultWidgets, - ...widgets - } + ...widgets, + }, }; - + return ( - - - - - + + + ); - } + }; } From 53685870a7ac7a85cad39358344540c0b097dbec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=98=94=E6=A2=A6?= Date: Fri, 29 Nov 2024 16:56:33 +0800 Subject: [PATCH 27/38] =?UTF-8?q?fix:=E4=BF=AE=E5=A4=8Dnodes=E5=A4=9A?= =?UTF-8?q?=E4=B8=AA=E9=87=8D=E5=A4=8D=E8=8A=82=E7=82=B9=EF=BC=8C=E5=AF=BC?= =?UTF-8?q?=E8=87=B4=E8=8A=82=E7=82=B9=E4=BF=A1=E6=81=AF=E6=B2=A1=E6=9C=89?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=88=B0=E6=9C=80=E6=96=B0=E7=9A=84=E8=8A=82?= =?UTF-8?q?=E7=82=B9=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/x-flow/src/components/NodeEditor/index.tsx | 12 +++++++++--- .../x-flow/src/components/PanelContainer/index.tsx | 9 ++++++++- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/packages/x-flow/src/components/NodeEditor/index.tsx b/packages/x-flow/src/components/NodeEditor/index.tsx index 46e6378a4..c52d44398 100644 --- a/packages/x-flow/src/components/NodeEditor/index.tsx +++ b/packages/x-flow/src/components/NodeEditor/index.tsx @@ -3,8 +3,8 @@ import produce from 'immer'; import { debounce } from 'lodash'; import React, { FC, useContext, useEffect, useState } from 'react'; import { useShallow } from 'zustand/react/shallow'; -import { ConfigContext } from '../../models/context'; import { useStore } from '../../hooks/useStore'; +import { ConfigContext } from '../../models/context'; interface INodeEditorProps { data: any; @@ -36,12 +36,18 @@ const NodeEditor: FC = (props: any) => { setCustomVal(data); } else { } - }, [JSON.stringify(data), id]); const handleNodeValueChange = debounce((data: any) => { const newNodes = produce(nodes, draft => { - const node = draft.find(n => n.id === id); + let node = null; + // 反向查询ID,因为有多个ID相同的元素 + for (let i = draft?.length - 1; i >= 0; i--) { + if (draft[i].id === id) { + node = draft[i]; + break; + } + } if (node) { // 更新节点的 data node.data = { ...node.data, ...data }; diff --git a/packages/x-flow/src/components/PanelContainer/index.tsx b/packages/x-flow/src/components/PanelContainer/index.tsx index d07a72ca3..025294323 100644 --- a/packages/x-flow/src/components/PanelContainer/index.tsx +++ b/packages/x-flow/src/components/PanelContainer/index.tsx @@ -59,7 +59,14 @@ const Panel: FC = (props: any) => { const handleNodeValueChange = debounce((data: any) => { const newNodes = produce(nodes, draft => { - const node = draft.find(n => n.id === id); + let node = null; + // 反向查询ID,因为有多个ID相同的元素 + for (let i = draft?.length - 1; i >= 0; i--) { + if (draft[i].id === id) { + node = draft[i]; + break; + } + } if (node) { // 更新节点的 data node.data = { ...node.data, ...data }; From 04bda843b9128874ba5a858de98f351162667179 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=98=94=E6=A2=A6?= Date: Mon, 2 Dec 2024 20:22:33 +0800 Subject: [PATCH 28/38] =?UTF-8?q?feat:=E8=8A=82=E7=82=B9=E5=90=8D=E7=A7=B0?= =?UTF-8?q?=E5=92=8C=E6=8F=8F=E8=BF=B0=E8=B6=85=E9=95=BF=E7=94=A8...+?= =?UTF-8?q?=E6=B0=94=E6=B3=A1=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/NodeContainer/index.tsx | 46 ++++++++++++++----- 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/packages/x-flow/src/components/NodeContainer/index.tsx b/packages/x-flow/src/components/NodeContainer/index.tsx index 89a81d71e..0e21141bb 100644 --- a/packages/x-flow/src/components/NodeContainer/index.tsx +++ b/packages/x-flow/src/components/NodeContainer/index.tsx @@ -1,25 +1,47 @@ +import { Typography } from 'antd'; +import classNames from 'classnames'; import React, { memo } from 'react'; import IconView from '../IconView'; -import classNames from 'classnames'; import './index.less'; +const { Text, Paragraph } = Typography; + export default memo((props: any) => { const { className, onClick, children, icon, title, desc, hideDesc } = props; return ( -
-
- - {title} +
+
+ + + + {/* {title} */} + + {title} +
-
{children}
- {(!hideDesc && !!desc) && ( -
+
{children}
+ {/* {!hideDesc && !!desc &&
{desc}
} */} + {!hideDesc && !!desc && ( + {desc} -
+ )} + {/* 在这里的节点下方添加一个自定义组件 */}
); -}) - - +}); From 6c07d3efac4f15f8fff866205159c5b150405925 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=98=94=E6=A2=A6?= Date: Tue, 3 Dec 2024 16:40:47 +0800 Subject: [PATCH 29/38] =?UTF-8?q?feat:1.=E6=96=B0=E5=A2=9Enodewidgets?= =?UTF-8?q?=E8=87=AA=E5=AE=9A=E4=B9=89=E8=8A=82=E7=82=B9=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=20=202.=E5=86=85=E7=BD=AEswitch=E6=9D=A1=E4=BB=B6=E8=8A=82?= =?UTF-8?q?=E7=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/xflow/api.md | 21 +++-- docs/xflow/custom-flow.md | 9 +- docs/xflow/demo/nodeWidget/index.tsx | 53 +++++++++++ docs/xflow/demo/nodeWidget/setting.tsx | 90 ++++++++++++++++++ docs/xflow/demo/switchNode/index.tsx | 23 +++++ docs/xflow/demo/switchNode/setting.tsx | 29 ++++++ docs/xflow/nodeBuildIn.md | 13 +++ .../src/components/CustomNode/index.less | 9 ++ .../src/components/CustomNode/index.tsx | 94 ++++++++++--------- .../components/CustomNode/sourceHandle.tsx | 66 +++++++++++++ .../src/components/NodeContainer/index.less | 7 +- .../src/components/NodeContainer/index.tsx | 9 +- .../src/components/NodeEditor/index.tsx | 19 +++- .../x-flow/src/components/NodesMenu/index.tsx | 24 ++--- packages/x-flow/src/nodes/index.tsx | 4 +- .../x-flow/src/nodes/node-common/index.tsx | 4 +- packages/x-flow/src/nodes/node-end/index.tsx | 4 +- .../x-flow/src/nodes/node-start/index.tsx | 4 +- .../node-switch/SwitchBuildInNodeWidget.tsx | 11 +++ .../x-flow/src/nodes/node-switch/index.less | 22 +++++ .../x-flow/src/nodes/node-switch/index.tsx | 33 +++++++ .../src/nodes/node-switch/setting/index.tsx | 59 ++++++++++++ 22 files changed, 527 insertions(+), 80 deletions(-) create mode 100644 docs/xflow/demo/nodeWidget/index.tsx create mode 100644 docs/xflow/demo/nodeWidget/setting.tsx create mode 100644 docs/xflow/demo/switchNode/index.tsx create mode 100644 docs/xflow/demo/switchNode/setting.tsx create mode 100644 docs/xflow/nodeBuildIn.md create mode 100644 packages/x-flow/src/components/CustomNode/sourceHandle.tsx create mode 100644 packages/x-flow/src/nodes/node-switch/SwitchBuildInNodeWidget.tsx create mode 100644 packages/x-flow/src/nodes/node-switch/index.less create mode 100644 packages/x-flow/src/nodes/node-switch/index.tsx create mode 100644 packages/x-flow/src/nodes/node-switch/setting/index.tsx diff --git a/docs/xflow/api.md b/docs/xflow/api.md index 92ab7458d..d13cbdc56 100644 --- a/docs/xflow/api.md +++ b/docs/xflow/api.md @@ -29,16 +29,17 @@ title: API 单个节点配置 -| 属性 | 描述 | 类型 | 默认值 | -| ------------------ | ------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | -| title | 节点名称 | `string` | | -| type | 节点类型 | `string` | | -| hidden | 是否在配置面板中显示节点 | `boolean` | false | -| targetHandleHidden | 是否隐藏左侧输入连接头 | `boolean` | false | -| sourceHandleHidden | 是否隐藏右侧输出连接头 | `boolean` | false | -| icon | 节点的图标配置 | `{type:string;bgColor:string}` | | -| settingSchema | 节点的业务配置信息,详见[form-render 文档](/form-render/api-schema) |
SchemaBase | | -| settingWidget | 自定义节点的业务配置组件 | `string` | | +| 属性 | 描述 | 类型 | 默认值 | +| ------------------ | --------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | +| title | 节点名称 | `string` | | +| type | 节点类型 | `string` | | +| hidden | 是否在配置面板中显示节点 | `boolean` | false | +| targetHandleHidden | 是否隐藏左侧输入连接头 | `boolean` | false | +| sourceHandleHidden | 是否隐藏右侧输出连接头 | `boolean` | false | +| icon | 节点的图标配置 | `{type:string;bgColor:string}` | | +| settingSchema | 节点的业务配置信息,详见[form-render 文档](/form-render/api-schema)。同时设置`settingSchema`和`settingWidget`只生效`settingWidget` | SchemaBase | | +| settingWidget | 自定义节点的业务配置组件,在弹窗中展示。同时设置`settingSchema`和`settingWidget`只生效`settingWidget`。定义之后需要在`widgets`中引入自定义组件。 | `string` | | +| nodeWidget | 自定义节点的业务配置信息展示组件,在节点内部展示业务配置信息。定义之后需要在`widgets`中引入自定义组件。 | `string` | | ## TNodeSelector diff --git a/docs/xflow/custom-flow.md b/docs/xflow/custom-flow.md index ffcea9090..ef3ecb2c9 100644 --- a/docs/xflow/custom-flow.md +++ b/docs/xflow/custom-flow.md @@ -12,7 +12,7 @@ group: 使用`settingWidget`自定义业务配置组件 - + ## 在schema中自定义组件 @@ -70,4 +70,9 @@ export default () => { } -``` +``` + +## 自定义业务配置信息展示组件 +使用`nodeWidget`自定义节点的业务配置信息展示组件,在节点内部展示业务配置信息 + + diff --git a/docs/xflow/demo/nodeWidget/index.tsx b/docs/xflow/demo/nodeWidget/index.tsx new file mode 100644 index 000000000..5acd4e865 --- /dev/null +++ b/docs/xflow/demo/nodeWidget/index.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import XFlow from '@xrenders/xflow'; +import settings from './setting'; +import { Space, Tag, Typography } from 'antd'; + + +const customNodeWidget = ({ data }) => { + return
+

变量一:{data?.input}

+

变量二:{data?.select}

+
+} + + +const endNodeWidget = ({ data }) => { + return
+ {data?.input} +
+} + + +const LLMNodeWidget = ({ data }) => { + const labels = Object.keys(data) || []; + return + {labels?.map(item => {data[item]})} + +} + +export default () => { + const nodes = [ + { id: '1', type: 'Start', data: { input: '开始节点', select: "b" }, position: { x: 40, y: 240 } }, + { id: '2', type: 'LLM', data: { input1: 'input1' }, position: { x: 400, y: 240 } }, + { id: '3', type: 'End', data: { input: '通过nodeWidget自定义配置展示' }, position: { x: 800, y: 240 } } + ]; + + const edges = [ + { source: '1', target: '2', id: '234123' }, + { source: '2', target: '3', id: '56891' } + ] + + return ( +
+ +
+ ); +} diff --git a/docs/xflow/demo/nodeWidget/setting.tsx b/docs/xflow/demo/nodeWidget/setting.tsx new file mode 100644 index 000000000..26d8c6aca --- /dev/null +++ b/docs/xflow/demo/nodeWidget/setting.tsx @@ -0,0 +1,90 @@ +export default [ + { + title: '开始', + type: 'Start', + hidden: true, + targetHandleHidden: true, + icon: { + type: 'icon-start', + bgColor: '#17B26A', + }, + settingSchema: { + type: "object", + properties: { + input: { + title: '变量一', + type: 'string', + widget: 'input', + }, + select: { + title: '变量二', + type: 'string', + widget: 'select', + props: { + options: [ + { label: 'a', value: 'a' }, + { label: 'b', value: 'b' }, + { label: 'c', value: 'c' }, + ], + }, + }, + } + }, + nodeWidget:'customNodeWidget' + }, + { + title: '结束', + type: 'End', + hidden: true, + sourceHandleHidden: true, + icon: { + type: 'icon-end', + bgColor: '#F79009', + }, + settingSchema: { + type: "object", + properties: { + input: { + title: '结束原因', + type: 'string', + widget: 'textArea', + }, + } + }, + nodeWidget: 'endNodeWidget', + }, + { + title: 'LLM', + type: 'LLM', + description: '调用大语言模型回答问题或者对自然语言进行处理', + icon: { + type: 'icon-model', + bgColor: '#6172F3', + }, + settingSchema: { + type: 'object', + displayType: 'row', + labelCol: 6, + fieldCol:18, + properties: { + input1: { + title: 'Field A', + type: 'string', + }, + input2: { + title: 'Field B', + type: 'string' + }, + input3: { + title: 'Field C', + type: 'string' + }, + input4: { + title: 'Field D', + type: 'string' + } + } + }, + nodeWidget: 'LLMNodeWidget', + }, +]; diff --git a/docs/xflow/demo/switchNode/index.tsx b/docs/xflow/demo/switchNode/index.tsx new file mode 100644 index 000000000..5a9c47fdd --- /dev/null +++ b/docs/xflow/demo/switchNode/index.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import XFlow from '@xrenders/xflow'; +import settings from './setting'; + +export default () => { + const nodes = [ + { id: '1', type: 'Switch', data: { input: '开始节点', select: "b" }, position: { x: 40, y: 240 } }, + ]; + + const edges = [] + + return ( +
+ +
+ ); +} diff --git a/docs/xflow/demo/switchNode/setting.tsx b/docs/xflow/demo/switchNode/setting.tsx new file mode 100644 index 000000000..1c790f4ef --- /dev/null +++ b/docs/xflow/demo/switchNode/setting.tsx @@ -0,0 +1,29 @@ +export default [ + { + title: 'Switch', + type: 'Switch', + description: '允许你根据 if/else 条件将 workflow 拆分成两个分支', + icon: { + type: 'icon-switch', + bgColor: '#06AED4', + }, + }, + { + title: 'Prompt', + type: 'Prompt', + description: '通过精心设计提示词,提升大语言模型回答效果', + icon: { + type: 'icon-prompt', + bgColor: '#17B26A', + }, + }, + { + title: '知识库', + type: 'knowledge', + description: '允许你从知识库中查询与用户问题相关的文本内容', + icon: { + type: 'icon-knowledge', + bgColor: '#6172F3', + }, + }, +]; diff --git a/docs/xflow/nodeBuildIn.md b/docs/xflow/nodeBuildIn.md new file mode 100644 index 000000000..0df7c0a19 --- /dev/null +++ b/docs/xflow/nodeBuildIn.md @@ -0,0 +1,13 @@ +--- +order: 4 +title: '内置节点' +mobile: false +group: + title: 最佳展示 + order: 2 +--- +# 内置节点 + +## 条件内置节点 +SwitchNode + diff --git a/packages/x-flow/src/components/CustomNode/index.less b/packages/x-flow/src/components/CustomNode/index.less index d967276d5..5c060dc6b 100644 --- a/packages/x-flow/src/components/CustomNode/index.less +++ b/packages/x-flow/src/components/CustomNode/index.less @@ -46,6 +46,15 @@ pointer-events: none; transition: all 0.3s; } + + .xflow-node-switch-title{ + position: absolute; + left: -45px; + top: 6px; + font-weight: 600; + text-align: right; + width: 35px; + } } .xflow-node-container-tb { diff --git a/packages/x-flow/src/components/CustomNode/index.tsx b/packages/x-flow/src/components/CustomNode/index.tsx index 6297499b3..67f46228f 100644 --- a/packages/x-flow/src/components/CustomNode/index.tsx +++ b/packages/x-flow/src/components/CustomNode/index.tsx @@ -1,15 +1,31 @@ -import { PlusOutlined } from '@ant-design/icons'; import { Handle, Position, useReactFlow } from '@xyflow/react'; -import { Tooltip } from 'antd'; import classNames from 'classnames'; import produce from 'immer'; -import React, { memo, useContext, useRef, useState } from 'react'; +import React, { memo, useContext, useState } from 'react'; import { useShallow } from 'zustand/react/shallow'; -import { ConfigContext } from '../../models/context'; import { useStore } from '../../hooks/useStore'; +import { ConfigContext } from '../../models/context'; import { capitalize, uuid } from '../../utils'; -import NodeSelectPopover from '../NodesPopover'; import './index.less'; +import SourceHandle from './sourceHandle'; + +const generateRandomArray = (x: number, type?: string) => { + const randomArray = []; + for (let i = 0; i < x; i++) { + if (type === 'Switch') { + if (i === 0) { + randomArray.push({ id: uuid(), switchTitle: 'IF' }); + } else if (i === x - 1) { + randomArray.push({ id: uuid(), switchTitle: 'ELSE' }); + } else { + randomArray.push({ id: uuid(), switchTitle: 'ELIF' }); + } + } else { + randomArray.push({ id: uuid() }); + } + } + return randomArray; +}; export default memo((props: any) => { const { id, type, data, layout, isConnectable, selected, onClick } = props; @@ -17,10 +33,6 @@ export default memo((props: any) => { const NodeWidget = widgets[`${capitalize(type)}Node`] || widgets['CommonNode']; const [isHovered, setIsHovered] = useState(false); - const [isShowTooltip, setIsShowTooltip] = useState(false); - const popoverRef = useRef(null); - const [openNodeSelectPopover, setOpenNodeSelectPopover] = useState(false); - const reactflow = useReactFlow(); const { edges, nodes, setNodes, setEdges, mousePosition } = useStore( useShallow((state: any) => ({ @@ -32,6 +44,18 @@ export default memo((props: any) => { onEdgesChange: state.onEdgesChange, })) ); + // data中的switchData的长度,即:if和if else 的数量,条件数量 + const switchDataLength = + data?.switchData?.length >= 1 ? Number(data?.switchData?.length + 1) : 2; + const nodeHeight = 45; // 每次增长的节点高度 + // 1.switch节点 if,else数量 + const sourceHandleNum = + type === 'Switch' + ? generateRandomArray(switchDataLength, 'Switch') + : generateRandomArray(1); + const nodeMinHeight = + type === 'Switch' ? Number(switchDataLength * nodeHeight) : undefined; + // 增加节点并进行联系 const handleAddNode = (data: any) => { @@ -89,45 +113,23 @@ export default memo((props: any) => { type={type} data={data} onClick={() => onClick(data)} + nodeMinHeight={nodeMinHeight} /> {!settingMap?.[type]?.sourceHandleHidden && ( - setIsShowTooltip(true)} - onMouseLeave={() => setIsShowTooltip(false)} - onClick={e => { - e.stopPropagation(); - popoverRef?.current?.changeOpen(true); - setIsShowTooltip(false); - setOpenNodeSelectPopover(true); - }} - > - {(selected || isHovered || openNodeSelectPopover) && ( -
- setOpenNodeSelectPopover(val)} - > - - - - -
- )} -
+ <> + {sourceHandleNum?.map((item, key) => ( + + ))} + )}
); diff --git a/packages/x-flow/src/components/CustomNode/sourceHandle.tsx b/packages/x-flow/src/components/CustomNode/sourceHandle.tsx new file mode 100644 index 000000000..9202b000b --- /dev/null +++ b/packages/x-flow/src/components/CustomNode/sourceHandle.tsx @@ -0,0 +1,66 @@ +import { PlusOutlined } from '@ant-design/icons'; +import { Handle } from '@xyflow/react'; +import { Tooltip } from 'antd'; +import React, { memo, useRef, useState } from 'react'; +import NodeSelectPopover from '../NodesPopover'; + +export default memo((props: any) => { + const { + position, + isConnectable, + selected, + isHovered, + handleAddNode, + id, + switchTitle, + ...rest + } = props; + const [isShowTooltip, setIsShowTooltip] = useState(false); + const [openNodeSelectPopover, setOpenNodeSelectPopover] = useState(false); + const popoverRef = useRef(null); + + return ( + setIsShowTooltip(true)} + onMouseLeave={() => setIsShowTooltip(false)} + onClick={e => { + e.stopPropagation(); + popoverRef?.current?.changeOpen(true); + setIsShowTooltip(false); + setOpenNodeSelectPopover(true); + }} + id={id} + {...rest} + > + {(selected || isHovered || openNodeSelectPopover) && ( + <> + {switchTitle &&
{switchTitle}
} +
+ setOpenNodeSelectPopover(val)} + > + + + + +
+ + )} +
+ ); +}); diff --git a/packages/x-flow/src/components/NodeContainer/index.less b/packages/x-flow/src/components/NodeContainer/index.less index 57ef6d71b..c268b45a4 100644 --- a/packages/x-flow/src/components/NodeContainer/index.less +++ b/packages/x-flow/src/components/NodeContainer/index.less @@ -20,6 +20,7 @@ color: #676f83; font-size: 12px; padding-top: 4px; + margin-top: 10px; } .icon-box { @@ -30,4 +31,8 @@ align-items: center; justify-content: center; } -} \ No newline at end of file + + .node-widget{ + width: 100%; + } +} diff --git a/packages/x-flow/src/components/NodeContainer/index.tsx b/packages/x-flow/src/components/NodeContainer/index.tsx index 0e21141bb..5340bc2c1 100644 --- a/packages/x-flow/src/components/NodeContainer/index.tsx +++ b/packages/x-flow/src/components/NodeContainer/index.tsx @@ -7,7 +7,7 @@ import './index.less'; const { Text, Paragraph } = Typography; export default memo((props: any) => { - const { className, onClick, children, icon, title, desc, hideDesc } = props; + const { className, onClick, children, icon, title, desc, hideDesc, NodeWidget, nodeMinHeight } = props; return (
{ [className]: !!className, })} onClick={onClick} + style={nodeMinHeight ? { minHeight: nodeMinHeight } : {}} >
@@ -30,6 +31,11 @@ export default memo((props: any) => {
{children}
{/* {!hideDesc && !!desc &&
{desc}
} */} + { + NodeWidget &&
+ {NodeWidget} +
+ } {!hideDesc && !!desc && ( { {desc} )} - {/* 在这里的节点下方添加一个自定义组件 */}
); }); diff --git a/packages/x-flow/src/components/NodeEditor/index.tsx b/packages/x-flow/src/components/NodeEditor/index.tsx index c52d44398..49f622b4b 100644 --- a/packages/x-flow/src/components/NodeEditor/index.tsx +++ b/packages/x-flow/src/components/NodeEditor/index.tsx @@ -20,6 +20,8 @@ const NodeEditor: FC = (props: any) => { const { settingMap, widgets } = useContext(ConfigContext); const nodeSetting = settingMap[nodeType] || {}; const [customVal, setCustomVal] = useState(data); + const CustomSettingWidget = widgets[`${nodeType}NodeSettingWidget`]; // 内置setting组件 + const NodeWidget = widgets[nodeSetting?.settingWidget]; // 自定义面板配置组件 const { nodes, setNodes } = useStore( useShallow((state: any) => ({ @@ -30,11 +32,14 @@ const NodeEditor: FC = (props: any) => { useEffect(() => { if (nodeSetting?.settingSchema) { + // 自定义Schema form.resetFields(); form.setValues(data || {}); } else if (nodeSetting?.settingWidget) { + // 自定义组件 setCustomVal(data); } else { + //可能为内置schema或者是没有 } }, [JSON.stringify(data), id]); @@ -63,14 +68,12 @@ const NodeEditor: FC = (props: any) => { }, }; - if (nodeSetting?.settingWidget) { - const NodeWidget = widgets[nodeSetting?.settingWidget]; + if (nodeSetting?.settingWidget && NodeWidget) { return ( { setCustomVal(values); - // onChange({ id, values: { ...values } }); handleNodeValueChange({ ...values }); }} /> @@ -85,6 +88,16 @@ const NodeEditor: FC = (props: any) => { size={'small'} /> ); + } else if (CustomSettingWidget) { + // 内置节点 + return ( + { + handleNodeValueChange({ ...val }); + }} + value={data} + /> + ); } else { return null; } diff --git a/packages/x-flow/src/components/NodesMenu/index.tsx b/packages/x-flow/src/components/NodesMenu/index.tsx index afa40bbb5..d0281eff8 100644 --- a/packages/x-flow/src/components/NodesMenu/index.tsx +++ b/packages/x-flow/src/components/NodesMenu/index.tsx @@ -24,7 +24,7 @@ const searchNodeList = (query: string, list: any[]) => { if (currentNode.title.toLowerCase().includes(searchTerm)) { result.push(currentNode); - } else if (currentNode.type === '_group' && currentNode.items) { + } else if (currentNode?.type === '_group' && currentNode.items) { const matchingItems = searchList(currentNode.items); if (matchingItems.length > 0) { result.push({ ...currentNode, items: matchingItems }); @@ -40,7 +40,7 @@ const MenuTooltip = ({ icon, title, description }: any) => { return (
- +
{title} @@ -58,18 +58,18 @@ const MenuItem = (props: any) => { return ( } - placement='right' + title={} + placement='right' arrow={false} > -
- {title} @@ -84,9 +84,9 @@ const filterHiddenMenu = (list: any) => { } /** - * + * * 节点菜单List - * + * */ const NodesMenu = (props: TNodeMenu, ref: Ref) => { const { items, showSearch, onClick } = props; @@ -135,4 +135,4 @@ const NodesMenu = (props: TNodeMenu, ref: Ref) => { ); }; -export default forwardRef(NodesMenu); \ No newline at end of file +export default forwardRef(NodesMenu); diff --git a/packages/x-flow/src/nodes/index.tsx b/packages/x-flow/src/nodes/index.tsx index 86b4967eb..18c684726 100644 --- a/packages/x-flow/src/nodes/index.tsx +++ b/packages/x-flow/src/nodes/index.tsx @@ -1,3 +1,5 @@ export { default as StartNode } from './node-start'; export { default as EndNode } from './node-end'; -export { default as CommonNode } from './node-common'; \ No newline at end of file +export { default as CommonNode } from './node-common'; +export { default as SwitchNode } from './node-switch'; +export { default as SwitchNodeSettingWidget } from './node-switch/setting'; diff --git a/packages/x-flow/src/nodes/node-common/index.tsx b/packages/x-flow/src/nodes/node-common/index.tsx index fe5b9be9f..fa863ae3d 100644 --- a/packages/x-flow/src/nodes/node-common/index.tsx +++ b/packages/x-flow/src/nodes/node-common/index.tsx @@ -4,8 +4,9 @@ import { ConfigContext } from '../../models/context'; export default memo((props: any) => { const { type, onClick, data } = props; - const { settingMap } = useContext(ConfigContext); + const { settingMap, widgets } = useContext(ConfigContext); const nodeSetting = settingMap[type] || {}; + const NodeWidget = widgets[nodeSetting?.nodeWidget] || undefined; return ( { onClick={onClick} hideDesc={nodeSetting?.hideDesc || !data?.desc} desc={data?.desc} + NodeWidget={NodeWidget ? : undefined} /> ); }); diff --git a/packages/x-flow/src/nodes/node-end/index.tsx b/packages/x-flow/src/nodes/node-end/index.tsx index c31ecca2c..890357a49 100644 --- a/packages/x-flow/src/nodes/node-end/index.tsx +++ b/packages/x-flow/src/nodes/node-end/index.tsx @@ -4,8 +4,9 @@ import { ConfigContext } from '../../models/context'; export default memo((props: any) => { const { onClick, type, data } = props; - const { settingMap } = useContext(ConfigContext); + const { settingMap,widgets } = useContext(ConfigContext); const nodeSetting = settingMap[type] || {}; + const NodeWidget = widgets[nodeSetting?.nodeWidget] || undefined; return ( { onClick={onClick} hideDesc={nodeSetting?.hideDesc || !data?.desc} desc={data?.desc} + NodeWidget={NodeWidget ? : undefined} /> ); }); diff --git a/packages/x-flow/src/nodes/node-start/index.tsx b/packages/x-flow/src/nodes/node-start/index.tsx index 36c588b85..923cadfd9 100644 --- a/packages/x-flow/src/nodes/node-start/index.tsx +++ b/packages/x-flow/src/nodes/node-start/index.tsx @@ -4,8 +4,9 @@ import { ConfigContext } from '../../models/context'; export default memo((props: any) => { const { onClick, type, data } = props; - const { settingMap } = useContext(ConfigContext); + const { settingMap, widgets } = useContext(ConfigContext); const nodeSetting = settingMap[type] || {}; + const NodeWidget = widgets[nodeSetting?.nodeWidget] || undefined; return ( { onClick={onClick} hideDesc={nodeSetting?.hideDesc || !data?.desc} desc={data?.desc} + NodeWidget={NodeWidget ? : undefined} /> ); }); diff --git a/packages/x-flow/src/nodes/node-switch/SwitchBuildInNodeWidget.tsx b/packages/x-flow/src/nodes/node-switch/SwitchBuildInNodeWidget.tsx new file mode 100644 index 000000000..e6efb73fa --- /dev/null +++ b/packages/x-flow/src/nodes/node-switch/SwitchBuildInNodeWidget.tsx @@ -0,0 +1,11 @@ +import { Space,Typography } from 'antd'; +import React, { memo } from 'react'; + +export default memo((props: any) => { + const { data } = props; + return + {(data?.switchData || [])?.map(item => + {item?.value} + )} + +}); diff --git a/packages/x-flow/src/nodes/node-switch/index.less b/packages/x-flow/src/nodes/node-switch/index.less new file mode 100644 index 000000000..5a2c37ce3 --- /dev/null +++ b/packages/x-flow/src/nodes/node-switch/index.less @@ -0,0 +1,22 @@ +.custom-node-start { + width: 240px; + padding: 0 12px; + background: #fff; + border-radius: 12px; + + .title { + display: flex; + height: 50px; + align-items: center; + span { + font-weight: bold; + margin-left: 8px; + } + } +} + +.custom-node-switch-setting{ + .fr-list-simple,.ant-form-item,.fr-panel,.fr-inline-container{ + width: 100%; + } +} diff --git a/packages/x-flow/src/nodes/node-switch/index.tsx b/packages/x-flow/src/nodes/node-switch/index.tsx new file mode 100644 index 000000000..53824f8de --- /dev/null +++ b/packages/x-flow/src/nodes/node-switch/index.tsx @@ -0,0 +1,33 @@ +import React, { memo, useContext } from 'react'; +import NodeContainer from '../../components/NodeContainer'; +import { ConfigContext } from '../../models/context'; +import SwitchBuildInNodeWidget from './SwitchBuildInNodeWidget'; + +export default memo((props: any) => { + const { + onClick, + type, + data, + nodeMinHeight + } = props; + const { settingMap, widgets } = useContext(ConfigContext); + const nodeSetting = settingMap[type] || {}; + const NodeWidget = widgets[nodeSetting?.nodeWidget] || undefined; + + return ( + : } + nodeMinHeight={nodeMinHeight} + /> + ); +}); diff --git a/packages/x-flow/src/nodes/node-switch/setting/index.tsx b/packages/x-flow/src/nodes/node-switch/setting/index.tsx new file mode 100644 index 000000000..c4e3f9e01 --- /dev/null +++ b/packages/x-flow/src/nodes/node-switch/setting/index.tsx @@ -0,0 +1,59 @@ +import FormRender, { Schema, useForm } from 'form-render'; +import React, { memo } from 'react'; +import '../index.less' + +interface INodeSwitchSettingPorps { + onChange: (val: any) => void; + value: any; +} + +const schema: Schema = { + type: 'object', + span: 24, + displayType: "row", + properties: { + switchData: { + type: 'array', + widget: 'simpleList', + display:"block", + props: { + hideCopy: true, + hideMove: true, + hideDelete: true, + }, + items: { + type: 'object', + properties: { + value: { + title: '条件', + type: 'string', + }, + }, + }, + }, + }, +}; + +export default memo((props: INodeSwitchSettingPorps) => { + const form = useForm(); + const { onChange, value } = props; + + const watch = { + '#': (allValues: any) => { + onChange({ ...allValues }); + }, + }; + + return ( + { + form.setValues(value || {}); + }} + className='custom-node-switch-setting' + /> + ); +}); From 675a35205fd04296412b9eac142c0eff531de9e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=98=94=E6=A2=A6?= Date: Tue, 3 Dec 2024 17:11:37 +0800 Subject: [PATCH 30/38] =?UTF-8?q?fix:=E4=BF=AE=E5=A4=8D=E6=99=AE=E9=80=9A?= =?UTF-8?q?=E8=8A=82=E7=82=B9=E9=93=BE=E6=8E=A5=E5=A4=B4=E4=BD=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../x-flow/src/components/CustomNode/index.tsx | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/x-flow/src/components/CustomNode/index.tsx b/packages/x-flow/src/components/CustomNode/index.tsx index 67f46228f..201ee9af8 100644 --- a/packages/x-flow/src/components/CustomNode/index.tsx +++ b/packages/x-flow/src/components/CustomNode/index.tsx @@ -12,16 +12,17 @@ import SourceHandle from './sourceHandle'; const generateRandomArray = (x: number, type?: string) => { const randomArray = []; for (let i = 0; i < x; i++) { + const id=`id_${i}` if (type === 'Switch') { if (i === 0) { - randomArray.push({ id: uuid(), switchTitle: 'IF' }); + randomArray.push({ id , switchTitle: 'IF' }); } else if (i === x - 1) { - randomArray.push({ id: uuid(), switchTitle: 'ELSE' }); + randomArray.push({ id, switchTitle: 'ELSE' }); } else { - randomArray.push({ id: uuid(), switchTitle: 'ELIF' }); + randomArray.push({ id, switchTitle: 'ELIF' }); } } else { - randomArray.push({ id: uuid() }); + randomArray.push({ id }); } } return randomArray; @@ -117,15 +118,15 @@ export default memo((props: any) => { /> {!settingMap?.[type]?.sourceHandleHidden && ( <> - {sourceHandleNum?.map((item, key) => ( + {(sourceHandleNum||[])?.map((item, key) => ( ))} From bcd8ed47b72229616c31d1931d8d42d99217f438 Mon Sep 17 00:00:00 2001 From: "zhanbo.lh" Date: Wed, 4 Dec 2024 00:01:34 +0800 Subject: [PATCH 31/38] =?UTF-8?q?feat:=20=E8=BF=AD=E4=BB=A3=E5=BC=80?= =?UTF-8?q?=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/xflow/index.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/xflow/index.md b/docs/xflow/index.md index 54be7ec80..1602e98fe 100644 --- a/docs/xflow/index.md +++ b/docs/xflow/index.md @@ -25,7 +25,6 @@ mobile: false 画布流程编排解决方案 - ## 安装 ```shell npm i @xrenders/xflow --save From 365024335ad8c7a9e233a2e38d4dccb0612d2d82 Mon Sep 17 00:00:00 2001 From: "zhanbo.lh" Date: Wed, 4 Dec 2024 00:55:27 +0800 Subject: [PATCH 32/38] feat: git pull --- packages/x-flow/src/components/FAutoComplete/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/x-flow/src/components/FAutoComplete/index.tsx b/packages/x-flow/src/components/FAutoComplete/index.tsx index 9fe41edb2..5f1e86a0b 100644 --- a/packages/x-flow/src/components/FAutoComplete/index.tsx +++ b/packages/x-flow/src/components/FAutoComplete/index.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { AutoComplete, InputNumber } from 'antd'; import _ from 'lodash'; From 03bb4e94ba6b651f7449ba1d80ea67cacb2f3c28 Mon Sep 17 00:00:00 2001 From: "zhanbo.lh" Date: Wed, 4 Dec 2024 01:13:08 +0800 Subject: [PATCH 33/38] =?UTF-8?q?feat:=20=E8=B0=83=E6=95=B4=E6=96=87?= =?UTF-8?q?=E6=A1=A3=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/form-render/advanced-widget.md | 2 +- docs/xflow/XFlowProvider.md | 6 ++--- docs/xflow/basic.md | 11 -------- ...{custom-flow.md => custom-node-setting.md} | 25 ++++++++----------- docs/xflow/custom-node-view.md | 15 +++++++++++ docs/xflow/index.md | 5 ---- docs/xflow/layout.md | 6 ++--- 7 files changed, 33 insertions(+), 37 deletions(-) delete mode 100644 docs/xflow/basic.md rename docs/xflow/{custom-flow.md => custom-node-setting.md} (60%) create mode 100644 docs/xflow/custom-node-view.md diff --git a/docs/form-render/advanced-widget.md b/docs/form-render/advanced-widget.md index 8d9e2fa89..e78bce8c3 100644 --- a/docs/form-render/advanced-widget.md +++ b/docs/form-render/advanced-widget.md @@ -1,5 +1,5 @@ --- -order: 4 +order: 2 toc: content mobile: false group: diff --git a/docs/xflow/XFlowProvider.md b/docs/xflow/XFlowProvider.md index 60feb2caa..09c52c6a1 100644 --- a/docs/xflow/XFlowProvider.md +++ b/docs/xflow/XFlowProvider.md @@ -1,10 +1,10 @@ --- -order: 2 -title: 'XFlowProvider' +order: 3 +title: '多实例画布' mobile: false group: title: 最佳展示 - order: 2 + order: 4 --- # 基础交互 diff --git a/docs/xflow/basic.md b/docs/xflow/basic.md deleted file mode 100644 index 37699522f..000000000 --- a/docs/xflow/basic.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -order: 2 -title: '基础交互' -mobile: false -group: - title: 最佳展示 - order: 2 ---- -# 基础交互 - - diff --git a/docs/xflow/custom-flow.md b/docs/xflow/custom-node-setting.md similarity index 60% rename from docs/xflow/custom-flow.md rename to docs/xflow/custom-node-setting.md index ef3ecb2c9..0bb976f60 100644 --- a/docs/xflow/custom-flow.md +++ b/docs/xflow/custom-node-setting.md @@ -1,21 +1,21 @@ --- order: 2 -title: '自定义组件' +toc: content mobile: false group: title: 最佳展示 - order: 2 + order: 1 --- -# 自定义组件 -## 自定义配置组件 +# 节点配置面板 +节点配置面板支持以下两种渲染方式自定义: -使用`settingWidget`自定义业务配置组件 +- **Schema 方式**: 适用于节点配置较为简单的场景。通过 FormRender 配置 schema 来实现快速渲染。 +- **Widget 方式**: 针对复杂的配置需求,schema 无法满足时,可以通过自定义组件进行灵活渲染。 - - -## 在schema中自定义组件 +## Schema +通过配置节点的 settingSchema 属性,实现节点数据配置项的自定义渲染。 ```jsx import { Input } from 'antd'; @@ -60,9 +60,6 @@ export default () => {
@@ -72,7 +69,7 @@ export default () => { ``` -## 自定义业务配置信息展示组件 -使用`nodeWidget`自定义节点的业务配置信息展示组件,在节点内部展示业务配置信息 +## Widget +通过配置节点的 `settingWidget` 属性,实现节点数据配置项的自定义渲染。 - + diff --git a/docs/xflow/custom-node-view.md b/docs/xflow/custom-node-view.md new file mode 100644 index 000000000..787884b37 --- /dev/null +++ b/docs/xflow/custom-node-view.md @@ -0,0 +1,15 @@ +--- +order: 2 +toc: content +mobile: false +group: + title: 最佳展示 + order: 1 +--- + +# 节点内容展示 +当需要额外展示节点内容时,可以通过 `nodeWidget` 进行灵活渲染。 + +使用`nodeWidget`自定义节点的业务配置信息展示组件,在节点内部展示业务配置信息 + + diff --git a/docs/xflow/index.md b/docs/xflow/index.md index 1602e98fe..b11d9abd8 100644 --- a/docs/xflow/index.md +++ b/docs/xflow/index.md @@ -41,8 +41,6 @@ npm i @xrenders/xflow --save */ import React from 'react'; import XFlow from '@xrenders/xflow'; -import schema from './schema/basic'; -import data from './data/basic'; import settings from './schema/settings'; export default () => { @@ -76,9 +74,6 @@ export default () => {
); diff --git a/docs/xflow/layout.md b/docs/xflow/layout.md index 00100a152..c3bb48e7a 100644 --- a/docs/xflow/layout.md +++ b/docs/xflow/layout.md @@ -1,10 +1,10 @@ --- -order: 3 -title: '布局方向' +order: 1 +title: '画布布局' mobile: false group: title: 最佳展示 - order: 2 + order: 1 --- # 布局方向 From 9cac8dc011c2129344acf44af57fb4121dbf2158 Mon Sep 17 00:00:00 2001 From: "zhanbo.lh" Date: Wed, 4 Dec 2024 01:30:46 +0800 Subject: [PATCH 34/38] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=20xflow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../9af73f40ad414c73-turbo.log.2024-12-03 | 10 + docs/xflow/layout.md | 2 +- docs/xflow/nodeSetting.md | 6 +- packages/x-flow/src/constants/index.ts | 261 ------------------ .../src/nodes/node-common/setting/index.tsx | 130 --------- .../src/nodes/node-end/setting/index.tsx | 130 --------- .../src/nodes/node-start/setting/index.tsx | 106 ------- packages/x-flow/src/withProvider.tsx | 2 +- 8 files changed, 15 insertions(+), 632 deletions(-) create mode 100644 .turbo/daemon/9af73f40ad414c73-turbo.log.2024-12-03 delete mode 100644 packages/x-flow/src/constants/index.ts delete mode 100644 packages/x-flow/src/nodes/node-common/setting/index.tsx delete mode 100644 packages/x-flow/src/nodes/node-end/setting/index.tsx delete mode 100644 packages/x-flow/src/nodes/node-start/setting/index.tsx diff --git a/.turbo/daemon/9af73f40ad414c73-turbo.log.2024-12-03 b/.turbo/daemon/9af73f40ad414c73-turbo.log.2024-12-03 new file mode 100644 index 000000000..a2ece7970 --- /dev/null +++ b/.turbo/daemon/9af73f40ad414c73-turbo.log.2024-12-03 @@ -0,0 +1,10 @@ +2024-12-03T17:26:27.588772Z WARN turborepo_lib::package_changes_watcher: changed_files: {AnchoredSystemPathBuf("packages/x-flow/src/types.ts")} +2024-12-03T17:26:27.588794Z WARN turborepo_lib::package_changes_watcher: changed_packages: Ok(Some({WorkspacePackage { name: Other("@xrenders/xflow"), path: AnchoredSystemPathBuf("packages/x-flow") }})) +2024-12-03T17:27:03.185223Z WARN turborepo_lib::package_changes_watcher: changed_files: {AnchoredSystemPathBuf("packages/x-flow/src/types.ts")} +2024-12-03T17:27:03.185237Z WARN turborepo_lib::package_changes_watcher: changed_packages: Ok(Some({WorkspacePackage { name: Other("@xrenders/xflow"), path: AnchoredSystemPathBuf("packages/x-flow") }})) +2024-12-03T17:27:46.488909Z WARN turborepo_lib::package_changes_watcher: changed_files: {AnchoredSystemPathBuf("packages/x-flow/src/withProvider.tsx")} +2024-12-03T17:27:46.488931Z WARN turborepo_lib::package_changes_watcher: changed_packages: Ok(Some({WorkspacePackage { name: Other("@xrenders/xflow"), path: AnchoredSystemPathBuf("packages/x-flow") }})) +2024-12-03T17:27:51.584147Z WARN turborepo_lib::package_changes_watcher: changed_files: {AnchoredSystemPathBuf("packages/x-flow/src/types.ts")} +2024-12-03T17:27:51.584179Z WARN turborepo_lib::package_changes_watcher: changed_packages: Ok(Some({WorkspacePackage { name: Other("@xrenders/xflow"), path: AnchoredSystemPathBuf("packages/x-flow") }})) +2024-12-03T17:29:05.583324Z WARN turborepo_lib::package_changes_watcher: changed_files: {AnchoredSystemPathBuf("packages/x-flow/src/withProvider.tsx")} +2024-12-03T17:29:05.583342Z WARN turborepo_lib::package_changes_watcher: changed_packages: Ok(Some({WorkspacePackage { name: Other("@xrenders/xflow"), path: AnchoredSystemPathBuf("packages/x-flow") }})) diff --git a/docs/xflow/layout.md b/docs/xflow/layout.md index c3bb48e7a..937ef17e7 100644 --- a/docs/xflow/layout.md +++ b/docs/xflow/layout.md @@ -1,5 +1,5 @@ --- -order: 1 +order: 5 title: '画布布局' mobile: false group: diff --git a/docs/xflow/nodeSetting.md b/docs/xflow/nodeSetting.md index 38e2930e2..ac9e72810 100644 --- a/docs/xflow/nodeSetting.md +++ b/docs/xflow/nodeSetting.md @@ -1,10 +1,10 @@ --- -order: 4 -title: '节点配置' +order: 1 +title: '节点菜单配置' mobile: false group: title: 最佳展示 - order: 2 + order: 1 --- # 节点配置 diff --git a/packages/x-flow/src/constants/index.ts b/packages/x-flow/src/constants/index.ts deleted file mode 100644 index c33d923f7..000000000 --- a/packages/x-flow/src/constants/index.ts +++ /dev/null @@ -1,261 +0,0 @@ -import type { Var } from '../types' -import { BlockEnum, VarType } from '../types'; - -export const CUSTOM_ITERATION_START_NODE = 'custom-iteration-start' - - -export const NODE_WIDTH = 240 -export const X_OFFSET = 60 -export const NODE_WIDTH_X_OFFSET = NODE_WIDTH + X_OFFSET -export const Y_OFFSET = 39 -export const MAX_TREE_DEPTH = 50 -export const START_INITIAL_POSITION = { x: 80, y: 282 } -export const AUTO_LAYOUT_OFFSET = { - x: -42, - y: 243, -} -export const ITERATION_NODE_Z_INDEX = 1 -export const ITERATION_CHILDREN_Z_INDEX = 1002 -export const ITERATION_PADDING = { - top: 65, - right: 16, - bottom: 20, - left: 16, -} -export const PARALLEL_LIMIT = 10 -export const PARALLEL_DEPTH_LIMIT = 3 - -export const RETRIEVAL_OUTPUT_STRUCT = `{ - "content": "", - "title": "", - "url": "", - "icon": "", - "metadata": { - "dataset_id": "", - "dataset_name": "", - "document_id": [], - "document_name": "", - "document_data_source_type": "", - "segment_id": "", - "segment_position": "", - "segment_word_count": "", - "segment_hit_count": "", - "segment_index_node_hash": "", - "score": "" - } -}` - -export const SUPPORT_OUTPUT_VARS_NODE = [ - BlockEnum.Start, BlockEnum.LLM, BlockEnum.KnowledgeRetrieval, BlockEnum.Code, BlockEnum.TemplateTransform, - BlockEnum.HttpRequest, BlockEnum.Tool, BlockEnum.VariableAssigner, BlockEnum.VariableAggregator, BlockEnum.QuestionClassifier, - BlockEnum.ParameterExtractor, BlockEnum.Iteration, -] - -export const LLM_OUTPUT_STRUCT: Var[] = [ - { - variable: 'text', - type: VarType.string, - }, -] - -export const KNOWLEDGE_RETRIEVAL_OUTPUT_STRUCT: Var[] = [ - { - variable: 'result', - type: VarType.arrayObject, - }, -] - -export const TEMPLATE_TRANSFORM_OUTPUT_STRUCT: Var[] = [ - { - variable: 'output', - type: VarType.string, - }, -] - -export const QUESTION_CLASSIFIER_OUTPUT_STRUCT = [ - { - variable: 'class_name', - type: VarType.string, - }, -] - -export const HTTP_REQUEST_OUTPUT_STRUCT: Var[] = [ - { - variable: 'body', - type: VarType.string, - }, - { - variable: 'status_code', - type: VarType.number, - }, - { - variable: 'headers', - type: VarType.object, - }, - { - variable: 'files', - type: VarType.arrayFile, - }, -] - -export const TOOL_OUTPUT_STRUCT: Var[] = [ - { - variable: 'text', - type: VarType.string, - }, - { - variable: 'files', - type: VarType.arrayFile, - }, - { - variable: 'json', - type: VarType.arrayObject, - }, -] - -export const PARAMETER_EXTRACTOR_COMMON_STRUCT: Var[] = [ - { - variable: '__is_success', - type: VarType.number, - }, - { - variable: '__reason', - type: VarType.string, - }, -] - -export const WORKFLOW_DATA_UPDATE = 'WORKFLOW_DATA_UPDATE' -export const CUSTOM_NODE = 'custom' -export const CUSTOM_EDGE = 'custom' -export const DSL_EXPORT_CHECK = 'DSL_EXPORT_CHECK' - - - - -export const iconSettingMap: Record = { - Start: { - icon: { - type: 'icon-start', - style: { fontSize: 14, color: '#fff' }, - bgColor: '#17B26A' - } - }, - End: { - icon: { - type: 'icon-end', - style: { fontSize: 14, color: '#fff' }, - bgColor: '#F79009' - } - }, - Code: { - title: '代码执行', - icon: { - type: 'icon-code', - style: { fontSize: 14, color: '#fff' }, - bgColor: '#2E90FA' - } - }, - Prompt: { - borderColor: '#3b82f6', - bgColor: '#f0f6fe', - icon: 'icon-prompt', - }, - LLM: { - borderColor: '#15afb3', - bgColor: '#e7f7f7', - icon: 'icon-model', - }, - Knowledge: { - borderColor: '#e7365d', - bgColor: '#fad4d7', - icon: 'icon-knowledge', - }, - Switch: { - title: '条件分支', - icon: { - type: 'icon-switch', - style: { fontSize: 14, color: '#fff' }, - bgColor: '#06AED4' - } - }, - HSF: { - title: 'HSF 请求', - icon: { - type: 'icon-hsf', - style: { fontSize: 14, color: '#fff' }, - bgColor: '#875BF7' - } - }, - Http: { - title: 'Http 请求', - icon: { - type: 'icon-http', - style: { fontSize: 14, color: '#fff' }, - bgColor: '#875BF7' - } - }, - Tool: { - title: '工具', - icon: { - type: 'icon-gongju', - style: { fontSize: 14, color: '#fff' }, - bgColor: '#2E90FA' - } - }, -}; - - -export const nodeConfigList = [ - { - title: 'Prompt', - type: 'prompt' - - }, - { - title: 'LLM', - type: 'llm' - }, - { - title: '知识库', - icon: 'icon-knowledge', - type: 'knowledge' - - - }, - { - title: 'Switch', - type: 'switch', - icon: { - type: 'icon-switch', - style: { fontSize: 14, color: '#fff' }, - bgColor: '#06AED4' - } - }, - { - title: 'HSF', - type: 'hsf', - icon: { - type: 'icon-hsf', - style: { fontSize: 14, color: '#fff' }, - bgColor: '#875BF7' - } - }, - { - title: 'Http', - type: 'http', - icon: { - type: 'icon-http', - style: { fontSize: 14, color: '#fff' }, - bgColor: '#875BF7' - } - }, - { - title: '脚步语言', - type: 'group', - items: [ - { title: 'Groovy', icon: 'icon-groovy', type: 'groovy' }, - { title: 'Javascript', icon: 'icon-js', type: 'javascript' }, - { title: 'Pathon', icon: 'icon-pathon', type: 'pathon' }, - ]} -]; - diff --git a/packages/x-flow/src/nodes/node-common/setting/index.tsx b/packages/x-flow/src/nodes/node-common/setting/index.tsx deleted file mode 100644 index ab584a305..000000000 --- a/packages/x-flow/src/nodes/node-common/setting/index.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import { useEffect, useRef } from 'react'; -import { AnyObject } from 'antd/lib/_util/type'; -import FormRender, { useForm } from 'form-render'; -import { ICard, TYPES } from '../../constant'; -import FAutoComplete from '../../../components/FAutoComplete'; - -export interface GlobalOutputProps { - data?: AnyObject; - onChange: (data: AnyObject) => void; - flowList: ICard[]; - inputItem: ICard; - readonly?: boolean; -} - -const getSchema = (request: any) => ({ - type: 'object', - displayType: 'row', - properties: { - list: { - type: 'array', - widget: 'tableList', - props: { - hideMove: true, - hideCopy: true, - size: 'small', - addBtnProps: { - type: 'dashed', - }, - actionColumnProps: { - width: 60, - }, - }, - items: { - type: 'object', - properties: { - name: { - title: '名称', - type: 'string', - width: 200, - placeholder: '请输入', - rules: [ - { - pattern: /^[a-zA-Z_][a-zA-Z0-9_]*$/, - message: '只能包含字母、数字和下划线且以字母或划线开头', - }, - ], - }, - dataType: { - title: '类型', - type: 'string', - enum: TYPES.map((el) => el.toUpperCase()), - enumNames: TYPES, - width: 120, - widget: 'select', - placeholder: '请选择', - }, - value: { - title: '值', - type: 'string', - widget: 'FAutoComplete', - props: { - placeholder: '${组件名.output}', - request, - }, - }, - }, - }, - }, - }, -}); - -export default (props: GlobalOutputProps) => { - const { data, onChange, inputItem, flowList, readonly } = props; - - const form = useForm(); - const flowListRef = useRef(); - const inputRef = useRef(); - - useEffect(() => { - flowListRef.current = flowList; - inputRef.current = inputItem; - }, [flowList, inputItem]); - - const watch = { - '#': (allValues: any) => { - onChange({ ...data, ...allValues }); - } - }; - - const request = (val: string) => { - return new Promise((resolve) => { - setTimeout(() => { - const inputValue = inputRef.current?.data; - const inputText = 'inputs'; - const options = (inputValue?.list || []) - .filter((el: any) => !!el.name) - .map((item: any) => '${#' + `${inputText}.${item.name}` + `}`); - const nodes = (flowListRef?.current || []) - .filter((el: any) => el.code !== 'Output') - .map((item: any) => { - return '${#' + `${item.code}.output` + `}`; - }); - - resolve( - [...options, ...nodes] - .filter((el: string) => val && el.includes(val)) - .map((el: string) => { - return { - value: el, - }; - }), - ); - }, 10); - }); - }; - const schema = getSchema(request); - - return ( - { - form.setValues({ list: data?.list }); - }} - /> - ); -} diff --git a/packages/x-flow/src/nodes/node-end/setting/index.tsx b/packages/x-flow/src/nodes/node-end/setting/index.tsx deleted file mode 100644 index ab584a305..000000000 --- a/packages/x-flow/src/nodes/node-end/setting/index.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import { useEffect, useRef } from 'react'; -import { AnyObject } from 'antd/lib/_util/type'; -import FormRender, { useForm } from 'form-render'; -import { ICard, TYPES } from '../../constant'; -import FAutoComplete from '../../../components/FAutoComplete'; - -export interface GlobalOutputProps { - data?: AnyObject; - onChange: (data: AnyObject) => void; - flowList: ICard[]; - inputItem: ICard; - readonly?: boolean; -} - -const getSchema = (request: any) => ({ - type: 'object', - displayType: 'row', - properties: { - list: { - type: 'array', - widget: 'tableList', - props: { - hideMove: true, - hideCopy: true, - size: 'small', - addBtnProps: { - type: 'dashed', - }, - actionColumnProps: { - width: 60, - }, - }, - items: { - type: 'object', - properties: { - name: { - title: '名称', - type: 'string', - width: 200, - placeholder: '请输入', - rules: [ - { - pattern: /^[a-zA-Z_][a-zA-Z0-9_]*$/, - message: '只能包含字母、数字和下划线且以字母或划线开头', - }, - ], - }, - dataType: { - title: '类型', - type: 'string', - enum: TYPES.map((el) => el.toUpperCase()), - enumNames: TYPES, - width: 120, - widget: 'select', - placeholder: '请选择', - }, - value: { - title: '值', - type: 'string', - widget: 'FAutoComplete', - props: { - placeholder: '${组件名.output}', - request, - }, - }, - }, - }, - }, - }, -}); - -export default (props: GlobalOutputProps) => { - const { data, onChange, inputItem, flowList, readonly } = props; - - const form = useForm(); - const flowListRef = useRef(); - const inputRef = useRef(); - - useEffect(() => { - flowListRef.current = flowList; - inputRef.current = inputItem; - }, [flowList, inputItem]); - - const watch = { - '#': (allValues: any) => { - onChange({ ...data, ...allValues }); - } - }; - - const request = (val: string) => { - return new Promise((resolve) => { - setTimeout(() => { - const inputValue = inputRef.current?.data; - const inputText = 'inputs'; - const options = (inputValue?.list || []) - .filter((el: any) => !!el.name) - .map((item: any) => '${#' + `${inputText}.${item.name}` + `}`); - const nodes = (flowListRef?.current || []) - .filter((el: any) => el.code !== 'Output') - .map((item: any) => { - return '${#' + `${item.code}.output` + `}`; - }); - - resolve( - [...options, ...nodes] - .filter((el: string) => val && el.includes(val)) - .map((el: string) => { - return { - value: el, - }; - }), - ); - }, 10); - }); - }; - const schema = getSchema(request); - - return ( - { - form.setValues({ list: data?.list }); - }} - /> - ); -} diff --git a/packages/x-flow/src/nodes/node-start/setting/index.tsx b/packages/x-flow/src/nodes/node-start/setting/index.tsx deleted file mode 100644 index 4754e202b..000000000 --- a/packages/x-flow/src/nodes/node-start/setting/index.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import FormRender, { useForm } from 'form-render'; -import ExpandInput from '@/components/ExpandInput'; -import { TYPES } from '../../constant'; - -const schema = { - type: 'object', - displayType: 'row', - properties: { - list: { - type: 'array', - widget: 'tableList', - props: { - hideMove: true, - hideCopy: true, - onRemove: 'onRemove', - size: 'small', - addBtnProps: { - type: 'dashed' - }, - pagination: { - pageSize: 15 - }, - actionColumnProps: { - width: 55 - } - }, - items: { - type: 'object', - properties: { - name: { - title: '参数名称', - type: 'string', - width: 200, - placeholder: '请输入', - disabled: `{{ rootValue.name === 'session_id' }}`, - rules: [ - { - pattern: /^[a-zA-Z_][a-zA-Z0-9_]*$/, - message: '只能包含字母、数字和下划线且以字母或划线开头' - } - ] - }, - dataType: { - title: '参数类型', - type: 'string', - enum: TYPES.map((el) => el.toUpperCase()), - enumNames: TYPES, - widget: 'select', - width: 120, - placeholder: '请选择', - disabled: `{{ rootValue.name === 'session_id' }}` - }, - value: { - title: '参数值', - type: 'string', - widget: 'ExpandInput', - placeholder: '变量:${变量名称}', - disabled: `{{ rootValue.name === 'session_id' }}` - } - } - } - } - } -}; - - - - -/** - * - * 全局输入参数配置 - * - */ -export default (props: any) => { - const { data, onChange, readonly } = props; - const form = useForm(); - - const onRemove = (deleteFn: any, params: any) => { - if (params.data?.name === 'session_id') { - props.setIsChatFlow(false); - } - deleteFn(); - }; - - const watch = { - '#': (allValues: any) => { - onChange({ ...data, ...allValues }); - }, - }; - - return ( -
- { - form.setValues({list: data?.list || []}); - }} - /> -
- ); -} diff --git a/packages/x-flow/src/withProvider.tsx b/packages/x-flow/src/withProvider.tsx index 4cd8ef796..6d3436256 100644 --- a/packages/x-flow/src/withProvider.tsx +++ b/packages/x-flow/src/withProvider.tsx @@ -13,7 +13,7 @@ interface ProviderProps { } export default function withProvider( - Element: React.ComponentType, + Element: any, defaultWidgets?: any ): React.ComponentType { return (props: ProviderProps) => { From ceb151e8ce6895ae5a60f48ef1c4e05e05da6fea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=B4=AB=E5=8D=87?= Date: Wed, 4 Dec 2024 08:06:36 +0800 Subject: [PATCH 35/38] =?UTF-8?q?chore:=20=E5=A4=9A=E5=AE=9E=E4=BE=8B?= =?UTF-8?q?=E7=94=BB=E5=B8=83=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{XFlowProvider.md => FlowProvider.md} | 2 +- docs/xflow/demo/flow-provider/edges.ts | 1 + docs/xflow/demo/flow-provider/index.tsx | 48 ++++++++ docs/xflow/demo/flow-provider/nodes.ts | 20 +++ .../setting.tsx | 0 docs/xflow/demo/xflow-provider/index.tsx | 48 -------- packages/x-flow/src/Wrapper.tsx | 28 ----- packages/x-flow/src/XFlow.tsx | 40 +++--- .../src/components/CandidateNode/index.tsx | 63 +++++----- .../src/components/CustomEdge/index.tsx | 116 +++++++++--------- .../src/components/CustomNode/index.tsx | 22 ++-- .../src/components/FlowProvider/index.tsx | 44 +++++++ .../src/components/NodeEditor/index.tsx | 7 +- .../src/components/PanelContainer/index.tsx | 23 ++-- .../src/components/XFlowProvider/index.tsx | 27 ---- packages/x-flow/src/hooks/useEdges.ts | 19 +++ packages/x-flow/src/hooks/useFlow.ts | 40 ++++++ packages/x-flow/src/hooks/useNodes.ts | 18 +++ packages/x-flow/src/hooks/useStore.ts | 31 +---- packages/x-flow/src/hooks/useTemporalStore.ts | 22 ++++ packages/x-flow/src/hooks/useXFlow.ts | 13 -- packages/x-flow/src/index.ts | 8 +- packages/x-flow/src/models/context.ts | 5 +- packages/x-flow/src/models/store.ts | 47 ++++--- packages/x-flow/src/operator/index.tsx | 2 +- packages/x-flow/src/types.ts | 4 +- packages/x-flow/src/withProvider.tsx | 21 +++- 27 files changed, 395 insertions(+), 324 deletions(-) rename docs/xflow/{XFlowProvider.md => FlowProvider.md} (68%) create mode 100644 docs/xflow/demo/flow-provider/edges.ts create mode 100644 docs/xflow/demo/flow-provider/index.tsx create mode 100644 docs/xflow/demo/flow-provider/nodes.ts rename docs/xflow/demo/{xflow-provider => flow-provider}/setting.tsx (100%) delete mode 100644 docs/xflow/demo/xflow-provider/index.tsx delete mode 100644 packages/x-flow/src/Wrapper.tsx create mode 100644 packages/x-flow/src/components/FlowProvider/index.tsx delete mode 100644 packages/x-flow/src/components/XFlowProvider/index.tsx create mode 100644 packages/x-flow/src/hooks/useEdges.ts create mode 100644 packages/x-flow/src/hooks/useFlow.ts create mode 100644 packages/x-flow/src/hooks/useNodes.ts create mode 100644 packages/x-flow/src/hooks/useTemporalStore.ts delete mode 100644 packages/x-flow/src/hooks/useXFlow.ts diff --git a/docs/xflow/XFlowProvider.md b/docs/xflow/FlowProvider.md similarity index 68% rename from docs/xflow/XFlowProvider.md rename to docs/xflow/FlowProvider.md index 09c52c6a1..086b200ce 100644 --- a/docs/xflow/XFlowProvider.md +++ b/docs/xflow/FlowProvider.md @@ -9,4 +9,4 @@ group: # 基础交互 - + diff --git a/docs/xflow/demo/flow-provider/edges.ts b/docs/xflow/demo/flow-provider/edges.ts new file mode 100644 index 000000000..d3f67bd08 --- /dev/null +++ b/docs/xflow/demo/flow-provider/edges.ts @@ -0,0 +1 @@ +export const edges = [{ source: '1', target: '2', id: '234123' }]; diff --git a/docs/xflow/demo/flow-provider/index.tsx b/docs/xflow/demo/flow-provider/index.tsx new file mode 100644 index 000000000..39287d968 --- /dev/null +++ b/docs/xflow/demo/flow-provider/index.tsx @@ -0,0 +1,48 @@ +import React from "react"; + +import XFlow, { FlowProvider, useNodes } from '@xrenders/xflow'; +import { edges as initialEdges } from './edges'; +import { nodes as initialNodes } from './nodes'; +import settings from './setting'; + +const App = () => { + return ( + +
+ +
+ +
+ ); +}; + +function Sidebar() { + // This hook will only work if the component it's used in is a child of a + // . + const nodes = useNodes(); + + return ( + + ); +} + +export default () => { + return ( + + + + ); +}; diff --git a/docs/xflow/demo/flow-provider/nodes.ts b/docs/xflow/demo/flow-provider/nodes.ts new file mode 100644 index 000000000..79e981129 --- /dev/null +++ b/docs/xflow/demo/flow-provider/nodes.ts @@ -0,0 +1,20 @@ +export const nodes = [ + { + id: '1', + type: 'Start', + data: {}, + position: { + x: 40, + y: 240, + }, + }, + { + id: '2', + type: 'End', + data: {}, + position: { + x: 500, + y: 240, + }, + }, +]; diff --git a/docs/xflow/demo/xflow-provider/setting.tsx b/docs/xflow/demo/flow-provider/setting.tsx similarity index 100% rename from docs/xflow/demo/xflow-provider/setting.tsx rename to docs/xflow/demo/flow-provider/setting.tsx diff --git a/docs/xflow/demo/xflow-provider/index.tsx b/docs/xflow/demo/xflow-provider/index.tsx deleted file mode 100644 index 61bac2959..000000000 --- a/docs/xflow/demo/xflow-provider/index.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import React from "react"; -import XFlow, { XFlowProvider } from '@xrenders/xflow'; -import settings from './setting'; - -const App = () => { - const nodes = [ - { - id: '1', - type: 'Start', - data: {}, - position: { - x: 40, - y: 240, - }, - }, - { - id: '2', - type: 'End', - data: {}, - position: { - x: 500, - y: 240, - }, - }, - ]; - - const edges = [{ source: '1', target: '2', id: '234123' }]; - - return ( -
- -
- ); -}; - -export default () => { - return ( - - - - ); -}; diff --git a/packages/x-flow/src/Wrapper.tsx b/packages/x-flow/src/Wrapper.tsx deleted file mode 100644 index 46ecaa30f..000000000 --- a/packages/x-flow/src/Wrapper.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import React, { useContext } from 'react'; - -import { XFlowProvider } from './components/XFlowProvider'; -import StoreContext from './models/context'; - -export const Wrapper = ({ - children, - nodes, - edges, -}: { - children: React.ReactNode; - nodes: any[]; - edges: any[]; -}) => { - const isWrapped = useContext(StoreContext); - - if (isWrapped) { - // we need to wrap it with a fragment because it's not allowed for children to be a ReactNode - // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18051 - return <>{children}; - } - - return ( - - {children} - - ); -}; diff --git a/packages/x-flow/src/XFlow.tsx b/packages/x-flow/src/XFlow.tsx index f7d66e107..405dfa550 100644 --- a/packages/x-flow/src/XFlow.tsx +++ b/packages/x-flow/src/XFlow.tsx @@ -11,7 +11,6 @@ import produce, { setAutoFreeze } from 'immer'; import { debounce } from 'lodash'; import type { FC } from 'react'; import React, { memo, useEffect, useMemo, useRef, useState } from 'react'; -import { useShallow } from 'zustand/react/shallow'; import CandidateNode from './components/CandidateNode'; import CustomEdge from './components/CustomEdge'; import PanelContainer from './components/PanelContainer'; @@ -19,14 +18,17 @@ import { useEventEmitterContextContext } from './models/event-emitter'; import CustomNodeComponent from './components/CustomNode'; import { useStore, useStoreApi } from './hooks/useStore'; +import { useTemporalStore } from './hooks/useTemporalStore'; + import Operator from './operator'; -import XFlowProps from './types'; +import FlowProps from './types'; import { transformNodes, uuid } from './utils'; import autoLayoutNodes from './utils/autoLayoutNodes'; +import { shallow } from 'zustand/shallow'; import NodeEditor from './components/NodeEditor'; +import { useFlow } from './hooks/useFlow'; import './index.less'; -import { Wrapper } from './Wrapper'; const CustomNode = memo(CustomNodeComponent); const edgeTypes = { buttonedge: memo(CustomEdge) }; @@ -36,12 +38,11 @@ const edgeTypes = { buttonedge: memo(CustomEdge) }; * XFlow 入口 * */ -const App: FC = memo(props => { +const XFlow: FC = memo(props => { const { initialValues, settings } = props; const workflowContainerRef = useRef(null); const store = useStoreApi(); - const { updateEdge, zoomTo } = useReactFlow(); - // const { undo, redo, record } = useTemporalStore(); + const { zoomTo } = useReactFlow(); const { layout, nodes, @@ -49,32 +50,26 @@ const App: FC = memo(props => { onNodesChange, onEdgesChange, onConnect, - setNodes, - setEdges, - addNodes, - addEdges, setLayout, setCandidateNode, setMousePosition, } = useStore( - useShallow(state => ({ + state => ({ nodes: state.nodes, edges: state.edges, layout: state.layout, setLayout: state.setLayout, - setNodes: state.setNodes, - setEdges: state.setEdges, - addNodes: state.addNodes, - addEdges: state.addEdges, setMousePosition: state.setMousePosition, setCandidateNode: state.setCandidateNode, onNodesChange: state.onNodesChange, onEdgesChange: state.onEdgesChange, onConnect: state.onConnect, - })) + }), + shallow ); + const { setNodes, setEdges } = useFlow(); const [activeNode, setActiveNode] = useState(null); - + const temporalStore = useTemporalStore(); useEffect(() => { zoomTo(0.8); setAutoFreeze(false); @@ -85,9 +80,10 @@ const App: FC = memo(props => { useEffect(() => { setLayout(props.layout); + // TODO: 默认关闭时间机器,可以向 zundo 贡献一个配置 + temporalStore.pause(); setNodes(transformNodes(initialValues?.nodes)); setEdges(initialValues?.edges); - store.temporal.getState().pause(); }, []); useEventListener('keydown', e => { @@ -288,12 +284,4 @@ const App: FC = memo(props => { ); }); -const XFlow: FC = (props) => { - const { initialValues } = props; - return ( - - - - ); -}; export default XFlow; diff --git a/packages/x-flow/src/components/CandidateNode/index.tsx b/packages/x-flow/src/components/CandidateNode/index.tsx index 8bb35d526..4e6bd6269 100644 --- a/packages/x-flow/src/components/CandidateNode/index.tsx +++ b/packages/x-flow/src/components/CandidateNode/index.tsx @@ -1,63 +1,55 @@ -import React from 'react'; -import { memo } from 'react'; -import produce from 'immer'; -import { useShallow } from 'zustand/react/shallow'; import { useReactFlow, useViewport } from '@xyflow/react'; import { useEventListener } from 'ahooks'; -import CustomNode from '../CustomNode'; +import React, { memo } from 'react'; +import { shallow } from 'zustand/shallow'; import { useStore } from '../../hooks/useStore'; +import CustomNode from '../CustomNode'; const CandidateNode = () => { const { zoom } = useViewport(); const reactflow = useReactFlow(); - - const { - nodes, - candidateNode, - mousePosition, - addNodes, - setCandidateNode - } = useStore( - useShallow((state: any) => ({ + const { candidateNode, mousePosition, setCandidateNode, addNodes } = useStore( + (state: any) => ({ nodes: state.nodes, edges: state.edges, + addNodes: state.addNodes, candidateNode: state.candidateNode, mousePosition: state.mousePosition, - addNodes: state.addNodes, setCandidateNode: state.setCandidateNode, onNodesChange: state.onNodesChange, onEdgesChange: state.onEdgesChange, - })) + }), + shallow ); - useEventListener('click', (ev) => { + useEventListener('click', ev => { if (!candidateNode) { return; } ev.preventDefault(); const { screenToFlowPosition } = reactflow; - const { x, y } = screenToFlowPosition({ x: mousePosition.pageX, y: mousePosition.pageY }); - - const newNodes = produce(nodes, (draft: any) => { - draft.push({ - ...candidateNode, - data: { - ...candidateNode.data, - _isCandidate: false, - }, - position: { x, y } - }); + const { x, y } = screenToFlowPosition({ + x: mousePosition.pageX, + y: mousePosition.pageY, }); + + const newNodes = { + ...candidateNode, + data: { + ...candidateNode.data, + _isCandidate: false, + }, + position: { x, y }, + }; + // @ts-ignore addNodes(newNodes); setCandidateNode(null); }); if (!candidateNode) { - return null + return null; } - console.log(mousePosition, '=======000000') - return (
{ transform: `scale(${zoom})`, transformOrigin: '0 0', position: 'absolute', - zIndex: 10000 + zIndex: 10000, }} > - +
); -} +}; export default memo(CandidateNode); diff --git a/packages/x-flow/src/components/CustomEdge/index.tsx b/packages/x-flow/src/components/CustomEdge/index.tsx index c07017715..975595e52 100644 --- a/packages/x-flow/src/components/CustomEdge/index.tsx +++ b/packages/x-flow/src/components/CustomEdge/index.tsx @@ -1,24 +1,21 @@ -import React, { memo, useState } from 'react'; -import { PlusOutlined, CloseOutlined } from '@ant-design/icons'; -import { BezierEdge, EdgeLabelRenderer, getBezierPath, useReactFlow } from '@xyflow/react'; -import { useShallow } from 'zustand/react/shallow'; +import { CloseOutlined, PlusOutlined } from '@ant-design/icons'; +import { + BezierEdge, + EdgeLabelRenderer, + getBezierPath, + useReactFlow, +} from '@xyflow/react'; import produce from 'immer'; -import { uuid } from '../../utils'; +import React, { memo, useState } from 'react'; +import { shallow } from 'zustand/shallow'; import { useStore } from '../../hooks/useStore'; +import { uuid } from '../../utils'; import NodeSelectPopover from '../NodesPopover'; import './index.less'; export default memo((edge: any) => { - const { - id, - selected, - sourceX, - sourceY, - targetX, - targetY, - source, - target, - } = edge; + const { id, selected, sourceX, sourceY, targetX, targetY, source, target } = + edge; const reactflow = useReactFlow(); const [isHovered, setIsHovered] = useState(false); @@ -38,21 +35,24 @@ export default memo((edge: any) => { onEdgesChange, layout, } = useStore( - useShallow((state: any) => ({ + (state: any) => ({ layout: state.layout, nodes: state.nodes, edges: state.edges, mousePosition: state.mousePosition, setNodes: state.setNodes, setEdges: state.setEdges, - onEdgesChange: state.onEdgesChange - })) + onEdgesChange: state.onEdgesChange, + }), + shallow ); const handleAddNode = (data: any) => { const { screenToFlowPosition } = reactflow; - const { x, y } = screenToFlowPosition({ x: mousePosition.pageX, y: mousePosition.pageY }); - + const { x, y } = screenToFlowPosition({ + x: mousePosition.pageX, + y: mousePosition.pageY, + }); const targetId = uuid(); const newNodes = produce(nodes, (draft: any) => { @@ -60,24 +60,25 @@ export default memo((edge: any) => { id: targetId, type: 'custom', data, - position: { x, y } + position: { x, y }, }); }); const newEdges = produce(edges, (draft: any) => { - draft.push(...[ - { - id: uuid(), - source, - target: targetId, - }, - { - id: uuid(), - source: targetId, - target, - } - ]) - + draft.push( + ...[ + { + id: uuid(), + source, + target: targetId, + }, + { + id: uuid(), + source: targetId, + target, + }, + ] + ); }); setNodes(newNodes); @@ -87,13 +88,13 @@ export default memo((edge: any) => { let edgeExtra: any = { sourceX: edge.sourceX - 15, - targetX: edge.targetX + 15 - } + targetX: edge.targetX + 15, + }; if (layout === 'TB') { edgeExtra = { sourceY: edge.sourceY - 15, - targetY: edge.targetY + 13 - } + targetY: edge.targetY + 13, + }; } return ( @@ -106,27 +107,32 @@ export default memo((edge: any) => { {...edgeExtra} edgePath={edgePath} label={ - isHovered && -
-
-
onEdgesChange([{ id, type: 'remove' }])}> + isHovered && ( + +
+
+
onEdgesChange([{ id, type: 'remove' }])} + >
- -
- -
-
+ +
+ +
+
+
-
- + + ) } /> ); -}) +}); diff --git a/packages/x-flow/src/components/CustomNode/index.tsx b/packages/x-flow/src/components/CustomNode/index.tsx index 201ee9af8..bbff9b7fb 100644 --- a/packages/x-flow/src/components/CustomNode/index.tsx +++ b/packages/x-flow/src/components/CustomNode/index.tsx @@ -2,7 +2,7 @@ import { Handle, Position, useReactFlow } from '@xyflow/react'; import classNames from 'classnames'; import produce from 'immer'; import React, { memo, useContext, useState } from 'react'; -import { useShallow } from 'zustand/react/shallow'; +import { shallow } from 'zustand/shallow'; import { useStore } from '../../hooks/useStore'; import { ConfigContext } from '../../models/context'; import { capitalize, uuid } from '../../utils'; @@ -12,10 +12,10 @@ import SourceHandle from './sourceHandle'; const generateRandomArray = (x: number, type?: string) => { const randomArray = []; for (let i = 0; i < x; i++) { - const id=`id_${i}` + const id = `id_${i}`; if (type === 'Switch') { if (i === 0) { - randomArray.push({ id , switchTitle: 'IF' }); + randomArray.push({ id, switchTitle: 'IF' }); } else if (i === x - 1) { randomArray.push({ id, switchTitle: 'ELSE' }); } else { @@ -36,14 +36,15 @@ export default memo((props: any) => { const [isHovered, setIsHovered] = useState(false); const reactflow = useReactFlow(); const { edges, nodes, setNodes, setEdges, mousePosition } = useStore( - useShallow((state: any) => ({ + (state: any) => ({ nodes: state.nodes, edges: state.edges, mousePosition: state.mousePosition, setNodes: state.setNodes, setEdges: state.setEdges, onEdgesChange: state.onEdgesChange, - })) + }), + shallow ); // data中的switchData的长度,即:if和if else 的数量,条件数量 const switchDataLength = @@ -57,7 +58,6 @@ export default memo((props: any) => { const nodeMinHeight = type === 'Switch' ? Number(switchDataLength * nodeHeight) : undefined; - // 增加节点并进行联系 const handleAddNode = (data: any) => { const { screenToFlowPosition } = reactflow; @@ -118,7 +118,7 @@ export default memo((props: any) => { /> {!settingMap?.[type]?.sourceHandleHidden && ( <> - {(sourceHandleNum||[])?.map((item, key) => ( + {(sourceHandleNum || [])?.map((item, key) => ( { isHovered={isHovered} handleAddNode={handleAddNode} id={item?.id} - style={item?.switchTitle ? key === 0 ? { top: 40 } : { top: 40 * key + 40 }:{}} + style={ + item?.switchTitle + ? key === 0 + ? { top: 40 } + : { top: 40 * key + 40 } + : {} + } switchTitle={item?.switchTitle} /> ))} diff --git a/packages/x-flow/src/components/FlowProvider/index.tsx b/packages/x-flow/src/components/FlowProvider/index.tsx new file mode 100644 index 000000000..ed34fcf5c --- /dev/null +++ b/packages/x-flow/src/components/FlowProvider/index.tsx @@ -0,0 +1,44 @@ +import StoreContext, { Provider } from '../../models/context'; +import { createStore } from '../../models/store'; + +import type { ReactNode } from 'react'; +import React, { memo, useContext, useState } from 'react'; + +export const FlowProvider = memo<{ + initialNodes: any[]; + initialEdges: any[]; + children: ReactNode; +}>(({ initialNodes: nodes = [], initialEdges: edges = [], children }) => { + const [store] = useState(() => + createStore({ + nodes, + edges, + }) + ); + + return {children}; +}); + +export const FlowProviderWrapper = ({ + children, + nodes, + edges, +}: { + children: React.ReactNode; + nodes: any[]; + edges: any[]; +}) => { + const isWrapped = useContext(StoreContext); + + if (isWrapped) { + // we need to wrap it with a fragment because it's not allowed for children to be a ReactNode + // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18051 + return <>{children}; + } + + return ( + + {children} + + ); +}; diff --git a/packages/x-flow/src/components/NodeEditor/index.tsx b/packages/x-flow/src/components/NodeEditor/index.tsx index 49f622b4b..91a75d60a 100644 --- a/packages/x-flow/src/components/NodeEditor/index.tsx +++ b/packages/x-flow/src/components/NodeEditor/index.tsx @@ -2,7 +2,7 @@ import FormRender, { useForm } from 'form-render'; import produce from 'immer'; import { debounce } from 'lodash'; import React, { FC, useContext, useEffect, useState } from 'react'; -import { useShallow } from 'zustand/react/shallow'; +import { shallow } from 'zustand/shallow'; import { useStore } from '../../hooks/useStore'; import { ConfigContext } from '../../models/context'; @@ -24,10 +24,11 @@ const NodeEditor: FC = (props: any) => { const NodeWidget = widgets[nodeSetting?.settingWidget]; // 自定义面板配置组件 const { nodes, setNodes } = useStore( - useShallow((state: any) => ({ + (state: any) => ({ nodes: state.nodes, setNodes: state.setNodes, - })) + }), + shallow ); useEffect(() => { diff --git a/packages/x-flow/src/components/PanelContainer/index.tsx b/packages/x-flow/src/components/PanelContainer/index.tsx index 025294323..bc9778c59 100644 --- a/packages/x-flow/src/components/PanelContainer/index.tsx +++ b/packages/x-flow/src/components/PanelContainer/index.tsx @@ -2,9 +2,9 @@ import { Divider, Drawer, Input, Space } from 'antd'; import produce from 'immer'; import { debounce } from 'lodash'; import React, { FC, useContext, useEffect, useState } from 'react'; -import { useShallow } from 'zustand/react/shallow'; -import { ConfigContext } from '../../models/context'; +import { shallow } from 'zustand/shallow'; import { useStore } from '../../hooks/useStore'; +import { ConfigContext } from '../../models/context'; import IconView from '../IconView'; import './index.less'; @@ -31,24 +31,17 @@ const getDescription = (nodeType: string, description: string) => { const Panel: FC = (props: any) => { // disabled属性取的地方可能不对------to do - const { - onClose, - children, - nodeType, - disabled, - node, - description, - id, - data, - } = props; + const { onClose, children, nodeType, disabled, node, description, id, data } = + props; // 1.获取节点配置信息 const { settingMap } = useContext(ConfigContext); const nodeSetting = settingMap[nodeType] || {}; const { nodes, setNodes } = useStore( - useShallow((state: any) => ({ + (state: any) => ({ nodes: state.nodes, setNodes: state.setNodes, - })) + }), + shallow ); const isDisabled = ['Input', 'Output'].includes(nodeType) || disabled; @@ -80,7 +73,6 @@ const Panel: FC = (props: any) => { setTitleVal(data?.title || nodeSetting?.title); }, [JSON.stringify(data), id]); - return ( = (props: any) => { onChange={e => { setTitleVal(e.target.value); handleNodeValueChange({ title: e.target.value }); - }} /> )} diff --git a/packages/x-flow/src/components/XFlowProvider/index.tsx b/packages/x-flow/src/components/XFlowProvider/index.tsx deleted file mode 100644 index 3daffe9b7..000000000 --- a/packages/x-flow/src/components/XFlowProvider/index.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { Provider } from '../../models/context'; -import { createStore } from '../../models/store'; - -import type { ReactNode } from 'react'; -import React, { memo, useEffect, useState } from 'react'; - -export const XFlowProvider = memo<{ - initialNodes: any[]; - initialEdges: any[]; - children: ReactNode; -}>(({ initialNodes: nodes, initialEdges: edges, children }) => { - const [store] = useState(() => - createStore({ - nodes, - edges, - }) - ); - - useEffect(() => { - // TODO: 默认暂停时间,向 zundo 贡献代码 - console.info("暂停时间") - // store.temporal.getState().pause(); - }, []); - - // TODO: 合并 Wrapper 和 withProvider - return {children}; -}); diff --git a/packages/x-flow/src/hooks/useEdges.ts b/packages/x-flow/src/hooks/useEdges.ts new file mode 100644 index 000000000..f9c5a3eba --- /dev/null +++ b/packages/x-flow/src/hooks/useEdges.ts @@ -0,0 +1,19 @@ +import type { Edge } from "@xyflow/react"; +import { shallow } from 'zustand/shallow'; + +import { useStore } from './useStore'; +import type { FlowState } from '../models/store'; + +const nodesSelector = (state: FlowState) => state.edges; + +/** + * Hook for getting the current edges from the store. + * + * @public + * @returns An array of edges + */ +export function useEdges(): Edge[] { + const nodes = useStore(nodesSelector, shallow) as EdgeType[]; + + return nodes; +} diff --git a/packages/x-flow/src/hooks/useFlow.ts b/packages/x-flow/src/hooks/useFlow.ts new file mode 100644 index 000000000..baf2c8b49 --- /dev/null +++ b/packages/x-flow/src/hooks/useFlow.ts @@ -0,0 +1,40 @@ +import { useMemo } from 'react'; +import { useMemoizedFn } from 'ahooks'; +import { useStoreApi } from './useStore'; +import { useTemporalStore } from './useTemporalStore'; +import { FlowNode } from '../models/store'; +import { Edge } from '@xyflow/react'; + +export const useFlow = () => { + const storeApi = useStoreApi(); + const instance = storeApi.getState(); + const temporalStore = useTemporalStore(); + + const getNodes = useMemoizedFn(() => storeApi.getState().nodes); + const getEdges = useMemoizedFn(() => storeApi.getState().edges); + const setNodes = useMemoizedFn((nodes: FlowNode[]) => { + temporalStore.record(() => { + storeApi.getState().setNodes(nodes); + }) + }) + const addNodes = useMemoizedFn((nodes: FlowNode[]) => { + temporalStore.record(() => { + storeApi.getState().addNodes(nodes); + }) + }) + const setEdges = useMemoizedFn((edges: Edge[]) => { + storeApi.getState().setEdges(edges); + }) + const addEdges = useMemoizedFn((edges: Edge[]) => { + storeApi.getState().addEdges(edges); + }) + + return useMemo(() => ({ + setNodes, + addNodes, + setEdges, + addEdges, + getNodes, + getEdges, + }), [instance]); +}; diff --git a/packages/x-flow/src/hooks/useNodes.ts b/packages/x-flow/src/hooks/useNodes.ts new file mode 100644 index 000000000..da54e91b4 --- /dev/null +++ b/packages/x-flow/src/hooks/useNodes.ts @@ -0,0 +1,18 @@ +import { shallow } from 'zustand/shallow'; + +import { useStore } from '../hooks/useStore'; +import type { FlowNode, FlowState } from '../models/store'; + +const nodesSelector = (state: FlowState) => state.nodes; + +/** + * Hook for getting the current nodes from the store. + * + * @public + * @returns An array of nodes + */ +export function useNodes(): NodeType[] { + const nodes = useStore(nodesSelector, shallow) as NodeType[]; + + return nodes; +} diff --git a/packages/x-flow/src/hooks/useStore.ts b/packages/x-flow/src/hooks/useStore.ts index e1fff4d85..7dc21dbb7 100644 --- a/packages/x-flow/src/hooks/useStore.ts +++ b/packages/x-flow/src/hooks/useStore.ts @@ -1,12 +1,12 @@ import { useContext, useMemo } from 'react'; import StoreContext from '../models/context'; -import { XFlowNode, XFlowState } from '../models/store'; +import { FlowNode, FlowState } from '../models/store'; import { Edge } from '@xyflow/react'; import { useStoreWithEqualityFn } from 'zustand/traditional'; const useStore = ( - selector: (state: XFlowState) => T, + selector: (state: FlowState) => T, equalityFn?: (a: T, b: T) => boolean ) => { const store = useContext(StoreContext); @@ -21,7 +21,7 @@ const useStore = ( }; const useStoreApi = < - NodeType extends XFlowNode = XFlowNode, + NodeType extends FlowNode = FlowNode, EdgeType extends Edge = Edge >() => { const store = useContext(StoreContext); @@ -42,27 +42,4 @@ const useStoreApi = < ); }; -const useTemporalStore = () => { - const store = useContext(StoreContext); - - if (store === null) { - throw new Error( - '[XFlow]: Seems like you have not used zustand provider as an ancestor.' - ); - } - - return { - ...store.temporal.getState(), - record: (callback: () => void) => { - const temporalStore = store.temporal.getState(); - temporalStore.resume(); - callback(); - temporalStore.pause(); - }, - }; -}; - -// 默认关闭时间机器 -// useStoreApi().temporal.getState(); - -export { useStore, useStoreApi, useTemporalStore }; +export { useStore, useStoreApi }; diff --git a/packages/x-flow/src/hooks/useTemporalStore.ts b/packages/x-flow/src/hooks/useTemporalStore.ts new file mode 100644 index 000000000..baa26653e --- /dev/null +++ b/packages/x-flow/src/hooks/useTemporalStore.ts @@ -0,0 +1,22 @@ +import StoreContext from '../models/context'; +import { useContext } from 'react'; + +export const useTemporalStore = () => { + const store = useContext(StoreContext); + + if (store === null) { + throw new Error( + '[XFlow]: Seems like you have not used zustand provider as an ancestor.' + ); + } + + return { + ...store.temporal.getState(), + record: (callback: () => void) => { + const temporalStore = store.temporal.getState(); + temporalStore.resume(); + callback(); + temporalStore.pause(); + }, + }; +}; diff --git a/packages/x-flow/src/hooks/useXFlow.ts b/packages/x-flow/src/hooks/useXFlow.ts deleted file mode 100644 index 24a1d4cf9..000000000 --- a/packages/x-flow/src/hooks/useXFlow.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { useMemo } from 'react'; -import { useStoreApi } from '../hooks/useStore'; - -export const useXFlow = () => { - const store = useStoreApi(); - - const instance = store.getState(); - - return useMemo(() => ({ - ...instance, - // TODO: 扩展 useXFlow 的方法 - }), [instance]); -}; diff --git a/packages/x-flow/src/index.ts b/packages/x-flow/src/index.ts index dd4ffa59b..85dc9d99a 100644 --- a/packages/x-flow/src/index.ts +++ b/packages/x-flow/src/index.ts @@ -7,7 +7,9 @@ export type { default as FR, } from './types'; -export { XFlowProvider } from './components/XFlowProvider'; -export { useXFlow } from './hooks/useXFlow'; -export { useStore, useStoreApi } from './hooks/useStore'; +export { FlowProvider } from './components/FlowProvider'; +export { useFlow } from './hooks/useFlow'; +export { useNodes } from './hooks/useNodes'; +export { useEdges } from './hooks/useEdges'; + export default withProvider(XFlow, nodes); diff --git a/packages/x-flow/src/models/context.ts b/packages/x-flow/src/models/context.ts index b1d20cc7a..2930f6619 100644 --- a/packages/x-flow/src/models/context.ts +++ b/packages/x-flow/src/models/context.ts @@ -1,9 +1,8 @@ import { createContext } from 'react'; -import { XFlowStore } from './store'; +import { FlowStore } from './store'; -// TODO: 合并到 StoreContext export const ConfigContext = createContext(null); -const StoreContext = createContext(null); +const StoreContext = createContext(null); export const Provider = StoreContext.Provider; export default StoreContext; diff --git a/packages/x-flow/src/models/store.ts b/packages/x-flow/src/models/store.ts index cde8b640c..06f0e1bf3 100644 --- a/packages/x-flow/src/models/store.ts +++ b/packages/x-flow/src/models/store.ts @@ -1,4 +1,3 @@ -import { useTemporalStore } from '../hooks/useStore'; import { addEdge, applyEdgeChanges, @@ -11,50 +10,48 @@ import { } from '@xyflow/react'; import isDeepEqual from 'fast-deep-equal'; import { temporal } from 'zundo'; -import { createStore as createZustandStore } from 'zustand'; +import { createWithEqualityFn } from 'zustand/traditional'; -export type XFlowProps = { +export type FlowProps = { nodes?: Node[]; edges?: Edge[]; layout?: 'LR' | 'TB'; }; -export type XFlowStore = ReturnType; +export type FlowStore = ReturnType; -export type XFlowNode = Node; +export type FlowNode = Node; -export type XFlowState = { +export type FlowState = { layout?: 'LR' | 'TB'; - nodes?: XFlowNode[]; + nodes?: FlowNode[]; edges?: Edge[]; candidateNode: any; mousePosition: any; - onNodesChange: OnNodesChange; + onNodesChange: OnNodesChange; onEdgesChange: OnEdgesChange; onConnect: OnConnect; - setNodes: (nodes: XFlowNode[]) => void; + setNodes: (nodes: FlowNode[]) => void; setEdges: (edges: Edge[]) => void; - addNodes: (nodes: XFlowNode[]) => void; + addNodes: (nodes: FlowNode[]) => void; addEdges: (edges: Edge[]) => void; setLayout: (layout: 'LR' | 'TB') => void; setCandidateNode: (candidateNode: any) => void; setMousePosition: (mousePosition: any) => void; }; -const createStore = (initProps?: Partial) => { - const DEFAULT_PROPS: XFlowProps = { +const createStore = (initProps?: Partial) => { + const DEFAULT_PROPS: FlowProps = { layout: 'LR', nodes: [], edges: [] }; - return createZustandStore()( + return createWithEqualityFn()( temporal( (set, get) => ({ ...DEFAULT_PROPS, - initProps, - nodes: [], - edges: [], + ...initProps, candidateNode: null, nodeMenus: [], mousePosition: { pageX: 0, pageY: 0, elementX: 0, elementY: 0 }, @@ -73,20 +70,21 @@ const createStore = (initProps?: Partial) => { edges: addEdge(connection, get().edges), }); }, + getNodes: () => { + return get().nodes; + }, setNodes: nodes => { - // 只记录节点变化 - // useTemporalStore().record(() => { - set({ nodes }); - // }); + set({ nodes }); }, setEdges: edges => { set({ edges }); }, + getEdges: () => { + return get().nodes; + }, addNodes: payload => { const newNodes = get().nodes.concat(payload); - // useTemporalStore().record(() => { - set({ nodes: newNodes }); - // }); + set({ nodes: newNodes }); }, addEdges: payload => { set({ edges: get().edges.concat(payload) }); @@ -122,7 +120,8 @@ const createStore = (initProps?: Partial) => { console.log('onSave', pastState, currentState); }, } - ) + ), + Object.is ); }; diff --git a/packages/x-flow/src/operator/index.tsx b/packages/x-flow/src/operator/index.tsx index 300ae95e1..e30cf1b02 100644 --- a/packages/x-flow/src/operator/index.tsx +++ b/packages/x-flow/src/operator/index.tsx @@ -5,7 +5,7 @@ import UndoRedo from './UndoRedo'; import Control from './Control'; import './index.less'; -import { useTemporalStore } from '../hooks/useStore'; +import { useTemporalStore } from '../hooks/useTemporalStore'; export type OperatorProps = { addNode: any; diff --git a/packages/x-flow/src/types.ts b/packages/x-flow/src/types.ts index e58c98555..c23c315bc 100644 --- a/packages/x-flow/src/types.ts +++ b/packages/x-flow/src/types.ts @@ -32,7 +32,7 @@ export interface TNodeSelector { items: (TNodeGroup | TNodeItem)[]; } -export interface XFlowProps { +export interface FlowProps { initialValues: { nodes: any[]; edges: any; @@ -43,4 +43,4 @@ export interface XFlowProps { nodeSelector: TNodeSelector; } -export default XFlowProps; +export default FlowProps; diff --git a/packages/x-flow/src/withProvider.tsx b/packages/x-flow/src/withProvider.tsx index 6d3436256..2f5b4a9c2 100644 --- a/packages/x-flow/src/withProvider.tsx +++ b/packages/x-flow/src/withProvider.tsx @@ -1,6 +1,7 @@ import { ReactFlowProvider } from '@xyflow/react'; import { ConfigProvider } from 'antd'; import React, { useMemo } from 'react'; +import { FlowProviderWrapper } from './components/FlowProvider'; import { ConfigContext } from './models/context'; import { TNodeGroup, TNodeItem } from './types'; interface ProviderProps { @@ -23,6 +24,7 @@ export default function withProvider( methods, nodeSelector, settings, + initialValues, ...restProps } = props; @@ -53,11 +55,20 @@ export default function withProvider( return ( - - - - - + + + + + + + ); }; From 939c2c3b6129603032f6789422e14501da00a462 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=B4=AB=E5=8D=87?= Date: Wed, 4 Dec 2024 08:28:48 +0800 Subject: [PATCH 36/38] =?UTF-8?q?chore:=20=E6=8C=87=E9=92=88=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F=E5=92=8C=E6=89=8B=E6=A8=A1=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/x-flow/src/XFlow.tsx | 3 ++ packages/x-flow/src/models/store.ts | 3 ++ .../x-flow/src/operator/Control/index.tsx | 30 +++++++++++++------ 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/packages/x-flow/src/XFlow.tsx b/packages/x-flow/src/XFlow.tsx index 405dfa550..9f8e5e590 100644 --- a/packages/x-flow/src/XFlow.tsx +++ b/packages/x-flow/src/XFlow.tsx @@ -47,6 +47,7 @@ const XFlow: FC = memo(props => { layout, nodes, edges, + panOnDrag, onNodesChange, onEdgesChange, onConnect, @@ -58,6 +59,7 @@ const XFlow: FC = memo(props => { nodes: state.nodes, edges: state.edges, layout: state.layout, + panOnDrag: state.panOnDrag, setLayout: state.setLayout, setMousePosition: state.setMousePosition, setCandidateNode: state.setCandidateNode, @@ -231,6 +233,7 @@ const XFlow: FC = memo(props => { return (
; @@ -43,6 +45,7 @@ export type FlowState = { const createStore = (initProps?: Partial) => { const DEFAULT_PROPS: FlowProps = { layout: 'LR', + panOnDrag: true, nodes: [], edges: [] }; diff --git a/packages/x-flow/src/operator/Control/index.tsx b/packages/x-flow/src/operator/Control/index.tsx index 516aac763..cb8d9da65 100644 --- a/packages/x-flow/src/operator/Control/index.tsx +++ b/packages/x-flow/src/operator/Control/index.tsx @@ -10,24 +10,32 @@ import { Tooltip, Button } from 'antd'; import IconView from '../../components/IconView'; import { useEventEmitterContextContext } from '../../models/event-emitter'; import NodeSelectPopover from '../../components/NodesPopover'; +import { useStoreApi, useStore } from '../../hooks/useStore'; + import './index.less'; const Control = (props: any) => { const { addNode } = props; - + const addNote = (e: MouseEvent) => { e.stopPropagation(); }; + const storeApi = useStoreApi(); + const panOnDrag = useStore(s => s.panOnDrag); const { eventEmitter } = useEventEmitterContextContext() + const handleInteractionModeChange = (panOnDrag) => { + storeApi.setState({ panOnDrag }) + } + return (
-