├── .gitignore
├── LICENSE
├── README.md
├── package-lock.json
├── package.json
├── public
├── favicon.ico
├── index.html
├── logo192.png
├── logo512.png
├── manifest.json
└── robots.txt
├── src
├── App.css
├── App.test.tsx
├── App.tsx
├── example
│ ├── README.md
│ ├── ShellContainer.tsx
│ ├── WorkflowEditor
│ │ ├── PublishButton.tsx
│ │ ├── README.md
│ │ ├── WorkFlowEditorInner.tsx
│ │ └── index.tsx
│ ├── icons.tsx
│ ├── index.tsx
│ ├── interfaces.ts
│ ├── materialUis.tsx
│ └── setters
│ │ ├── ApproverPanel.tsx
│ │ ├── AuditPanel.tsx
│ │ ├── ConditionPanel.tsx
│ │ ├── FormAuth.tsx
│ │ ├── NotifierPanel.tsx
│ │ ├── README.md
│ │ └── StartPanel.tsx
├── index.css
├── index.tsx
├── logo.svg
├── react-app-env.d.ts
├── reportWebVitals.ts
├── setupTests.ts
└── workflow-editor
│ ├── FlowEditor
│ ├── ConfigRoot.tsx
│ ├── FlowEditorCanvas.tsx
│ ├── FlowEditorScope
│ │ ├── FlowEditorScopeInner.tsx
│ │ └── index.tsx
│ ├── OperationBar
│ │ └── index.tsx
│ ├── SettingsPanel
│ │ ├── Footer.tsx
│ │ ├── NodeTitle.tsx
│ │ ├── NodeTitleEditor.tsx
│ │ └── index.tsx
│ ├── ZoomBar
│ │ └── index.tsx
│ ├── defaultMaterials.ts
│ └── index.ts
│ ├── README.md
│ ├── actions.ts
│ ├── classes
│ ├── EditorEngine.ts
│ └── index.ts
│ ├── components
│ ├── ButtonSelect.tsx
│ ├── ContentPlaceholder.tsx
│ ├── ExpressionInput
│ │ ├── AddMenu.tsx
│ │ ├── DefaultExpressionInput.tsx
│ │ ├── ExpressionGroup.tsx
│ │ ├── ExpressionInputProps.ts
│ │ ├── ExpressionItem.tsx
│ │ ├── ExpressionTreeInput.tsx
│ │ ├── OperatorSelect.tsx
│ │ └── index.ts
│ ├── MemberSelect
│ │ ├── AddDialog.tsx
│ │ └── index.tsx
│ ├── NavTabs.tsx
│ ├── Toolbar.tsx
│ └── index.ts
│ ├── contexts.ts
│ ├── hooks
│ ├── index.ts
│ ├── useEditorEngine.ts
│ ├── useError.ts
│ ├── useExport.ts
│ ├── useImport.ts
│ ├── useMaterialUI.ts
│ ├── useNodeMaterial.ts
│ ├── useRedoList.ts
│ ├── useSelectedId.ts
│ ├── useSelectedNode.ts
│ ├── useStartNode.ts
│ └── useUndoList.ts
│ ├── icons.tsx
│ ├── index.ts
│ ├── interfaces
│ ├── index.ts
│ ├── listeners.ts
│ ├── material.ts
│ ├── settings.ts
│ ├── state.ts
│ └── workflow.ts
│ ├── locales.ts
│ ├── nodes
│ ├── AddButton
│ │ ├── ContentPanel.tsx
│ │ ├── MaterialItem.tsx
│ │ └── index.tsx
│ ├── ChildNode.tsx
│ ├── CloseButton.tsx
│ ├── EndNode.tsx
│ ├── ErrorTip.tsx
│ ├── NodeTitle.tsx
│ ├── NormalNode.tsx
│ ├── RouteNode
│ │ ├── AddBranchButton.tsx
│ │ ├── BranchNode.tsx
│ │ ├── ConditionButtons.tsx
│ │ ├── ConditionNodeTitle.tsx
│ │ ├── ConditionPriority.tsx
│ │ └── index.tsx
│ └── StartNode.tsx
│ ├── react-locales
│ ├── contexts.ts
│ ├── hooks
│ │ ├── index.ts
│ │ ├── useLocalesManager.ts
│ │ └── useTranslate.ts
│ └── index.ts
│ ├── reducers
│ ├── changeFlagReducer.ts
│ ├── conditionNodeListReducer.ts
│ ├── errorsReducer.ts
│ ├── index.ts
│ ├── nodeReducer.ts
│ ├── redoListReducer.ts
│ ├── selectedIdReducer.ts
│ ├── startNodeReducer.ts
│ ├── undoListReducer.ts
│ └── validatedReducer.ts
│ ├── styled.d.ts
│ ├── theme.ts
│ └── utils
│ ├── canvasColor.ts
│ ├── create-uuid.ts
│ ├── getFIles.ts
│ ├── lineColor.ts
│ ├── nodeColor.ts
│ └── saveFile.tsx
└── tsconfig.json
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Water.Li
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 演示地址: https://dingflow.vercel.app/
2 | 相关文章:
3 | [用 React 仿钉钉审批流(掘金)](https://juejin.cn/post/7263858443329191996)
4 | [用 React 仿钉钉审批流(知乎)](https://zhuanlan.zhihu.com/p/648307778)
5 |
6 | # Getting Started with Create React App
7 |
8 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
9 |
10 | ## Available Scripts
11 |
12 | In the project directory, you can run:
13 |
14 | ### `npm start`
15 |
16 | Runs the app in the development mode.\
17 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
18 |
19 | The page will reload if you make edits.\
20 | You will also see any lint errors in the console.
21 |
22 | ### `npm test`
23 |
24 | Launches the test runner in the interactive watch mode.\
25 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
26 |
27 | ### `npm run build`
28 |
29 | Builds the app for production to the `build` folder.\
30 | It correctly bundles React in production mode and optimizes the build for the best performance.
31 |
32 | The build is minified and the filenames include the hashes.\
33 | Your app is ready to be deployed!
34 |
35 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
36 |
37 | ### `npm run eject`
38 |
39 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!**
40 |
41 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
42 |
43 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
44 |
45 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
46 |
47 | ## Learn More
48 |
49 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
50 |
51 | To learn React, check out the [React documentation](https://reactjs.org/).
52 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "dingflow",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@ant-design/icons": "^5.1.4",
7 | "@reduxjs/toolkit": "^1.9.5",
8 | "@rxdrag/locales": "^0.2.2",
9 | "@types/jest": "^27.5.2",
10 | "@types/node": "^16.18.39",
11 | "@types/react": "^18.2.18",
12 | "@types/react-dom": "^18.2.7",
13 | "antd": "^5.3.3",
14 | "classnames": "^2.3.2",
15 | "react": "^17.0.2",
16 | "react-dom": "^17.0.2",
17 | "react-scripts": "5.0.1",
18 | "redux": "^4.2.1",
19 | "styled-components": "^6.0.5",
20 | "typescript": "^4.9.5",
21 | "uuid": "^9.0.0",
22 | "web-vitals": "^2.1.4"
23 | },
24 | "scripts": {
25 | "start": "react-scripts start",
26 | "build": "react-scripts build",
27 | "test": "react-scripts test",
28 | "eject": "react-scripts eject"
29 | },
30 | "eslintConfig": {
31 | "extends": [
32 | "react-app",
33 | "react-app/jest"
34 | ]
35 | },
36 | "browserslist": {
37 | "production": [
38 | ">0.2%",
39 | "not dead",
40 | "not op_mini all"
41 | ],
42 | "development": [
43 | "last 1 chrome version",
44 | "last 1 firefox version",
45 | "last 1 safari version"
46 | ]
47 | },
48 | "devDependencies": {
49 | "@types/styled-components": "^5.1.26",
50 | "@types/uuid": "^9.0.2"
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codebdy/dingflow/c2fbf7964206234e0496e3949f12d95e97051511/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codebdy/dingflow/c2fbf7964206234e0496e3949f12d95e97051511/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/codebdy/dingflow/c2fbf7964206234e0496e3949f12d95e97051511/public/logo512.png
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | }
4 |
5 | .App-logo {
6 | height: 40vmin;
7 | pointer-events: none;
8 | }
9 |
10 | @media (prefers-reduced-motion: no-preference) {
11 | .App-logo {
12 | animation: App-logo-spin infinite 20s linear;
13 | }
14 | }
15 |
16 | .App-header {
17 | background-color: #282c34;
18 | min-height: 100vh;
19 | display: flex;
20 | flex-direction: column;
21 | align-items: center;
22 | justify-content: center;
23 | font-size: calc(10px + 2vmin);
24 | color: white;
25 | }
26 |
27 | .App-link {
28 | color: #61dafb;
29 | }
30 |
31 | @keyframes App-logo-spin {
32 | from {
33 | transform: rotate(0deg);
34 | }
35 | to {
36 | transform: rotate(360deg);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/App.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import App from './App';
4 |
5 | test('renders learn react link', () => {
6 |
7 |
8 | });
9 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Example } from './example';
3 |
4 | function App() {
5 | return (
6 |
7 | );
8 | }
9 |
10 | export default App;
11 |
--------------------------------------------------------------------------------
/src/example/README.md:
--------------------------------------------------------------------------------
1 | 如何使用审批流样例,根据这个目录的演示,集成到目标项目
--------------------------------------------------------------------------------
/src/example/ShellContainer.tsx:
--------------------------------------------------------------------------------
1 | import { memo } from "react"
2 | import styled from "styled-components"
3 |
4 | const Container = styled.div`
5 | width: 100%;
6 | height: 100vh;
7 | display: flex;
8 | flex-flow: column;
9 | `
10 |
11 | export const ShellContainer = memo((
12 | props: {
13 | children?: React.ReactNode
14 | }
15 | ) => {
16 |
17 | return (
18 |
19 | {props.children}
20 |
21 | )
22 | })
--------------------------------------------------------------------------------
/src/example/WorkflowEditor/PublishButton.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Modal, message } from "antd"
2 | import { memo, useState } from "react"
3 | import { useEditorEngine } from "../../workflow-editor"
4 | import { useTranslate } from "../../workflow-editor/react-locales"
5 | import { CloseCircleOutlined } from "@ant-design/icons"
6 | import { styled } from "styled-components"
7 |
8 | const Title = styled.div`
9 | display: flex;
10 | align-items: center;
11 | `
12 |
13 | const ErrorIcon = styled(CloseCircleOutlined)`
14 | color: red;
15 | font-size: 20px;
16 | margin-right: 8px;
17 | `
18 |
19 | const Tip = styled.div`
20 | color: ${props => props.theme.token?.colorTextSecondary};
21 | `
22 |
23 | const ErrorItem = styled.div`
24 | background-color: ${props => props.theme.token?.colorBorderSecondary};
25 | display: flex;
26 | justify-content: space-between;
27 | align-items: center;
28 | margin: 8px 0;
29 | padding: 0 16px;
30 | border-radius: 5px;
31 | min-height: 48px;
32 | `
33 |
34 | const ErrorCagetory = styled.div`
35 | color:${props => props.theme.token?.colorTextSecondary};
36 | opacity: 0.8;
37 | `
38 |
39 | const ErrorMessage = styled.div`
40 | font-size: 13px;
41 | `
42 |
43 | export interface IErrorItem {
44 | category: string,
45 | message: string,
46 | }
47 |
48 | export const PublishButton = memo(() => {
49 | const [errors, setErrors] = useState();
50 |
51 | const t = useTranslate()
52 | const editorStore = useEditorEngine()
53 |
54 | const handleValidate = () => {
55 | const result = editorStore?.validate()
56 | if (result !== true && result !== undefined) {
57 | const errs: IErrorItem[] = [];
58 | for (const nodeId of Object.keys(result)) {
59 | const msg = result[nodeId]
60 | const node = editorStore?.getNode(nodeId)
61 | errs.push(
62 | { category: t("flowDesign"), message: node?.name + ": " + msg }
63 | )
64 | }
65 | setErrors(errs);
66 | } else {
67 | message.info("验证成功")
68 | }
69 | };
70 |
71 | const handleOk = () => {
72 | setErrors(undefined);
73 | };
74 |
75 | const handleCancel = () => {
76 | setErrors(undefined);
77 | };
78 |
79 | return (
80 | <>
81 |
82 |
85 |
86 | {t("cantNotPublish")}
87 |
88 | }
89 | open={!!errors?.length}
90 | cancelText={t("gotIt")}
91 | okText={t("gotoEdit")}
92 | onOk={handleOk}
93 | onCancel={handleCancel}
94 | >
95 |
96 | {t("canNotPublishTip")}
97 |
98 | {
99 | errors?.map((err, index) => {
100 | return (
101 |
102 | {err.category}
103 |
104 |
105 | {err.message}
106 |
107 | )
108 | })
109 | }
110 |
111 | >
112 | )
113 | })
--------------------------------------------------------------------------------
/src/example/WorkflowEditor/README.md:
--------------------------------------------------------------------------------
1 | 自定义编辑器参考这个
--------------------------------------------------------------------------------
/src/example/WorkflowEditor/WorkFlowEditorInner.tsx:
--------------------------------------------------------------------------------
1 | import { EllipsisOutlined, ExportOutlined, ImportOutlined, LeftOutlined, MobileOutlined, QuestionCircleOutlined, RocketOutlined, SaveOutlined } from "@ant-design/icons"
2 | import { Avatar, Button, Dropdown, MenuProps, Space } from "antd"
3 | import { memo, useCallback, useMemo, useState } from "react"
4 | import { styled } from "styled-components"
5 | import classNames from "classnames"
6 | import { NavTabs, Toolbar, FlowEditorCanvas, useImport } from "../../workflow-editor"
7 | import { useTranslate } from "../../workflow-editor/react-locales"
8 | import { useExport } from "../../workflow-editor"
9 | import { PublishButton } from "./PublishButton"
10 |
11 | const Container = styled.div`
12 | flex:1;
13 | display: flex;
14 | flex-flow: column;
15 | background-color: ${props => props.theme.token?.colorBgBase};
16 | color: ${props => props.theme.token?.colorText};
17 | height: 0;
18 | `
19 | export enum TabType {
20 | baseSettings = "baseSettings",
21 | formDesign = "formDesign",
22 | flowDesign = "flowDesign",
23 | addvancedSettings = "addvancedSettings"
24 | }
25 |
26 | export const WorkFlowEditorInner = memo((props: {
27 | className?: string
28 | }) => {
29 | const { className, ...other } = props
30 | const [selectedTab, setSelectedTab] = useState(TabType.flowDesign)
31 | const t = useTranslate()
32 | const exportjson = useExport()
33 | const importJson = useImport()
34 |
35 | const items: MenuProps['items'] = useMemo(() => [
36 | {
37 | label: t("import"),
38 | key: 'import',
39 | icon: ,
40 | onClick: importJson,
41 | },
42 | {
43 | label: t("export"),
44 | key: 'export',
45 | icon: ,
46 | onClick: exportjson,
47 | },
48 | ], [exportjson, importJson, t]);
49 |
50 | const handleNavChange = useCallback((key?: string) => {
51 | setSelectedTab((key || TabType.flowDesign) as TabType)
52 | }, [])
53 |
54 | return (
55 |
56 |
59 | } />
60 | } />
61 | 请假管理
62 |
63 | }
64 | actions={
65 |
66 | }>{t("help")}
67 | }>{t("preview")}
68 | }>{t("save")}
69 |
70 |
71 | } />
72 |
73 |
74 | }
75 | >
76 |
100 |
101 |
102 | {
103 | selectedTab === TabType.flowDesign &&
104 |
105 | }
106 |
107 | )
108 | })
--------------------------------------------------------------------------------
/src/example/WorkflowEditor/index.tsx:
--------------------------------------------------------------------------------
1 | import { memo } from "react"
2 | import { WorkFlowEditorInner } from "./WorkFlowEditorInner"
3 | import { ILocales } from "@rxdrag/locales"
4 | import { IThemeToken } from "../../workflow-editor"
5 | import { IMaterialUIs, FlowEditorScope } from "../../workflow-editor/"
6 |
7 | export type WorkflowEditorProps = {
8 | themeMode?: 'dark' | 'light',
9 | themeToken?: IThemeToken,
10 | lang?: string,
11 | locales?: ILocales,
12 | materialUis?: IMaterialUIs,
13 | }
14 |
15 | export const WorkflowEditor = memo((props: WorkflowEditorProps) => {
16 | const { themeMode, themeToken, lang, locales, materialUis, ...other } = props;
17 | return (
18 |
25 |
26 |
27 | )
28 | })
--------------------------------------------------------------------------------
/src/example/icons.tsx:
--------------------------------------------------------------------------------
1 | export const darkIcon =
2 |
5 |
6 |
7 | export const lightIcon =
8 |
11 |
--------------------------------------------------------------------------------
/src/example/index.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Space } from "antd"
2 | import { memo, useCallback, useState } from "react"
3 | import { ShellContainer } from "./ShellContainer"
4 | import { styled } from "styled-components"
5 | import { WorkflowEditor } from "./WorkflowEditor"
6 | import { materialUis } from "./materialUis"
7 |
8 | const Toolbar = styled.div`
9 | height: 56px;
10 | border-bottom: solid 1px rgba(0,0,0, 0.1);
11 | display: flex;
12 | align-items: center;
13 | padding: 8px 16px;
14 | justify-content: space-between;
15 | box-sizing: border-box;
16 | `
17 |
18 | export enum Lang {
19 | cn = "zh-CN",
20 | en = "en-US"
21 | }
22 |
23 | export const Example = memo(() => {
24 | const [lang, setlang] = useState(Lang.cn)
25 | const [themeMode, setThemeMode] = useState<"dark" | "light">("light")
26 |
27 | const handleToggleTheme = useCallback(() => {
28 | setThemeMode(mode => mode === "light" ? "dark" : "light")
29 | }, [])
30 |
31 | const handleSwitchLang = useCallback(() => {
32 | setlang(lang => lang === Lang.cn ? Lang.en : Lang.cn)
33 | }, [])
34 |
35 | return (
36 |
37 |
38 |
39 | 审批流演示
40 |
41 |
42 |
43 |
44 |
45 |
46 |
51 |
52 | )
53 | })
--------------------------------------------------------------------------------
/src/example/interfaces.ts:
--------------------------------------------------------------------------------
1 | import { IWorkFlowNode } from "../workflow-editor";
2 |
3 | //后端需要的扩展,增加一些冗余配置项
4 | export interface IExtWorkflowNode extends IWorkFlowNode {
5 | flowId?: string
6 | parentId?: string
7 | nodeLevel?: number
8 | parentNodeType?: string
9 | sort?: number
10 | }
11 |
12 |
--------------------------------------------------------------------------------
/src/example/materialUis.tsx:
--------------------------------------------------------------------------------
1 | import { ContentPlaceholder, IMaterialUIs, IWorkFlowNode, NodeType } from "../workflow-editor";
2 | import { ApproverPanel, IApproverSettings } from "./setters/ApproverPanel";
3 | import { AuditPanel, IAuditSettings } from "./setters/AuditPanel";
4 | import { ConditionPanel, IConditionSettings } from "./setters/ConditionPanel";
5 | import { INotifierSettings, NotifierPanel } from "./setters/NotifierPanel";
6 | import { IStartSettings, StartPanel } from "./setters/StartPanel";
7 |
8 | export const materialUis: IMaterialUIs = {
9 | //审批人物料UI
10 | [NodeType.approver]: {
11 | //节点内容区,只实现了空逻辑,具体过几天实现
12 | viewContent: (node: IWorkFlowNode, { t }) => {
13 | return
14 | },
15 | //属性面板
16 | settersPanel: ApproverPanel,
17 | //校验,目前仅实现了空校验,其它校验过几天实现
18 | validate: (node: IWorkFlowNode, { t }) => {
19 | if (!node.config) {
20 | return (t("noSelectedApprover"))
21 | }
22 | return true
23 | }
24 | },
25 | //办理人节点
26 | [NodeType.audit]: {
27 | //节点内容区
28 | viewContent: (node: IWorkFlowNode, { t }) => {
29 | return
30 | },
31 | //属性面板
32 | settersPanel: AuditPanel,
33 | //校验函数
34 | validate: (node: IWorkFlowNode, { t }) => {
35 | if (!node.config) {
36 | return t("noSelectedDealer")
37 | }
38 | return true
39 | }
40 | },
41 | //条件分支节点的分支子节点
42 | [NodeType.condition]: {
43 | //节点内容区
44 | viewContent: (node: IWorkFlowNode, { t }) => {
45 | return
46 | },
47 | //属性面板
48 | settersPanel: ConditionPanel,
49 | //校验函数
50 | validate: (node: IWorkFlowNode, { t }) => {
51 | if (!node.config) {
52 | return t("noSetCondition")
53 | }
54 | return true
55 | }
56 | },
57 | //通知人节点
58 | [NodeType.notifier]: {
59 | viewContent: (node: IWorkFlowNode, { t }) => {
60 | return
61 | },
62 | settersPanel: NotifierPanel,
63 | },
64 | //发起人节点
65 | [NodeType.start]: {
66 | viewContent: (node: IWorkFlowNode, { t }) => {
67 | return
68 | },
69 | settersPanel: StartPanel,
70 | },
71 | }
--------------------------------------------------------------------------------
/src/example/setters/ApproverPanel.tsx:
--------------------------------------------------------------------------------
1 | import { memo, useState } from "react"
2 | import { ButtonSelect } from "../../workflow-editor/components/ButtonSelect"
3 | import { QuestionCircleOutlined } from "@ant-design/icons"
4 | import { FormAuth } from "./FormAuth"
5 | import { useTranslate } from "../../workflow-editor/react-locales"
6 | import { Form, Radio } from "antd"
7 | import FormItem from "antd/es/form/FormItem"
8 |
9 | export interface IApproverSettings {
10 |
11 | }
12 |
13 | export const ApproverPanel = memo((
14 | props: {
15 | value?: IApproverSettings
16 | onChange?: (value?: IApproverSettings) => void
17 | }
18 | ) => {
19 | const [settingsType, setSettingsType] = useState("node")
20 | const t = useTranslate()
21 |
22 | return (
23 |
51 | )
52 | })
--------------------------------------------------------------------------------
/src/example/setters/AuditPanel.tsx:
--------------------------------------------------------------------------------
1 | import { memo, useState } from "react"
2 | import { QuestionCircleOutlined } from "@ant-design/icons"
3 | import { FormAuth } from "./FormAuth"
4 | import { Form } from "antd"
5 | import { useTranslate } from "../../workflow-editor/react-locales"
6 | import { ButtonSelect } from "../../workflow-editor"
7 |
8 | export interface IAuditSettings {
9 |
10 | }
11 |
12 | export const AuditPanel = memo((
13 | props: {
14 | value?: IAuditSettings
15 | onChange?: (value?: IAuditSettings) => void
16 | }
17 | ) => {
18 | const [settingsType, setSettingsType] = useState("node")
19 | const t = useTranslate()
20 |
21 | return (
22 |
39 | )
40 | })
--------------------------------------------------------------------------------
/src/example/setters/ConditionPanel.tsx:
--------------------------------------------------------------------------------
1 | import { memo, useCallback, useState } from "react"
2 | import { Form } from "antd"
3 | import { DefaultExpressionInput, ExpressionGroupType, ExpressionNodeType, ExpressionTreeInput, IExpressionGroup } from "../../workflow-editor"
4 | import { createUuid } from "../../workflow-editor/utils/create-uuid"
5 |
6 | export interface IConditionSettings {
7 |
8 | }
9 |
10 | export const ConditionPanel = memo((
11 | props: {
12 | value?: IConditionSettings
13 | onChange?: (value?: IConditionSettings) => void
14 | }
15 | ) => {
16 | const [settingsType, setSettingsType] = useState("node")
17 | const [rootExpression, setRootExpression] = useState({
18 | id: "root",
19 | nodeType: ExpressionNodeType.Group,
20 | groupType: ExpressionGroupType.And,
21 | children: [
22 | {
23 | nodeType: ExpressionNodeType.Expression,
24 | id: createUuid(),
25 | }
26 | ]
27 | })
28 |
29 | const handleExpressionChange = useCallback((exp: IExpressionGroup) => {
30 | setRootExpression(exp)
31 | }, [])
32 |
33 | return (
34 |
43 | )
44 | })
--------------------------------------------------------------------------------
/src/example/setters/FormAuth.tsx:
--------------------------------------------------------------------------------
1 | import { memo } from "react"
2 |
3 | export const FormAuth = memo(()=>{
4 | return (
5 | <>
6 | FormAuth
7 | >
8 | )
9 | })
--------------------------------------------------------------------------------
/src/example/setters/NotifierPanel.tsx:
--------------------------------------------------------------------------------
1 | import { memo, useState } from "react"
2 | import { ButtonSelect } from "../../workflow-editor/components/ButtonSelect"
3 | import { QuestionCircleOutlined } from "@ant-design/icons"
4 | import { FormAuth } from "./FormAuth"
5 | import { useTranslate } from "../../workflow-editor/react-locales"
6 | import { Form } from "antd"
7 |
8 | export interface INotifierSettings {
9 |
10 | }
11 |
12 | export const NotifierPanel = memo((
13 | props: {
14 | value?: INotifierSettings
15 | onChange?: (value?: INotifierSettings) => void
16 | }
17 | ) => {
18 | const [settingsType, setSettingsType] = useState("node")
19 | const t = useTranslate()
20 |
21 | return (
22 |
39 | )
40 | })
--------------------------------------------------------------------------------
/src/example/setters/README.md:
--------------------------------------------------------------------------------
1 | 本目录定义属性面板表单,会跟业务结合
--------------------------------------------------------------------------------
/src/example/setters/StartPanel.tsx:
--------------------------------------------------------------------------------
1 | import { memo, useState } from "react"
2 | import { QuestionCircleOutlined } from "@ant-design/icons"
3 | import { FormAuth } from "./FormAuth"
4 | import { Form } from "antd"
5 | import { ButtonSelect, MemberSelect } from "../../workflow-editor"
6 | import { useTranslate } from "../../workflow-editor/react-locales"
7 |
8 | export interface IStartSettings {
9 |
10 | }
11 |
12 | export const StartPanel = memo((
13 | props: {
14 | value?: IStartSettings
15 | onChange?: (value?: IStartSettings) => void
16 | }
17 | ) => {
18 | const [settingsType, setSettingsType] = useState("node")
19 | const t = useTranslate()
20 |
21 | return (
22 |
44 | )
45 | })
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
15 | * {
16 | box-sizing: border-box;
17 | }
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import './index.css';
4 | import App from './App';
5 |
6 | ReactDOM.render(
7 |
8 |
9 | ,
10 | document.getElementById('root')
11 | );
--------------------------------------------------------------------------------
/src/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/src/reportWebVitals.ts:
--------------------------------------------------------------------------------
1 | import { ReportHandler } from 'web-vitals';
2 |
3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => {
4 | if (onPerfEntry && onPerfEntry instanceof Function) {
5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
6 | getCLS(onPerfEntry);
7 | getFID(onPerfEntry);
8 | getFCP(onPerfEntry);
9 | getLCP(onPerfEntry);
10 | getTTFB(onPerfEntry);
11 | });
12 | }
13 | };
14 |
15 | export default reportWebVitals;
16 |
--------------------------------------------------------------------------------
/src/setupTests.ts:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom';
6 |
--------------------------------------------------------------------------------
/src/workflow-editor/FlowEditor/ConfigRoot.tsx:
--------------------------------------------------------------------------------
1 | import { ConfigProvider, theme } from "antd";
2 | import { memo } from "react"
3 |
4 | export const ConfigRoot = memo((
5 | props: {
6 | themeMode?: 'dark' | 'light',
7 | children?: React.ReactNode,
8 | }
9 | ) => {
10 | const { themeMode, children } = props;
11 | return (
16 | {
17 | children
18 | }
19 | )
20 | })
--------------------------------------------------------------------------------
/src/workflow-editor/FlowEditor/FlowEditorCanvas.tsx:
--------------------------------------------------------------------------------
1 | import { CSSProperties, memo, useCallback, useRef, useState } from "react"
2 | import { styled } from "styled-components"
3 | import { StartNode } from "../nodes/StartNode"
4 | import { canvasColor } from "../utils/canvasColor"
5 | import { ZoomBar } from "./ZoomBar"
6 | import { SettingsPanel } from "./SettingsPanel"
7 | import { OperationBar } from "./OperationBar"
8 |
9 | const CanvasContainer = styled.div`
10 | flex: 1;
11 | display: flex;
12 | flex-flow: column;
13 | background-color: ${canvasColor} ;
14 | position: relative;
15 | height: 0;
16 | `
17 |
18 | const Canvas = styled.div`
19 | flex: 1;
20 | padding: 56px 16px;
21 | padding-bottom: 0;
22 | overflow: auto;
23 | cursor: grab;//grabbing
24 | display: flex;
25 | `
26 |
27 | const CanvasInner = styled.div`
28 | flex: 1;
29 | transform-origin: 0px 0px;
30 | `
31 | function toDecimal(x: number) {
32 | const f = Math.round(x * 10) / 10;
33 | return f;
34 | }
35 |
36 | export interface IPosition {
37 | x: number,
38 | y: number,
39 | scrollLeft: number,
40 | scrollTop: number
41 | }
42 |
43 | export const FlowEditorCanvas = memo((
44 | props: {
45 | className?: string,
46 | style?: CSSProperties,
47 | }
48 | ) => {
49 | const [zoom, setZoom] = useState(1)
50 | const [scrolled, setScrolled] = useState(false)
51 | const [mousePressedPoint, setMousePressedPoint] = useState()
52 | const canvasRef = useRef(null)
53 |
54 | const haneldZoomIn = useCallback(() => {
55 | setZoom(zoom => toDecimal(zoom < 3 ? (zoom + 0.1) : zoom))
56 | }, [])
57 |
58 | const haneldZoomOut = useCallback(() => {
59 | setZoom(zoom => toDecimal(zoom > 0.1 ? (zoom - 0.1) : zoom))
60 | }, [])
61 |
62 | const handleMouseDown = useCallback((e: React.MouseEvent) => {
63 | canvasRef.current && setMousePressedPoint({
64 | x: e.clientX,
65 | y: e.clientY,
66 | scrollLeft: canvasRef.current.scrollLeft,
67 | scrollTop: canvasRef.current.scrollTop
68 | })
69 | }, [])
70 |
71 | const handleMouseUp = useCallback(() => {
72 | setMousePressedPoint(undefined)
73 | }, [])
74 |
75 | const handleMouseMove = useCallback((e: React.MouseEvent) => {
76 | if (!mousePressedPoint) {
77 | return
78 | }
79 |
80 | const dragMoveDiff = {
81 | x: mousePressedPoint.x - e.clientX,
82 | y: mousePressedPoint.y - e.clientY
83 | }
84 |
85 | if (canvasRef.current) {
86 | canvasRef.current.scrollLeft = mousePressedPoint.scrollLeft + dragMoveDiff.x;
87 | canvasRef.current.scrollTop = mousePressedPoint.scrollTop + dragMoveDiff.y;
88 | }
89 |
90 | }, [mousePressedPoint])
91 |
92 | const handleScroll = useCallback((e: React.UIEvent) => {
93 | if (e.currentTarget.scrollTop > 60 || e.currentTarget.scrollLeft > 60) {
94 | setScrolled(true)
95 | } else {
96 | setScrolled(false)
97 | }
98 | }, [])
99 |
100 | return (
101 |
102 |
124 |
125 |
131 |
132 |
133 | )
134 | })
--------------------------------------------------------------------------------
/src/workflow-editor/FlowEditor/FlowEditorScope/FlowEditorScopeInner.tsx:
--------------------------------------------------------------------------------
1 | import { useToken } from "antd/es/theme/internal";
2 | import { memo, useMemo, useEffect } from "react";
3 | import { ThemeProvider } from "styled-components";
4 | import { EditorEngine } from "../../classes";
5 | import { WorkflowEditorStoreContext } from "../../contexts";
6 | import { INodeMaterial, IMaterialUIs } from "../../interfaces";
7 | import { useTranslate } from "../../react-locales";
8 | import { IThemeToken } from "../../theme";
9 | import { defaultMaterials } from "../defaultMaterials";
10 |
11 | export const FlowEditorScopeInner = memo((props: {
12 | mode?: 'dark' | 'light',
13 | themeToken?: IThemeToken,
14 | children?: React.ReactNode,
15 | materials?: INodeMaterial[],
16 | materialUis?: IMaterialUIs,
17 | }) => {
18 | const { mode, children, themeToken, materials, materialUis } = props;
19 | const [, token] = useToken();
20 | const t = useTranslate()
21 | const theme: { token: IThemeToken, mode?: 'dark' | 'light' } = useMemo(() => {
22 | return {
23 | token: themeToken || token,
24 | mode
25 | }
26 | }, [mode, themeToken, token])
27 | const store: EditorEngine = useMemo(() => {
28 | return new EditorEngine()
29 | }, [])
30 |
31 | useEffect(() => {
32 | store.t = t
33 | }, [store, t])
34 |
35 | useEffect(() => {
36 | const oldMaterials = store.materials
37 | const oldMaterialUis = store.materialUis
38 | store.materials = [...oldMaterials, ...defaultMaterials, ...materials || []]
39 | store.materialUis = { ...oldMaterialUis, ...materialUis }
40 | return () => {
41 | store.materials = oldMaterials;
42 | store.materialUis = oldMaterialUis;
43 | }
44 | }, [materialUis, materials, store])
45 |
46 | return (
47 |
48 |
49 | {
50 | store && children
51 | }
52 |
53 |
54 | )
55 | })
--------------------------------------------------------------------------------
/src/workflow-editor/FlowEditor/FlowEditorScope/index.tsx:
--------------------------------------------------------------------------------
1 | import { memo, useEffect, useState } from "react"
2 | import { IThemeToken } from "../../theme"
3 | import { ConfigRoot } from "../ConfigRoot"
4 | import { ILocales, LocalesManager } from "@rxdrag/locales"
5 | import { LocalesContext } from "../../react-locales"
6 | import { defalutLocales } from "../../locales"
7 | import { IMaterialUIs, INodeMaterial } from "../../interfaces/material"
8 | import { FlowEditorScopeInner } from "./FlowEditorScopeInner"
9 |
10 | export const FlowEditorScope = memo((props: {
11 | //当前主题模式
12 | mode?: 'dark' | 'light',
13 | //主题定义
14 | themeToken?: IThemeToken,
15 | children?: React.ReactNode,
16 | //当前语言
17 | lang?: string,
18 | //多语言资源
19 | locales?: ILocales,
20 | //自定义物料
21 | materials?: INodeMaterial[],
22 | //所有物料的Ui配置,包括自定义物料跟预定义物料
23 | materialUis?: IMaterialUIs,
24 | }) => {
25 | const { children, lang, locales, ...other } = props
26 | const [localesManager, setLocalesManager] = useState(new LocalesManager(lang, defalutLocales))
27 |
28 | useEffect(() => {
29 | locales && localesManager.registerLocales(locales)
30 | }, [localesManager, locales])
31 |
32 | useEffect(() => {
33 | //暂时这么处理,后面把语言切换移动到locales-react
34 | setLocalesManager(new LocalesManager(lang, defalutLocales))
35 | }, [lang])
36 |
37 | return (
38 |
39 |
40 |
41 | {children}
42 |
43 |
44 |
45 | )
46 | })
--------------------------------------------------------------------------------
/src/workflow-editor/FlowEditor/OperationBar/index.tsx:
--------------------------------------------------------------------------------
1 | import { Space, Button } from "antd"
2 | import { memo, useCallback } from "react"
3 | import { MiniFloatContainer } from "../ZoomBar"
4 | import { undoIcon, redoIcon } from "../../icons"
5 | import { useRedoList } from "../../hooks/useRedoList"
6 | import { useUndoList } from "../../hooks/useUndoList"
7 | import { useEditorEngine } from "../../hooks"
8 |
9 | export const OperationBar = memo((
10 | props: {
11 | float?: boolean,
12 | }
13 | ) => {
14 | const { float } = props
15 | const redoList = useRedoList();
16 | const undoList = useUndoList();
17 |
18 | const store = useEditorEngine();
19 |
20 | const handleUndo = useCallback(()=>{
21 | store?.undo()
22 | },[store])
23 |
24 | const handleRedo = useCallback(()=>{
25 | store?.redo()
26 | },[store])
27 |
28 | return (
29 |
30 |
31 |
38 |
45 |
46 |
47 | )
48 | })
--------------------------------------------------------------------------------
/src/workflow-editor/FlowEditor/SettingsPanel/Footer.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Space } from "antd"
2 | import { memo } from "react"
3 | import { styled } from "styled-components"
4 | import { useTranslate } from "../../react-locales"
5 |
6 | const Shell = styled.div`
7 | display: flex;
8 | align-items: center;
9 | justify-content: flex-end;
10 | height: 48px;
11 | `
12 |
13 | export const Footer = memo((props: {
14 | onConfirm: () => void,
15 | onCancel: () => void
16 | }) => {
17 | const { onConfirm, onCancel } = props
18 | const t = useTranslate()
19 | return (
20 |
21 |
22 |
25 |
28 |
29 |
30 | )
31 | })
--------------------------------------------------------------------------------
/src/workflow-editor/FlowEditor/SettingsPanel/NodeTitle.tsx:
--------------------------------------------------------------------------------
1 | import { memo } from "react"
2 | import { styled } from "styled-components"
3 | import { IWorkFlowNode, NodeType } from "../../interfaces"
4 | import { useTranslate } from "../../react-locales"
5 | import { NodeTitleEditor } from "./NodeTitleEditor"
6 |
7 | const Title = styled.div`
8 | font-weight: normal;
9 | `
10 |
11 | export const TitleText = styled.span`
12 | margin-right: 8px;
13 | `
14 |
15 | export const NodeTitle = memo((
16 | props: {
17 | node: IWorkFlowNode
18 | onNameChange: (value?: string) => void
19 | }
20 | ) => {
21 | const { node, onNameChange } = props
22 |
23 | const t = useTranslate()
24 |
25 | return (
26 |
27 | {
28 | node.nodeType === NodeType.start
29 | ? {t("promoter")}
30 | :
31 | }
32 |
33 |
34 | )
35 | })
--------------------------------------------------------------------------------
/src/workflow-editor/FlowEditor/SettingsPanel/NodeTitleEditor.tsx:
--------------------------------------------------------------------------------
1 | import { EditOutlined } from "@ant-design/icons"
2 | import { memo, useCallback, useEffect, useState } from "react"
3 | import { TitleText } from "./NodeTitle"
4 | import { styled } from "styled-components"
5 | import { Input } from "antd"
6 |
7 | const TextResonse = styled.div`
8 | display: flex;
9 | cursor: pointer;
10 | `
11 |
12 | export const NodeTitleEditor = memo((
13 | props: {
14 | value?: string
15 | onChange?: (value?: string) => void
16 | }
17 | ) => {
18 | const { value, onChange } = props
19 | const [editting, setEditting] = useState(false)
20 | const [inputValue, setInputValue] = useState(value)
21 |
22 | useEffect(() => {
23 | setInputValue(value)
24 | }, [value])
25 |
26 | const changeName = useCallback(() => {
27 | onChange?.(inputValue)
28 | }, [inputValue, onChange])
29 |
30 | const handleNameClick = useCallback((e: React.MouseEvent) => {
31 | e.stopPropagation()
32 | setEditting(true)
33 | }, [])
34 |
35 |
36 | const handleBlur = useCallback(() => {
37 | changeName()
38 | setEditting(false)
39 | }, [changeName])
40 |
41 | const handleKeyDown = useCallback((event: React.KeyboardEvent) => {
42 | if (event.key === "Enter") {
43 | handleBlur()
44 | }
45 | }, [handleBlur])
46 |
47 | const handleChange = useCallback((e: React.ChangeEvent) => {
48 | setInputValue(e.target.value)
49 | }, [])
50 |
51 | return (
52 | <>
53 | {
54 | !editting &&
55 | {inputValue}
56 |
57 |
58 | }
59 | {
60 | editting &&
67 | }
68 | >
69 | )
70 | })
--------------------------------------------------------------------------------
/src/workflow-editor/FlowEditor/SettingsPanel/index.tsx:
--------------------------------------------------------------------------------
1 | import { CloseOutlined } from "@ant-design/icons"
2 | import { Button, Drawer } from "antd"
3 | import { memo, useCallback } from "react"
4 | import { NodeTitle } from "./NodeTitle"
5 | import { Footer } from "./Footer"
6 | import { useSelectedNode } from "../../hooks/useSelectedNode"
7 | import { useEditorEngine } from "../../hooks"
8 | import { styled } from "styled-components"
9 | import { useMaterialUI } from "../../hooks/useMaterialUI"
10 |
11 | const Content = styled.div`
12 | display: flex;
13 | flex-flow: column;
14 | `
15 | export const SettingsPanel = memo(() => {
16 | const selectedNode = useSelectedNode()
17 | const materialUi = useMaterialUI(selectedNode)
18 | const store = useEditorEngine()
19 | const handelClose = useCallback(() => {
20 | store?.selectNode(undefined)
21 | }, [store])
22 |
23 | const handleConfirm = useCallback(() => {
24 | store?.selectNode(undefined)
25 | }, [store])
26 |
27 | const handleNameChange = useCallback((name?: string) => {
28 |
29 | }, [])
30 |
31 | const handleSettingsChange = useCallback((value: any) => {
32 |
33 | }, [])
34 | return (
35 |
41 | }
42 | placement="right"
43 | width={656}
44 | closable={false}
45 | extra={
46 | }
50 | onClick={handelClose}
51 | />
52 | }
53 | footer={
54 |
58 | }
59 | onClose={handelClose}
60 | open={!!selectedNode}
61 | >
62 |
63 | {materialUi?.settersPanel && }
64 |
65 |
66 | )
67 | })
--------------------------------------------------------------------------------
/src/workflow-editor/FlowEditor/ZoomBar/index.tsx:
--------------------------------------------------------------------------------
1 | import { MinusOutlined, PlusOutlined } from "@ant-design/icons"
2 | import { Button, Space } from "antd"
3 | import { memo } from "react"
4 | import { styled } from "styled-components"
5 | import { canvasColor } from "../../utils/canvasColor"
6 |
7 | export const MiniFloatContainer = styled.div`
8 | display: flex;
9 | align-items: center;
10 | position: absolute;
11 | user-select: none;
12 | background-color: ${canvasColor};
13 | padding: 4px 8px;
14 | border-radius: 5px;
15 | top: 16px;
16 | &.float{
17 | box-shadow: 0 2px 8px 0 rgba(0, 0, 0, ${props => props.theme.mode === "dark" ? "0.5" : "0.15"});
18 | transform: ${props => props.theme.mode === "dark" ? "" : "scale(1.05)"};
19 | }
20 | transition: all 0.3s;
21 | &.workflow-editor-zoombar{
22 | right: 32px;
23 | }
24 | &.workflow-operation-bar{
25 | left: 32px;
26 | }
27 | `
28 |
29 | export const ZoomBar = memo((
30 | props: {
31 | float?: boolean,
32 | zoom: number;
33 | onZoomIn: () => void,
34 | onZoomOut: () => void
35 | }
36 | ) => {
37 | const { float, zoom, onZoomIn, onZoomOut } = props
38 |
39 | return (
40 |
41 |
42 | }
46 | disabled={zoom <= 0.1}
47 | onClick={onZoomOut}
48 | />
49 | {Math.round(zoom * 100)}%
50 | }
54 | disabled={zoom >= 3}
55 | onClick={onZoomIn}
56 | />
57 |
58 |
59 | )
60 | })
--------------------------------------------------------------------------------
/src/workflow-editor/FlowEditor/defaultMaterials.ts:
--------------------------------------------------------------------------------
1 | import { routeIcon, dealIcon, notifierIcon, sealIcon } from "../icons";
2 | import { NodeType } from "../interfaces";
3 | import { INodeMaterial } from "../interfaces/material";
4 | import { createUuid } from "../utils/create-uuid";
5 |
6 | export const defaultMaterials: INodeMaterial[] = [
7 | //发起人节点
8 | {
9 | //标题,引擎会通过国际化t函数翻译
10 | label: "promoter",
11 | //颜色
12 | color: "rgb(87, 106, 149)",
13 | //引擎会直接去defaultConfig来生成一个节点,会克隆一份defaultConfig数据保证immutable
14 | defaultConfig: {
15 | //默认配置,可以把类型上移一层,但是如果增加其它默认属性的话,不利于扩展
16 | nodeType: NodeType.start,
17 | },
18 | //不在物料板显示
19 | hidden: true,
20 | },
21 | //审批人节点
22 | {
23 | color: "#ff943e",
24 | label: "approver",
25 | icon: sealIcon,
26 | defaultConfig: {
27 | nodeType: NodeType.approver,
28 | },
29 | },
30 | //通知人节点
31 | {
32 | color: "#4ca3fb",
33 | label: "notifier",
34 | icon: notifierIcon,
35 | defaultConfig: {
36 | nodeType: NodeType.notifier,
37 | },
38 | },
39 | {
40 | color: "#fb602d",
41 | label: "dealer",
42 | icon: dealIcon,
43 | defaultConfig: {
44 | nodeType: NodeType.audit,
45 | },
46 | },
47 | //条件节点
48 | {
49 | color: "#15bc83",
50 | label: "routeNode",
51 | icon: routeIcon,
52 | createDefault: ({ t }) => {
53 | return {
54 | id: createUuid(),
55 | nodeType: NodeType.route,
56 | conditionNodeList: [
57 | {
58 | id: createUuid(),
59 | nodeType: NodeType.condition,
60 | name: t?.("condition") + "1"
61 | },
62 | {
63 | id: createUuid(),
64 | nodeType: NodeType.condition,
65 | name: t?.("condition") + "2"
66 | }
67 | ]
68 | }
69 | },
70 |
71 | },
72 | //分支节点
73 | {
74 | label: "condition",
75 | color: "",
76 | defaultConfig: {
77 | nodeType: NodeType.condition,
78 | },
79 | //不在物料板显示
80 | hidden: true,
81 | },
82 | ]
--------------------------------------------------------------------------------
/src/workflow-editor/FlowEditor/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./defaultMaterials"
2 | export * from "./FlowEditorCanvas"
3 | export * from "./FlowEditorScope"
--------------------------------------------------------------------------------
/src/workflow-editor/README.md:
--------------------------------------------------------------------------------
1 | 本目录会作为NPM包发布
--------------------------------------------------------------------------------
/src/workflow-editor/actions.ts:
--------------------------------------------------------------------------------
1 | import { IBranchNode, IWorkFlowNode } from "./interfaces";
2 | import { IErrors, ISnapshot } from "./interfaces/state";
3 |
4 | export enum ActionType {
5 | SET_CHANGE_FLAG = "workflow/SET_CHANGE_FLAG",
6 | DELETE_NODE = "workflow/DELETE_NODE",
7 | DELETE_CONDITION = "workflow/DELETE_CONDITION",
8 | ADD_NODE = "workflow/ADD_NODE",
9 | ADD_CONDITION = "workflow/ADD_CONDITION",
10 | SET_REDOLIST = 'workflow/SET_REDOLIST',
11 | SET_UNOLIST = 'workflow/SET_UNOLIST',
12 | SET_START_NODE = 'workflow/SET_START_NODE',
13 | CHANGE_NODE = 'workflow/CHANGE_NODE',
14 | SELECT_NODE = 'workflow/SELECTED_NODE',
15 | SET_VALIDATED = 'workflow/SET_VALIDATED',
16 | SET_ERRORS = 'workflow/SET_ERRORS',
17 | }
18 |
19 | export interface Action {
20 | type: ActionType,
21 | }
22 |
23 | export interface DeleteNodePayload {
24 | id: string,
25 | }
26 |
27 | export interface DeleteNodeAction extends Action {
28 | payload: DeleteNodePayload
29 | }
30 |
31 | export interface AddNodePayLoad {
32 | parentId: string,
33 | node: IWorkFlowNode,
34 | }
35 |
36 | export interface AddNodeAction extends Action {
37 | payload: AddNodePayLoad
38 | }
39 |
40 | export interface AddConditionPayLoad {
41 | nodeId: string,
42 | conditionNode: IBranchNode,
43 | }
44 |
45 | export interface AddConditionAction extends Action {
46 | payload: AddConditionPayLoad
47 | }
48 |
49 | export interface DeleteConditionPayLoad {
50 | conditionId: string,
51 | }
52 |
53 | export interface DeleteConditionAction extends Action {
54 | payload: DeleteConditionPayLoad
55 | }
56 |
57 | export interface UnRedoListPayLoad {
58 | list: ISnapshot[]
59 | }
60 |
61 | export interface UnRedoListAction extends Action {
62 | payload: UnRedoListPayLoad
63 | }
64 |
65 | export interface SetChangeFlagPayload {
66 | changeFlag: boolean
67 | }
68 |
69 | export interface SetChangeFlagAction extends Action {
70 | payload: SetChangeFlagPayload
71 | }
72 |
73 | export interface ChangeNodePayload {
74 | node: IWorkFlowNode
75 | }
76 |
77 | export interface ChangeNodeAction extends Action {
78 | payload: ChangeNodePayload
79 | }
80 |
81 | export interface SetStartNodeAction extends Action {
82 | payload: ChangeNodePayload
83 | }
84 |
85 | export interface SelectNodePayload {
86 | id: string | undefined,
87 | }
88 |
89 |
90 | export interface SelectNodeAction extends Action {
91 | payload: SelectNodePayload
92 | }
93 |
94 | export interface SetValidatedPayload {
95 | validated: boolean | undefined,
96 | }
97 |
98 | export interface SetValidatedAction extends Action {
99 | payload: SetValidatedPayload
100 | }
101 |
102 | export interface SetErrorsPayload {
103 | errors: IErrors,
104 | }
105 |
106 | export interface SetErrorsAction extends Action {
107 | payload: SetErrorsPayload
108 | }
--------------------------------------------------------------------------------
/src/workflow-editor/classes/EditorEngine.ts:
--------------------------------------------------------------------------------
1 | import { Store } from "redux"
2 | import { IErrors, IState } from "../interfaces/state"
3 | import { configureStore } from "@reduxjs/toolkit"
4 | import { mainReducer } from "../reducers"
5 | import { ErrorsListener, RedoListChangeListener, SelectedListener, StartNodeListener, UndoListChangeListener } from "../interfaces/listeners"
6 | import { IBranchNode, IRouteNode, IWorkFlowNode, NodeType } from "../interfaces"
7 | import { Action, ActionType, AddNodeAction, ChangeNodeAction, DeleteNodeAction, SelectNodeAction, SetErrorsAction, SetStartNodeAction, SetValidatedAction, UnRedoListAction } from "../actions"
8 | import { IMaterialUIs, INodeMaterial, Translate } from "../interfaces/material"
9 | import { createUuid } from "../utils/create-uuid"
10 |
11 | export class EditorEngine {
12 | store: Store
13 | t: Translate = (msg: string) => msg
14 | materials: INodeMaterial[] = []
15 | materialUis: IMaterialUIs = {}
16 | constructor(debugMode?: boolean,) {
17 | this.store = makeStoreInstance(debugMode || false)
18 | }
19 |
20 | getNode(nodeId: string, parentNode?: IWorkFlowNode): IWorkFlowNode | undefined {
21 | const startNode = parentNode || this.store.getState().startNode
22 | if (startNode?.id === nodeId && nodeId) {
23 | return startNode
24 | }
25 | if (startNode?.childNode) {
26 | const foundNode = this.getNode(nodeId, startNode?.childNode)
27 | if (foundNode) {
28 | return foundNode
29 | }
30 | }
31 | if (startNode?.nodeType === NodeType.route) {
32 | for (const conditionNode of (startNode as IRouteNode).conditionNodeList) {
33 | const foundNode = this.getNode(nodeId, conditionNode)
34 | if (foundNode) {
35 | return foundNode
36 | }
37 | }
38 | }
39 | return undefined
40 | }
41 |
42 | validate = (): IErrors | true => {
43 | const setValidatedAction: SetValidatedAction = {
44 | type: ActionType.SET_VALIDATED,
45 | payload: {
46 | validated: true
47 | }
48 | }
49 | this.dispatch(setValidatedAction)
50 |
51 | const errors: IErrors = {}
52 | this.setErrors({})
53 | this.doValidateNode(this.store.getState().startNode, errors)
54 | if (Object.keys(errors).length > 0) {
55 | this.setErrors(errors)
56 | return errors
57 | }
58 | return true;
59 | }
60 |
61 |
62 | setErrors(errors: IErrors) {
63 | const setErrorsAction: SetErrorsAction = {
64 | type: ActionType.SET_ERRORS,
65 | payload: {
66 | errors
67 | }
68 | }
69 | this.store.dispatch(setErrorsAction)
70 | }
71 |
72 | dispatch = (action: Action) => {
73 | this.store.dispatch(action)
74 | }
75 |
76 | backup = () => {
77 | const state = this.store.getState();
78 | const setUndoListAction: UnRedoListAction = {
79 | type: ActionType.SET_UNOLIST,
80 | payload: {
81 | list: [...state.undoList, { startNode: state.startNode, validated: state.validated }]
82 | }
83 | }
84 | this.dispatch(setUndoListAction)
85 | const setRedoListAction: UnRedoListAction = {
86 | type: ActionType.SET_REDOLIST,
87 | payload: {
88 | list: []
89 | }
90 | }
91 | this.dispatch(setRedoListAction)
92 | }
93 |
94 | undo = () => {
95 | const state = this.store.getState();
96 | const newUndoList = [...state.undoList]
97 | const snapshot = newUndoList.pop()
98 | if (!snapshot) {
99 | console.error("No element in undo list")
100 | return
101 | }
102 | const setUndoListAction: UnRedoListAction = {
103 | type: ActionType.SET_UNOLIST,
104 | payload: {
105 | list: newUndoList
106 | }
107 | }
108 |
109 | this.dispatch(setUndoListAction)
110 |
111 | const setRedoListAction: UnRedoListAction = {
112 | type: ActionType.SET_REDOLIST,
113 | payload: {
114 | list: [...state.redoList, { startNode: state.startNode }]
115 | }
116 | }
117 | this.dispatch(setRedoListAction)
118 | const setStartNodeAction: SetStartNodeAction = {
119 | type: ActionType.SET_START_NODE,
120 | payload: {
121 | node: snapshot?.startNode
122 | }
123 | }
124 | this.dispatch(setStartNodeAction)
125 |
126 | const setValidatedAction: SetValidatedAction = {
127 | type: ActionType.SET_VALIDATED,
128 | payload: {
129 | validated: snapshot?.validated,
130 | }
131 | }
132 | this.dispatch(setValidatedAction)
133 | }
134 |
135 | redo = () => {
136 | const state = this.store.getState();
137 | const newRedoList = [...state.redoList]
138 | const snapshot = newRedoList.pop()
139 | if (!snapshot) {
140 | console.error("No element in redo list")
141 | return
142 | }
143 | const setRedoListAction: UnRedoListAction = {
144 | type: ActionType.SET_REDOLIST,
145 | payload: {
146 | list: newRedoList
147 | }
148 | }
149 |
150 | this.dispatch(setRedoListAction)
151 |
152 | const setUndoListAction: UnRedoListAction = {
153 | type: ActionType.SET_UNOLIST,
154 | payload: {
155 | list: [...state.undoList, { startNode: state.startNode }]
156 | }
157 | }
158 | this.dispatch(setUndoListAction)
159 |
160 | const setStartNodeAction: SetStartNodeAction = {
161 | type: ActionType.SET_START_NODE,
162 | payload: {
163 | node: snapshot?.startNode
164 | }
165 | }
166 | this.dispatch(setStartNodeAction)
167 |
168 | const setValidatedAction: SetValidatedAction = {
169 | type: ActionType.SET_VALIDATED,
170 | payload: {
171 | validated: snapshot?.validated,
172 | }
173 | }
174 | this.dispatch(setValidatedAction)
175 | }
176 |
177 | setStartNode(node: IWorkFlowNode) {
178 | this.backup()
179 | const setStartNodeAction: SetStartNodeAction = {
180 | type: ActionType.SET_START_NODE,
181 | payload: {
182 | node
183 | }
184 | }
185 |
186 | this.dispatch(setStartNodeAction)
187 | this.revalidate()
188 | }
189 |
190 | changeNode(node: IWorkFlowNode) {
191 | this.backup()
192 | const changeNodeAction: ChangeNodeAction = {
193 | type: ActionType.CHANGE_NODE,
194 | payload: {
195 | node
196 | }
197 | }
198 |
199 | this.dispatch(changeNodeAction)
200 | this.revalidate()
201 | }
202 |
203 | addCondition(node: IRouteNode, condition: IBranchNode) {
204 | const newNode: IRouteNode = { ...node, conditionNodeList: [...node.conditionNodeList, condition] };
205 | this.changeNode(newNode)
206 | }
207 |
208 | changeCondition(node: IRouteNode, condition: IBranchNode) {
209 | const newNode: IRouteNode = { ...node, conditionNodeList: node.conditionNodeList.map(con => con.id === condition.id ? condition : con) };
210 | this.changeNode(newNode)
211 | }
212 |
213 | removeCondition(node: IRouteNode, conditionId: string) {
214 | //如果只剩2个分支,则删除节点
215 | if (node.conditionNodeList.length <= 2) {
216 | this.removeNode(node.id)
217 | return
218 | }
219 | this.backup()
220 | const newNode: IRouteNode = { ...node, conditionNodeList: node.conditionNodeList.filter(co => co.id !== conditionId) };
221 | this.changeNode(newNode)
222 | }
223 |
224 | //条件左移一位
225 | transConditionOneStepToLeft(node: IRouteNode, index: number) {
226 | if (index > 0) {
227 | this.backup()
228 | const newConditions = [...node.conditionNodeList]
229 | newConditions[index] = newConditions.splice(index - 1, 1, newConditions[index])[0]
230 | const newNode: IRouteNode = { ...node, conditionNodeList: newConditions };
231 | this.changeNode(newNode)
232 | }
233 | }
234 |
235 | //条件右移一位
236 | transConditionOneStepToRight(node: IRouteNode, index: number) {
237 | const newConditions = [...node.conditionNodeList]
238 | if (index < newConditions.length - 1) {
239 | this.backup()
240 | newConditions[index] = newConditions.splice(index + 1, 1, newConditions[index])[0]
241 | const newNode: IRouteNode = { ...node, conditionNodeList: newConditions };
242 | this.changeNode(newNode)
243 | }
244 | }
245 |
246 | //克隆一个条件
247 | cloneCondition(node: IRouteNode, condition: IBranchNode) {
248 | const newCondition = JSON.parse(JSON.stringify(condition))
249 | newCondition.name = newCondition.name + this.t?.("ofCopy")
250 | //重写Id
251 | resetId(newCondition)
252 | const index = node.conditionNodeList.indexOf(condition)
253 | const newList = [...node.conditionNodeList]
254 | newList.splice(index + 1, 0, newCondition)
255 | const newNode: IRouteNode = { ...node, conditionNodeList: newList };
256 | this.changeNode(newNode)
257 | }
258 |
259 | addNode(parentId: string, node: IWorkFlowNode) {
260 | this.backup()
261 | const addAction: AddNodeAction = { type: ActionType.ADD_NODE, payload: { parentId, node } }
262 | this.store.dispatch(addAction)
263 | this.revalidate()
264 | }
265 |
266 | selectNode(id: string | undefined) {
267 | const selectAction: SelectNodeAction = { type: ActionType.SELECT_NODE, payload: { id } }
268 | this.store.dispatch(selectAction)
269 | }
270 |
271 | removeNode(id?: string) {
272 | if (id) {
273 | this.backup()
274 | const removeAction: DeleteNodeAction = { type: ActionType.DELETE_NODE, payload: { id } }
275 | this.store.dispatch(removeAction)
276 | this.revalidate()
277 | }
278 | }
279 |
280 | subscribeStartNodeChange(listener: StartNodeListener) {
281 | let previousState: IWorkFlowNode | undefined = this.store.getState().startNode
282 |
283 | const handleChange = () => {
284 | const nextState = this.store.getState().startNode
285 | if (nextState === previousState) {
286 | return
287 | }
288 | previousState = nextState
289 | listener(nextState)
290 | }
291 |
292 | return this.store.subscribe(handleChange)
293 | }
294 |
295 | subscribeSelectedChange(listener: SelectedListener) {
296 | let previousState: string | undefined = this.store.getState().selectedId
297 |
298 | const handleChange = () => {
299 | const nextState = this.store.getState().selectedId
300 | if (nextState === previousState) {
301 | return
302 | }
303 | previousState = nextState
304 | listener(nextState)
305 | }
306 |
307 | return this.store.subscribe(handleChange)
308 | }
309 |
310 | subscribeUndoListChange(listener: UndoListChangeListener) {
311 | let previousState = this.store.getState().undoList
312 |
313 | const handleChange = () => {
314 | const nextState = this.store.getState().undoList
315 | if (nextState === previousState) {
316 | return
317 | }
318 | previousState = nextState
319 | listener(nextState)
320 | }
321 |
322 | return this.store.subscribe(handleChange)
323 | }
324 |
325 | subscribeRedoListChange(listener: RedoListChangeListener) {
326 | let previousState = this.store.getState().redoList
327 |
328 | const handleChange = () => {
329 | const nextState = this.store.getState().redoList
330 | if (nextState === previousState) {
331 | return
332 | }
333 | previousState = nextState
334 | listener(nextState)
335 | }
336 |
337 | return this.store.subscribe(handleChange)
338 | }
339 |
340 | subscribeErrorsChange(listener: ErrorsListener) {
341 | let previousState = this.store.getState().errors
342 |
343 | const handleChange = () => {
344 | const nextState = this.store.getState().errors
345 | if (nextState === previousState) {
346 | return
347 | }
348 | previousState = nextState
349 | listener(nextState)
350 | }
351 |
352 | return this.store.subscribe(handleChange)
353 | }
354 |
355 |
356 | //审批流节点不多,节点变化全部重新校验一遍,无需担心性能问题,以后有需求再优化
357 | private revalidate = () => {
358 | if (this.store.getState().validated) {
359 | this.validate()
360 | }
361 | }
362 |
363 | private doValidateNode = (node: IWorkFlowNode, errors: IErrors) => {
364 | const materialUi = this.materialUis[node.nodeType]
365 | if (materialUi?.validate) {
366 | const result = materialUi.validate(node, { t: this.t })
367 | if (result !== true && result !== undefined) {
368 | errors[node.id] = result
369 | }
370 | }
371 | if (node.childNode) {
372 | this.doValidateNode(node.childNode, errors)
373 | }
374 | if (node.nodeType === NodeType.route) {
375 | for (const condition of (node as IRouteNode).conditionNodeList) {
376 | this.doValidateNode(condition, errors)
377 | }
378 | }
379 | }
380 | }
381 |
382 | function resetId(node: IWorkFlowNode) {
383 | node.id = createUuid()
384 | if (node.childNode) {
385 | resetId(node.childNode)
386 | }
387 | if (node.nodeType === NodeType.route) {
388 | for (const condition of (node as IRouteNode).conditionNodeList) {
389 | resetId(condition)
390 | }
391 | }
392 | }
393 |
394 | function makeStoreInstance(debugMode: boolean): Store {
395 | // TODO: if we ever make a react-native version of this,
396 | // we'll need to consider how to pull off dev-tooling
397 | const reduxDevTools =
398 | typeof window !== 'undefined' &&
399 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
400 | (window as any).__REDUX_DEVTOOLS_EXTENSION__
401 | return configureStore(
402 | {
403 | reducer: mainReducer,
404 | middleware: (getDefaultMiddleware) => getDefaultMiddleware({
405 | immutableCheck: false,
406 | serializableCheck: false,
407 | }),
408 | devTools: debugMode &&
409 | reduxDevTools &&
410 | reduxDevTools({
411 | name: 'dnd-core',
412 | instanceId: 'dnd-core',
413 | }),
414 | }
415 | )
416 | }
417 |
--------------------------------------------------------------------------------
/src/workflow-editor/classes/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./EditorEngine"
--------------------------------------------------------------------------------
/src/workflow-editor/components/ButtonSelect.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Divider } from "antd"
2 | import { Fragment, memo, useCallback, useEffect, useState } from "react"
3 | import { styled } from "styled-components"
4 |
5 | const Shell = styled.div`
6 | display: flex;
7 | align-items: center;
8 | background-color: ${props => props.theme.token?.colorBorderSecondary};
9 | padding: 2px 1px;
10 | border-radius: 6px;
11 | `
12 |
13 | const StyledDivider = styled(Divider)`
14 | margin: 0 4px;
15 | border-color: ${props => props.theme.token?.colorBorder};
16 | `
17 |
18 | const StyleButton = styled(Button)`
19 | flex:1;
20 | color: ${props => props.theme.token?.colorTextSecondary};
21 | &.active{
22 | color: ${props => props.theme.token?.colorText};
23 | cursor: default;
24 | &:hover{
25 | border: solid 1px ${props => props.theme.token?.colorBorder};
26 | color: ${props => props.theme.token?.colorText};
27 | }
28 | div{
29 | display: none;
30 | }
31 | }
32 | `
33 |
34 | export interface IButtonItem {
35 | key: string,
36 | label: React.ReactElement | string | undefined
37 | }
38 |
39 |
40 | export const ButtonSelect = memo((
41 | props: {
42 | options: IButtonItem[]
43 | value: string,
44 | onChange?: (value: string) => void
45 | }
46 | ) => {
47 | const { value, options, onChange } = props
48 | const [inputValue, setInputValue] = useState(value || props.options?.[0]?.key)
49 |
50 | useEffect(() => {
51 | setInputValue(value)
52 | }, [value])
53 |
54 | const handleNodeClick = useCallback((key: string) => {
55 | setInputValue(key)
56 | onChange?.(key)
57 | }, [onChange])
58 |
59 | return (
60 |
61 | {
62 | options.map((option, index) => {
63 | return (
64 |
65 | handleNodeClick(option.key)}
69 | >
70 | {option.label}
71 |
72 | {index < (options.length - 1) && }
73 |
74 | )
75 | })
76 | }
77 |
78 | )
79 | })
--------------------------------------------------------------------------------
/src/workflow-editor/components/ContentPlaceholder.tsx:
--------------------------------------------------------------------------------
1 | import { memo } from "react"
2 | import className from "classnames"
3 |
4 | export const ContentPlaceholder = memo((
5 | props: {
6 | text?: string,
7 | secondary?: boolean,
8 | }
9 | ) => {
10 | const { text, secondary } = props
11 | return (
12 |
14 | {text}
15 |
16 | )
17 | })
--------------------------------------------------------------------------------
/src/workflow-editor/components/ExpressionInput/AddMenu.tsx:
--------------------------------------------------------------------------------
1 | import { Dropdown, MenuProps } from "antd";
2 | import { memo, useMemo } from "react"
3 | import { ExpressionGroupType, ExpressionNodeType } from "../../interfaces";
4 | import { useTranslate } from "../../react-locales";
5 |
6 | export const AddMenu = memo((
7 | props: {
8 | onOpenChange?: (open: boolean) => void,
9 | onAddExpression: () => void,
10 | onAddGroup: (groupType: ExpressionGroupType) => void,
11 | children?: React.ReactNode,
12 | }
13 | ) => {
14 | const { onOpenChange, onAddExpression, onAddGroup, children } = props;
15 | const t = useTranslate();
16 | const items: MenuProps['items'] = useMemo(() => [
17 | {
18 | label: t("addExpression"),
19 | key: ExpressionNodeType.Expression,
20 | onClick: onAddExpression,
21 | },
22 | {
23 | label: t("addAndGroup"),
24 | key: ExpressionGroupType.And,
25 | onClick: () => onAddGroup(ExpressionGroupType.And)
26 | },
27 | {
28 | label: t("addOrGroup"),
29 | key: ExpressionGroupType.Or,
30 | onClick: () => onAddGroup(ExpressionGroupType.Or)
31 | },
32 | ], [onAddExpression, onAddGroup, t]);
33 |
34 | return (
35 |
40 | {children}
41 |
42 | )
43 | })
--------------------------------------------------------------------------------
/src/workflow-editor/components/ExpressionInput/DefaultExpressionInput.tsx:
--------------------------------------------------------------------------------
1 | import { Select, Input, Space } from "antd"
2 | import { memo } from "react"
3 | import { OperatorSelect } from "./OperatorSelect"
4 | import { ExpressionInputProps } from "./ExpressionInputProps"
5 |
6 | export const DefaultExpressionInput = memo((
7 | props: ExpressionInputProps
8 | ) => {
9 | return (
10 |
11 |
18 |
19 |
20 |
21 | )
22 | })
--------------------------------------------------------------------------------
/src/workflow-editor/components/ExpressionInput/ExpressionGroup.tsx:
--------------------------------------------------------------------------------
1 | import { styled } from "styled-components"
2 | import { Button, Select } from "antd"
3 | import { useTranslate } from "../../react-locales";
4 | import { memo, useCallback } from "react";
5 | import { ExpressionGroupType, ExpressionNodeType, IExpression, IExpressionGroup, IExpressionNode } from "../../interfaces";
6 | import { ExpressionInputProps } from "./ExpressionInputProps";
7 | import { ExpressionItem, Item, itemHeight } from "./ExpressionItem";
8 | import { DeleteOutlined, PlusOutlined } from "@ant-design/icons";
9 | import { AddMenu } from "./AddMenu";
10 | import { createUuid } from "../../utils/create-uuid";
11 |
12 | const ExpressionGroupShell = styled.div`
13 | display: flex;
14 | align-items: stretch;
15 | min-height: 88px;
16 | `
17 | const GroupOperator = styled.div`
18 | position: relative;
19 | width: 80px;
20 | //border: solid 1px;
21 | display: flex;
22 | align-items: center;
23 | padding-right: 16px;
24 | `
25 |
26 | const GroupOperatorLine = styled.div`
27 | position: absolute;
28 | left: calc(50% - 8px);
29 | width: 30px;
30 | border: solid 1px ${props => props.theme.token?.colorBorder};
31 | border-right: 0;
32 | border-radius: 5px 0 0 5px;
33 | height: calc(100% - ${itemHeight}px);
34 | &::before{
35 | content: "";
36 | position: absolute;
37 | top:0;
38 | right:0;
39 | transform: translateX(100%) translateY(-50%);
40 | width: 6px;
41 | height: 6px;
42 | border: solid 1px ${props => props.theme.token?.colorBorder};
43 | border-radius: 50%;
44 | }
45 | &::after{
46 | content: "";
47 | position: absolute;
48 | bottom:0;
49 | right:0;
50 | transform: translateX(100%) translateY(50%);
51 | width: 6px;
52 | height: 6px;
53 | border: solid 1px ${props => props.theme.token?.colorBorder};
54 | border-radius: 50%;
55 | }
56 | `
57 |
58 | const ExpressionChildren = styled.div`
59 | flex: 1;
60 | display: flex;
61 | flex-flow: column;
62 | `
63 |
64 | const GroupAction = styled.div`
65 | display: flex;
66 | width: 100%;
67 | align-items: center;
68 | `
69 |
70 | // const SuccessIcon = styled(CheckCircleFilled)`
71 | // color:${props => props.theme.token?.colorSuccess};
72 | // `
73 | // const ErrorIcon = styled(CloseCircleFilled)`
74 | // color:${props => props.theme.token?.colorError};
75 | // `
76 |
77 | export const ExpressionGroup = memo((
78 | props: {
79 | ExpressInput: React.FC
80 | value: IExpressionGroup,
81 | onChange?: (value: IExpressionGroup) => void,
82 | onRemove?: (nodeId: string) => void
83 | root?: boolean,
84 | }
85 | ) => {
86 | const { ExpressInput, value, onChange, onRemove, root } = props
87 | const t = useTranslate()
88 |
89 | const handleAddExp = useCallback(() => {
90 | value && onChange?.({
91 | ...value,
92 | children: [
93 | ...value.children,
94 | {
95 | id: createUuid(),
96 | nodeType: ExpressionNodeType.Expression
97 | }
98 | ]
99 | })
100 | }, [onChange, value])
101 |
102 | const handleAddGroup = useCallback((groupType: ExpressionGroupType) => {
103 | const newNode: IExpressionGroup = {
104 | id: createUuid(),
105 | nodeType: ExpressionNodeType.Group,
106 | groupType: groupType,
107 | children: [
108 | {
109 | id: createUuid(),
110 | nodeType: ExpressionNodeType.Expression
111 | }
112 | ]
113 | }
114 | onChange?.({
115 | ...value,
116 | children: [
117 | ...value.children,
118 | newNode,
119 | ]
120 | })
121 | }, [onChange, value])
122 |
123 | const handleAddExpAfter = useCallback((index: number) => {
124 | const newNode: IExpression = {
125 | id: createUuid(),
126 | nodeType: ExpressionNodeType.Expression
127 | }
128 | const newChildren = [...value.children]
129 | newChildren.splice(index + 1, 0, newNode)
130 | onChange?.({
131 | ...value,
132 | children: newChildren
133 | })
134 | }, [onChange, value])
135 |
136 | const handleAddGroupAfter = useCallback((index: number, groupType: ExpressionGroupType) => {
137 | const newNode: IExpressionGroup = {
138 | id: createUuid(),
139 | nodeType: ExpressionNodeType.Group,
140 | groupType,
141 | children: [
142 | {
143 | id: createUuid(),
144 | nodeType: ExpressionNodeType.Expression
145 | }
146 | ]
147 | }
148 | const newChildren = [...value.children]
149 | newChildren.splice(index + 1, 0, newNode)
150 | onChange?.({
151 | ...value,
152 | children: newChildren
153 | })
154 | }, [onChange, value])
155 |
156 | const handleChildChange = useCallback((node: IExpressionNode) => {
157 | onChange?.({
158 | ...value,
159 | children: value.children?.map(child => child.id === node.id ? node : child)
160 | })
161 | }, [onChange, value])
162 |
163 | const handelGroupTypeChange = useCallback((groupType: ExpressionGroupType) => {
164 | onChange?.({
165 | ...value,
166 | groupType,
167 | })
168 | }, [onChange, value])
169 |
170 | const handleRemoveChild = useCallback((nodeId: string) => {
171 | onChange?.({
172 | ...value,
173 | children: (value.children as IExpressionNode[]).filter((child) => child.id !== nodeId)
174 | })
175 | }, [onChange, value])
176 |
177 | const handleDeleteClick = useCallback(() => {
178 | onRemove?.(value.id)
179 | }, [onRemove, value.id])
180 |
181 | return (
182 |
183 |
184 |
185 |
194 |
195 |
196 | {
197 | value?.children?.map((child, index) => {
198 | return (
199 | child.nodeType === ExpressionNodeType.Group ?
200 | handleRemoveChild(child.id)}
206 | />
207 | : handleAddExpAfter(index)}
210 | onAddGroup={(nodType) => handleAddGroupAfter(index, nodType)}
211 | onRemove={() => handleRemoveChild(child.id)}
212 | >
213 |
214 |
215 | )
216 | })
217 | }
218 | {
219 | !value?.children.length &&
220 | }
221 | -
222 |
223 |
227 | }
231 | >
232 | {t("add")}
233 |
234 |
235 | {
236 | !root && }
239 | style={{ marginLeft: 8 }}
240 | onClick={handleDeleteClick}
241 | />
242 | }
243 |
244 |
245 |
246 |
247 | )
248 | })
--------------------------------------------------------------------------------
/src/workflow-editor/components/ExpressionInput/ExpressionInputProps.ts:
--------------------------------------------------------------------------------
1 | import { IExpression } from "../../interfaces"
2 |
3 | export type ExpressionInputProps = {
4 | value?: IExpression,
5 | onChange?: (value?: IExpression) => void
6 | }
--------------------------------------------------------------------------------
/src/workflow-editor/components/ExpressionInput/ExpressionItem.tsx:
--------------------------------------------------------------------------------
1 | import { PlusOutlined, DeleteOutlined } from "@ant-design/icons"
2 | import { memo } from "react"
3 | import styled from "styled-components"
4 | import { Button } from "antd"
5 | import { AddMenu } from "./AddMenu";
6 | import classNames from "classnames";
7 | import { ExpressionGroupType } from "../../interfaces";
8 |
9 | export const itemHeight = 48;
10 |
11 | export const Item = styled.div`
12 | display: flex;
13 | align-items: center;
14 | min-height: ${itemHeight}px;
15 | .actions-space{
16 | display: none;
17 | &.add-open{
18 | display: flex;
19 | }
20 | }
21 | &:hover{
22 | .actions-space{
23 | display: flex;
24 | }
25 | }
26 | `
27 |
28 | export const ActionSpace = styled.div`
29 | display: flex;
30 | `
31 |
32 | export const ExpressionContent = styled.div`
33 | flex: 1;
34 | `
35 |
36 | export const Actions = styled.div`
37 | width: 60px;
38 | display: flex;
39 | justify-content: flex-end;
40 | align-items: center;
41 | `
42 | export const AddIcon = styled(PlusOutlined)`
43 | font-size:12px;
44 | `
45 | export const RemoveIcon = styled(DeleteOutlined)`
46 | font-size:12px;
47 | `
48 | export const ExpressionItem = memo((
49 | props: {
50 | onAddExpression?: () => void,
51 | onAddGroup?: (nodeType: ExpressionGroupType) => void,
52 | onRemove?: () => void,
53 | children?: React.ReactNode,
54 | }
55 | ) => {
56 | const { onAddExpression, onAddGroup, onRemove, children } = props
57 | //const [addOpen, setAddOpen] = useState(false);
58 |
59 | // const handleOpenChange = useCallback((open: boolean) => {
60 | // setAddOpen(open)
61 | // }, [])
62 |
63 | return (
64 | -
65 |
66 | {children}
67 |
68 |
69 |
72 | {
73 | onAddExpression && onAddGroup &&
74 |
79 | } />
80 |
81 | }
82 |
83 | }
86 | onClick={onRemove}
87 | />
88 |
89 |
90 |
91 | )
92 | })
--------------------------------------------------------------------------------
/src/workflow-editor/components/ExpressionInput/ExpressionTreeInput.tsx:
--------------------------------------------------------------------------------
1 | import { memo } from "react";
2 | import { IExpressionGroup } from "../../interfaces";
3 | import { ExpressionInputProps } from "./ExpressionInputProps";
4 | import { ExpressionGroup } from "./ExpressionGroup";
5 |
6 | export const ExpressionTreeInput = memo((
7 | props: {
8 | ExpressInput: React.FC,
9 | value: IExpressionGroup,
10 | onChange?: (value: IExpressionGroup) => void
11 | }
12 | ) => {
13 | const { ExpressInput, value, onChange } = props
14 |
15 | return (
16 |
22 | )
23 | })
--------------------------------------------------------------------------------
/src/workflow-editor/components/ExpressionInput/OperatorSelect.tsx:
--------------------------------------------------------------------------------
1 | import { Select } from "antd"
2 | import { memo } from "react"
3 | import { OperatorType } from "../../interfaces"
4 | import { useTranslate } from "../../react-locales"
5 |
6 | export const OperatorSelect = memo(() => {
7 | const t = useTranslate()
8 |
9 | return (
10 |
28 | )
29 | })
--------------------------------------------------------------------------------
/src/workflow-editor/components/ExpressionInput/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./DefaultExpressionInput"
2 | export * from "./ExpressionInputProps"
3 | export * from "./ExpressionTreeInput"
4 | export * from "./OperatorSelect"
--------------------------------------------------------------------------------
/src/workflow-editor/components/MemberSelect/AddDialog.tsx:
--------------------------------------------------------------------------------
1 | import { memo, useCallback, useState } from "react"
2 | import { PlusOutlined, SearchOutlined } from "@ant-design/icons"
3 | import { Button, Input, Modal, Space, Typography } from "antd"
4 | import styled from "styled-components"
5 | import { useTranslate } from "../../react-locales";
6 |
7 | const { Text } = Typography;
8 |
9 | const AddButton = styled(Button)`
10 | font-size: 12px !important;
11 | border: solid 1px ${props => props.theme.token?.colorBorder};
12 | margin-right: 8px;
13 | `
14 |
15 | const Dialog = styled(Modal)`
16 | .ant-modal-content{
17 | padding: 0;
18 | .ant-modal-header{
19 | padding: 16px 16px;
20 | background-color: ${props => props.theme.token?.colorBorderSecondary};
21 | margin-bottom: 0;
22 | }
23 | }
24 | `
25 |
26 | const Content = styled.div`
27 | display: flex;
28 | height: 500px;
29 | `
30 |
31 | const SubContent = styled.div`
32 | height: 100%;
33 | flex:1;
34 | display: flex;
35 | flex-flow: column;
36 | padding: 8px 16px;
37 | &.left{
38 | border-right: solid 1px ${props => props.theme.token?.colorBorderSecondary};
39 | }
40 | `
41 |
42 | const SelectedContent = styled.div`
43 | flex: 1;
44 | display: flex;
45 | flex-flow: column;
46 | `
47 |
48 | const SelectedTitle = styled.div`
49 | .ant-typography-secondary{
50 | font-size: 12px;
51 | }
52 | `
53 |
54 | const Footer = styled.div`
55 | height: 48px;
56 | display: flex;
57 | align-items: center;
58 | justify-content: flex-end;
59 | `
60 |
61 | const StyledSearch = styled(SearchOutlined)`
62 | color: ${props => props.theme.token?.colorTextSecondary};
63 | opacity: 0.8;
64 | `
65 |
66 | export const AddDialog = memo(() => {
67 | const [open, setOpen] = useState(false);
68 | const t = useTranslate()
69 |
70 | const handleClick = useCallback(() => {
71 | setOpen(true);
72 | }, []);
73 |
74 | const handleOk = useCallback(() => {
75 | setOpen(false);
76 | }, []);
77 |
78 | const handleCancel = useCallback(() => {
79 | setOpen(false);
80 | }, []);
81 | return (
82 | <>
83 | }
86 | size="small"
87 | onClick={handleClick}
88 | >{t("add")}
89 |
126 | >
127 | )
128 | })
--------------------------------------------------------------------------------
/src/workflow-editor/components/MemberSelect/index.tsx:
--------------------------------------------------------------------------------
1 | import { CloseCircleFilled } from "@ant-design/icons"
2 | import { Tag } from "antd"
3 | import { memo } from "react"
4 | import styled from "styled-components"
5 | import { AddDialog } from "./AddDialog"
6 |
7 | const Shell = styled.div`
8 | position: relative;
9 | display: flex;
10 | align-items: center;
11 | border: solid 1px ${props => props.theme.token?.colorBorder};
12 | padding: 2px 2px;
13 | border-radius: 5px;
14 | &:hover{
15 | border: solid 1px ${props => props.theme.token?.colorPrimary};
16 | .clear{
17 | display: inline-flex;
18 | }
19 | }
20 | .clear{
21 | display: none;
22 | font-size: 12px;
23 | position: absolute;
24 | right: 8px;
25 | top: 50%;
26 | transform: translateY(-50%);
27 | color:${props => props.theme.token?.colorTextSecondary};
28 | opacity: 0.5;
29 | cursor: pointer;
30 | &:hover{
31 | opacity: 1;
32 | color:${props => props.theme.token?.colorTextSecondary};
33 | }
34 | }
35 | `
36 |
37 | const MemberTag = styled(Tag)`
38 | border: 0;
39 | background-color: ${props => props.theme.token?.colorBorderSecondary};
40 | cursor: default;
41 | user-select: none;
42 | `
43 |
44 | export const MemberSelect = memo(() => {
45 |
46 | return (
47 |
48 |
49 | { }}
52 | >
53 | 代码边界
54 |
55 | { }}
58 | >
59 | 张三
60 |
61 | { }}
64 | >
65 | 李四
66 |
67 |
68 |
69 | )
70 | })
--------------------------------------------------------------------------------
/src/workflow-editor/components/NavTabs.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "antd"
2 | import { memo, useCallback, useEffect, useState } from "react"
3 | import { styled } from "styled-components"
4 | import classNames from "classnames"
5 |
6 | const Shell = styled.div`
7 | flex:1;
8 | display: flex;
9 | justify-content: center;
10 | color: ${props => props.theme.token?.colorTextSecondary};
11 | `
12 | const NavIcon = styled.span`
13 | width: 18px;
14 | height: 18px;
15 | border-radius: 50%;
16 | border: solid 1px ${props => props.theme.token?.colorTextSecondary};
17 | line-height: 16px;
18 | display: flex;
19 | justify-content: center;
20 | align-items: center;
21 | transform: translateY(-1px);
22 | font-size: 12px;
23 | &.selected{
24 | background-color: ${props => props.theme.token?.colorPrimary};
25 | border-color: ${props => props.theme.token?.colorPrimary};
26 | color: #fff;
27 | }
28 | `
29 | export interface INavItem {
30 | key: string,
31 | label: React.ReactElement | string | undefined
32 | }
33 |
34 | export const NavTabs = memo((
35 | props: {
36 | options: INavItem[],
37 | defaultValue?: string,
38 | value?: string,
39 | onChange?: (value?: string) => void
40 | }
41 | ) => {
42 | const { defaultValue, value, options, onChange } = props
43 | const [inputValue, setInputValue] = useState(defaultValue || value || props.options?.[0]?.key)
44 |
45 | useEffect(() => {
46 | setInputValue(value)
47 | }, [value])
48 |
49 | const handleItemClick = useCallback((key: string) => {
50 | setInputValue(key)
51 | onChange?.(key)
52 | }, [onChange])
53 |
54 | return (
55 |
56 | {
57 | options?.map((option, index) => {
58 | return (
59 |
72 | )
73 | })
74 | }
75 |
76 | )
77 | })
--------------------------------------------------------------------------------
/src/workflow-editor/components/Toolbar.tsx:
--------------------------------------------------------------------------------
1 | import { memo } from "react"
2 | import styled from "styled-components"
3 |
4 | const ToolbarShell = styled.div`
5 | height: 48px;
6 | display: flex;
7 | align-items: center;
8 | justify-content: space-between;
9 | box-sizing: border-box;
10 | padding: 8px 16px;
11 | box-shadow: 0 2px 3px 1px rgba(0, 0, 0, 0.05);
12 | z-index: 1;
13 | background-color: ${props => props.theme.mode === "dark" ? "rgba(255, 255, 255, 0.1)" : ""};
14 | border: solid ${props => props.theme.mode === "dark" ? props.theme.token?.colorBorder + " 1px" : "0px"};
15 | `
16 | const ToolbarTitle = styled.div`
17 | display: flex;
18 | `
19 |
20 | const ToolbarActions = styled.div`
21 | display: flex;
22 | `
23 |
24 | export const Toolbar = memo((
25 | props: {
26 | title?: React.ReactNode,
27 | actions?: React.ReactNode,
28 | children?: React.ReactNode,
29 | }
30 | ) => {
31 | const { title, actions, children } = props
32 | return (
33 |
34 |
35 | {title}
36 |
37 | {children}
38 |
39 | {actions}
40 |
41 |
42 | )
43 | })
--------------------------------------------------------------------------------
/src/workflow-editor/components/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./ExpressionInput"
2 | export * from "./MemberSelect"
3 | export * from "./ButtonSelect"
4 | export * from "./ContentPlaceholder"
5 | export * from "./NavTabs"
6 | export * from "./Toolbar"
--------------------------------------------------------------------------------
/src/workflow-editor/contexts.ts:
--------------------------------------------------------------------------------
1 | import { createContext } from "react";
2 | import { EditorEngine } from "./classes";
3 |
4 | export const WorkflowEditorStoreContext = createContext(undefined)
--------------------------------------------------------------------------------
/src/workflow-editor/hooks/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./useEditorEngine"
2 | export * from "./useExport"
3 | export * from "./useImport"
4 | export * from "./useSelectedNode"
5 | export * from "./useStartNode"
--------------------------------------------------------------------------------
/src/workflow-editor/hooks/useEditorEngine.ts:
--------------------------------------------------------------------------------
1 | import { useContext } from "react";
2 | import { WorkflowEditorStoreContext } from "../contexts";
3 |
4 | export function useEditorEngine() {
5 | return useContext(WorkflowEditorStoreContext)
6 | }
--------------------------------------------------------------------------------
/src/workflow-editor/hooks/useError.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect, useState } from "react"
2 | import { IErrors } from "../interfaces/state"
3 | import { useEditorEngine } from "./useEditorEngine"
4 |
5 | export function useError(nodeId: string) {
6 | const [errors, setErrors] = useState()
7 |
8 | const store = useEditorEngine()
9 |
10 | const handleErrorsChange = useCallback((errs: IErrors) => {
11 | setErrors(errs)
12 | }, [])
13 |
14 | useEffect(() => {
15 | const unsub = store?.subscribeErrorsChange(handleErrorsChange)
16 | return unsub
17 | }, [handleErrorsChange, store])
18 |
19 | useEffect(() => {
20 | setErrors(store?.store.getState().errors)
21 | }, [store?.store])
22 |
23 | return errors?.[nodeId]
24 | }
--------------------------------------------------------------------------------
/src/workflow-editor/hooks/useExport.ts:
--------------------------------------------------------------------------------
1 | import { message } from "antd";
2 | import { useCallback } from "react";
3 | import { saveFile } from "../utils/saveFile";
4 | import { useTranslate } from "../react-locales";
5 | import { useStartNode } from "./useStartNode";
6 |
7 | export function useExport() {
8 | const t = useTranslate();
9 | const startNode = useStartNode();
10 | const doExport = useCallback(() => {
11 |
12 | saveFile(`approvalflow`, JSON.stringify({ startNode }, null, 2)).then(
13 | (savedName) => {
14 | if (savedName) {
15 | message.success(t("operateSuccess"))
16 | }
17 | }
18 | ).catch(err => {
19 | console.error(err)
20 | });
21 | }, [startNode, t]);
22 |
23 | return doExport
24 | }
--------------------------------------------------------------------------------
/src/workflow-editor/hooks/useImport.ts:
--------------------------------------------------------------------------------
1 | import { message } from "antd";
2 | import { useCallback } from "react";
3 | import { getTheFiles } from "../utils/getFIles";
4 | import { IWorkFlowNode } from "../interfaces";
5 | import { useEditorEngine } from "./useEditorEngine";
6 | import { useTranslate } from "../react-locales";
7 |
8 | export interface IFlowJson {
9 | startNode?: IWorkFlowNode
10 | }
11 |
12 | export function useImport() {
13 | const edtorStore = useEditorEngine()
14 | const t = useTranslate()
15 |
16 | const doImport = useCallback(() => {
17 | getTheFiles(".json").then((fileHandles) => {
18 | fileHandles?.[0]?.getFile().then((file: any) => {
19 | file.text().then((fileData: any) => {
20 | try {
21 | const flowJson: IFlowJson = JSON.parse(fileData);
22 | if (flowJson.startNode) {
23 | edtorStore?.setStartNode(flowJson.startNode)
24 | } else {
25 | message.error(t("fileIllegal"));
26 | }
27 | } catch (error: any) {
28 | console.error(error);
29 | message.error(t("fileIllegal"));
30 | }
31 | });
32 | });
33 | }).catch(err => {
34 | console.error(err)
35 | });
36 | }, [edtorStore, t]);
37 |
38 | return doImport
39 | }
--------------------------------------------------------------------------------
/src/workflow-editor/hooks/useMaterialUI.ts:
--------------------------------------------------------------------------------
1 | import { IWorkFlowNode } from "../interfaces";
2 | import { useEditorEngine } from "./useEditorEngine";
3 |
4 | export function useMaterialUI(node?: IWorkFlowNode) {
5 | const store = useEditorEngine()
6 |
7 | return store?.materialUis?.[node?.nodeType || ""]
8 | }
--------------------------------------------------------------------------------
/src/workflow-editor/hooks/useNodeMaterial.ts:
--------------------------------------------------------------------------------
1 | import { IWorkFlowNode } from "../interfaces";
2 | import { useEditorEngine } from "./useEditorEngine";
3 |
4 | export function useNodeMaterial(node?: IWorkFlowNode) {
5 | const store = useEditorEngine()
6 |
7 | return store?.materials.find(material => material.defaultConfig?.nodeType === node?.nodeType)
8 | }
--------------------------------------------------------------------------------
/src/workflow-editor/hooks/useRedoList.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect, useState } from "react"
2 | import { useEditorEngine } from "./useEditorEngine"
3 | import { ISnapshot } from "../interfaces/state"
4 |
5 | export function useRedoList() {
6 | const [redoList, setRedoList] = useState([])
7 | const store = useEditorEngine()
8 |
9 | const handleRedoListChange = useCallback((list: ISnapshot[]) => {
10 | setRedoList(list)
11 | }, [])
12 |
13 | useEffect(() => {
14 | const unsub = store?.subscribeRedoListChange(handleRedoListChange)
15 | return unsub
16 | }, [handleRedoListChange, store])
17 |
18 | useEffect(() => {
19 | setRedoList(store?.store.getState().redoList || [])
20 | }, [store?.store])
21 |
22 | return redoList
23 | }
--------------------------------------------------------------------------------
/src/workflow-editor/hooks/useSelectedId.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect, useState } from "react"
2 | import { useEditorEngine } from "./useEditorEngine"
3 |
4 | export function useSelectedId() {
5 | const [selectedId, setSelectedId] = useState()
6 | const store = useEditorEngine()
7 |
8 | const handleSelectedChange = useCallback((selected: string | undefined) => {
9 | setSelectedId(selected)
10 | }, [])
11 |
12 | useEffect(() => {
13 | const unsub = store?.subscribeSelectedChange(handleSelectedChange)
14 | return unsub
15 | }, [handleSelectedChange, store])
16 |
17 | useEffect(() => {
18 | setSelectedId(store?.store.getState().selectedId)
19 | }, [store?.store])
20 |
21 | return selectedId
22 | }
--------------------------------------------------------------------------------
/src/workflow-editor/hooks/useSelectedNode.ts:
--------------------------------------------------------------------------------
1 | import { useEditorEngine } from "./useEditorEngine";
2 | import { useSelectedId } from "./useSelectedId";
3 |
4 | export function useSelectedNode() {
5 | const selectedId = useSelectedId();
6 | const store = useEditorEngine();
7 |
8 | return selectedId ? store?.getNode(selectedId) : undefined
9 | }
--------------------------------------------------------------------------------
/src/workflow-editor/hooks/useStartNode.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect, useState } from "react"
2 | import { IWorkFlowNode } from "../interfaces"
3 | import { useEditorEngine } from "./useEditorEngine"
4 |
5 | //获取起始节点
6 | export function useStartNode() {
7 | const [startNode, setStartNode] = useState()
8 | const engine = useEditorEngine()
9 |
10 | //引擎起始节点变化事件处理函数
11 | const handleStartNodeChange = useCallback((startNode: IWorkFlowNode) => {
12 | setStartNode(startNode)
13 | }, [])
14 |
15 | useEffect(() => {
16 | //订阅起始节点变化事件
17 | const unsub = engine?.subscribeStartNodeChange(handleStartNodeChange)
18 | return unsub
19 | }, [handleStartNodeChange, engine])
20 |
21 | //初始化时,先拿到最新数据
22 | useEffect(() => {
23 | setStartNode(engine?.store.getState().startNode)
24 | }, [engine?.store])
25 |
26 | return startNode
27 | }
--------------------------------------------------------------------------------
/src/workflow-editor/hooks/useUndoList.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect, useState } from "react"
2 | import { useEditorEngine } from "./useEditorEngine"
3 | import { ISnapshot } from "../interfaces/state"
4 |
5 | export function useUndoList() {
6 | const [undoList, setSetUndoList] = useState([])
7 | const store = useEditorEngine()
8 |
9 | const handleUndoListChange = useCallback((list: ISnapshot[]) => {
10 | setSetUndoList(list)
11 | }, [])
12 |
13 | useEffect(() => {
14 | const unsub = store?.subscribeUndoListChange(handleUndoListChange)
15 | return unsub
16 | }, [handleUndoListChange, store])
17 |
18 | useEffect(() => {
19 | setSetUndoList(store?.store.getState().undoList || [])
20 | }, [store?.store])
21 |
22 | return undoList
23 | }
--------------------------------------------------------------------------------
/src/workflow-editor/icons.tsx:
--------------------------------------------------------------------------------
1 | export const redoIcon =
2 |
5 |
6 | export const undoIcon =
7 |
10 |
11 | export const copyIcon =
12 |
15 |
16 |
17 | export const sealIcon =
18 |
21 |
22 |
23 | export const notifierIcon =
24 |
27 |
28 |
29 | export const dealIcon =
30 |
33 |
34 |
35 | export const routeIcon =
36 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/src/workflow-editor/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./classes"
2 | export * from "./components"
3 | export * from "./hooks"
4 | export * from "./interfaces"
5 | export * from "./theme"
6 | export * from "./FlowEditor"
--------------------------------------------------------------------------------
/src/workflow-editor/interfaces/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./material"
2 | export * from "./workflow"
3 | export * from "./settings"
--------------------------------------------------------------------------------
/src/workflow-editor/interfaces/listeners.ts:
--------------------------------------------------------------------------------
1 | import { IErrors, ISnapshot } from "./state"
2 | import { IWorkFlowNode } from "./workflow"
3 |
4 | export type ZoomChangeListener = (zoom: number) => void
5 | export type StartNodeListener = (rootNode: IWorkFlowNode) => void
6 | export type UndoListChangeListener = (undos: ISnapshot[]) => void
7 | export type RedoListChangeListener = (redos: ISnapshot[]) => void
8 | export type ChangeFlagChangeListener = (changeFlag: number) => void
9 | export type SelectedListener = (id: string | undefined) => void
10 | export type ErrorsListener = (errors: IErrors) => void
--------------------------------------------------------------------------------
/src/workflow-editor/interfaces/material.ts:
--------------------------------------------------------------------------------
1 | import { IWorkFlowNode, NodeType } from "./workflow"
2 |
3 | //国际化翻译函数,外部注入,这里使用的是@rxdrag/locales的实现(通过react hooks转了一下)
4 | export type Translate = (msg: string) => string | undefined
5 |
6 | //物料上下文
7 | export interface IContext {
8 | //翻译
9 | t: Translate
10 | }
11 |
12 | //节点物料
13 | export interface INodeMaterial {
14 | //颜色
15 | color: string
16 | //标题
17 | label: string
18 | //图标
19 | icon?: React.ReactElement
20 | //默认配置
21 | defaultConfig?: { nodeType: NodeType | string }
22 | //创建一个默认节点,跟defaultCofig只选一个
23 | createDefault?: (context: Context) => IWorkFlowNode
24 | //从物料面板隐藏,比如发起人节点、条件分支内的分支节点
25 | hidden?: boolean
26 | }
27 |
28 | //物料UI配置
29 | export interface IMaterialUI {
30 | //节点内容区
31 | viewContent?: (node: FlowNode, context: Context) => React.ReactNode
32 | //属性面板设置组件
33 | settersPanel?: React.FC<{ value: Config, onChange: (value: Config) => void }>
34 | //校验失败返回错误消息,成功返回ture
35 | validate?: (node: FlowNode, context: Context) => string | true | undefined
36 | }
37 |
38 | //物料UI的一个map,用于组件间通过props传递物料UI,key是节点类型
39 | export interface IMaterialUIs {
40 | [nodeType: string]: IMaterialUI | undefined
41 | }
--------------------------------------------------------------------------------
/src/workflow-editor/interfaces/settings.ts:
--------------------------------------------------------------------------------
1 | //表达式操作符
2 | export enum OperatorType {
3 | //等于
4 | Eq = "eq",
5 | //不等于
6 | Ne = "ne",
7 | //大于
8 | Gt = "gt",
9 | //小于,
10 | Lt = "lt",
11 | //小于等于
12 | Le = "le",
13 | //大于等于
14 | Ge = "ge",
15 | //包含
16 | Like = "like",
17 | //开头包含
18 | LikeStart = "like_start",
19 | //结尾包含
20 | LikeEnd = "like_end",
21 | //不为空
22 | NotEmpty = "not_empty",
23 | //为空
24 | Empty = "empty"
25 | }
26 |
27 | //这个命名需要优化
28 | // export interface IExpression {
29 | // fieldEnName?: string,
30 | // fieldName?: string,
31 | // fieldValue?: unknown,
32 | // operatorType?: OperatorType,
33 | // }
34 |
35 | export enum ExpressionNodeType {
36 | Expression = "expression",
37 | Group = "group"
38 | }
39 |
40 | export interface IExpressionNode {
41 | id: string
42 | nodeType: ExpressionNodeType
43 | }
44 |
45 | export interface IExpression extends IExpressionNode {
46 | name?: string,
47 | value?: unknown,
48 | operator?: OperatorType,
49 | }
50 |
51 | export enum ExpressionGroupType {
52 | And = "and",
53 | Or = "or"
54 | }
55 |
56 | export interface IExpressionGroup extends IExpressionNode {
57 | groupType: ExpressionGroupType,
58 | children: IExpression[] | IExpressionGroup[]
59 | }
60 |
61 | export interface IExtCondition extends IExpression {
62 | flowId?: string
63 | flowNodeId?: string
64 | sort?: number
65 | groupIndex?: number
66 | }
67 |
68 | export enum AuthType {
69 | read = "read",
70 | edit = "edit",
71 | hide = "hide",
72 | }
73 |
74 | export interface IAuthItem {
75 | fieldEnName: string,
76 | type: AuthType,
77 | }
--------------------------------------------------------------------------------
/src/workflow-editor/interfaces/state.ts:
--------------------------------------------------------------------------------
1 | import { IWorkFlowNode, NodeType } from "./workflow";
2 |
3 | //操作快照,用于撤销、重做
4 | export interface ISnapshot {
5 | //开始节点
6 | startNode: IWorkFlowNode,
7 | //是否校验过
8 | validated?: boolean,
9 | }
10 |
11 | //错误消息
12 | export interface IErrors {
13 | [nodeId: string]: string | undefined
14 | }
15 |
16 | //状态
17 | export interface IState {
18 | //是否被修改,该标识用于提示是否需要保存
19 | changeFlag: boolean,
20 | //撤销快照列表
21 | undoList: ISnapshot[],
22 | //重做快照列表
23 | redoList: ISnapshot[],
24 | //zoom: number,
25 | startNode: IWorkFlowNode,
26 | //被选中的节点,用于弹出属性面板
27 | selectedId?: string,
28 | //是否校验过,如果校验过,后面加入的节点会自动校验
29 | validated?: boolean,
30 | //校验错误
31 | errors: IErrors,
32 | }
33 |
34 | export const initialState: IState = {
35 | changeFlag: false,
36 | undoList: [],
37 | redoList: [],
38 | startNode: {
39 | id: "start",
40 | nodeType: NodeType.start,
41 | },
42 | errors: {}
43 | }
44 |
--------------------------------------------------------------------------------
/src/workflow-editor/interfaces/workflow.ts:
--------------------------------------------------------------------------------
1 | export enum NodeType {
2 | //开始节点
3 | start = "start",
4 | //审批人
5 | approver = "approver",
6 | //抄送人?
7 | notifier = "notifier",
8 | //处理人?
9 | audit = "audit",
10 | //路由(条件节点),下面包含分支节点
11 | route = "route",
12 | //分支节点
13 | condition = "condition",
14 | }
15 |
16 | //审批流节点
17 | export interface IWorkFlowNode{
18 | id: string
19 | //名称
20 | name?: string
21 | //string可以用于自定义节点,暂时用不上
22 | nodeType: NodeType | string
23 | //描述
24 | desc?: string
25 | //子节点
26 | childNode?: IWorkFlowNode
27 | //配置
28 | config?: Config
29 | }
30 |
31 | //条件根节点,下面包含各分支节点
32 | export interface IRouteNode extends IWorkFlowNode {
33 | //分支节点
34 | conditionNodeList: IBranchNode[]
35 | }
36 |
37 | //条件分支的子节点,分支节点
38 | export interface IBranchNode extends IWorkFlowNode {
39 | //条件表达式,后端就是这样的名字,保留了
40 | //后面考虑通过泛型放入config,视条件复杂度决定
41 | //flowNodeConditionVOList?: IExpression[]
42 | }
43 |
44 | //审批流,代表一张审批流图
45 | export interface IWorkflow {
46 | //审批流Id
47 | flowId: string;
48 | //审批流名称
49 | name?:string;
50 | //开始节点
51 | startNode: IWorkFlowNode;
52 | }
--------------------------------------------------------------------------------
/src/workflow-editor/locales.ts:
--------------------------------------------------------------------------------
1 | import { ILocales } from "@rxdrag/locales";
2 |
3 | export const defalutLocales: ILocales = {
4 | "zh-CN": {
5 | baseSettings: "基础设置",
6 | formDesign: "表单设计",
7 | flowDesign: "流程设计",
8 | addvancedSettings: "高级设置",
9 | save: "保存",
10 | publish: "发布",
11 | preview: "预览",
12 | help: "帮助",
13 | import: "导入",
14 | export: "导出",
15 | approver: "审批人",
16 | notifier: "抄送人",
17 | dealer: "办理人",
18 | routeNode: "条件分支",
19 | condition: "条件",
20 | promoter: "发起人",
21 | allMember: "所有人",
22 | flowFinished: "流程结束",
23 | addCondition: "添加条件",
24 | copyCodition: "复制条件",
25 | priority: "优先级",
26 | pleaseSetCondition: "请设置条件",
27 | pleaseChooseApprover: "请选择审批人",
28 | pleaseChooseNotifier: "发起人自选",
29 | pleaseChooseDealer: "请选择办理人",
30 | ofCopy: "的副本",
31 | confirm: "确定",
32 | cancel: "取消",
33 | formAuth: "表单操作权限",
34 | promoterSettings: "发起人设置",
35 | setPromoter: "设置发起人",
36 | setApprover: "设置审批人",
37 | setDealer: "设置办理人",
38 | setNotifier: "设置抄送人",
39 | approveType: "审批类型",
40 | manualApproval: "人工审批",
41 | autoPass: "自动审批",
42 | autoReject: "自动拒绝",
43 | whoCanSubmit: "谁可以提交",
44 | add: "添加",
45 | departmentsAndMembersVisable: "以下部门和人员可看到审批模板",
46 | search: "搜索",
47 | operateSuccess: "操作成功",
48 | fileIllegal: "文件不合法",
49 | noSelectedApprover: "未选择审批人",
50 | noSelectedDealer: "未选择办理人",
51 | noSetCondition: "未设置条件",
52 | cantNotPublish: "当前无法发布",
53 | canNotPublishTip: "以下内容不完善,请修改后发布",
54 | gotIt: "我知道了",
55 | gotoEdit: "前往修改",
56 | or: "或",
57 | and: "且",
58 | eq: "等于",
59 | ne: "不等于",
60 | gt: "大于",
61 | lt: "小于",
62 | le: "小于等于",
63 | ge: "大于等于",
64 | like: "包含",
65 | like_start: "开头包含",
66 | like_end: "结尾包含",
67 | not_empty: "不为空",
68 | empty: "为空",
69 | addExpression: "添加条件",
70 | addAndGroup: "添加且组",
71 | addOrGroup: "添加或组"
72 | },
73 | 'en-US': {
74 | baseSettings: "Base Settings",
75 | formDesign: "Form Design",
76 | flowDesign: "Flow Design",
77 | addvancedSettings: "Advanced Settings",
78 | save: "Save",
79 | publish: "Publish",
80 | preview: "Preview",
81 | help: "Help",
82 | import: "Import",
83 | export: "Export",
84 | approver: "Approver",
85 | notifier: "Notifier",
86 | dealer: "Dealer",
87 | routeNode: "Condition Branch",
88 | condition: "Condition",
89 | promoter: "Promoter",
90 | allMember: "All",
91 | flowFinished: "Flow Finished",
92 | addCondition: "Add Condition",
93 | copyCodition: "Copy Condition",
94 | priority: "Priority",
95 | pleaseSetCondition: "Please set condition",
96 | pleaseChooseApprover: "Choose Approver",
97 | pleaseChooseNotifier: "Selft Choose",
98 | pleaseChooseDealer: "Choose Dealer",
99 | ofCopy: " of Copy",
100 | confirm: "Confirm",
101 | cancel: "Cancel",
102 | formAuth: "Form Auth",
103 | promoterSettings: "Promoter Settings",
104 | setPromoter: "Set Promoter",
105 | setApprover: "Set Approver",
106 | setDealer: "Set Dealer",
107 | setNotifier: "Set Notifier",
108 | approveType: "Approve Type",
109 | manualApproval: "Manual approval",
110 | autoPass: "Auto Pass",
111 | autoReject: "Auto Reject",
112 | whoCanSubmit: "Who can submit",
113 | add: "Add",
114 | departmentsAndMembersVisable: "Departments and Members",
115 | search: "Search",
116 | operateSuccess: "Operate success",
117 | fileIllegal: "File Illegal",
118 | noSelectedApprover: "No selected approver",
119 | noSelectedDealer: "NO selected dealer",
120 | noSetCondition: "Not set condition",
121 | cantNotPublish: "Can not publish",
122 | canNotPublishTip: "The following content is incomplete, please modify and publish it",
123 | gotIt: "Got it",
124 | gotoEdit: "Goto Edit",
125 | or: "Or",
126 | and: "And",
127 | eq: "Equal",
128 | ne: "Not Equal",
129 | gt: "More than",
130 | lt: "Less than",
131 | le: "Less than or equal",
132 | ge: "More than or equal",
133 | like: "Contain",
134 | like_start: "Contain on start",
135 | like_end: "Contian on end",
136 | not_empty: "Not empty",
137 | empty: "Empty",
138 | addExpression: "Add Expression",
139 | addAndGroup: "Add And Group",
140 | addOrGroup: "Add Or Group",
141 | }
142 | }
--------------------------------------------------------------------------------
/src/workflow-editor/nodes/AddButton/ContentPanel.tsx:
--------------------------------------------------------------------------------
1 | import { memo } from "react"
2 | import { styled } from "styled-components"
3 | import { useEditorEngine } from "../../hooks"
4 | import { MaterialItem } from "./MaterialItem"
5 | import { INodeMaterial } from "../../interfaces/material"
6 |
7 | const Container = styled.div`
8 | display: flex;
9 | flex-wrap: wrap;
10 | width: 360px;
11 | `
12 |
13 |
14 | export const ContentPanel = memo((
15 | props: {
16 | nodeId: string
17 | onClickMaterial: (material: INodeMaterial) => void,
18 | }
19 | ) => {
20 | const { nodeId, onClickMaterial } = props
21 | const editorStore = useEditorEngine()
22 | return (
23 |
24 | {
25 | editorStore?.materials?.filter(material => !material.hidden).map((material, index) => {
26 | return (
27 | onClickMaterial?.(material)}
32 | />
33 | )
34 | })
35 | }
36 |
37 | )
38 | })
--------------------------------------------------------------------------------
/src/workflow-editor/nodes/AddButton/MaterialItem.tsx:
--------------------------------------------------------------------------------
1 | import { memo, useCallback } from "react"
2 | import { INodeMaterial } from "../../interfaces/material"
3 | import { useTranslate } from "../../react-locales"
4 | import { styled } from "styled-components"
5 | import { useEditorEngine } from "../../hooks"
6 | import { createUuid } from "../../utils/create-uuid"
7 |
8 | const MaterialSchell = styled.div`
9 | width: 50%;
10 | padding: 4px 8px;
11 | `
12 |
13 | const MItem = styled.div`
14 | padding: 0px 8px;
15 | height: 64px;
16 | border-radius: 5px;
17 | display: flex;
18 | align-items: center;
19 | cursor: pointer;
20 | &:hover{
21 | box-shadow: 1px 2px 8px 2px rgba(0, 0, 0, ${props => props.theme.mode === "light" ? "0.08" : "0.2"});
22 | }
23 | `
24 |
25 | const MaterialIcon = styled.div`
26 | display: flex;
27 | height: 40px;
28 | width: 40px;
29 | border: solid 1px ${props => props.theme.token?.colorBorder};
30 | margin-right: 16px;
31 | border-radius: 16px;
32 | justify-content: center;
33 | align-items: center;
34 | font-size: 24px;
35 | color: #ff943e;
36 | `
37 | export const MaterialItem = memo((
38 | props: {
39 | nodeId: string,
40 | material: INodeMaterial,
41 | onClick?: () => void
42 | }
43 | ) => {
44 | const { nodeId, material, onClick } = props
45 | const t = useTranslate();
46 | const editorStore = useEditorEngine()
47 |
48 | const handleClick = useCallback(() => {
49 | const newId = createUuid()
50 | const newName = t(material.label)
51 | if (material.defaultConfig) {
52 | //复制一份配置数据,保证immutable
53 | editorStore?.addNode(nodeId, { ...JSON.parse(JSON.stringify(material.defaultConfig)), id: newId, name: newName })
54 | } else if (material.createDefault) {
55 | editorStore?.addNode(nodeId, { ...material.createDefault({ t }), name: newName })
56 | } else {
57 | console.error("Material no defutConfig or createDefault")
58 | }
59 |
60 | editorStore?.selectNode(newId);
61 | onClick?.()
62 | }, [editorStore, material, nodeId, onClick, t])
63 |
64 | return (
65 |
66 |
67 |
68 | {material.icon}
69 |
70 | {t(material.label)}
71 |
72 |
73 | )
74 | })
--------------------------------------------------------------------------------
/src/workflow-editor/nodes/AddButton/index.tsx:
--------------------------------------------------------------------------------
1 | import { PlusOutlined } from "@ant-design/icons"
2 | import { Popover } from "antd"
3 | import { memo, useCallback, useState } from "react"
4 | import { styled } from "styled-components"
5 | import { ContentPanel } from "./ContentPanel"
6 |
7 | const AddButtonBox = styled.div`
8 | width: 240px;
9 | display: -webkit-inline-box;
10 | display: -ms-inline-flexbox;
11 | display: inline-flex;
12 | -ms-flex-negative: 0;
13 | flex-shrink: 0;
14 | -webkit-box-flex: 1;
15 | -ms-flex-positive: 1;
16 | position: relative;
17 | &:before {
18 | content: "";
19 | position: absolute;
20 | top: 0;
21 | left: 0;
22 | right: 0;
23 | bottom: 0;
24 | z-index: -1;
25 | margin: auto;
26 | width: 2px;
27 | height: 100%;
28 | background-color: ${props => props.theme.mode === "light" ? "#cacaca" : "rgba(255,255,255,0.35)"};
29 | }
30 | `
31 |
32 | const ButtonShell = styled.div`
33 | user-select: none;
34 | width: 240px;
35 | padding: 20px 0 32px;
36 | display: flex;
37 | -webkit-box-pack: center;
38 | justify-content: center;
39 | flex-shrink: 0;
40 | -webkit-box-flex: 1;
41 | flex-grow: 1;
42 | .btn {
43 | //display: flex;
44 | //justify-content: center;
45 | //align-items: center;
46 | padding-left: 4.5px;
47 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, .1);
48 | width: 30px;
49 | height: 30px;
50 | background: ${props => props.theme?.token?.colorPrimary};
51 | border-radius: 50%;
52 | position: relative;
53 | border: none;
54 | line-height: 28px;
55 | -webkit-transition: all .3s cubic-bezier(.645, .045, .355, 1);
56 | transition: all .3s cubic-bezier(.645, .045, .355, 1);
57 | cursor: pointer;
58 | color:#fff;
59 | font-size: 20px;
60 | &:hover {
61 | transform: scale(1.3);
62 | box-shadow: 0 13px 27px 0 rgba(0, 0, 0, .1)
63 | }
64 | &:active {
65 | transform: none;
66 | background: ${props => props.theme?.token?.colorPrimary};
67 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, .1)
68 | }
69 | }
70 | `
71 |
72 | export const AddButton = memo((
73 | props: {
74 | nodeId: string
75 | }
76 | ) => {
77 | const { nodeId } = props
78 | const [open, setOpen] = useState(false)
79 |
80 | const handleOpenChange = useCallback((status: boolean) => {
81 | setOpen(status)
82 | }, [])
83 |
84 | const handleMaterialClick = useCallback(() => {
85 | setOpen(false)
86 | }, [])
87 |
88 | return (
89 |
90 |
91 | }
94 | trigger="click"
95 | open={open}
96 | onOpenChange={handleOpenChange}
97 | >
98 |
101 |
102 |
103 |
104 | )
105 | })
--------------------------------------------------------------------------------
/src/workflow-editor/nodes/ChildNode.tsx:
--------------------------------------------------------------------------------
1 | import { memo } from "react"
2 | import { IRouteNode, IWorkFlowNode, NodeType } from "../interfaces"
3 | import { RouteNode } from "./RouteNode"
4 | import { NormalNode } from "./NormalNode"
5 |
6 | export const ChildNode = memo((
7 | props: {
8 | node: IWorkFlowNode
9 | }
10 | ) => {
11 | const { node } = props
12 | return (
13 | node.nodeType === NodeType.route
14 | ?
15 | :
16 | )
17 | })
--------------------------------------------------------------------------------
/src/workflow-editor/nodes/CloseButton.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback } from "react"
2 | import { useEditorEngine } from "../hooks"
3 | import { CloseOutlined } from "@ant-design/icons"
4 | import { styled } from "styled-components"
5 | import { Button } from "antd"
6 |
7 | const CloseStyledButton = styled(Button)`
8 | position: absolute;
9 | right: 10px;
10 | top: 50%;
11 | transform: translateY(-50%);
12 | font-size: 14px;
13 | display: flex;
14 | justify-content: center;
15 | align-items: center;
16 | `
17 | export const CloseButton = ((
18 | props: {
19 | nodeId?: string
20 | }
21 | ) => {
22 | const { nodeId } = props
23 | const store = useEditorEngine()
24 |
25 | const handleClose = useCallback(() => {
26 | store?.removeNode(nodeId)
27 | }, [nodeId, store])
28 |
29 | return (
30 | }
36 | onClick={handleClose}
37 | />
38 | )
39 | })
--------------------------------------------------------------------------------
/src/workflow-editor/nodes/EndNode.tsx:
--------------------------------------------------------------------------------
1 | import { memo } from "react"
2 | import { styled } from "styled-components"
3 | import { useTranslate } from "../react-locales"
4 |
5 | const Container = styled.div`
6 | width: 100%;
7 | font-size: 14px;
8 | color: ${props => props.theme.token?.colorTextSecondary};
9 | text-align: left;
10 | user-select: none;
11 | margin-bottom: 56px;
12 | .end-node-circle {
13 | width: 10px;
14 | height: 10px;
15 | margin: auto;
16 | border-radius: 50%;
17 | background: ${props => props.theme.mode === "light" ? "#cacaca" : "rgba(255,255,255,0.35)"};
18 | }
19 | .end-node-text {
20 | margin-top: 5px;
21 | text-align: center
22 | }
23 | `
24 |
25 | export const EndNode = memo(() => {
26 | const t = useTranslate()
27 | return (
28 |
29 |
30 | {t("flowFinished")}
31 |
32 | )
33 | })
--------------------------------------------------------------------------------
/src/workflow-editor/nodes/ErrorTip.tsx:
--------------------------------------------------------------------------------
1 | import { InfoCircleOutlined } from "@ant-design/icons"
2 | import { Tooltip } from "antd"
3 | import { memo } from "react"
4 | import { styled } from "styled-components"
5 | import { useError } from "../hooks/useError"
6 |
7 | const Schell = styled.div`
8 | position: absolute;
9 | z-index: 2;
10 | top: 0;
11 | right: -40px;
12 | `
13 | const ErrorIcon = styled(InfoCircleOutlined)`
14 | color:red;
15 | font-size: 24px;
16 | `
17 |
18 | export const ErrorTip = memo((props: {
19 | nodeId: string
20 | }) => {
21 | const { nodeId } = props
22 | const errorMsg = useError(nodeId)
23 | return (
24 |
25 | {
26 | errorMsg &&
27 |
28 |
29 | }
30 |
31 | )
32 | })
--------------------------------------------------------------------------------
/src/workflow-editor/nodes/NodeTitle.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo, useCallback, useEffect, useState } from "react";
2 | import { styled } from "styled-components";
3 | import { IWorkFlowNode } from "../interfaces";
4 | import { CloseButton } from "./CloseButton";
5 | import { INodeMaterial } from "../interfaces/material";
6 | import { useEditorEngine } from "../hooks";
7 |
8 | export const NodeTitleShell = styled.div`
9 | position: relative;
10 | display: flex;
11 | align-items: center;
12 | padding-left: 16px;
13 | padding-right: 30px;
14 | width: 100%;
15 | height: 24px;
16 | line-height: 24px;
17 | font-size: 12px;
18 | color: #fff;
19 | text-align: left;
20 | //background: #576a95;
21 | border-radius: 4px 4px 0 0;
22 | user-select: none;
23 | &.start-node-title{
24 | //background: rgb(87, 106, 149);
25 | }
26 | `
27 | export const NodeIcon = styled.div`
28 | font-size: 14px;
29 | margin-right: 8px;
30 | `
31 |
32 | export const TitleResponse = styled.div`
33 | flex:1;
34 | display: flex;
35 | padding: 2px 0;
36 | align-items: center;
37 | `
38 |
39 | export const NodeTitleText = styled.div`
40 | border: solid transparent 1px;
41 | &:hover{
42 | line-height: 16px;
43 | border-bottom: dashed 1px #fff;
44 | }
45 | `
46 |
47 | export const Input = styled.input`
48 | flex: 1;
49 | height: 18px;
50 | padding-left: 4px;
51 | text-indent: 0;
52 | font-size: 12px;
53 | line-height: 18px;
54 | z-index: 1;
55 | outline: solid 2px rgba(80,80,80, 0.3);
56 | border: 0;
57 | border-radius: 4px;
58 | background-color: ${props => props.theme?.token?.colorBgBase};
59 | color: ${props => props.theme?.token?.colorText};
60 | `
61 |
62 | export const NodeTitle = memo((props: {
63 | node: IWorkFlowNode,
64 | material?: INodeMaterial,
65 | }) => {
66 | const { node, material } = props;
67 | const [editting, setEditting] = useState(false)
68 | const [inputValue, setInputValue] = useState(node.name)
69 |
70 | const editorStore = useEditorEngine()
71 |
72 | useEffect(() => {
73 | setInputValue(node.name)
74 | }, [node.name])
75 |
76 | const changeName = useCallback(() => {
77 | editorStore?.changeNode({ ...node, name: inputValue })
78 | }, [editorStore, inputValue, node])
79 |
80 | const handleNameClick = useCallback((e: React.MouseEvent) => {
81 | e.stopPropagation()
82 | setEditting(true)
83 | }, [])
84 |
85 | const handleInputClick = useCallback((e: React.MouseEvent) => {
86 | e.stopPropagation()
87 | }, [])
88 |
89 | const handleBlur = useCallback(() => {
90 | changeName()
91 | setEditting(false)
92 | }, [changeName])
93 |
94 | const handleKeyDown = useCallback((event: React.KeyboardEvent) => {
95 | if (event.key === "Enter") {
96 | handleBlur()
97 | }
98 | }, [handleBlur])
99 |
100 | const handleChange = useCallback((e: React.ChangeEvent) => {
101 | setInputValue(e.target.value)
102 | }, [])
103 |
104 | return
105 |
106 | {material?.icon}
107 |
108 | {!editting &&
109 | <>
110 |
111 | {node.name}
112 |
113 |
114 | >
115 | }
116 | {
117 | editting &&
125 | }
126 |
127 | })
--------------------------------------------------------------------------------
/src/workflow-editor/nodes/NormalNode.tsx:
--------------------------------------------------------------------------------
1 | import { memo, useCallback } from "react"
2 | import { IWorkFlowNode } from "../interfaces"
3 | import { RightOutlined } from "@ant-design/icons"
4 | import { AddButton } from "./AddButton"
5 | import { ChildNode } from "./ChildNode"
6 | import { styled } from "styled-components"
7 | import { canvasColor } from "../utils/canvasColor"
8 | import { lineColor } from "../utils/lineColor"
9 | import { nodeColor } from "../utils/nodeColor"
10 | import { useTranslate } from "../react-locales"
11 | import { useEditorEngine } from "../hooks"
12 | import { NodeTitle } from "./NodeTitle"
13 | import { useNodeMaterial } from "../hooks/useNodeMaterial"
14 | import { useMaterialUI } from "../hooks/useMaterialUI"
15 | import { ErrorTip } from "./ErrorTip"
16 |
17 | export const NodeWrap = styled.div`
18 | display: flex;
19 | flex-direction: column;
20 | justify-content: flex-start;
21 | align-items: center;
22 | flex-wrap: wrap;
23 | -webkit-box-flex: 1;
24 | -ms-flex-positive: 1;
25 | padding: 0 50px;
26 | position: relative;
27 | user-select: none;
28 | &::before{
29 | content: "";
30 | position: absolute;
31 | top: -12px;
32 | left: 50%;
33 | transform: translateX(-50%);
34 | width: 0;
35 | height: 4px;
36 | border-style: solid;
37 | border-width: 8px 6px 4px;
38 | border-color: ${lineColor} transparent transparent;
39 | background: ${canvasColor};
40 | }
41 | &.start{
42 | &::before{
43 | height:0;
44 | border-width: 0;
45 | }
46 | }
47 | `
48 |
49 | export const NodeWrapBox = styled.div`
50 | display: inline-flex;
51 | flex-direction: column;
52 | position: relative;
53 | width: 220px;
54 | min-height: 72px;
55 | flex-shrink: 0;
56 | background: ${nodeColor};
57 | border: solid ${props => props.theme.mode === "dark" ? "1px" : 0} ${props => props.theme?.token?.colorBorder};
58 | border-radius: 4px;
59 | cursor: pointer;
60 | user-select: none;
61 | &:after{
62 | pointer-events: none;
63 | content: "";
64 | position: absolute;
65 | top: 0;
66 | bottom: 0;
67 | left: 0;
68 | right: 0;
69 | z-index: 2;
70 | border-radius: 4px;
71 | border: 1px solid transparent;
72 | transition: all .1s cubic-bezier(.645, .045, .355, 1);
73 | box-shadow: 0 2px 5px 0 rgba(0, 0, 0, .1)
74 | }
75 | .close {
76 | display: none;
77 | }
78 | &:hover{
79 | outline: solid 1px ${props => props.theme.token?.colorPrimary};
80 | .close {
81 | display: inline-flex;
82 | }
83 | }
84 | `
85 | export const NodeContent = styled.div`
86 | position: relative;
87 | font-size: 14px;
88 | padding: 16px;
89 | padding-right: 30px;
90 | user-select: none;
91 | .text{
92 | overflow: hidden;
93 | text-overflow: ellipsis;
94 | user-select: none;
95 | display: -webkit-box;
96 | -webkit-line-clamp: 3;
97 | -webkit-box-orient: vertical;
98 | white-space: nowrap;
99 | }
100 | .secondary{
101 | color: ${props => props.theme.token?.colorTextSecondary};
102 | opacity: 0.8;
103 | }
104 | .arrow {
105 | position: absolute;
106 | right: 10px;
107 | top: 50%;
108 | transform: translateY(-50%);
109 | width: 20px;
110 | height: 14px;
111 | font-size: 14px;
112 | color: ${props => props.theme.token?.colorTextSecondary};
113 | }
114 | `
115 | export const NormalNode = memo((
116 | props: {
117 | node: IWorkFlowNode
118 | }
119 | ) => {
120 | const { node } = props
121 | const t = useTranslate()
122 | const material = useNodeMaterial(node)
123 | const materialUi = useMaterialUI(node)
124 | const store = useEditorEngine();
125 |
126 | const handleClick = useCallback(() => {
127 | store?.selectNode(node?.id)
128 | }, [node?.id, store])
129 |
130 | return (
131 |
132 |
133 |
134 |
135 | {materialUi?.viewContent && materialUi?.viewContent(node, { t })}
136 |
137 |
138 |
139 |
140 | {node?.id && }
141 | {node?.childNode && }
142 |
143 | )
144 | })
--------------------------------------------------------------------------------
/src/workflow-editor/nodes/RouteNode/AddBranchButton.tsx:
--------------------------------------------------------------------------------
1 | import { memo, useCallback } from "react"
2 | import styled from "styled-components"
3 | import { useTranslate } from "../../react-locales"
4 | import { nodeColor } from "../../utils/nodeColor"
5 | import { IRouteNode, NodeType } from "../../interfaces"
6 | import { useEditorEngine } from "../../hooks"
7 | import { createUuid } from "../../utils/create-uuid"
8 |
9 | const AddBranch = styled.button`
10 | border: none;
11 | outline: none;
12 | user-select: none;
13 | justify-content: center;
14 | font-size: 12px;
15 | padding: 0 10px;
16 | height: 30px;
17 | line-height: 30px;
18 | border-radius: 15px;
19 | color: ${props => props.theme.token?.colorPrimary};
20 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, .1);
21 | position: absolute;
22 | top: -16px;
23 | left: 50%;
24 | transform: translateX(-50%);
25 | transform-origin: center center;
26 | cursor: pointer;
27 | z-index: 1;
28 | display: inline-flex;
29 | align-items: center;
30 | -webkit-transition: all .3s cubic-bezier(.645, .045, .355, 1);
31 | white-space: nowrap;
32 | transition: all .3s cubic-bezier(.645, .045, .355, 1);
33 | background: ${nodeColor};
34 | border: solid ${props => props.theme.mode === "dark" ? "1px" : 0} ${props => props.theme?.token?.colorBorder};
35 | &:hover{
36 | transform: translateX(-50%) scale(1.1);
37 | box-shadow: 0 8px 16px 0 rgba(0, 0, 0, .1)
38 | }
39 | &:active{
40 | transform: translateX(-50%);
41 | box-shadow: none
42 | }
43 | `
44 |
45 | export const AddBranchButton = memo((
46 | props: {
47 | node: IRouteNode
48 | }
49 | ) => {
50 | const { node } = props
51 | const t = useTranslate()
52 | const editorStore = useEditorEngine()
53 |
54 | const handleClick = useCallback(() => {
55 | const newId = createUuid()
56 | editorStore?.addCondition(node, {
57 | id: newId,
58 | nodeType: NodeType.condition,
59 | name: t("condition") + (node.conditionNodeList.length + 1)
60 | })
61 | editorStore?.selectNode(newId);
62 | }, [editorStore, node, t])
63 |
64 | return
65 | {t("addCondition")}
66 |
67 | })
--------------------------------------------------------------------------------
/src/workflow-editor/nodes/RouteNode/BranchNode.tsx:
--------------------------------------------------------------------------------
1 | import { memo, useCallback } from "react"
2 | import { IBranchNode, IRouteNode } from "../../interfaces"
3 | import { styled } from "styled-components"
4 | import { lineColor } from "../../utils/lineColor"
5 | import { nodeColor } from "../../utils/nodeColor"
6 | import { canvasColor } from "../../utils/canvasColor"
7 | import { AddButton } from "../AddButton"
8 | import { ChildNode } from "../ChildNode"
9 | import { useTranslate } from "../../react-locales"
10 | import { useEditorEngine } from "../../hooks"
11 | import { ConditionNodeTitle } from "./ConditionNodeTitle"
12 | import { useMaterialUI } from "../../hooks/useMaterialUI"
13 | import { ErrorTip } from "../ErrorTip"
14 |
15 | const ColBox = styled.div`
16 | display: inline-flex;
17 | -webkit-box-orient: vertical;
18 | -webkit-box-direction: normal;
19 | flex-direction: column;
20 | -webkit-box-align: center;
21 | align-items: center;
22 | position: relative;
23 | user-select: none;
24 | background-color: ${canvasColor};
25 | &::before{
26 | content: "";
27 | position: absolute;
28 | top: 0;
29 | left: 0;
30 | right: 0;
31 | bottom: 0;
32 | z-index: 0;
33 | margin: auto;
34 | width: 2px;
35 | height: 100%;
36 | background-color: ${lineColor};
37 | }
38 | `
39 |
40 | const BranchStyleNode = styled.div`
41 | min-height: 220px;
42 | display: inline-flex;
43 | -webkit-box-orient: vertical;
44 | -webkit-box-direction: normal;
45 | flex-direction: column;
46 | -webkit-box-flex: 1;
47 | user-select: none;
48 | `
49 | const BranchNodeBox = styled.div`
50 | padding-top: 30px;
51 | padding-right: 50px;
52 | padding-left: 50px;
53 | -webkit-box-pack: center;
54 | justify-content: center;
55 | -webkit-box-align: center;
56 | align-items: center;
57 | flex-grow: 1;
58 | position: relative;
59 | display: inline-flex;
60 | -webkit-box-orient: vertical;
61 | -webkit-box-direction: normal;
62 | flex-direction: column;
63 | -webkit-box-flex: 1;
64 | user-select: none;
65 | `
66 |
67 | const AutoJudge = styled.div`
68 | position: relative;
69 | width: 220px;
70 | min-height: 72px;
71 | background: ${nodeColor};
72 | border: solid ${props => props.theme.mode === "dark" ? "1px" : 0} ${props => props.theme?.token?.colorBorder};
73 | border-radius: 4px;
74 | padding: 8px 16px;
75 | user-select: none;
76 | cursor: pointer;
77 | &::after{
78 | pointer-events: none;
79 | content: "";
80 | position: absolute;
81 | top: 0;
82 | bottom: 0;
83 | left: 0;
84 | right: 0;
85 | z-index: 2;
86 | border-radius: 4px;
87 | border: 1px solid transparent;
88 | transition: all .1s cubic-bezier(.645, .045, .355, 1);
89 | box-shadow: 0 2px 5px 0 rgba(0, 0, 0, .1);
90 | }
91 | &.active{
92 | &::after{
93 | border: 1px solid #3296fa;
94 | box-shadow: 0 0 6px 0 rgba(50, 150, 250, .3)
95 | }
96 | }
97 | .mini-bar{
98 | display: none;
99 | }
100 | .priority{
101 | display: flex;
102 | }
103 | &:hover{
104 | &::after{
105 | border: 1px solid #3296fa;
106 | box-shadow: 0 0 6px 0 rgba(50, 150, 250, .3)
107 | }
108 | .sort-handler{
109 | display: flex;
110 | align-items: center;
111 | }
112 | .mini-bar{
113 | display: flex;
114 | }
115 | .priority{
116 | display: none;
117 | }
118 | }
119 | `
120 | const LineCover = styled.div`
121 | position: absolute;
122 | height: 8px;
123 | width: 50%;
124 | background-color: ${canvasColor};
125 | &.left{
126 | left:-1px;
127 | }
128 | &.right{
129 | right: -1px;
130 | }
131 | &.top{
132 | top: -4px;
133 | }
134 | &.bottom{
135 | bottom: -4px;
136 | }
137 | `
138 |
139 | const SortHandler = styled.div`
140 | position: absolute;
141 | top: 0;
142 | bottom: 0;
143 | display: none;
144 | z-index: 1;
145 | height: 100%;
146 | color: ${props => props.theme.token?.colorTextSecondary};
147 | &.left{
148 | left: 0;
149 | border-right: 1px solid ${props => props.theme.token?.colorBorder};
150 | }
151 | &.right{
152 | right: 0;
153 | border-left: 1px solid ${props => props.theme.token?.colorBorder};
154 | }
155 | &:hover{
156 | background-color: ${props => props.theme.token?.colorBorderSecondary};
157 | }
158 | `
159 | const NodeContent = styled.div`
160 | position: relative;
161 | font-size: 14px;
162 | padding: 16px 0;
163 | padding-right: 30px;
164 | user-select: none;
165 | `
166 |
167 | export const BranchNode = memo((props: { parent: IRouteNode, node: IBranchNode, index: number, length: number }) => {
168 | const { parent, node, index, length } = props
169 | const t = useTranslate()
170 | const editorStore = useEditorEngine()
171 | const materialUi = useMaterialUI(node)
172 |
173 | const handleClick = useCallback(() => {
174 | editorStore?.selectNode(node?.id)
175 | }, [editorStore, node?.id])
176 |
177 | const hanldeMoveLeft = useCallback((e: React.MouseEvent) => {
178 | e.stopPropagation()
179 | node.id && editorStore?.transConditionOneStepToLeft(parent, index)
180 | }, [editorStore, index, node.id, parent])
181 |
182 | const handleMoveRight = useCallback((e: React.MouseEvent) => {
183 | e.stopPropagation()
184 | node.id && editorStore?.transConditionOneStepToRight(parent, index)
185 | }, [editorStore, index, node.id, parent])
186 |
187 | return (
188 |
189 |
190 |
191 |
192 | {
193 | index !== 0 &&
194 |
195 | <
196 |
197 | }
198 |
199 |
200 | {materialUi?.viewContent && materialUi?.viewContent(node, { t })}
201 |
202 | {
203 | index !== (length - 1) &&
204 |
205 | >
206 |
207 | }
208 |
209 |
210 | {node?.id && }
211 | {node?.childNode && }
212 |
213 |
214 | {
215 | index === 0 &&
216 | <>
217 |
218 |
219 | >
220 | }
221 | {
222 | index === (length - 1) &&
223 | <>
224 |
225 |
226 | >
227 | }
228 |
229 | )
230 | })
--------------------------------------------------------------------------------
/src/workflow-editor/nodes/RouteNode/ConditionButtons.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback } from "react"
2 | import { CloseOutlined } from "@ant-design/icons"
3 | import { styled } from "styled-components"
4 | import { Button, Tooltip } from "antd"
5 | import { useEditorEngine } from "../../hooks"
6 | import { copyIcon } from "../../icons"
7 | import { useTranslate } from "../../react-locales"
8 | import { IRouteNode, IBranchNode } from "../../interfaces"
9 |
10 | const Container = styled.div`
11 | position: absolute;
12 | right: -4px;
13 | top: -4px;
14 | display: flex;
15 | opacity: 0.7;
16 | font-size: 11px;
17 | `
18 |
19 | export const ConditionButtons = ((
20 | props: {
21 | parent: IRouteNode,
22 | node: IBranchNode
23 | }
24 | ) => {
25 | const { parent, node } = props
26 | const store = useEditorEngine()
27 | const t = useTranslate()
28 |
29 | const handleClose = useCallback(() => {
30 | node.id && store?.removeCondition(parent, node.id)
31 | }, [node.id, parent, store])
32 |
33 | const handleClone = useCallback(() => {
34 | store?.cloneCondition(parent, node)
35 | }, [node, parent, store])
36 |
37 | return (
38 |
39 |
40 |
47 |
48 | }
53 | onClick={handleClose}
54 | />
55 |
56 | )
57 | })
--------------------------------------------------------------------------------
/src/workflow-editor/nodes/RouteNode/ConditionNodeTitle.tsx:
--------------------------------------------------------------------------------
1 | import { memo, useCallback, useEffect, useState } from "react"
2 | import { IBranchNode, IRouteNode } from "../../interfaces"
3 | import { styled } from "styled-components"
4 | import { ConditionButtons } from "./ConditionButtons"
5 | import { ConditionPriority } from "./ConditionPriority"
6 | import { useTranslate } from "../../react-locales"
7 | import { useEditorEngine } from "../../hooks"
8 | import { Input, TitleResponse } from "../NodeTitle"
9 |
10 | const TitleWrapper = styled.div`
11 | position: relative;
12 | font-size: 12px;
13 | color: ${props => props.theme?.token?.colorTextSecondary};
14 | text-align: left;
15 | line-height: 16px;
16 | display: flex;
17 | user-select: none;
18 | `
19 |
20 | export const TitleText = styled.div`
21 | border: solid transparent 1px;
22 | &:hover{
23 | line-height: 16px;
24 | border-bottom: dashed 1px ${props => props.theme.token?.colorTextSecondary};
25 | }
26 | user-select: none;
27 | `
28 |
29 | export const ConditionNodeTitle = memo((
30 | props: {
31 | node: IBranchNode,
32 | parent: IRouteNode,
33 | index: number,
34 | }
35 | ) => {
36 | const { node, parent, index } = props
37 | const [editting, setEditting] = useState(false)
38 | const [inputValue, setInputValue] = useState(node.name)
39 |
40 |
41 | useEffect(() => {
42 | setInputValue(node.name)
43 | }, [node.name])
44 |
45 |
46 | const t = useTranslate()
47 | const editorStore = useEditorEngine()
48 |
49 | const changeName = useCallback(() => {
50 | editorStore?.changeCondition(parent, { ...node, name: inputValue })
51 | }, [editorStore, inputValue, node, parent])
52 |
53 | const handleNameClick = useCallback((e: React.MouseEvent) => {
54 | e.stopPropagation()
55 | setEditting(true)
56 | }, [])
57 |
58 | const handleInputClick = useCallback((e: React.MouseEvent) => {
59 | e.stopPropagation()
60 | }, [])
61 |
62 | const handleBlur = useCallback(() => {
63 | changeName()
64 | setEditting(false)
65 | }, [changeName])
66 |
67 | const handleKeyDown = useCallback((event: React.KeyboardEvent) => {
68 | if (event.key === "Enter") {
69 | handleBlur()
70 | }
71 | }, [handleBlur])
72 |
73 | const handleChange = useCallback((e: React.ChangeEvent) => {
74 | setInputValue(e.target.value)
75 | }, [])
76 |
77 | return (
78 |
79 | {
80 | !editting && <>
81 |
82 |
83 | {node.name || t("condition")}
84 |
85 |
86 |
87 | >
88 | }
89 | {
90 | editting &&
98 | }
99 | {!editting && }
100 |
101 | )
102 | })
--------------------------------------------------------------------------------
/src/workflow-editor/nodes/RouteNode/ConditionPriority.tsx:
--------------------------------------------------------------------------------
1 | import { styled } from "styled-components"
2 | import { useTranslate } from "../../react-locales"
3 |
4 | const Container = styled.div`
5 | position: absolute;
6 | right: 0px;
7 | top: 0px;
8 | display: flex;
9 | `
10 |
11 | export const ConditionPriority = ((
12 | props: {
13 | index: number
14 | }
15 | ) => {
16 | const { index } = props
17 | const t = useTranslate()
18 | return (
19 |
20 | {t("priority") + (index + 1)}
21 |
22 | )
23 | })
--------------------------------------------------------------------------------
/src/workflow-editor/nodes/RouteNode/index.tsx:
--------------------------------------------------------------------------------
1 | import { memo } from "react"
2 | import { IRouteNode } from "../../interfaces"
3 | import { styled } from "styled-components"
4 | import { AddBranchButton } from "./AddBranchButton"
5 | import { AddButton } from "../AddButton"
6 | import { lineColor } from "../../utils/lineColor"
7 | import { ChildNode } from "../ChildNode"
8 | import { BranchNode } from "./BranchNode"
9 |
10 | const RouteWrap = styled.div`
11 | display: inline-flex;
12 | `
13 | const RouteBoxWrap = styled.div`
14 | display: flex;
15 | -webkit-box-orient: vertical;
16 | -webkit-box-direction: normal;
17 | -ms-flex-direction: column;
18 | flex-direction: column;
19 | -ms-flex-wrap: wrap;
20 | flex-wrap: wrap;
21 | -webkit-box-align: center;
22 | -ms-flex-align: center;
23 | align-items: center;
24 | min-height: 270px;
25 | width: 100%;
26 | -ms-flex-negative: 0;
27 | flex-shrink: 0;
28 | `
29 |
30 | const RouteBox = styled.div`
31 | display: flex;
32 | overflow: visible;
33 | min-height: 180px;
34 | height: auto;
35 | border-bottom: 2px solid;
36 | border-top: 2px solid;
37 | border-color: ${lineColor};
38 | position: relative;
39 | margin-top: 15px;
40 | `
41 |
42 | export const RouteNode = memo((
43 | props: {
44 | node: IRouteNode
45 | }
46 | ) => {
47 | const { node } = props
48 | return (
49 |
50 |
51 |
52 |
53 | {
54 | node.conditionNodeList?.map((child, index) => {
55 | return (
56 |
57 | )
58 | })
59 | }
60 |
61 | {node?.id && }
62 | {node?.childNode && }
63 |
64 |
65 | )
66 | })
--------------------------------------------------------------------------------
/src/workflow-editor/nodes/StartNode.tsx:
--------------------------------------------------------------------------------
1 | import { memo, useCallback } from "react"
2 | import { useStartNode } from "../hooks/useStartNode"
3 | import { AddButton } from "./AddButton"
4 | import { useTranslate } from "../react-locales"
5 | import { RightOutlined } from "@ant-design/icons"
6 | import { ChildNode } from "./ChildNode"
7 | import { NodeWrap, NodeWrapBox, NodeContent } from "./NormalNode"
8 | import { EndNode } from "./EndNode"
9 | import { useEditorEngine } from "../hooks"
10 | import { NodeTitleShell } from "./NodeTitle"
11 | import { useNodeMaterial } from "../hooks/useNodeMaterial"
12 | import { useMaterialUI } from "../hooks/useMaterialUI"
13 | import { ErrorTip } from "./ErrorTip"
14 |
15 | export const StartNode = memo(() => {
16 | const startNode = useStartNode()
17 | const t = useTranslate()
18 | const materialUi = useMaterialUI(startNode)
19 | const store = useEditorEngine();
20 | const material = useNodeMaterial(startNode)
21 | const handleClick = useCallback(() => {
22 | store?.selectNode(startNode?.id)
23 | }, [startNode?.id, store])
24 |
25 | return (
26 |
27 |
28 |
29 | {t(material?.label || "")}
30 |
31 |
32 | {materialUi?.viewContent && materialUi?.viewContent(startNode, { t })}
33 |
34 |
35 | {startNode?.id && }
36 |
37 | {startNode?.id && }
38 | {startNode?.childNode && }
39 |
40 |
41 | )
42 | })
--------------------------------------------------------------------------------
/src/workflow-editor/react-locales/contexts.ts:
--------------------------------------------------------------------------------
1 | import { createContext } from "react"
2 | import {ILocalesManager} from "@rxdrag/locales"
3 |
4 | export const LocalesContext = createContext(undefined)
--------------------------------------------------------------------------------
/src/workflow-editor/react-locales/hooks/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./useLocalesManager"
2 | export * from "./useTranslate"
--------------------------------------------------------------------------------
/src/workflow-editor/react-locales/hooks/useLocalesManager.ts:
--------------------------------------------------------------------------------
1 | import { useContext } from "react";
2 | import { LocalesContext } from "../contexts";
3 |
4 | export function useLocalesManager(){
5 | return useContext(LocalesContext)
6 | }
--------------------------------------------------------------------------------
/src/workflow-editor/react-locales/hooks/useTranslate.ts:
--------------------------------------------------------------------------------
1 | import { useCallback } from "react";
2 | import { useLocalesManager } from "./useLocalesManager";
3 |
4 | export function useTranslate(module?: string) {
5 | const localesManager = useLocalesManager()
6 | const t = useCallback((key: string) => {
7 | const keyPath = module ? module + "." + key : key
8 | return localesManager?.getMessage(keyPath) || key
9 | }, [localesManager, module])
10 |
11 | return t
12 | }
--------------------------------------------------------------------------------
/src/workflow-editor/react-locales/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./hooks"
2 | export * from "./contexts"
3 |
--------------------------------------------------------------------------------
/src/workflow-editor/reducers/changeFlagReducer.ts:
--------------------------------------------------------------------------------
1 | import { Action, ActionType, SetChangeFlagAction } from "../actions"
2 |
3 | export function changeFlagReducer(state: boolean, action: Action): boolean {
4 | switch (action.type) {
5 | case ActionType.SET_CHANGE_FLAG: {
6 | return (action as SetChangeFlagAction).payload?.changeFlag
7 | }
8 | case ActionType.SET_START_NODE: {
9 | return false
10 | }
11 | case ActionType.SET_REDOLIST:
12 | case ActionType.SET_UNOLIST:
13 | return true
14 | }
15 | return state
16 | }
--------------------------------------------------------------------------------
/src/workflow-editor/reducers/conditionNodeListReducer.ts:
--------------------------------------------------------------------------------
1 | import { Action } from "../actions";
2 | import { IWorkFlowNode } from "../interfaces";
3 |
4 | export function conditionNodeListReducer(state: IWorkFlowNode[], action: Action) {
5 | return state
6 | }
--------------------------------------------------------------------------------
/src/workflow-editor/reducers/errorsReducer.ts:
--------------------------------------------------------------------------------
1 | import { Action, ActionType, SetErrorsAction } from "../actions";
2 | import { IErrors } from "../interfaces/state";
3 |
4 | export function errorsReducer(state: IErrors, action: Action): IErrors {
5 | switch (action.type) {
6 | case ActionType.SET_ERRORS: {
7 | return (action as SetErrorsAction).payload?.errors
8 | }
9 | }
10 | return state
11 | }
--------------------------------------------------------------------------------
/src/workflow-editor/reducers/index.ts:
--------------------------------------------------------------------------------
1 | import { Action } from "../actions";
2 | import { IState, initialState } from "../interfaces/state";
3 | import { changeFlagReducer } from "./changeFlagReducer";
4 | import { errorsReducer } from "./errorsReducer";
5 | import { redoListReducer } from "./redoListReducer";
6 | import { selectedIdReducer } from "./selectedIdReducer";
7 | import { startNodeReducer } from "./startNodeReducer";
8 | import { undoListReducer } from "./undoListReducer";
9 | import { validatedReducer } from "./validatedReducer";
10 |
11 | export const mainReducer = (
12 | { changeFlag, redoList, undoList, startNode, selectedId, validated, errors }: IState = initialState,
13 | action: Action
14 | ): IState => ({
15 | changeFlag: changeFlagReducer(changeFlag, action),
16 | redoList: redoListReducer(redoList, action),
17 | undoList: undoListReducer(undoList, action),
18 | startNode: startNodeReducer(startNode, action),
19 | selectedId: selectedIdReducer(selectedId, action),
20 | validated: validatedReducer(validated, action),
21 | errors: errorsReducer(errors, action),
22 | });
23 |
--------------------------------------------------------------------------------
/src/workflow-editor/reducers/nodeReducer.ts:
--------------------------------------------------------------------------------
1 | import { Action, ActionType, AddNodeAction, ChangeNodeAction, DeleteNodeAction } from "../actions";
2 | import { IRouteNode, IWorkFlowNode, NodeType } from "../interfaces";
3 |
4 | export function nodeReducer(state: IWorkFlowNode, action: Action): IWorkFlowNode {
5 | const deleteNodeAction = action as DeleteNodeAction
6 | const addNodeAction = action as AddNodeAction
7 | const changeNodeAction = action as ChangeNodeAction
8 |
9 | switch (action.type) {
10 | case ActionType.DELETE_NODE: {
11 | const idToDelete = deleteNodeAction.payload.id
12 | //子节点被删除(不是分支)
13 | if (idToDelete === state.childNode?.id) {
14 | return { ...state, childNode: state.childNode.childNode }
15 | }
16 | return recursive(state, action)
17 | }
18 | case ActionType.ADD_NODE: {
19 | if (state.id === addNodeAction.payload.parentId) {
20 | return { ...state, childNode: { ...addNodeAction.payload.node, childNode: state.childNode } }
21 | }
22 |
23 | return recursive(state, action)
24 | }
25 | case ActionType.CHANGE_NODE: {
26 | if (state.id === changeNodeAction.payload.node.id) {
27 | return changeNodeAction.payload.node
28 | }
29 | return recursive(state, action)
30 | }
31 | }
32 | return state
33 | }
34 |
35 | function recursive(state: IWorkFlowNode, action: Action) {
36 | let childNode = state.childNode
37 | let newState = state
38 | if (state.childNode) {
39 | childNode = nodeReducer(state.childNode, action)
40 | }
41 | //如果childNode有变化
42 | if (childNode !== state.childNode) {
43 | newState = { ...state, childNode }
44 | }
45 |
46 | //所有condition list可能会全部刷新,体量不大,暂时不需要处理
47 | if (newState.nodeType === NodeType.route) {
48 | if (newState === state) {
49 | newState = { ...state }
50 | }
51 | const routeNode = newState as IRouteNode
52 | routeNode.conditionNodeList = routeNode.conditionNodeList.map(con => nodeReducer(con, action))
53 | }
54 | return newState
55 | }
--------------------------------------------------------------------------------
/src/workflow-editor/reducers/redoListReducer.ts:
--------------------------------------------------------------------------------
1 | import { Action, ActionType, UnRedoListAction } from "../actions";
2 | import { ISnapshot } from "../interfaces/state";
3 |
4 | export function redoListReducer(state: ISnapshot[], action: Action): ISnapshot[] {
5 | switch (action.type) {
6 | case ActionType.SET_REDOLIST: {
7 | return (action as UnRedoListAction).payload.list
8 | }
9 | }
10 | return state
11 | }
--------------------------------------------------------------------------------
/src/workflow-editor/reducers/selectedIdReducer.ts:
--------------------------------------------------------------------------------
1 | import { Action, ActionType, SelectNodeAction } from "../actions"
2 |
3 | export function selectedIdReducer(state: string | undefined, action: Action): string | undefined {
4 | switch (action.type) {
5 | case ActionType.SELECT_NODE: {
6 | return (action as SelectNodeAction).payload?.id
7 | }
8 | }
9 | return state
10 | }
--------------------------------------------------------------------------------
/src/workflow-editor/reducers/startNodeReducer.ts:
--------------------------------------------------------------------------------
1 | import { Action, ActionType, SetStartNodeAction } from "../actions";
2 | import { IWorkFlowNode } from "../interfaces";
3 | import { nodeReducer } from "./nodeReducer";
4 |
5 | export function startNodeReducer(state: IWorkFlowNode, action: Action): IWorkFlowNode {
6 | switch (action.type) {
7 | case ActionType.SET_START_NODE: {
8 | return (action as SetStartNodeAction).payload.node
9 | }
10 | case ActionType.DELETE_NODE:
11 | case ActionType.ADD_NODE:
12 | case ActionType.CHANGE_NODE: {
13 | return nodeReducer(state, action)
14 | }
15 | }
16 | return state
17 | }
--------------------------------------------------------------------------------
/src/workflow-editor/reducers/undoListReducer.ts:
--------------------------------------------------------------------------------
1 | import { Action, ActionType, UnRedoListAction } from "../actions";
2 | import { ISnapshot } from "../interfaces/state";
3 |
4 | export function undoListReducer(state: ISnapshot[], action: Action): ISnapshot[] {
5 | switch (action.type) {
6 | case ActionType.SET_UNOLIST: {
7 | return (action as UnRedoListAction).payload.list
8 | }
9 | }
10 | return state
11 | }
--------------------------------------------------------------------------------
/src/workflow-editor/reducers/validatedReducer.ts:
--------------------------------------------------------------------------------
1 | import { Action, ActionType, SetValidatedAction } from "../actions"
2 |
3 | export function validatedReducer(state: boolean | undefined, action: Action): boolean | undefined {
4 | switch (action.type) {
5 | case ActionType.SET_VALIDATED: {
6 | return (action as SetValidatedAction).payload?.validated
7 | }
8 | }
9 | return state
10 | }
--------------------------------------------------------------------------------
/src/workflow-editor/styled.d.ts:
--------------------------------------------------------------------------------
1 | // import original module declarations
2 | import 'styled-components';
3 | import { IDefaultTheme } from './theme';
4 |
5 |
6 | // and extend them!
7 | declare module 'styled-components' {
8 | export interface DefaultTheme extends IDefaultTheme {
9 | }
10 | }
--------------------------------------------------------------------------------
/src/workflow-editor/theme.ts:
--------------------------------------------------------------------------------
1 | export interface IThemeToken {
2 | colorBorder?: string;
3 | colorBorderSecondary?: string;
4 | colorBgContainer?: string;
5 | colorText?: string;
6 | colorTextSecondary?: string;
7 | colorBgBase?: string;
8 | colorPrimary?: string;
9 | colorError?: string;
10 | colorSuccess?: string;
11 | }
12 |
13 | //styled-components 的typescript使用
14 | export interface IDefaultTheme{
15 | token?: IThemeToken
16 | mode?: 'dark' | 'light'
17 | }
--------------------------------------------------------------------------------
/src/workflow-editor/utils/canvasColor.ts:
--------------------------------------------------------------------------------
1 | import { IDefaultTheme } from "../theme";
2 |
3 | export const canvasColor = (props: { theme: IDefaultTheme }) => props.theme.mode === "light" ? "#f5f5f7" : props.theme.token?.colorBgBase
--------------------------------------------------------------------------------
/src/workflow-editor/utils/create-uuid.ts:
--------------------------------------------------------------------------------
1 | import { v4 as uuidv4 } from 'uuid';
2 |
3 | export const createUuid = () => {
4 | return uuidv4();
5 | };
6 |
--------------------------------------------------------------------------------
/src/workflow-editor/utils/getFIles.ts:
--------------------------------------------------------------------------------
1 | export async function getTheFiles(accept: string, multiple?: boolean) {
2 | // open file picker
3 | const fileHandles = await (window as any).showOpenFilePicker({
4 | types: [{
5 | accept: {
6 | "file/*": accept?.split(",")
7 | },
8 | }],
9 | excludeAcceptAllOption: false,
10 | multiple: multiple,
11 | });
12 |
13 | return fileHandles;
14 | }
15 |
--------------------------------------------------------------------------------
/src/workflow-editor/utils/lineColor.ts:
--------------------------------------------------------------------------------
1 | import { IDefaultTheme } from "../theme";
2 |
3 | export const lineColor = (props: { theme: IDefaultTheme }) => props.theme?.mode === "light" ? "#cacaca" : "rgba(255,255,255,0.35)"
--------------------------------------------------------------------------------
/src/workflow-editor/utils/nodeColor.ts:
--------------------------------------------------------------------------------
1 | import { IDefaultTheme } from "../theme";
2 |
3 | export const nodeColor = (props: { theme: IDefaultTheme }) => props.theme.token?.colorBgContainer
--------------------------------------------------------------------------------
/src/workflow-editor/utils/saveFile.tsx:
--------------------------------------------------------------------------------
1 | export const pickerTypes = [
2 | {
3 | accept: {
4 | "text/json": [".json"],
5 | },
6 | },
7 | ];
8 |
9 |
10 | export async function saveFile(name: string, content: string) {
11 | //const handle = getHandle();
12 | // create a new handle
13 | try{
14 | const newHandle = await (window as any).showSaveFilePicker({
15 | suggestedName: name + ".json",
16 | types: pickerTypes,
17 | });
18 |
19 | // create a FileSystemWritableFileStream to write to
20 | const writableStream = await newHandle.createWritable();
21 |
22 | // write our file
23 | await writableStream.write(content);
24 |
25 | // close the file and write the contents to disk.
26 | await writableStream.close();
27 |
28 | //setHandle(newHandle);
29 |
30 | return newHandle.name;
31 | }
32 | catch(error){
33 | console.error(error);
34 | return false;
35 | }
36 |
37 | }
38 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "noFallthroughCasesInSwitch": true,
16 | "module": "esnext",
17 | "moduleResolution": "node",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "noEmit": true,
21 | "jsx": "react-jsx"
22 | },
23 | "include": [
24 | "src"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------