├── .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 | 67 | 68 | 69 | 70 | 71 | 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 |
24 | 25 | 26 | {t("manualApproval")} 27 | {t("autoPass")} 28 | {t("autoReject")} 29 | 30 | 31 | {t("formAuth")} 40 | }, 41 | { 42 | key: "addvancedSettings", 43 | label: t("addvancedSettings") 44 | } 45 | ]} 46 | value={settingsType} 47 | onChange={setSettingsType} 48 | /> 49 | {settingsType === 'formAuth' && } 50 | 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 |
23 | {t("formAuth")} 32 | } 33 | ]} 34 | value={settingsType} 35 | onChange={setSettingsType} 36 | /> 37 | {settingsType === 'formAuth' && } 38 | 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 |
35 | 42 | 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 |
23 | {t("formAuth")} 32 | } 33 | ]} 34 | value={settingsType} 35 | onChange={setSettingsType} 36 | /> 37 | {settingsType === 'formAuth' && } 38 | 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 |
23 | {t("formAuth")} 32 | } 33 | ]} 34 | value={settingsType} 35 | onChange={setSettingsType} 36 | /> 37 | {settingsType === "node" && <> 38 | 39 | 40 | 41 | } 42 | {settingsType === 'formAuth' && } 43 | 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 | 115 | 121 | 122 | 123 | 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 | 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 | ? <TitleText className="title-text">{t("promoter")}</TitleText> 30 | : <NodeTitleEditor value={node.name} onChange={onNameChange} /> 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 | 120 | 121 | 122 | 123 | 124 | 125 | 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 | 3 | 4 | 5 | 6 | export const undoIcon = 7 | 8 | 9 | 10 | 11 | export const copyIcon = 12 | 13 | 14 | 15 | 16 | 17 | export const sealIcon = 18 | 19 | 20 | 21 | 22 | 23 | export const notifierIcon = 24 | 25 | 26 | 27 | 28 | 29 | export const dealIcon = 30 | 31 | 32 | 33 | 34 | 35 | export const routeIcon = 36 | 37 | 38 | 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 |
99 | 100 |
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 |