├── .all-contributorsrc ├── .circleci └── config.yml ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ └── checker.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── CONTRIBUTING.md ├── LICENSE ├── README.en-US.md ├── README.md ├── __tests__ └── core │ └── src │ ├── api │ └── index.test.js │ ├── common │ ├── baseCell.test.js │ ├── const.test.js │ ├── previewCell.test.js │ └── shortcuts.test.js │ ├── components │ ├── colorPicker.test.js │ ├── miniMapSimpleNode.test.js │ └── xIcon.test.js │ ├── mods │ ├── flowChart.test.js │ ├── header.test.jsx │ ├── layout.test.js │ ├── settingBar.test.js │ ├── sideBar.test.js │ └── toolBar.test.js │ └── utils │ ├── flowChartUtils.test.js │ └── index.test.js ├── config ├── cli.jest.config.js ├── core.jest.config.js ├── editor.jest.config.js └── plugin.jest.config.js ├── cypress.json ├── cypress ├── fixtures │ └── example.json ├── integration │ └── examples │ │ ├── configuration.test.js │ │ ├── element.test.js │ │ └── tools.test.js ├── plugins │ └── index.js └── support │ ├── commands.js │ └── index.js ├── docs ├── api │ ├── CONTRIBUTING.md │ ├── ctx.md │ ├── data.md │ ├── edge.md │ ├── group.md │ ├── layout.md │ ├── module.md │ ├── node.md │ └── schema.md ├── config │ └── index.md ├── docs │ └── index.md ├── guide │ └── index.md └── plugins │ └── index.md ├── example ├── .env ├── .umirc.ts ├── mock │ ├── .gitkeep │ ├── api.ts │ └── db.json ├── package.json ├── src │ └── pages │ │ ├── index.less │ │ └── index.tsx ├── tsconfig.json └── typings.d.ts ├── lerna.json ├── mlc_config.json ├── package.json ├── packages ├── cli │ ├── .npmignore │ ├── README.md │ ├── index.js │ ├── package.json │ └── src │ │ ├── cmd │ │ ├── base │ │ │ └── index.js │ │ ├── dev │ │ │ ├── index.js │ │ │ └── mergePkg.js │ │ ├── editor │ │ │ ├── build.js │ │ │ ├── index.js │ │ │ └── template │ │ │ │ ├── app.jsx │ │ │ │ └── index.html │ │ ├── index.js │ │ └── init │ │ │ ├── index.js │ │ │ └── template │ │ │ └── imove.config.js.tpl │ │ └── utils │ │ ├── getConfig.js │ │ └── server │ │ └── index.js ├── compile-code │ ├── .npmignore │ ├── README.md │ ├── package.json │ ├── rollup.config.js │ ├── scripts │ │ └── prepublish.js │ ├── src │ │ ├── addPlugins.ts │ │ ├── compileForOnline.ts │ │ ├── compileForProject.ts │ │ ├── extractNodeFns.ts │ │ ├── index.ts │ │ ├── simplifyDSL.ts │ │ └── template │ │ │ ├── context.ts │ │ │ ├── index.ts │ │ │ ├── logic.ts │ │ │ └── runOnline.ts │ └── tsconfig.json ├── core │ ├── .npmignore │ ├── .npmrc │ ├── README.md │ ├── package.json │ ├── rollup.config.js │ ├── scripts │ │ └── prepublish.js │ ├── src │ │ ├── api │ │ │ └── index.ts │ │ ├── common │ │ │ ├── baseCell │ │ │ │ ├── behavior.ts │ │ │ │ ├── branch.ts │ │ │ │ ├── index.ts │ │ │ │ └── start.ts │ │ │ ├── const.ts │ │ │ ├── previewCell │ │ │ │ ├── behavior.tsx │ │ │ │ ├── branch.tsx │ │ │ │ ├── index.module.less │ │ │ │ ├── index.ts │ │ │ │ └── start.tsx │ │ │ └── shortcuts.ts │ │ ├── components │ │ │ ├── codeEditor │ │ │ │ ├── index.tsx │ │ │ │ └── theme-monokai.ts │ │ │ ├── codeRun │ │ │ │ ├── index.module.less │ │ │ │ ├── index.tsx │ │ │ │ └── inputPanel │ │ │ │ │ ├── index.module.less │ │ │ │ │ └── index.tsx │ │ │ ├── colorPicker │ │ │ │ └── index.tsx │ │ │ ├── console │ │ │ │ ├── index.module.less │ │ │ │ └── index.tsx │ │ │ ├── miniMapSimpleNode │ │ │ │ └── index.tsx │ │ │ ├── schemaForm │ │ │ │ ├── index.module.less │ │ │ │ └── index.tsx │ │ │ └── xIcon │ │ │ │ ├── index.less │ │ │ │ └── index.tsx │ │ ├── global.index.less │ │ ├── hooks │ │ │ └── useClickAway.ts │ │ ├── index.tsx │ │ ├── mods │ │ │ ├── flowChart │ │ │ │ ├── codeEditorModal │ │ │ │ │ ├── index.module.less │ │ │ │ │ └── index.tsx │ │ │ │ ├── codeRunModal │ │ │ │ │ ├── index.module.less │ │ │ │ │ └── index.tsx │ │ │ │ ├── contextMenu │ │ │ │ │ ├── index.tsx │ │ │ │ │ └── menuConfig │ │ │ │ │ │ ├── blank.tsx │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── node.tsx │ │ │ │ ├── createFlowChart.ts │ │ │ │ ├── index.module.less │ │ │ │ ├── index.tsx │ │ │ │ └── registerServerStorage.ts │ │ │ ├── header │ │ │ │ ├── configuration │ │ │ │ │ ├── editModal.tsx │ │ │ │ │ ├── index.module.less │ │ │ │ │ └── index.tsx │ │ │ │ ├── connectStatus │ │ │ │ │ ├── index.module.less │ │ │ │ │ └── index.tsx │ │ │ │ ├── export │ │ │ │ │ ├── exportModal.tsx │ │ │ │ │ ├── index.module.less │ │ │ │ │ └── index.tsx │ │ │ │ ├── guide │ │ │ │ │ ├── guideModal.tsx │ │ │ │ │ ├── index.module.less │ │ │ │ │ └── index.tsx │ │ │ │ ├── importDSL │ │ │ │ │ ├── index.module.less │ │ │ │ │ └── index.tsx │ │ │ │ ├── index.module.less │ │ │ │ └── index.tsx │ │ │ ├── layout │ │ │ │ ├── index.module.less │ │ │ │ └── index.tsx │ │ │ ├── settingBar │ │ │ │ ├── components │ │ │ │ │ ├── checkbox │ │ │ │ │ │ ├── index.module.less │ │ │ │ │ │ └── index.tsx │ │ │ │ │ ├── input │ │ │ │ │ │ ├── index.module.less │ │ │ │ │ │ └── index.tsx │ │ │ │ │ └── json │ │ │ │ │ │ ├── index.less │ │ │ │ │ │ ├── index.module.less │ │ │ │ │ │ ├── index.tsx │ │ │ │ │ │ └── json.ts │ │ │ │ ├── index.module.less │ │ │ │ ├── index.tsx │ │ │ │ └── mods │ │ │ │ │ ├── basic │ │ │ │ │ ├── index.module.less │ │ │ │ │ └── index.tsx │ │ │ │ │ └── testCase │ │ │ │ │ └── index.tsx │ │ │ ├── sideBar │ │ │ │ ├── index.module.less │ │ │ │ └── index.tsx │ │ │ └── toolBar │ │ │ │ ├── index.module.less │ │ │ │ ├── index.tsx │ │ │ │ └── widgets │ │ │ │ ├── bgColor.tsx │ │ │ │ ├── bold.tsx │ │ │ │ ├── borderColor.tsx │ │ │ │ ├── bringToBack.tsx │ │ │ │ ├── bringToTop.tsx │ │ │ │ ├── common │ │ │ │ ├── makeBtnWidget.tsx │ │ │ │ └── makeDropdownWidget.tsx │ │ │ │ ├── fitWindow.tsx │ │ │ │ ├── fontSize.tsx │ │ │ │ ├── horizontalAlign.tsx │ │ │ │ ├── index.module.less │ │ │ │ ├── index.tsx │ │ │ │ ├── italic.tsx │ │ │ │ ├── lineStyle.tsx │ │ │ │ ├── modifyStatus.tsx │ │ │ │ ├── nodeAlign.tsx │ │ │ │ ├── redo.tsx │ │ │ │ ├── save.tsx │ │ │ │ ├── textColor.tsx │ │ │ │ ├── underline.tsx │ │ │ │ ├── undo.tsx │ │ │ │ ├── verticalAlign.tsx │ │ │ │ └── zoom.tsx │ │ ├── typings.d.ts │ │ └── utils │ │ │ ├── analyzeDeps.ts │ │ │ ├── flowChartUtils.ts │ │ │ └── index.ts │ └── tsconfig.json └── plugin-store │ ├── .npmignore │ ├── README.md │ ├── package.json │ ├── rollup.config.js │ ├── scripts │ └── prepublish.js │ ├── src │ └── index.ts │ └── tsconfig.json ├── rollup.config.js ├── sandbox.config.json ├── scripts └── prepublish.js └── tsconfig.json /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "iMove", 3 | "projectOwner": "i5ting", 4 | "repoType": "github", 5 | "repoHost": "https://github.com", 6 | "files": [ 7 | "README.md" 8 | ], 9 | "imageSize": 100, 10 | "commit": true, 11 | "commitConvention": "angular", 12 | "contributors": [], 13 | "contributorsPerLine": 7 14 | } 15 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: circleci/node:latest 6 | 7 | working_directory: ~/repo 8 | 9 | steps: 10 | - checkout 11 | 12 | # 恢复缓存 13 | - restore_cache: 14 | key: node-modules-{{ checksum "package.json" }} 15 | # 安装依赖 16 | - run: 17 | name: Install Dependencies 18 | command: yarn --frozen-lockfile 19 | # 格式化代码 20 | - run: 21 | name: Format Code 22 | command: yarn lint 23 | # 写缓存 24 | - save_cache: 25 | key: node-modules-{{ checksum "package-lock.json" }} 26 | paths: 27 | - ./node_modules 28 | - ~/.cache/yarn 29 | # 代码打包构建 30 | # - run: 31 | # name: Run Build 32 | # command: yarn build 33 | # 跑单元测试代码 34 | - run: 35 | name: Run Tests 36 | command: yarn test 37 | no_output_timeout: 300m 38 | # 跑端对端测试代码 39 | - run: 40 | name: Run E2E Tests 41 | command: yarn e2e 42 | no_output_timeout: 300m 43 | # 集成文档 44 | - run: 45 | name: Generate Doc 46 | command: yarn doc 47 | no_output_timeout: 300m -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [Makefile] 16 | indent_style = tab 17 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .circleci 2 | .github 3 | .husky 4 | 5 | dist/ 6 | node_modules/ 7 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | "prettier/@typescript-eslint", 4 | "plugin:react/recommended", 5 | "plugin:@typescript-eslint/recommended", 6 | "plugin:prettier/recommended" 7 | ], 8 | env: { 9 | node: true, 10 | browser: true, 11 | }, 12 | settings: { 13 | react: { 14 | version: "detect" 15 | } 16 | }, 17 | rules: { 18 | 'prettier/prettier': 'error', 19 | 'react/jsx-filename-extension': ['warn', { extensions: ['.ts', '.tsx'] }], // 修复 tsx 文件引用 tsx 文件报错的问题 20 | "no-var": 0,//禁用var,用let和const代替 21 | "no-use-before-define": 2,//未定义前不能使用 22 | "no-unused-expressions": 2,//禁止无用的表达式 23 | "space-before-function-paren": 'off', 24 | 'comma-dangle': 'off', 25 | 'import/extensions': 'off', 26 | }, 27 | overrides: [{ 28 | files: ['*.ts', '*.tsx'], 29 | parser: '@typescript-eslint/parser', 30 | plugins: ['@typescript-eslint'], 31 | extends: [ 32 | 'plugin:@typescript-eslint/eslint-recommended', 33 | 'plugin:@typescript-eslint/recommended', 34 | 'prettier/@typescript-eslint', 35 | ], 36 | rules: { 37 | '@typescript-eslint/no-explicit-any': 'off', 38 | 'no-param-reassign': 'off', 39 | '@typescript-eslint/interface-name-prefix': 'off', 40 | '@typescript-eslint/no-use-before-define': 'off', 41 | '@typescript-eslint/no-unused-vars': 0, 42 | '@typescript-eslint/explicit-function-return-type': 0, 43 | }, 44 | },], 45 | }; 46 | -------------------------------------------------------------------------------- /.github/workflows/checker.yml: -------------------------------------------------------------------------------- 1 | name: 🐱 Checker 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | # 确保 PR 中超链接有效性 10 | markdown-link-checker: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: gaurav-nelson/github-action-markdown-link-check@v1 15 | with: 16 | use-quiet-mode: 'yes' 17 | use-verbose-mode: 'yes' 18 | check-modified-files-only: 'yes' 19 | 20 | # 检查 PR 中英文单词拼写正确性 21 | spell-checker: 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v2 25 | - name: Install mispell 26 | run: | 27 | wget -O - -q https://git.io/misspell | sh -s -- -b . 28 | - name: Check 29 | run: | 30 | find -E . -regex ".*\.(ts|js|json)" -type f | xargs ./misspell -error 31 | find docs -type f | xargs ./misspell -error 32 | find . -type f -maxdepth 1 | xargs ./misspell -error 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .vscode 3 | .eslintcache 4 | .size-snapshot.json 5 | .husky 6 | npm-debug.log* 7 | yarn-error.log* 8 | lerna-debug.log 9 | node_modules 10 | coverage 11 | lib 12 | es 13 | # production 14 | dist 15 | 16 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 17 | 18 | # dependencies 19 | yarn.lock 20 | package-lock.json 21 | 22 | # misc 23 | .DS_Store 24 | 25 | # umi 26 | example/node_modules 27 | example/src/.umi 28 | example/src/.umi-production 29 | example/src/.umi-test 30 | example/.env.local 31 | 32 | # eslint 33 | .eslint-error.log 34 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .cache 2 | package.json 3 | package-lock.json 4 | public 5 | 6 | **/*.md 7 | **/*.svg 8 | **/*.ejs 9 | **/*.html 10 | 11 | example/.umi 12 | example/.umi-production 13 | example/.umi-test 14 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "endOfLine": "lf", 3 | "semi": true, 4 | "tabWidth": 2, 5 | "singleQuote": true, 6 | "trailingComma": "all", 7 | "printWidth": 80, 8 | "overrides": [ 9 | { 10 | "files": ".prettierrc", 11 | "options": { "parser": "json" } 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 拾邑 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.en-US.md: -------------------------------------------------------------------------------- 1 | # iMove 2 | 3 | 4 | 5 | [![All Contributors](https://img.shields.io/badge/all_contributors-0-orange.svg?style=flat-square)](#contributors-) 6 | 7 | 8 | 9 |
10 | 11 |
12 | 13 |

14 | iMove is a logic-reusable, function-oriented and process-visualized JavaScript tool library. 15 |

16 | 17 | [English](./README.en-US.md) | 简体中文 18 | 19 | ## Features 20 | 21 | - [x] **Process visualization:** iMove is easy to use and easy to draw. Its logical expression is more intuitive and easy to understand. 22 | - [x] **Logic re-usage:** iMove node supports multiplexing, and its single node supports parameter configuration. 23 | - [x] **Flexible**: We need to write an only function. The node can also be extended. iMove can also support plug-in integration. 24 | - [ ] **Multi-language compilation**: There is no language compiling code limitation (example: support JavaScript, Java compiling code). 25 | 26 | ## Quickstart 27 | 28 | ### Step1. Run 29 | 30 | Download this project, install dependencies and start the project. 31 | 32 | ```bash 33 | $ git clone https://github.com/ykfe/imove.git 34 | $ cd imove/example 35 | $ npm install 36 | $ npm start 37 | ``` 38 | 39 | Open http://localhost:8000/ and you can see the online effect. 40 | 41 | 42 | ### Step2. Draw flowchart 43 | 44 | Drag nodes from the left panel and drop them into the center, then we can get a flowchart. 45 | 46 | ![flowchart](https://img.alicdn.com/tfs/TB1aoYe4pP7gK0jSZFjXXc5aXXa-3090-1806.jpg) 47 | 48 | ### Step3. Configure nodes 49 | 50 | Select the node, modify its display name and complete the code. 51 | 52 | ![flowchart-usage1](https://img.alicdn.com/tfs/TB1z6DKoZieb18jSZFvXXaI3FXa-1924-1125.png) 53 | 54 | ![flowchart-usage2](https://img.alicdn.com/tfs/TB1lC26tTM11u4jSZPxXXahcXXa-1924-1125.png) 55 | 56 | ## Contributing 57 | 58 | 1. Fork this repository 59 | 2. Create a new branch (`git checkout -b my-new-feature`) 60 | 3. Commit your changes (`git commit -am 'Add some feature'`) 61 | 4. Push (`git push origin my-new-feature`) 62 | 5. File a PR 63 | 64 | ## Welcome to fork and feedback 65 | 66 | If you have any suggestion, welcome to GitHub to raise [issues](https://github.com/imgcook/imove/issues). 67 | 68 | ## License 69 | 70 | This project follows the [MIT](http://www.opensource.org/licenses/MIT) license. 71 | 72 | ## Contributors ✨ 73 | 74 | Thanks goes to these excellent ([contributors](https://allcontributors.org/docs/en/emoji-key)): 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind are welcome! 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | [![gitter][gitter-image]][gitter-url] 5 | [![NPM version][npm-image]][npm-url] 6 | [![build status][travis-image]][travis-url] 7 | [![Test coverage][coveralls-image]][coveralls-url] 8 | [![PR's Welcome][pr-welcoming-image]][pr-welcoming-url] 9 | 10 |
11 | 12 |

13 | INACTIVE: iMove 是一个逻辑可复用的,面向函数的,流程可视化的 JavaScript 工具库。 14 |

15 | 16 | [English](./README.en-US.md) | 简体中文 17 | 18 | iMove是一个面向前端开发者的逻辑编排工具,核心解决的是复杂逻辑复用的问题。 19 | 20 | iMove由2部分组成:画布和imove-sdk。通过本地起一个http服务运行画布,在画布上完成代码编写和节点编排,最终将流程导出dsl,放到项目中,通过imove-sdk调用执行。 21 | 22 | ## 特性 23 | 24 | - [x] **流程可视化**:上手简单,绘图方便,逻辑表达更直观,易于理解 25 | - [x] **逻辑复用**:iMove 节点支持复用,单节点支持参数配置 26 | - [x] **灵活可扩展**:仅需写一个函数,节点可扩展,支持插件集成 27 | - [x] **适用于JavaScript所有场景**:比如前端点击事件,Ajax 请求和 Node.js 后端 API等 28 | - [ ] **多语言编译**:无语言编译出码限制(例:支持 JavaScript, Java 编译出码) 29 | 30 | ## 使用场景 31 | 32 | ![usage](https://img.alicdn.com/imgextra/i1/O1CN01kRXnfQ1LFhesOA6cn_!!6000000001270-2-tps-2212-1166.png) 33 | 34 | 1. 前端流程:比如点击事件,组件生命周期回调等。 35 | 1. 后端流程:比如 Node.js 或 Serverless 领域。 36 | 1. 前端+后端:比如前端点击事件,Ajax 请求和后端 API。 37 | 38 | ## 快速开始 39 | 40 | ### 步骤 1. 准备 41 | 42 | 下载仓库,安装并启动 43 | 44 | ```bash 45 | $ git clone https://github.com/ykfe/imove.git 46 | $ cd imove/example 47 | $ npm install 48 | $ npm start 49 | ``` 50 | 51 | 52 | 此时浏览器会自动打开 `http://localhost:8000/` ,可以看到运行效果。 53 | 54 | 55 | ### 步骤 2. 绘制流程图 56 | 57 | 从左侧拖动节点至中央画布,绘制流程图 58 | 59 | ![flowchart](https://img.alicdn.com/tfs/TB1aoYe4pP7gK0jSZFjXXc5aXXa-3090-1806.jpg) 60 | 61 | ### 步骤 3. 配置节点 62 | 63 | 选择节点,修改节点名,编辑节点代码 64 | 65 | ![flowchart-usage1](https://img.alicdn.com/tfs/TB1z6DKoZieb18jSZFvXXaI3FXa-1924-1125.png) 66 | 67 | ![flowchart-usage2](https://img.alicdn.com/tfs/TB1lC26tTM11u4jSZPxXXahcXXa-1924-1125.png) 68 | 69 | ## Authors 70 | 71 | * qilei0529 - 飞羽「Leader」 72 | * SmallStoneSK - 菉竹「Core Team」 73 | * suanmei - 拾邑「Core Team」 74 | * iloveyou11 - 冷卉「Core Team」 75 | * i5ting - 狼叔「Core Team」 76 | 77 | 团队博客,各种原理,设计初衷,实现,思考等都在这里: https://www.yuque.com/imove/blog 78 | 79 | See also the list of [contributors](https://github.com/imgcook/imove/graphs/contributors) who participated in this project. 80 | 81 | ## 贡献 82 | 83 | 1. Fork 仓库 84 | 2. 创建分支 (`git checkout -b my-new-feature`) 85 | 3. 提交修改 (`git commit -am 'Add some feature'`) 86 | 4. 推送 (`git push origin my-new-feature`) 87 | 5. 创建 PR 88 | 89 | ## 欢迎 fork 和反馈 90 | 91 | 如有建议或意见,欢迎在 github [issues](https://github.com/imgcook/imove/issues) 区提问 92 | 93 | ## 协议 94 | 95 | 本仓库遵循 [MIT 协议](http://www.opensource.org/licenses/MIT) 96 | 97 | ## 贡献者 ✨ 98 | 99 | 感谢 [蚂蚁 X6 团队](https://github.com/antvis/X6) 提供的绘图引擎 100 | 101 | 感谢所有贡献的人 ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 本仓库遵循 [all-contributors](https://github.com/all-contributors/all-contributors) 规范,欢迎贡献! 111 | 112 | [npm-image]: https://img.shields.io/npm/v/@imove/core.svg?style=flat-square 113 | [npm-url]: https://www.npmjs.com/package/@imove/core 114 | [travis-image]: https://img.shields.io/travis/imgcook/imove/master.svg?style=flat-square 115 | [travis-url]: https://travis-ci.org/imgcook/imove/ 116 | [coveralls-image]: https://img.shields.io/codecov/c/github/imgcook/imove.svg?style=flat-square 117 | [coveralls-url]: https://codecov.io/github/imgcook/imove?branch=master 118 | [backers-image]: https://opencollective.com/imgcook/imove/backers/badge.svg?style=flat-square 119 | [sponsors-image]: https://opencollective.com/imgcook/imove/sponsors/badge.svg?style=flat-square 120 | [gitter-image]: https://img.shields.io/gitter/room/i5ting/imove.svg?style=flat-square 121 | [gitter-url]: https://gitter.im/i5ting/imove?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge 122 | [#imgcook/imove]: https://webchat.freenode.net/?channels=#imgcook/imove 123 | [pr-welcoming-image]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square 124 | [pr-welcoming-url]: https://github.com/imgcook/imove/pull/new 125 | 126 | ## 项目 Star 数增长趋势 127 | 128 | [![Stargazers over time](https://starchart.cc/ykfe/imove.svg)](https://starchart.cc/ykfe/imove) 129 | 130 | -------------------------------------------------------------------------------- /__tests__/core/src/api/index.test.js: -------------------------------------------------------------------------------- 1 | describe('test', () => { 2 | it('test suite', () => { 3 | 4 | }) 5 | }) -------------------------------------------------------------------------------- /__tests__/core/src/common/baseCell.test.js: -------------------------------------------------------------------------------- 1 | describe('test', () => { 2 | it('test suite', () => { 3 | 4 | }) 5 | }) -------------------------------------------------------------------------------- /__tests__/core/src/common/const.test.js: -------------------------------------------------------------------------------- 1 | describe('test', () => { 2 | it('test suite', () => { 3 | 4 | }) 5 | }) -------------------------------------------------------------------------------- /__tests__/core/src/common/previewCell.test.js: -------------------------------------------------------------------------------- 1 | describe('test', () => { 2 | it('test suite', () => { 3 | 4 | }) 5 | }) -------------------------------------------------------------------------------- /__tests__/core/src/common/shortcuts.test.js: -------------------------------------------------------------------------------- 1 | describe('test', () => { 2 | it('test suite', () => { 3 | 4 | }) 5 | }) -------------------------------------------------------------------------------- /__tests__/core/src/components/colorPicker.test.js: -------------------------------------------------------------------------------- 1 | describe('test', () => { 2 | it('test suite', () => { 3 | 4 | }) 5 | }) -------------------------------------------------------------------------------- /__tests__/core/src/components/miniMapSimpleNode.test.js: -------------------------------------------------------------------------------- 1 | describe('test', () => { 2 | it('test suite', () => { 3 | 4 | }) 5 | }) -------------------------------------------------------------------------------- /__tests__/core/src/components/xIcon.test.js: -------------------------------------------------------------------------------- 1 | describe('test', () => { 2 | it('test suite', () => { 3 | 4 | }) 5 | }) -------------------------------------------------------------------------------- /__tests__/core/src/mods/flowChart.test.js: -------------------------------------------------------------------------------- 1 | describe('test', () => { 2 | it('test suite', () => { 3 | 4 | }) 5 | }) -------------------------------------------------------------------------------- /__tests__/core/src/mods/header.test.jsx: -------------------------------------------------------------------------------- 1 | describe('test', () => { 2 | it('test suite', () => { 3 | 4 | }) 5 | }) -------------------------------------------------------------------------------- /__tests__/core/src/mods/layout.test.js: -------------------------------------------------------------------------------- 1 | describe('test', () => { 2 | it('test suite', () => { 3 | 4 | }) 5 | }) -------------------------------------------------------------------------------- /__tests__/core/src/mods/settingBar.test.js: -------------------------------------------------------------------------------- 1 | describe('test', () => { 2 | it('test suite', () => { 3 | 4 | }) 5 | }) -------------------------------------------------------------------------------- /__tests__/core/src/mods/sideBar.test.js: -------------------------------------------------------------------------------- 1 | describe('test', () => { 2 | it('test suite', () => { 3 | 4 | }) 5 | }) -------------------------------------------------------------------------------- /__tests__/core/src/mods/toolBar.test.js: -------------------------------------------------------------------------------- 1 | describe('test', () => { 2 | it('test suite', () => { 3 | 4 | }) 5 | }) -------------------------------------------------------------------------------- /__tests__/core/src/utils/flowChartUtils.test.js: -------------------------------------------------------------------------------- 1 | describe('test', () => { 2 | it('test suite', () => { 3 | 4 | }) 5 | }) -------------------------------------------------------------------------------- /__tests__/core/src/utils/index.test.js: -------------------------------------------------------------------------------- 1 | describe('test', () => { 2 | it('test suite', () => { 3 | 4 | }) 5 | }) -------------------------------------------------------------------------------- /config/cli.jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | clearMocks: true, 3 | coverageDirectory: "coverage", 4 | coverageProvider: "v8", 5 | testEnvironment: "node", 6 | rootDir: '../', 7 | moduleFileExtensions: ['js', 'json', 'jsx', 'node'], 8 | testMatch: [ 9 | "/__tests__/cli/src/**/*.(spec|test).[jt]s?(x)" 10 | ] 11 | }; -------------------------------------------------------------------------------- /config/core.jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | clearMocks: true, 3 | coverageDirectory: "coverage", 4 | coverageProvider: "v8", 5 | testEnvironment: "node", 6 | rootDir: '../', 7 | moduleFileExtensions: ['js', 'json', 'jsx', 'node'], 8 | testMatch: [ 9 | "/__tests__/core/src/**/*.(spec|test).[jt]s?(x)" 10 | ] 11 | }; -------------------------------------------------------------------------------- /config/editor.jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | clearMocks: true, 3 | coverageDirectory: "coverage", 4 | coverageProvider: "v8", 5 | testEnvironment: "node", 6 | rootDir: '../', 7 | moduleFileExtensions: ['js', 'json', 'jsx', 'node'], 8 | testMatch: [ 9 | "/__tests__/json-schema-editor/src/**/*.(spec|test).[jt]s?(x)" 10 | ] 11 | }; -------------------------------------------------------------------------------- /config/plugin.jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | clearMocks: true, 3 | coverageDirectory: "coverage", 4 | coverageProvider: "v8", 5 | testEnvironment: "node", 6 | rootDir: '../', 7 | moduleFileExtensions: ['js', 'json', 'jsx', 'node'], 8 | testMatch: [ 9 | "/__tests__/plugin-store/src/**/*.(spec|test).[jt]s?(x)" 10 | ] 11 | }; -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } -------------------------------------------------------------------------------- /cypress/integration/examples/configuration.test.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context('Actions', () => { 4 | beforeEach(() => { 5 | cy.visit('http://localhost:8000/') 6 | cy.get('div.ant-modal-confirm-btns > button:nth-child(1)').click({ force: true }) 7 | }) 8 | 9 | function moveNodeAndSelect() { 10 | cy.get('svg > g > g.x6-graph-svg-stage > g:nth-child(1)').eq(0) 11 | .trigger('mousedown', { which: 1, force: true }) 12 | .trigger('mousemove', { clientX: 400, clientY: 300, force: true }) 13 | .trigger('mouseup', { force: true }) 14 | cy.get('svg > g > g.x6-graph-svg-stage > g:nth-child(1)').eq(1).click({ force: true }) // 点击移动到画布中的节点 15 | } 16 | 17 | function assertEdit(nth, content) { 18 | moveNodeAndSelect() 19 | const ele = cy.get(`#rc-tabs-0-panel-basic > div > div:nth-child(${nth}) > button`).eq(0) 20 | ele.should('have.text', '编 辑') 21 | ele.click({ force: true }) 22 | cy.get('.ant-modal-content').should('exist') 23 | cy.get('.ace_content').type(content) 24 | cy.get('button.ant-btn.ant-btn-primary').eq(0).click({ force: true }) 25 | ele.click({ force: true }) 26 | cy.get('.ace_content').should('contain', content) 27 | } 28 | 29 | it('修改节点名称', () => { 30 | moveNodeAndSelect() 31 | const ele = cy.get('#rc-tabs-0-panel-basic > div > div:nth-child(1) > input').eq(0) 32 | ele.clear({ force: true }) 33 | ele.type('新的开始') 34 | ele.should('have.value', '新的开始') 35 | }) 36 | it('修改节点代码', () => { 37 | assertEdit(2, 'console.log(1)') 38 | }) 39 | it('修改投放配置schema', () => { 40 | assertEdit(3, `"a":1`) 41 | }) 42 | it('修改npm依赖', () => { 43 | assertEdit(4, `"@ant-design/icons": "^4.0.6"`) 44 | }) 45 | }) -------------------------------------------------------------------------------- /cypress/integration/examples/element.test.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | context('Actions', () => { 4 | beforeEach(() => { 5 | cy.visit('http://localhost:8000/') 6 | cy.get('div.ant-modal-confirm-btns > button:nth-child(1)').click({ force: true }) 7 | }) 8 | 9 | function moveNode(selector, x, y) { 10 | cy.get(selector).eq(0) 11 | .trigger('mousedown', { which: 1, force: true }) 12 | .trigger('mousemove', { clientX: x, clientY: y, force: true }) 13 | .trigger('mouseup', { force: true }) 14 | } 15 | 16 | // 节点/边操作测试 17 | it('添加 开始/分支/处理 节点', () => { 18 | const DEFAULT_COUNT = 1 19 | moveNode('svg > g > g.x6-graph-svg-stage > g:nth-child(1) > circle', 300, 150) 20 | moveNode('svg > g > g.x6-graph-svg-stage > g:nth-child(2) > polygon', 400, 150) 21 | moveNode('svg > g > g.x6-graph-svg-stage > g:nth-child(3) > rect', 500, 150) 22 | cy.get('svg> g > g.x6-graph-svg-stage circle').its('length').should('be.gt', DEFAULT_COUNT) 23 | cy.get('svg> g > g.x6-graph-svg-stage polygon').its('length').should('be.gt', DEFAULT_COUNT) 24 | cy.get('svg> g > g.x6-graph-svg-stage rect').its('length').should('be.gt', DEFAULT_COUNT) 25 | }) 26 | 27 | it('边连线 & 删除节点/边', () => { 28 | moveNode('svg > g > g.x6-graph-svg-stage > g:nth-child(1) > circle', 300, 150) 29 | moveNode('svg > g > g.x6-graph-svg-stage > g:nth-child(2) > polygon', 400, 150) 30 | moveNode('svg > g > g.x6-graph-svg-stage > g:nth-child(3) > rect', 500, 150) 31 | const node1 = cy.get('svg > g > g.x6-graph-svg-stage > g:nth-child(1)').eq(1) 32 | const node2 = cy.get('svg > g > g.x6-graph-svg-stage > g:nth-child(2)').eq(1) 33 | const node3 = cy.get('svg > g > g.x6-graph-svg-stage > g:nth-child(3)').eq(1) 34 | node1.click('right', { force: true }).trigger('mousemove', { clientX: node2.clientX, clientY: node2.clientY, force: true }).trigger('mouseup', { force: true }) 35 | node2.click('right', { force: true }).trigger('mousemove', { clientX: node3.clientX, clientY: node3.clientY, force: true }).trigger('mouseup', { force: true }) 36 | cy.get('svg path:nth-child(1)').should('exist') 37 | cy.get('svg path:nth-child(1)').eq(1).click({ force: true }) 38 | cy.get('body').type('{del}') 39 | cy.get('svg path:nth-child(1)').eq(1).click({ force: true }) 40 | cy.get('body').type('{del}') 41 | }) 42 | }) -------------------------------------------------------------------------------- /cypress/integration/examples/tools.test.js: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | // 工具栏测试 4 | context('Actions', () => { 5 | const toolSelector = (i, j, k) => { 6 | if (k) { 7 | return `.index-module_container__1D841 > :nth-child(${i}) > :nth-child(${j})> :nth-child(${k})` 8 | } else { 9 | return `.index-module_container__1D841 > :nth-child(${i}) > :nth-child(${j})` 10 | } 11 | } 12 | beforeEach(() => { 13 | cy.visit('http://localhost:8000/') 14 | cy.get('div.ant-modal-confirm-btns > button:nth-child(1)').click({ force: true }) 15 | }) 16 | 17 | function moveNode(selector, x, y) { 18 | cy.get(selector).eq(0) 19 | .trigger('mousedown', { which: 1, force: true }) 20 | .trigger('mousemove', { clientX: x, clientY: y, force: true }) 21 | .trigger('mouseup', { force: true }) 22 | } 23 | 24 | // 第一组 25 | it('保存', () => { 26 | const save = cy.get(toolSelector(1, 1)) 27 | }) 28 | it('适配窗口', () => { 29 | const fitWindow = cy.get(toolSelector(1, 2)) 30 | }) 31 | it('撤销', () => { 32 | const undo = cy.get(toolSelector(1, 3)) 33 | const node = 'svg > g > g.x6-graph-svg-stage > g:nth-child(1)' 34 | moveNode(node, 350, 300) 35 | moveNode(node, 350, 300) 36 | undo.click({ multiple: true, force: true }) 37 | undo.click({ multiple: true, force: true }) 38 | cy.get(node).should('have.length', 2) 39 | }) 40 | it('重做', () => { 41 | const redo = cy.get(toolSelector(1, 4)) 42 | const node = 'svg > g > g.x6-graph-svg-stage > g:nth-child(1)' 43 | moveNode(node, 350, 300) 44 | redo.click() 45 | redo.click() 46 | cy.get(node).should('have.length', 2) 47 | }) 48 | 49 | // 第二组 50 | it('缩小', () => { 51 | const zoomOut = cy.get(toolSelector(2, 1, 1)) 52 | }) 53 | it('放大', () => { 54 | const zoomIn = cy.get(toolSelector(2, 1, 2)) 55 | }) 56 | 57 | // 第三组 58 | it('修改字号', () => { 59 | const fontSize = cy.get(toolSelector(3, 1)) 60 | }) 61 | it('修改字重', () => { 62 | const fontWeight = cy.get(toolSelector(3, 2)) 63 | }) 64 | it('修改斜体', () => { 65 | const italic = cy.get(toolSelector(3, 3)) 66 | }) 67 | it('修改下划线', () => { 68 | const underline = cy.get(toolSelector(3, 4)) 69 | }) 70 | 71 | // 第四组 72 | it('修改文字颜色', () => { 73 | const textColor = cy.get(toolSelector(4, 1)) 74 | }) 75 | it('修改背景颜色', () => { 76 | const bgColor = cy.get(toolSelector(4, 2)) 77 | }) 78 | it('修改边框颜色', () => { 79 | const borderColor = cy.get(toolSelector(4, 3)) 80 | }) 81 | it('修改线条样式', () => { 82 | const lineStyle = cy.get(toolSelector(4, 4)) 83 | }) 84 | 85 | // 第五组 86 | it('修改水平方向对齐', () => { 87 | const align = cy.get(toolSelector(5, 1)) 88 | }) 89 | it('修改垂直方向对齐', () => { 90 | const vertical = cy.get(toolSelector(5, 2)) 91 | }) 92 | 93 | // 第六组 94 | it('修改层级置顶', () => { 95 | const top = cy.get(toolSelector(6, 1)) 96 | }) 97 | it('修改层级置底', () => { 98 | const bottom = cy.get(toolSelector(6, 2)) 99 | }) 100 | }) -------------------------------------------------------------------------------- /cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | /// 2 | // *********************************************************** 3 | // This example plugins/index.js can be used to load plugins 4 | // 5 | // You can change the location of this file or turn off loading 6 | // the plugins file with the 'pluginsFile' configuration option. 7 | // 8 | // You can read more here: 9 | // https://on.cypress.io/plugins-guide 10 | // *********************************************************** 11 | 12 | // This function is called when a project is opened or re-opened (e.g. due to 13 | // the project's config changing) 14 | 15 | /** 16 | * @type {Cypress.PluginConfig} 17 | */ 18 | module.exports = (on, config) => { 19 | // `on` is used to hook into various events Cypress emits 20 | // `config` is the resolved Cypress config 21 | } 22 | -------------------------------------------------------------------------------- /cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add("login", (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This will overwrite an existing command -- 25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 26 | -------------------------------------------------------------------------------- /cypress/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /docs/api/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # 为iMove做贡献 2 | 3 | * 请先fork一份到自己的项目下,不要直接在仓库下建分支。 4 | * commit信息要以`[组件名]:描述信息`的形式填写,例如`[canvas]:fix xxx bug`。 5 | * 如果您已经修复了错误或者添加了应测试的代码,请添加测试! 6 | * 确保提交PR之前请先rebase,保持commit记录的整洁性。 7 | * 确保PR是提交到`dev`分支,而不是`master`分支。 8 | * 如果修复的是bug,请在PR中给出描述信息。 -------------------------------------------------------------------------------- /docs/api/ctx.md: -------------------------------------------------------------------------------- 1 | # 上下文(ctx) -------------------------------------------------------------------------------- /docs/api/data.md: -------------------------------------------------------------------------------- 1 | # 数据读写(data) -------------------------------------------------------------------------------- /docs/api/edge.md: -------------------------------------------------------------------------------- 1 | # 边(edge) -------------------------------------------------------------------------------- /docs/api/group.md: -------------------------------------------------------------------------------- 1 | # 节点组(group) -------------------------------------------------------------------------------- /docs/api/layout.md: -------------------------------------------------------------------------------- 1 | # 布局(layout) -------------------------------------------------------------------------------- /docs/api/module.md: -------------------------------------------------------------------------------- 1 | # 逻辑元件(module) -------------------------------------------------------------------------------- /docs/api/node.md: -------------------------------------------------------------------------------- 1 | # 节点(node) -------------------------------------------------------------------------------- /docs/api/schema.md: -------------------------------------------------------------------------------- 1 | # 流程图(schema) -------------------------------------------------------------------------------- /docs/config/index.md: -------------------------------------------------------------------------------- 1 | ## 配置文档 -------------------------------------------------------------------------------- /docs/docs/index.md: -------------------------------------------------------------------------------- 1 | ## 使用文档 -------------------------------------------------------------------------------- /docs/guide/index.md: -------------------------------------------------------------------------------- 1 | ## 问题指引 -------------------------------------------------------------------------------- /docs/plugins/index.md: -------------------------------------------------------------------------------- 1 | ## 插件文档 -------------------------------------------------------------------------------- /example/.env: -------------------------------------------------------------------------------- 1 | WATCH_IGNORED=node_modules/(?!(@imove/core)) umi dev -------------------------------------------------------------------------------- /example/.umirc.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'umi'; 2 | 3 | export default defineConfig({ 4 | nodeModulesTransform: { 5 | type: 'none', 6 | }, 7 | routes: [ 8 | { path: '/', component: '@/pages/index' }, 9 | ], 10 | }); 11 | -------------------------------------------------------------------------------- /example/mock/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/i5ting/imove/0529e8127132380196ae14ea5f505b332dfcf8a6/example/mock/.gitkeep -------------------------------------------------------------------------------- /example/mock/api.ts: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const low = require('lowdb'); 3 | const FileSync = require('lowdb/adapters/FileSync'); 4 | 5 | const adapter = new FileSync(path.join(__dirname, './db.json')); 6 | const db = low(adapter); 7 | 8 | const queryGraph = (req: any, res: any) => { 9 | const { projectId = 'default' } = req.body; 10 | const data = db.get(projectId).value() || []; 11 | res.send({ status: 200, code: 0, success: true, data: { cells: data } }); 12 | }; 13 | 14 | const modifyGraph = (req: any, res: any) => { 15 | const { projectId = 'default', actions = [] } = req.body; 16 | const projectData = db.get(projectId).value() || []; 17 | actions.forEach((action: any) => { 18 | const { data, actionType } = action; 19 | if (actionType === 'create') { 20 | projectData.push(data); 21 | } else if (actionType === 'update') { 22 | const foundIdx = projectData.findIndex((item: any) => item.id === data.id); 23 | if (foundIdx > -1) { 24 | projectData[foundIdx] = data; 25 | } 26 | } else if (actionType === 'remove') { 27 | const foundIdx = projectData.findIndex((item: any) => item.id === data.id); 28 | if (foundIdx > -1) { 29 | projectData.splice(foundIdx, 1); 30 | } 31 | } 32 | }); 33 | db.set(projectId, projectData).write(); 34 | res.send({ status: 200, code: 0, success: true, data: [] }); 35 | }; 36 | 37 | 38 | export default { 39 | 'POST /api/queryGraph': queryGraph, 40 | 'POST /api/modifyGraph': modifyGraph, 41 | }; 42 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "scripts": { 4 | "start": "umi dev", 5 | "build": "", 6 | "postinstall": "umi generate tmp" 7 | }, 8 | "dependencies": { 9 | "@ant-design/pro-layout": "^6.5.0", 10 | "@imove/core": "^0.3.9", 11 | "@umijs/preset-react": "1.x", 12 | "lowdb": "^1.0.0", 13 | "umi": "^3.3.7" 14 | }, 15 | "devDependencies": { 16 | "@types/react": "^16.9.0", 17 | "@types/react-dom": "^16.9.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /example/src/pages/index.less: -------------------------------------------------------------------------------- 1 | 2 | .normal { 3 | } 4 | 5 | .title { 6 | background: rgb(121, 242, 157); 7 | } 8 | -------------------------------------------------------------------------------- /example/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import IMove from '@imove/core'; 3 | 4 | const onSave = (data: { nodes: any; edges: any }): void => { 5 | console.log(data); 6 | }; 7 | 8 | function Arrange(): JSX.Element { 9 | return ( 10 |
11 | 12 |
13 | ); 14 | } 15 | 16 | export default Arrange; 17 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "importHelpers": true, 7 | "jsx": "react", 8 | "esModuleInterop": true, 9 | "sourceMap": true, 10 | "baseUrl": "./", 11 | "strict": true, 12 | "paths": { 13 | "@/*": ["src/*"], 14 | "@@/*": ["src/.umi/*"] 15 | }, 16 | "allowSyntheticDefaultImports": true 17 | }, 18 | "include": [ 19 | "mock/**/*", 20 | "src/**/*", 21 | "config/**/*", 22 | ".umirc.ts", 23 | "typings.d.ts" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /example/typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.css'; 2 | declare module '*.less'; 3 | declare module "*.png"; 4 | declare module '*.svg' { 5 | export function ReactComponent(props: React.SVGProps): React.ReactElement 6 | const url: string 7 | export default url 8 | } 9 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "packages/*", 4 | "example" 5 | ], 6 | "version": "0.3.3" 7 | } 8 | -------------------------------------------------------------------------------- /mlc_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignorePatterns": [ 3 | { 4 | "pattern": "^https?://0.0.0.0:\\d+" 5 | }, 6 | { 7 | "pattern": "^https?://127.0.0.1:\\d+" 8 | }, 9 | { 10 | "pattern": "^https?://localhost:\\d+" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "private": true, 4 | "scripts": { 5 | "test": "jest --coverage", 6 | "e2e": "cypress open", 7 | "doc": "dumi dev", 8 | "start": "concurrently \"lerna run watch --parallel\" \"cross-env APP_ROOT=example umi dev\"", 9 | "example": "concurrently \"lerna run watch --parallel\" \"cross-env APP_ROOT=example umi dev\" \"imove -d\"", 10 | "postinstall": "lerna init && lerna exec npm i && lerna exec npm run build && npm link packages/cli && lerna bootstrap", 11 | "prepare": "husky install" 12 | }, 13 | "jest": { 14 | "projects": [ 15 | "config/cli.jest.config.js", 16 | "config/core.jest.config.js", 17 | "config/editor.jest.config.js", 18 | "config/plugin.jest.config.js" 19 | ] 20 | }, 21 | "devDependencies": { 22 | "@rollup/plugin-commonjs": "^11.0.2", 23 | "@rollup/plugin-json": "^4.0.3", 24 | "@rollup/plugin-node-resolve": "^7.1.3", 25 | "@rollup/plugin-strip": "^1.3.2", 26 | "@types/jest": "^26.0.16", 27 | "@typescript-eslint/eslint-plugin": "^4.14.0", 28 | "@typescript-eslint/parser": "^2.29.0", 29 | "concurrently": "^5.3.0", 30 | "cross-env": "^7.0.3", 31 | "cypress": "^6.2.0", 32 | "dumi": "^1.0.38", 33 | "eslint": "^6.8.0", 34 | "eslint-config-prettier": "^7.2.0", 35 | "eslint-plugin-prettier": "^3.3.1", 36 | "eslint-plugin-react": "^7.22.0", 37 | "husky": "^7.0.1", 38 | "lerna": "^3.22.1", 39 | "less": "^3.12.2", 40 | "lint-staged": "^11.0.1", 41 | "lowdb": "^1.0.0", 42 | "ora": "^4.1.1", 43 | "postcss": "^8.2.1", 44 | "prettier": "^2.2.1", 45 | "rollup": "^2.6.1", 46 | "rollup-plugin-postcss": "^4.0.0", 47 | "rollup-plugin-size-snapshot": "^0.11.0", 48 | "rollup-plugin-typescript": "^1.0.1", 49 | "snazzy": "^9.0.0", 50 | "ts-jest": "^26.4.4", 51 | "typescript": "^4.1.3", 52 | "umi": "^3.3.3", 53 | "watch": "^1.0.2" 54 | }, 55 | "lint-staged": { 56 | "packages/**/*.{ts,tsx,js,jsx}": "eslint --cache --fix" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /packages/cli/.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /packages/cli/README.md: -------------------------------------------------------------------------------- 1 | ## iMove-cli 2 | 3 | ### Install 4 | 5 | ```bash 6 | npm i -g @imove/cli 7 | ``` 8 | 9 | ### Usage 10 | 11 | - 初始化项目,创建 `imove.config.js` 12 | 13 | ```bash 14 | imove -i 15 | # OR 16 | imove --init 17 | ``` 18 | 19 | - 启动开发服务及编辑器 20 | 21 | ```bash 22 | imove -de 23 | # OR 24 | imove --dev --editor 25 | ``` 26 | 27 | 浏览器打开 [http://127.0.0.1:3500/](http://127.0.0.1:3500/) 28 | -------------------------------------------------------------------------------- /packages/cli/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const path = require('path'); 3 | const cmds = require('./src/cmd'); 4 | const program = require('commander'); 5 | const getConfig = require('./src/utils/getConfig'); 6 | const pkg = require(path.join(__dirname, './package.json')); 7 | 8 | program 9 | .version(pkg.version) 10 | .option('-d, --dev', '本地开发') 11 | .option('-e, --editor', '开启编辑器') 12 | .option('-i, --init', '初始化配置文件') 13 | .parse(process.argv); 14 | 15 | Object.keys(cmds).forEach((cmd) => { 16 | const CmdCtor = cmds[cmd]; 17 | if (program[cmd]) { 18 | const cmdInst = new CmdCtor({ config: getConfig() }); 19 | cmdInst.run(); 20 | } 21 | }); 22 | -------------------------------------------------------------------------------- /packages/cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@imove/cli", 3 | "version": "0.0.5", 4 | "description": "imove client", 5 | "main": "index.js", 6 | "bin": { 7 | "imove": "index.js" 8 | }, 9 | "keywords": [ 10 | "imove", 11 | "compile" 12 | ], 13 | "scripts": { 14 | "build": "", 15 | "build:editor": "node src/cmd/editor/build.js", 16 | "postinstall": "npm run build:editor" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/suanmei/iMove.git" 21 | }, 22 | "author": "smallstonesk", 23 | "license": "MIT", 24 | "dependencies": { 25 | "@imove/compile-code": "^0.1.0", 26 | "@imove/core": "^0.3.9", 27 | "body-parser": "^1.19.0", 28 | "commander": "^6.0.0", 29 | "cors": "^2.8.5", 30 | "esbuild": "^0.12.15", 31 | "esbuild-plugin-less": "^1.0.7", 32 | "eventemitter3": "^4.0.7", 33 | "express": "^4.17.1", 34 | "fs-extra": "^9.0.1", 35 | "lowdb": "^1.0.0", 36 | "open": "^8.2.1" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/cli/src/cmd/base/index.js: -------------------------------------------------------------------------------- 1 | class Base { 2 | constructor({ config }) { 3 | this._config = config; 4 | return this; 5 | } 6 | 7 | get config() { 8 | return this._config; 9 | } 10 | 11 | get projectPath() { 12 | return process.cwd(); 13 | } 14 | } 15 | 16 | module.exports = Base; 17 | -------------------------------------------------------------------------------- /packages/cli/src/cmd/dev/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const path = require('path'); 3 | const fs = require('fs-extra'); 4 | const Base = require('../base'); 5 | const mergePkg = require('./mergePkg'); 6 | const { compileForProject } = require('@imove/compile-code'); 7 | const { DEFAULT_PORT, createServer } = require('../../utils/server'); 8 | 9 | const noop = function () { 10 | /* noop function */ 11 | }; 12 | const CACHE_PATH = path.join(process.cwd(), './.cache'); 13 | const CACHE_DSL_FILE = path.join(CACHE_PATH, 'imove.dsl.json'); 14 | 15 | class Dev extends Base { 16 | async writeOutputIntoFiles(curPath, output) { 17 | for (const key in output) { 18 | const newPath = path.join(curPath, key); 19 | if (path.extname(newPath)) { 20 | await fs.writeFile(newPath, output[key]); 21 | } else { 22 | await fs.ensureDir(newPath); 23 | await this.writeOutputIntoFiles(newPath, output[key]); 24 | } 25 | } 26 | } 27 | 28 | async save(req, res) { 29 | const { outputPath, plugins = [] } = this.config; 30 | 31 | // check outputPath whether exsited 32 | await fs.ensureDir(outputPath); 33 | 34 | // check dsl whether existed 35 | if (!req.body || !req.body.dsl) { 36 | res.status(500).json({ isCompiled: false }).end(); 37 | return; 38 | } 39 | 40 | // compile 41 | try { 42 | const { dsl } = req.body; 43 | const output = compileForProject(dsl, plugins); 44 | await this.writeOutputIntoFiles(outputPath, output); 45 | await mergePkg(dsl, this.projectPath); 46 | await fs.outputFile(CACHE_DSL_FILE, JSON.stringify(dsl, null, 2)); 47 | res.status(200).json({ isCompiled: true }).end(); 48 | console.log('compile successfully!'); 49 | } catch (err) { 50 | res.status(500).json({ isCompiled: false }).end(); 51 | console.log('compile failed! the error is:', err); 52 | } 53 | } 54 | 55 | async connect(req, res) { 56 | const { projectName } = this.config; 57 | const dsl = await fs.readJson(CACHE_DSL_FILE).catch(noop); 58 | res.status(200).json({ projectName, dsl }).end(); 59 | } 60 | 61 | run() { 62 | const app = createServer(DEFAULT_PORT); 63 | app.post('/api/save', this.save.bind(this)); 64 | app.get('/api/connect', this.connect.bind(this)); 65 | } 66 | } 67 | 68 | module.exports = Dev; 69 | -------------------------------------------------------------------------------- /packages/cli/src/cmd/dev/mergePkg.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs-extra'); 3 | 4 | const builtinDependencies = { 5 | eventemitter3: '^4.0.7', 6 | }; 7 | 8 | const extractDep = (dsl) => { 9 | const mergedDependencies = {}; 10 | const { cells = [] } = dsl; 11 | cells 12 | .filter((cell) => cell.shape !== 'edge') 13 | .forEach((cell) => { 14 | const { dependencies } = cell.data || {}; 15 | try { 16 | const json = JSON.parse(dependencies); 17 | Object.keys(json).forEach( 18 | (key) => (mergedDependencies[key] = json[key]), 19 | ); 20 | } catch (error) { 21 | console.log( 22 | 'extract dependencies failed, the error is:', 23 | error.message, 24 | ); 25 | } 26 | }); 27 | return mergedDependencies; 28 | }; 29 | 30 | const setup = async (dsl, projectRootPath) => { 31 | const pkgPath = path.join(projectRootPath, './package.json'); 32 | const pkgFile = await fs.readFile(pkgPath); 33 | const pkgJson = JSON.parse(pkgFile); 34 | const dslDependencies = extractDep(dsl); 35 | if (!pkgJson.dependencies) { 36 | pkgJson.dependencies = dslDependencies; 37 | } else { 38 | Object.keys(dslDependencies).forEach((key) => { 39 | pkgJson.dependencies[key] = dslDependencies[key]; 40 | }); 41 | Object.keys(builtinDependencies).forEach((key) => { 42 | pkgJson.dependencies[key] = builtinDependencies[key]; 43 | }); 44 | } 45 | await fs.writeFile(pkgPath, JSON.stringify(pkgJson, null, 2)); 46 | }; 47 | 48 | module.exports = setup; 49 | -------------------------------------------------------------------------------- /packages/cli/src/cmd/editor/build.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* eslint-disable @typescript-eslint/no-var-requires */ 4 | const path = require('path'); 5 | const { lessLoader } = require('esbuild-plugin-less'); 6 | 7 | // build js and css for editor 8 | const OUT_FILE = path.join(__dirname, 'template/dist/app.bundle.js'); 9 | require('esbuild') 10 | .build({ 11 | bundle: true, 12 | entryPoints: [path.join(__dirname, 'template/app.jsx')], 13 | outfile: OUT_FILE, 14 | plugins: [ 15 | // fix multi-react with lerna 16 | { 17 | name: 'resolve-multi-react', 18 | setup: (build) => { 19 | build.onResolve({ filter: /^react$/, namespace: 'file' }, () => { 20 | return { 21 | path: require.resolve('react'), 22 | watchFiles: undefined, 23 | }; 24 | }); 25 | }, 26 | }, 27 | // fix import('antd/dist/antd.less') 28 | { 29 | name: 'resolve-antd-dist-less', 30 | setup: (build) => { 31 | build.onResolve( 32 | { filter: /antd\/dist\/antd\.less$/, namespace: 'file' }, 33 | () => { 34 | return { 35 | path: '', 36 | watchFiles: undefined, 37 | }; 38 | }, 39 | ); 40 | }, 41 | }, 42 | // less 43 | lessLoader({ 44 | javascriptEnabled: true, 45 | }), 46 | ], 47 | }) 48 | .then(() => { 49 | console.log('imove editor builded'); 50 | }) 51 | .catch(() => process.exit(1)); 52 | -------------------------------------------------------------------------------- /packages/cli/src/cmd/editor/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const fs = require('fs-extra'); 3 | const path = require('path'); 4 | const low = require('lowdb'); 5 | const FileSync = require('lowdb/adapters/FileSync'); 6 | const express = require('express'); 7 | const Base = require('../base'); 8 | const { DEFAULT_PORT, createServer } = require('../../utils/server'); 9 | 10 | class Editor extends Base { 11 | constructor(...args) { 12 | super(...args); 13 | if (!this.dbFile) { 14 | fs.ensureDirSync(this.outputPath); 15 | this.dbFile = path.join(this.outputPath, 'db.json'); 16 | } 17 | // make sure dbFile existed 18 | fs.createFileSync(this.dbFile); 19 | 20 | const adapter = new FileSync(this.dbFile); 21 | this.db = low(adapter); 22 | } 23 | 24 | get projectName() { 25 | const { projectName = 'default' } = this.config || ''; 26 | return projectName; 27 | } 28 | 29 | get outputPath() { 30 | const { outputPath } = this.config || ''; 31 | return outputPath; 32 | } 33 | 34 | get dbFile() { 35 | const dbFile = this._dbFile || (this.config || '').dbFile; 36 | return dbFile; 37 | } 38 | 39 | set dbFile(val) { 40 | this._dbFile = val; 41 | } 42 | 43 | queryGraph(req, res) { 44 | const data = this.db.get(this.projectName).value() || []; 45 | res.send({ status: 200, code: 0, success: true, data: { cells: data } }); 46 | } 47 | 48 | modifyGraph(req, res) { 49 | const { actions = [] } = req.body; 50 | const projectData = this.db.get(this.projectName).value() || []; 51 | actions.forEach((action) => { 52 | const { data, actionType } = action; 53 | if (actionType === 'create') { 54 | projectData.push(data); 55 | } else if (actionType === 'update') { 56 | const foundIdx = projectData.findIndex((item) => item.id === data.id); 57 | if (foundIdx > -1) { 58 | projectData[foundIdx] = data; 59 | } 60 | } else if (actionType === 'remove') { 61 | const foundIdx = projectData.findIndex((item) => item.id === data.id); 62 | if (foundIdx > -1) { 63 | projectData.splice(foundIdx, 1); 64 | } 65 | } 66 | }); 67 | this.db.set(this.projectName, projectData).write(); 68 | res.send({ status: 200, code: 0, success: true, data: [] }); 69 | } 70 | 71 | run() { 72 | const server = createServer(DEFAULT_PORT, true); 73 | server.use(express.static(path.join(__dirname, './template'))); 74 | server.post('/api/queryGraph', this.queryGraph.bind(this)); 75 | server.post('/api/modifyGraph', this.modifyGraph.bind(this)); 76 | } 77 | } 78 | 79 | module.exports = Editor; 80 | -------------------------------------------------------------------------------- /packages/cli/src/cmd/editor/template/app.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import IMove from '@imove/core'; 4 | 5 | ReactDOM.render(, document.getElementById('root')); 6 | -------------------------------------------------------------------------------- /packages/cli/src/cmd/editor/template/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | iMove Editor 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /packages/cli/src/cmd/index.js: -------------------------------------------------------------------------------- 1 | const Dev = require('./dev'); 2 | const Init = require('./init'); 3 | const Editor = require('./editor'); 4 | 5 | module.exports = { 6 | dev: Dev, 7 | init: Init, 8 | editor: Editor, 9 | }; 10 | -------------------------------------------------------------------------------- /packages/cli/src/cmd/init/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const path = require('path'); 3 | const fs = require('fs-extra'); 4 | const Base = require('../base'); 5 | 6 | const CONFIG_TPL_PATH = path.join(__dirname, './template/imove.config.js.tpl'); 7 | const OUTPUT_FILE_PATH = path.join(process.cwd(), 'imove.config.js'); 8 | 9 | class Init extends Base { 10 | getProjectName() { 11 | const pkgPath = path.join(this.projectPath, 'package.json'); 12 | const pkgJson = fs.readJSONSync(pkgPath); 13 | return pkgJson.name; 14 | } 15 | 16 | getOutputContent() { 17 | const tplContent = fs.readFileSync(CONFIG_TPL_PATH, 'utf-8'); 18 | return tplContent.replace('{projectName}', `'${this.getProjectName()}'`); 19 | } 20 | 21 | run() { 22 | fs.outputFileSync(OUTPUT_FILE_PATH, this.getOutputContent()); 23 | console.log(`Create ${OUTPUT_FILE_PATH} at successfully!`); 24 | } 25 | } 26 | 27 | module.exports = Init; 28 | -------------------------------------------------------------------------------- /packages/cli/src/cmd/init/template/imove.config.js.tpl: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | projectName: {projectName}, 5 | outputPath: path.join(__dirname, './src/logic') 6 | }; 7 | -------------------------------------------------------------------------------- /packages/cli/src/utils/getConfig.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs'); 3 | 4 | const CONFIG_FILE_NAME = 'imove.config.js'; 5 | const CONFIG_FILE_PATH = path.join(process.cwd(), CONFIG_FILE_NAME); 6 | const DEFAULT_CONFIG = { 7 | outputPath: path.join(process.cwd(), './src/logic/'), 8 | }; 9 | const mergeConfig = (config, DEFAULT_CONFIG) => { 10 | // TODO: merge config 11 | return config; 12 | }; 13 | 14 | const getConfig = () => { 15 | const isConfigFileExisted = fs.existsSync(CONFIG_FILE_PATH); 16 | if (isConfigFileExisted) { 17 | return mergeConfig(require(CONFIG_FILE_PATH), DEFAULT_CONFIG); 18 | } else { 19 | return DEFAULT_CONFIG; 20 | } 21 | }; 22 | 23 | module.exports = getConfig; 24 | -------------------------------------------------------------------------------- /packages/cli/src/utils/server/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const express = require('express'); 3 | const bodyParser = require('body-parser'); 4 | const open = require('open'); 5 | 6 | const DEFAULT_PORT = 3500; 7 | const cachedServer = {}; 8 | 9 | function openBrowserIfNeeded(url, needOpen = false) { 10 | if (needOpen) { 11 | open(url); 12 | } 13 | } 14 | 15 | const createServer = (port = DEFAULT_PORT, needOpen = false) => { 16 | const url = `http://127.0.0.1:${port}`; 17 | if (cachedServer[port]) { 18 | openBrowserIfNeeded(url, needOpen); 19 | return cachedServer[port]; 20 | } 21 | const app = express(); 22 | cachedServer[port] = app; 23 | 24 | app.use(bodyParser.json({ limit: '50mb' })); 25 | app.use(bodyParser.urlencoded({ extended: false })); 26 | app.use((req, res, next) => { 27 | res.header('Access-Control-Allow-Origin', '*'); 28 | res.header( 29 | 'Access-Control-Allow-Headers', 30 | 'Origin, X-Requested-With, Content-Type, Accept', 31 | ); 32 | next(); 33 | }); 34 | app.listen(port, () => { 35 | console.log(`server starts at ${url}`); 36 | openBrowserIfNeeded(url, needOpen); 37 | }); 38 | return app; 39 | }; 40 | 41 | module.exports = { 42 | DEFAULT_PORT, 43 | createServer, 44 | }; 45 | -------------------------------------------------------------------------------- /packages/compile-code/.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /packages/compile-code/README.md: -------------------------------------------------------------------------------- 1 | # @imove/compile-code -------------------------------------------------------------------------------- /packages/compile-code/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@imove/compile-code", 3 | "version": "0.1.0", 4 | "description": "imove compile code", 5 | "main": "dist/core.common.js", 6 | "module": "dist/core.esm.js", 7 | "types": "dist/types/index.d.ts", 8 | "directories": { 9 | "dist": "dist" 10 | }, 11 | "files": [ 12 | "dist" 13 | ], 14 | "publishConfig": { 15 | "access": "public", 16 | "registry": "http://registry.npmjs.org/" 17 | }, 18 | "keywords": [ 19 | "imove", 20 | "compile code" 21 | ], 22 | "scripts": { 23 | "prepublishOnly": "node scripts/prepublish.js", 24 | "declare-type": "tsc --emitDeclarationOnly", 25 | "build": "rollup -c & npm run declare-type", 26 | "watch": "watch \"npm run build\" ./src" 27 | }, 28 | "repository": { 29 | "type": "git", 30 | "url": "git+https://github.com/imgcook/iMove.git" 31 | }, 32 | "author": "smallstonesk", 33 | "license": "MIT", 34 | "dependencies": { 35 | "@antv/x6": "^1.5.1" 36 | }, 37 | "devDependencies": {} 38 | } 39 | -------------------------------------------------------------------------------- /packages/compile-code/rollup.config.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import rollupBaseConfig from '../../rollup.config'; 3 | import typescript from 'rollup-plugin-typescript'; 4 | import pkg from './package.json'; 5 | 6 | export default Object.assign(rollupBaseConfig, { 7 | input: path.join(__dirname, './src/index.ts'), 8 | output: [ 9 | { 10 | file: pkg.main, 11 | format: 'cjs', 12 | }, 13 | { 14 | file: pkg.module, 15 | format: 'es', 16 | }, 17 | ], 18 | plugins: [ 19 | typescript(), 20 | ] 21 | }); 22 | -------------------------------------------------------------------------------- /packages/compile-code/scripts/prepublish.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const path = require('path'); 4 | const prePublish = require('../../../scripts/prepublish'); 5 | 6 | prePublish('@imove/compile-code', path.join(__dirname, '../')); 7 | -------------------------------------------------------------------------------- /packages/compile-code/src/addPlugins.ts: -------------------------------------------------------------------------------- 1 | const INSERT_IMPORT_PLUGINS_COMMENT = '// import plugins here'; 2 | const INSERT_USE_PLUGINS_COMMENT = '// use plugins here'; 3 | 4 | const addPlugins = (originalCode = '', plugins: string[] = []): string => { 5 | const modifiedContent: string = originalCode 6 | .replace(new RegExp(INSERT_IMPORT_PLUGINS_COMMENT), () => { 7 | return plugins 8 | .map((plugin, index) => `import plugin${index} from '${plugin}';`) 9 | .join('\n'); 10 | }) 11 | .replace(new RegExp(INSERT_USE_PLUGINS_COMMENT), () => { 12 | return plugins.map((_, index) => `logic.use(plugin${index});`).join('\n'); 13 | }); 14 | return modifiedContent; 15 | }; 16 | 17 | export default addPlugins; 18 | -------------------------------------------------------------------------------- /packages/compile-code/src/compileForOnline.ts: -------------------------------------------------------------------------------- 1 | import { Cell } from '@antv/x6'; 2 | import makeCode from './template/runOnline'; 3 | import simplifyDSL from './simplifyDSL'; 4 | 5 | interface DSL { 6 | cells: Cell.Properties[]; 7 | } 8 | 9 | /** 10 | * Solution 11 | * 12 | * 1. find the source node form dsl, and if it is not imove-start, 13 | * then insert a vitural imove-start at first. 14 | * 15 | * 2. transform node funciton, follows should be noted: 16 | * - import statement should be replaced with import('packge/from/network') 17 | * - export statement should be replace with return function 18 | * - each node function should be wrapped within a new function to avoid duplicate declaration global variable 19 | * 20 | * 3. assemble Logic, Context, simplyfied dsl and nodeFns map into one file 21 | * 22 | */ 23 | 24 | const INSERT_DSL_COMMENT = '// define dsl here'; 25 | const INSERT_NODE_FNS_COMMENT = '// define nodeFns here'; 26 | const importRegex = /import\s([\s\S]*?)\sfrom\s('|")((@\w[\w\.\-]+\/)?(\w[\w\.\-\/]+))\2/gm; 27 | const virtualSourceNode = { 28 | id: 'virtual-imove-start', 29 | shape: 'imove-start', 30 | data: { 31 | trigger: 'virtual-imove-start', 32 | configData: {}, 33 | code: 'export default async function(ctx) {\n \n}', 34 | }, 35 | }; 36 | 37 | const findStartNode = (dsl: DSL): Cell.Properties => { 38 | const nodes = dsl.cells.filter((cell) => cell.shape !== 'edge'); 39 | const edges = dsl.cells.filter((cell) => cell.shape === 'edge'); 40 | 41 | if (nodes.length === 0) { 42 | throw new Error('Compile failed, no node is selected'); 43 | } 44 | 45 | let foundEdge = null; 46 | let startNode = nodes[0]; 47 | while ( 48 | (foundEdge = edges.find((edge) => edge.target.cell === startNode.id)) 49 | ) { 50 | const newSourceId = foundEdge.source.cell; 51 | startNode = nodes.find( 52 | (node) => node.id === newSourceId, 53 | ) as Cell.Properties; 54 | } 55 | 56 | if (startNode.shape !== 'imove-start') { 57 | dsl.cells.push(virtualSourceNode, { 58 | shape: 'edge', 59 | source: { 60 | cell: 'virtual-imove-start', 61 | }, 62 | target: { 63 | cell: startNode.id, 64 | }, 65 | }); 66 | startNode = virtualSourceNode; 67 | } 68 | 69 | return startNode; 70 | }; 71 | 72 | const getNextNode = (curNode: Cell.Properties, dsl: DSL) => { 73 | const nodes = dsl.cells.filter((cell) => cell.shape !== 'edge'); 74 | const edges = dsl.cells.filter((cell) => cell.shape === 'edge'); 75 | 76 | const foundEdge = edges.find((edge) => edge.source.cell === curNode.id); 77 | if (foundEdge) { 78 | return nodes.find((node) => node.id === foundEdge.target.cell); 79 | } 80 | }; 81 | 82 | const compileSimplifiedDSL = (dsl: DSL): string => { 83 | const simplyfiedDSL = JSON.stringify(simplifyDSL(dsl), null, 2); 84 | return `const dsl = ${simplyfiedDSL};`; 85 | }; 86 | 87 | const compileNodeFn = (node: Cell.Properties): string => { 88 | const { 89 | data: { label, code }, 90 | } = node; 91 | const newCode = code 92 | .replace( 93 | importRegex, 94 | (match: string, p1: string, p2: string, p3: string) => { 95 | return `const ${p1} = (await import('https://jspm.dev/${p3}')).default;`; 96 | }, 97 | ) 98 | .replace(/export\s+default/, 'return'); 99 | 100 | return `await (async function() { 101 | ${newCode} 102 | }())`; 103 | }; 104 | 105 | const compileNodeFnsMap = (dsl: DSL): string => { 106 | const nodes = dsl.cells.filter((cell) => cell.shape !== 'edge'); 107 | const kvs = nodes.map((node) => { 108 | const { id } = node; 109 | return `'${id}': ${compileNodeFn(node)}`; 110 | }); 111 | 112 | return `const nodeFns = {\n ${kvs.join(',\n ')}\n}`; 113 | }; 114 | 115 | const compile = (dsl: DSL, mockInput: any): string => { 116 | const startNode = findStartNode(dsl); 117 | const mockNode = getNextNode(startNode, dsl); 118 | return makeCode(mockNode, mockInput) 119 | .replace(INSERT_DSL_COMMENT, compileSimplifiedDSL(dsl)) 120 | .replace(INSERT_NODE_FNS_COMMENT, compileNodeFnsMap(dsl)) 121 | .replace('$TRIGGER$', startNode.data.trigger); 122 | }; 123 | 124 | export default compile; 125 | -------------------------------------------------------------------------------- /packages/compile-code/src/compileForProject.ts: -------------------------------------------------------------------------------- 1 | import { Cell } from '@antv/x6'; 2 | import addPlugins from './addPlugins'; 3 | import simplifyDSL from './simplifyDSL'; 4 | import extractNodeFns from './extractNodeFns'; 5 | import logicTpl from './template/logic'; 6 | import indexTpl from './template/index'; 7 | import contextTpl from './template/context'; 8 | 9 | interface DSL { 10 | cells: Cell.Properties[]; 11 | } 12 | 13 | interface IOutput { 14 | nodeFns: { 15 | [fileName: string]: string; 16 | }; 17 | 'context.js': string; 18 | 'dsl.json': string; 19 | 'index.js': string; 20 | 'logic.js': string; 21 | } 22 | 23 | const compile = (dsl: DSL, plugins = []): IOutput => { 24 | const output: IOutput = { 25 | nodeFns: extractNodeFns(dsl), 26 | 'context.js': contextTpl, 27 | 'dsl.json': JSON.stringify(simplifyDSL(dsl), null, 2), 28 | 'index.js': addPlugins(indexTpl, plugins), 29 | 'logic.js': logicTpl, 30 | }; 31 | return output; 32 | }; 33 | 34 | export default compile; 35 | -------------------------------------------------------------------------------- /packages/compile-code/src/extractNodeFns.ts: -------------------------------------------------------------------------------- 1 | import { Cell } from '@antv/x6'; 2 | 3 | interface DSL { 4 | cells: Cell.Properties[]; 5 | } 6 | 7 | interface INodesFns { 8 | [fileName: string]: string; 9 | } 10 | 11 | const genEntryFile = (nodeIds: string[]): string => { 12 | const imports: string[] = []; 13 | const funcMaps: string[] = []; 14 | nodeIds.forEach((id, idx) => { 15 | const funcName = `fn_${idx}`; 16 | imports.push(`import ${funcName} from './${id}';`); 17 | funcMaps.push(`'${id}': ${funcName}`); 18 | }); 19 | const fileContent: string = [ 20 | imports.join('\n'), 21 | `const nodeFns = {\n ${funcMaps.join(',\n ')}\n};`, 22 | 'export default nodeFns;', 23 | ].join('\n'); 24 | return fileContent; 25 | }; 26 | 27 | const genNodeFns = (dsl: DSL): INodesFns => { 28 | const nodeFns: INodesFns = {}; 29 | const { cells = [] } = dsl; 30 | const nodes = cells.filter((cell) => cell.shape !== 'edge'); 31 | for (const { 32 | id, 33 | shape, 34 | data: { label, code }, 35 | } of nodes) { 36 | const fileName: string = id + '.js'; 37 | const descData = `// ${shape}: ${label}\n`; 38 | const saveData = `${descData}\n${code}`; 39 | nodeFns[fileName] = saveData; 40 | } 41 | return nodeFns; 42 | }; 43 | 44 | const extract = (dsl: DSL): INodesFns => { 45 | const nodeFns = genNodeFns(dsl); 46 | const nodeIds = Object.keys(nodeFns).map((fileName) => fileName.slice(0, -3)); 47 | const entryFileContent = genEntryFile(nodeIds); 48 | nodeFns['index.js'] = entryFileContent; 49 | return nodeFns; 50 | }; 51 | 52 | export default extract; 53 | -------------------------------------------------------------------------------- /packages/compile-code/src/index.ts: -------------------------------------------------------------------------------- 1 | import compileForOnline from './compileForOnline'; 2 | import compileForProject from './compileForProject'; 3 | 4 | export { compileForOnline, compileForProject }; 5 | -------------------------------------------------------------------------------- /packages/compile-code/src/simplifyDSL.ts: -------------------------------------------------------------------------------- 1 | import { Cell } from '@antv/x6'; 2 | 3 | interface DSL { 4 | cells: Cell.Properties[]; 5 | } 6 | 7 | const extractObj = ( 8 | obj: Cell.Properties = {}, 9 | keys: string[] = [], 10 | ): Cell.Properties => { 11 | const ret: Cell.Properties = {}; 12 | keys.forEach((key) => { 13 | if (obj[key]) { 14 | ret[key] = obj[key]; 15 | } 16 | }); 17 | return ret; 18 | }; 19 | 20 | const simplifyDSL = (dsl: DSL): Cell.Properties => { 21 | const { cells = [] } = dsl; 22 | return { 23 | cells: cells.map((cell) => { 24 | if (cell.shape === 'edge') { 25 | return extractObj(cell, ['id', 'shape', 'source', 'target']); 26 | } else { 27 | const newCell = extractObj(cell, ['id', 'shape', 'data']); 28 | newCell.data = extractObj(cell.data, [ 29 | 'trigger', 30 | 'configData', 31 | 'ports', 32 | ]); 33 | return newCell; 34 | } 35 | }), 36 | }; 37 | }; 38 | 39 | export default simplifyDSL; 40 | -------------------------------------------------------------------------------- /packages/compile-code/src/template/context.ts: -------------------------------------------------------------------------------- 1 | export default `export default class Context { 2 | constructor(opts) { 3 | this._init(opts); 4 | } 5 | 6 | _init(opts = {}) { 7 | const { payload = {} } = opts; 8 | this.curNode = null; 9 | this.context = {}; 10 | this.payload = Object.freeze({ ...payload }); 11 | } 12 | 13 | _transitTo(node, lastRet) { 14 | this.curNode = node; 15 | this.lastRet = lastRet; 16 | } 17 | 18 | getConfig() { 19 | return this.curNode.data.configData; 20 | } 21 | 22 | getPayload() { 23 | return this.payload; 24 | } 25 | 26 | getPipe() { 27 | return this.lastRet; 28 | } 29 | 30 | getContext() { 31 | return this.context; 32 | } 33 | 34 | setContext(data = {}) { 35 | Object.keys(data).forEach((key) => { 36 | this.context[key] = data[key]; 37 | }); 38 | } 39 | } 40 | `; 41 | -------------------------------------------------------------------------------- /packages/compile-code/src/template/index.ts: -------------------------------------------------------------------------------- 1 | export default `import Logic from './logic'; 2 | import dsl from './dsl.json'; 3 | // import plugins here 4 | 5 | const logic = new Logic({ dsl }); 6 | 7 | // use plugins here 8 | 9 | export default logic; 10 | `; 11 | -------------------------------------------------------------------------------- /packages/compile-code/src/template/runOnline.ts: -------------------------------------------------------------------------------- 1 | import logic from './logic'; 2 | import context from './context'; 3 | 4 | const makeCode = (mockNode: any, mockInput: any) => ` 5 | (async function run() { 6 | // Context 7 | ${context.replace(/export\s+default/, '')} 8 | 9 | // Logic 10 | ${logic 11 | .split('\n') 12 | .filter( 13 | (line) => !line.match(/import nodeFns/) && !line.match(/import Context/), 14 | ) 15 | .join('\n') 16 | .replace(/export\s+default/, '') 17 | .replace( 18 | `import EventEmitter from 'eventemitter3';`, 19 | `const EventEmitter = (await import('https://jspm.dev/eventemitter3')).default;`, 20 | )} 21 | 22 | // DSL 23 | // define dsl here 24 | 25 | // nodeFns map 26 | // define nodeFns here 27 | 28 | // imove plugin 29 | const mockPlugin = () => { 30 | const mockNode = ${JSON.stringify(mockNode)}; 31 | const mockInput = ${JSON.stringify(mockInput)}; 32 | const toMockTargets = [ 33 | ['pipe', 'getPipe'], 34 | ['config', 'getConfig'], 35 | ['payload', 'getPayload'], 36 | ['context', 'getContext'], 37 | ]; 38 | return { 39 | enterNode(ctx) { 40 | // hijack 41 | if(ctx.curNode.id === mockNode.id) { 42 | toMockTargets.forEach(item => { 43 | const [type, method] = item; 44 | item[2] = ctx[method]; 45 | ctx[method] = () => mockInput[type]; 46 | }); 47 | } 48 | }, 49 | leaveNode(ctx) { 50 | // restore 51 | if(ctx.curNode.id === mockNode.id) { 52 | toMockTargets.forEach(item => { 53 | const [type, method, originMethod] = item; 54 | ctx[method] = originMethod; 55 | }); 56 | } 57 | } 58 | }; 59 | }; 60 | 61 | // instantiation and invoke 62 | const logic = new Logic({ dsl }); 63 | logic.use(mockPlugin); 64 | logic.invoke('$TRIGGER$', {}, (pipe) => { 65 | const ctx = logic._getUnsafeCtx(); 66 | const context = ctx.getContext(); 67 | window.dispatchEvent(new CustomEvent('iMoveOnlineExecEnds', {detail: {pipe, context}})); 68 | }); 69 | })().catch(err => { 70 | console.error(err.message); 71 | window.dispatchEvent(new CustomEvent('iMoveOnlineExecEnds', {detail: {error: {message: err.message}}})); 72 | }); 73 | `; 74 | 75 | export default makeCode; 76 | -------------------------------------------------------------------------------- /packages/compile-code/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig", 3 | "compilerOptions": { 4 | "outDir": "dist/types" 5 | }, 6 | "include": ["./src", "./types"] 7 | } 8 | -------------------------------------------------------------------------------- /packages/core/.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | src 3 | -------------------------------------------------------------------------------- /packages/core/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false -------------------------------------------------------------------------------- /packages/core/README.md: -------------------------------------------------------------------------------- 1 | # `core` 2 | 3 | > TODO: description 4 | 5 | ## Usage 6 | 7 | ``` 8 | const core = require('core'); 9 | 10 | // TODO: DEMONSTRATE API 11 | ``` 12 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@imove/core", 3 | "version": "0.3.9", 4 | "description": "流程编排", 5 | "keywords": [ 6 | "imove", 7 | "x6", 8 | "flowchart" 9 | ], 10 | "author": "suanmei ", 11 | "homepage": "https://github.com/suanmei/iMove#readme", 12 | "license": "MIT", 13 | "main": "dist/core.common.js", 14 | "module": "dist/core.esm.js", 15 | "types": "dist/types/index.d.ts", 16 | "directories": { 17 | "dist": "dist" 18 | }, 19 | "files": [ 20 | "dist" 21 | ], 22 | "publishConfig": { 23 | "access": "public" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "git+https://github.com/suanmei/iMove.git" 28 | }, 29 | "scripts": { 30 | "prepublishOnly": "node scripts/prepublish.js", 31 | "declare-type": "tsc --emitDeclarationOnly", 32 | "build": "rollup -c & npm run declare-type", 33 | "watch": "watch \"npm run build\" ./src" 34 | }, 35 | "bugs": { 36 | "url": "https://github.com/suanmei/iMove/issues" 37 | }, 38 | "dependencies": { 39 | "@ant-design/icons": "^4.0.6", 40 | "@antv/x6": "^1.7.12", 41 | "@imove/compile-code": "^0.1.0", 42 | "@monaco-editor/react": "^3.7.4", 43 | "antd": "^4.5.3", 44 | "axios": "^0.21.0", 45 | "fr-generator": "^1.0.0", 46 | "jszip": "^3.5.0", 47 | "lodash.merge": "^4.6.2", 48 | "query-string": "^6.13.7", 49 | "react-color": "^2.18.1", 50 | "react-json-view": "^1.19.1", 51 | "react-split-pane-v2": "^1.0.3" 52 | }, 53 | "devDependencies": { 54 | "@types/lodash.merge": "^4.6.6", 55 | "@types/react": "^16.9.34", 56 | "@types/react-color": "^3.0.4", 57 | "@types/react-dom": "^16.9.7" 58 | }, 59 | "peerDependencies": { 60 | "@ant-design/icons": "^4.0.6", 61 | "@antv/x6": "0.10.59", 62 | "antd": "^4.5.3", 63 | "react": "^16.13.1", 64 | "react-dom": "^16.13.1" 65 | }, 66 | "gitHead": "60b114ff262513544204f33a9bbcb6220d6c1c5d" 67 | } 68 | -------------------------------------------------------------------------------- /packages/core/rollup.config.js: -------------------------------------------------------------------------------- 1 | import pkg from './package.json'; 2 | import postcss from 'rollup-plugin-postcss'; 3 | import commonjs from '@rollup/plugin-commonjs'; 4 | import typescript from 'rollup-plugin-typescript'; 5 | import rollupBaseConfig from '../../rollup.config'; 6 | 7 | export default Object.assign(rollupBaseConfig, { 8 | plugins: [ 9 | postcss(), 10 | typescript(), 11 | commonjs() 12 | ], 13 | output: [ 14 | { 15 | file: pkg.main, 16 | format: 'cjs', 17 | }, 18 | { 19 | file: pkg.module, 20 | format: 'es', 21 | }, 22 | ], 23 | external: Object.keys(pkg.peerDependencies), 24 | }); 25 | -------------------------------------------------------------------------------- /packages/core/scripts/prepublish.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const path = require('path'); 4 | const prePublish = require('../../../scripts/prepublish'); 5 | 6 | prePublish('@imove/core', path.join(__dirname, '../')); 7 | -------------------------------------------------------------------------------- /packages/core/src/api/index.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { message } from 'antd'; 3 | 4 | const LOCAL_CONFIG_KEY = 'IMOVE:LOCAL_CONFIG_KEY'; 5 | export interface ILocalConfig { 6 | ip: string; 7 | port: string; 8 | npmRegistry: string; 9 | } 10 | 11 | export enum ActionType { 12 | create = 'create', 13 | update = 'update', 14 | remove = 'remove', 15 | } 16 | 17 | export interface IModifyGraphAction { 18 | type: string; 19 | actionType: ActionType; 20 | data: any; 21 | } 22 | 23 | interface RequestConfig { 24 | url: string; 25 | method?: 'get' | 'post'; 26 | params?: { [key: string]: any }; 27 | headers?: { [key: string]: string }; 28 | } 29 | 30 | const request = (function () { 31 | const instance = axios.create(); 32 | instance.interceptors.response.use((response: any) => { 33 | const { data } = response || {}; 34 | const { success, msg } = data || {}; 35 | if (success) { 36 | return data; 37 | } else { 38 | message.error(msg); 39 | return Promise.reject(data); 40 | } 41 | }); 42 | return (config: RequestConfig) => { 43 | const { url, method = 'post', params, headers = {} } = config; 44 | return instance.request({ 45 | url, 46 | method, 47 | headers: { 48 | 'Content-Type': 'application/json;charset=utf-8', 49 | ...headers, 50 | }, 51 | data: params, 52 | timeout: 3000, 53 | }); 54 | }; 55 | })(); 56 | 57 | /** 58 | * get local config data (saved in localStorage) 59 | * @returns local config 60 | */ 61 | export const getLocalConfig = (): ILocalConfig => { 62 | const savedConfigString = localStorage.getItem(LOCAL_CONFIG_KEY) || ''; 63 | let savedConfig = {} as ILocalConfig; 64 | try { 65 | savedConfig = JSON.parse(savedConfigString); 66 | } catch (e) {} 67 | return { 68 | ip: savedConfig.ip || '127.0.0.1', 69 | port: savedConfig.port || '3500', 70 | npmRegistry: savedConfig.npmRegistry || 'https://registry.npm.taobao.org', 71 | }; 72 | }; 73 | 74 | /** 75 | * get local config data (saved in localStorage) 76 | */ 77 | export const updateLocalConfig = (config: ILocalConfig) => { 78 | const savedConfig = getLocalConfig(); 79 | savedConfig.ip = config.ip || savedConfig.ip; 80 | savedConfig.port = config.port || savedConfig.port; 81 | savedConfig.npmRegistry = ( 82 | config.npmRegistry || savedConfig.npmRegistry 83 | ).replace(/\/$/, ''); 84 | localStorage.setItem(LOCAL_CONFIG_KEY, JSON.stringify(savedConfig)); 85 | }; 86 | 87 | export const localConnect = () => { 88 | const localConfig = getLocalConfig(); 89 | return fetch(`http://${localConfig.ip}:${localConfig.port}/api/connect`, { 90 | method: 'GET', 91 | headers: { 'content-type': 'application/json' }, 92 | }); 93 | }; 94 | 95 | export const localSave = (data: any) => { 96 | const localConfig = getLocalConfig(); 97 | fetch(`http://${localConfig.ip}:${localConfig.port}/api/save`, { 98 | method: 'POST', 99 | headers: { 'content-type': 'application/json' }, 100 | body: JSON.stringify(data), 101 | }); 102 | }; 103 | 104 | export const queryGraph = (projectId: string) => { 105 | return request({ 106 | url: '/api/queryGraph', 107 | params: { 108 | projectId, 109 | }, 110 | }); 111 | }; 112 | 113 | export const modifyGraph = ( 114 | projectId: string, 115 | actions: IModifyGraphAction[], 116 | ) => { 117 | return request({ 118 | url: '/api/modifyGraph', 119 | params: { 120 | projectId, 121 | actions, 122 | }, 123 | }); 124 | }; 125 | -------------------------------------------------------------------------------- /packages/core/src/common/baseCell/behavior.ts: -------------------------------------------------------------------------------- 1 | import { Shape } from '@antv/x6'; 2 | 3 | const schema = { 4 | base: Shape.Rect, 5 | shape: 'imove-behavior', 6 | width: 60, 7 | height: 30, 8 | label: '处理', 9 | attrs: { 10 | body: { 11 | fill: '#BCD0FF', 12 | stroke: '#6B8CD7', 13 | rx: 4, 14 | ry: 4, 15 | }, 16 | label: { 17 | fill: '#333', 18 | fontSize: 13, 19 | fontWeight: 500, 20 | textWrap: { width: '100%' }, 21 | }, 22 | }, 23 | ports: { 24 | groups: { 25 | top: { 26 | position: 'top', 27 | attrs: { 28 | circle: { 29 | r: 2.5, 30 | magnet: true, 31 | stroke: '#4E68A3', 32 | strokeWidth: 2, 33 | fill: '#fff', 34 | }, 35 | }, 36 | }, 37 | right: { 38 | position: 'right', 39 | attrs: { 40 | circle: { 41 | r: 2.5, 42 | magnet: true, 43 | stroke: '#4E68A3', 44 | strokeWidth: 2, 45 | fill: '#fff', 46 | }, 47 | }, 48 | }, 49 | bottom: { 50 | position: 'bottom', 51 | attrs: { 52 | circle: { 53 | r: 2.5, 54 | magnet: true, 55 | stroke: '#4E68A3', 56 | strokeWidth: 2, 57 | fill: '#fff', 58 | }, 59 | }, 60 | }, 61 | left: { 62 | position: 'left', 63 | attrs: { 64 | circle: { 65 | r: 2.5, 66 | magnet: true, 67 | stroke: '#4E68A3', 68 | strokeWidth: 2, 69 | fill: '#fff', 70 | }, 71 | }, 72 | }, 73 | }, 74 | items: [ 75 | { 76 | id: 'top', 77 | group: 'top', 78 | }, 79 | { 80 | id: 'right', 81 | group: 'right', 82 | }, 83 | { 84 | id: 'bottom', 85 | group: 'bottom', 86 | }, 87 | { 88 | id: 'left', 89 | group: 'left', 90 | }, 91 | ], 92 | }, 93 | data: { 94 | label: '处理', 95 | configSchema: '{\n \n}', 96 | configData: {}, 97 | dependencies: '{\n \n}', 98 | code: 'export default async function(ctx) {\n \n}', 99 | }, 100 | }; 101 | 102 | export default schema; 103 | -------------------------------------------------------------------------------- /packages/core/src/common/baseCell/branch.ts: -------------------------------------------------------------------------------- 1 | import { Shape } from '@antv/x6'; 2 | 3 | const schema = { 4 | base: Shape.Polygon, 5 | shape: 'imove-branch', 6 | width: 40, 7 | height: 30, 8 | attrs: { 9 | body: { 10 | strokeWidth: 2, 11 | stroke: '#FFB96B', 12 | fill: '#FFF6D1', 13 | refPoints: '0,10 10,0 20,10 10,20', 14 | }, 15 | label: { 16 | refX: 0.5, 17 | refY: 1, 18 | text: '判断', 19 | fill: '#333', 20 | fontSize: 13, 21 | fontWeight: 500, 22 | textAnchor: 'middle', 23 | textVerticalAnchor: 'bottom', 24 | transform: 'matrix(1,0,0,1,0,-6)', 25 | }, 26 | }, 27 | ports: { 28 | groups: { 29 | top: { 30 | position: 'top', 31 | attrs: { 32 | circle: { 33 | r: 2.5, 34 | magnet: true, 35 | stroke: '#DD7500', 36 | strokeWidth: 2, 37 | fill: '#fff', 38 | }, 39 | }, 40 | }, 41 | right: { 42 | position: 'right', 43 | attrs: { 44 | circle: { 45 | r: 2.5, 46 | magnet: true, 47 | stroke: '#DD7500', 48 | strokeWidth: 2, 49 | fill: '#fff', 50 | }, 51 | }, 52 | label: { 53 | position: 'right', 54 | }, 55 | }, 56 | bottom: { 57 | position: 'bottom', 58 | attrs: { 59 | circle: { 60 | r: 2.5, 61 | magnet: true, 62 | stroke: '#DD7500', 63 | strokeWidth: 2, 64 | fill: '#fff', 65 | }, 66 | }, 67 | label: { 68 | position: 'bottom', 69 | }, 70 | }, 71 | left: { 72 | position: 'left', 73 | attrs: { 74 | circle: { 75 | r: 2.5, 76 | magnet: true, 77 | stroke: '#DD7500', 78 | strokeWidth: 2, 79 | fill: '#fff', 80 | }, 81 | }, 82 | }, 83 | }, 84 | items: [ 85 | { 86 | id: 'right', 87 | group: 'right', 88 | attrs: { 89 | text: { 90 | text: '', 91 | }, 92 | }, 93 | }, 94 | { 95 | id: 'bottom', 96 | group: 'bottom', 97 | attrs: { 98 | text: { 99 | text: '', 100 | }, 101 | }, 102 | }, 103 | { 104 | id: 'left', 105 | group: 'left', 106 | }, 107 | ], 108 | }, 109 | data: { 110 | label: '判断', 111 | configSchema: '{\n \n}', 112 | configData: {}, 113 | dependencies: '{\n \n}', 114 | ports: { 115 | right: { 116 | condition: 'true', 117 | }, 118 | bottom: { 119 | condition: 'false', 120 | }, 121 | }, 122 | code: 'export default async function(ctx) {\n return true;\n}', 123 | }, 124 | }; 125 | 126 | export default schema; 127 | -------------------------------------------------------------------------------- /packages/core/src/common/baseCell/index.ts: -------------------------------------------------------------------------------- 1 | import start from './start'; 2 | import branch from './branch'; 3 | import behavior from './behavior'; 4 | 5 | const cellSchemaMap: { [key: string]: any } = { 6 | 'imove-start': start, 7 | 'imove-branch': branch, 8 | 'imove-behavior': behavior, 9 | }; 10 | 11 | export default cellSchemaMap; 12 | -------------------------------------------------------------------------------- /packages/core/src/common/baseCell/start.ts: -------------------------------------------------------------------------------- 1 | import { Shape } from '@antv/x6'; 2 | 3 | const schema = { 4 | base: Shape.Circle, 5 | shape: 'imove-start', 6 | width: 60, 7 | height: 60, 8 | label: '开始', 9 | attrs: { 10 | body: { 11 | fill: '#6B8CD7', 12 | strokeWidth: 2, 13 | stroke: '#4E68A3', 14 | }, 15 | label: { 16 | fill: '#FFF', 17 | fontSize: 13, 18 | fontWeight: 500, 19 | textWrap: { width: '100%' }, 20 | }, 21 | }, 22 | ports: { 23 | groups: { 24 | top: { 25 | position: 'top', 26 | zIndex: 10, 27 | attrs: { 28 | circle: { 29 | r: 2.5, 30 | magnet: true, 31 | stroke: '#4E68A3', 32 | strokeWidth: 2, 33 | fill: '#fff', 34 | }, 35 | }, 36 | }, 37 | right: { 38 | position: 'right', 39 | zIndex: 10, 40 | attrs: { 41 | circle: { 42 | r: 2.5, 43 | magnet: true, 44 | stroke: '#4E68A3', 45 | strokeWidth: 2, 46 | fill: '#fff', 47 | }, 48 | }, 49 | }, 50 | bottom: { 51 | position: 'bottom', 52 | zIndex: 10, 53 | attrs: { 54 | circle: { 55 | r: 2.5, 56 | magnet: true, 57 | stroke: '#4E68A3', 58 | strokeWidth: 2, 59 | fill: '#fff', 60 | }, 61 | }, 62 | }, 63 | left: { 64 | position: 'left', 65 | zIndex: 10, 66 | attrs: { 67 | circle: { 68 | r: 2.5, 69 | magnet: true, 70 | stroke: '#4E68A3', 71 | strokeWidth: 2, 72 | fill: '#fff', 73 | }, 74 | }, 75 | }, 76 | }, 77 | items: [ 78 | { 79 | id: 'top', 80 | group: 'top', 81 | }, 82 | { 83 | id: 'right', 84 | group: 'right', 85 | }, 86 | { 87 | id: 'bottom', 88 | group: 'bottom', 89 | }, 90 | { 91 | id: 'left', 92 | group: 'left', 93 | }, 94 | ], 95 | }, 96 | data: { 97 | label: '开始', 98 | configSchema: '{\n \n}', 99 | configData: {}, 100 | trigger: 'start', 101 | dependencies: '{\n \n}', 102 | code: 'export default async function(ctx) {\n \n}', 103 | }, 104 | }; 105 | 106 | export default schema; 107 | -------------------------------------------------------------------------------- /packages/core/src/common/const.ts: -------------------------------------------------------------------------------- 1 | export const MIN_ZOOM = 0.5; 2 | export const MAX_ZOOM = 1.5; 3 | export const ZOOM_STEP = 0.1; 4 | -------------------------------------------------------------------------------- /packages/core/src/common/previewCell/behavior.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import styles from './index.module.less'; 4 | 5 | interface IProps { 6 | title?: string; 7 | } 8 | 9 | const Cell: React.FC = (props) => { 10 | const { title = '处理', ...rest } = props; 11 | return ( 12 |
13 |
14 | {title} 15 |
16 |
17 | ); 18 | }; 19 | 20 | export default Cell; 21 | -------------------------------------------------------------------------------- /packages/core/src/common/previewCell/branch.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import styles from './index.module.less'; 4 | 5 | interface IProps { 6 | title?: string; 7 | } 8 | 9 | const Cell: React.FC = (props) => { 10 | const { title = '判断', ...rest } = props; 11 | const z = (n: any) => n * 1.2; 12 | return ( 13 |
14 | 15 | 23 | 24 | {title} 25 |
26 | ); 27 | }; 28 | 29 | export default Cell; 30 | -------------------------------------------------------------------------------- /packages/core/src/common/previewCell/index.module.less: -------------------------------------------------------------------------------- 1 | .box { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | width: 60px; 6 | height: 60px; 7 | background-color: #f2f2f2; 8 | position: relative; 9 | cursor: pointer; 10 | .text { 11 | z-index: 1; 12 | position: absolute; 13 | top: 15px; 14 | left: 0; 15 | width: 100%; 16 | height: 100%; 17 | font-size: 11px; 18 | height: 30px; 19 | line-height: 30px; 20 | text-align: center; 21 | font-weight: 500; 22 | } 23 | } 24 | 25 | .circle { 26 | display: flex; 27 | justify-content: center; 28 | align-items: center; 29 | width: 40px; 30 | height: 40px; 31 | border-radius: 30px; 32 | color: #FFF; 33 | background-color: #6B8CD7; 34 | border: 2px solid #4E68A3; 35 | } 36 | 37 | .rect { 38 | display: flex; 39 | justify-content: center; 40 | align-items: center; 41 | width: 52px; 42 | height: 30px; 43 | font-size: 12px; 44 | color: #333; 45 | border-radius: 4px; 46 | background-color: #BCD0FF; 47 | border: 2px solid #6B8CD7; 48 | } -------------------------------------------------------------------------------- /packages/core/src/common/previewCell/index.ts: -------------------------------------------------------------------------------- 1 | import start from './start'; 2 | import branch from './branch'; 3 | import behavior from './behavior'; 4 | 5 | const cellMap: { [key: string]: any } = { 6 | 'imove-start': start, 7 | 'imove-branch': branch, 8 | 'imove-behavior': behavior, 9 | }; 10 | 11 | export default cellMap; 12 | -------------------------------------------------------------------------------- /packages/core/src/common/previewCell/start.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import styles from './index.module.less'; 4 | 5 | interface IProps extends React.HTMLProps { 6 | title?: string; 7 | } 8 | 9 | const Cell: React.FC = (props) => { 10 | const { title = '开始', ...rest } = props; 11 | return ( 12 |
13 |
14 | {title} 15 |
16 |
17 | ); 18 | }; 19 | 20 | export default Cell; 21 | -------------------------------------------------------------------------------- /packages/core/src/components/codeEditor/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo, useState, useEffect } from 'react'; 2 | 3 | import { 4 | monaco, 5 | EditorDidMount, 6 | ControlledEditor, 7 | ControlledEditorProps, 8 | } from '@monaco-editor/react'; 9 | 10 | import monokaiTheme from './theme-monokai'; 11 | 12 | let KeyMod: any = {}; 13 | let KeyCode: any = {}; 14 | monaco.init().then((monaco) => { 15 | KeyMod = monaco.KeyMod; 16 | KeyCode = monaco.KeyCode; 17 | monaco.editor.defineTheme('monokai', monokaiTheme); 18 | }); 19 | 20 | const CODE_EDITOR_OPTIONS = { 21 | fontSize: 14, 22 | }; 23 | 24 | interface IProps extends ControlledEditorProps { 25 | onSave?: (code: string) => void; 26 | } 27 | 28 | export const CodeEditor: React.FC = (props) => { 29 | const { options, editorDidMount, onSave, ...rest } = props; 30 | const [editorInst, setEditorInst] = useState(); 31 | 32 | const editorOptions = useMemo(() => { 33 | return Object.assign({}, CODE_EDITOR_OPTIONS, options); 34 | }, [options]); 35 | 36 | useEffect(() => { 37 | if (editorInst) { 38 | // NOTE: how to add command(https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.istandalonecodeeditor.html#addcommand) 39 | editorInst.addCommand(KeyMod.CtrlCmd | KeyCode.KEY_S, () => { 40 | onSave && onSave(editorInst.getValue()); 41 | }); 42 | } 43 | }, [editorInst, onSave]); 44 | 45 | const onEditorDidMount: EditorDidMount = (getEditorValue, editor) => { 46 | setEditorInst(editor); 47 | editorDidMount && editorDidMount(getEditorValue, editor); 48 | }; 49 | 50 | return ( 51 | 58 | ); 59 | }; 60 | 61 | export default CodeEditor; 62 | -------------------------------------------------------------------------------- /packages/core/src/components/codeEditor/theme-monokai.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | base: 'vs-dark', 3 | inherit: true, 4 | rules: [ 5 | { 6 | background: '272822', 7 | token: '', 8 | }, 9 | { 10 | foreground: '75715e', 11 | token: 'comment', 12 | }, 13 | { 14 | foreground: 'e6db74', 15 | token: 'string', 16 | }, 17 | { 18 | foreground: 'ae81ff', 19 | token: 'constant.numeric', 20 | }, 21 | { 22 | foreground: 'ae81ff', 23 | token: 'constant.language', 24 | }, 25 | { 26 | foreground: 'ae81ff', 27 | token: 'constant.character', 28 | }, 29 | { 30 | foreground: 'ae81ff', 31 | token: 'constant.other', 32 | }, 33 | { 34 | foreground: 'f92672', 35 | token: 'keyword', 36 | }, 37 | { 38 | foreground: 'f92672', 39 | token: 'storage', 40 | }, 41 | { 42 | foreground: '66d9ef', 43 | fontStyle: 'italic', 44 | token: 'storage.type', 45 | }, 46 | { 47 | foreground: 'a6e22e', 48 | fontStyle: 'underline', 49 | token: 'entity.name.class', 50 | }, 51 | { 52 | foreground: 'a6e22e', 53 | fontStyle: 'italic underline', 54 | token: 'entity.other.inherited-class', 55 | }, 56 | { 57 | foreground: 'a6e22e', 58 | token: 'entity.name.function', 59 | }, 60 | { 61 | foreground: 'fd971f', 62 | fontStyle: 'italic', 63 | token: 'variable.parameter', 64 | }, 65 | { 66 | foreground: 'f92672', 67 | token: 'entity.name.tag', 68 | }, 69 | { 70 | foreground: 'a6e22e', 71 | token: 'entity.other.attribute-name', 72 | }, 73 | { 74 | foreground: '66d9ef', 75 | token: 'support.function', 76 | }, 77 | { 78 | foreground: '66d9ef', 79 | token: 'support.constant', 80 | }, 81 | { 82 | foreground: '66d9ef', 83 | fontStyle: 'italic', 84 | token: 'support.type', 85 | }, 86 | { 87 | foreground: '66d9ef', 88 | fontStyle: 'italic', 89 | token: 'support.class', 90 | }, 91 | { 92 | foreground: 'f8f8f0', 93 | background: 'f92672', 94 | token: 'invalid', 95 | }, 96 | { 97 | foreground: 'f8f8f0', 98 | background: 'ae81ff', 99 | token: 'invalid.deprecated', 100 | }, 101 | { 102 | foreground: 'cfcfc2', 103 | token: 'meta.structure.dictionary.json string.quoted.double.json', 104 | }, 105 | { 106 | foreground: '75715e', 107 | token: 'meta.diff', 108 | }, 109 | { 110 | foreground: '75715e', 111 | token: 'meta.diff.header', 112 | }, 113 | { 114 | foreground: 'f92672', 115 | token: 'markup.deleted', 116 | }, 117 | { 118 | foreground: 'a6e22e', 119 | token: 'markup.inserted', 120 | }, 121 | { 122 | foreground: 'e6db74', 123 | token: 'markup.changed', 124 | }, 125 | { 126 | foreground: 'ae81ffa0', 127 | token: 'constant.numeric.line-number.find-in-files - match', 128 | }, 129 | { 130 | foreground: 'e6db74', 131 | token: 'entity.name.filename.find-in-files', 132 | }, 133 | ], 134 | colors: { 135 | 'editor.foreground': '#F8F8F2', 136 | 'editor.background': '#272822', 137 | 'editor.selectionBackground': '#49483E', 138 | 'editor.lineHighlightBackground': '#3E3D32', 139 | 'editorCursor.foreground': '#F8F8F0', 140 | 'editorWhitespace.foreground': '#3B3A32', 141 | 'editorIndentGuide.activeBackground': '#9D550FB0', 142 | 'editor.selectionHighlightBorder': '#222218', 143 | }, 144 | }; 145 | -------------------------------------------------------------------------------- /packages/core/src/components/codeRun/index.module.less: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | flex-direction: column; 4 | flex: 1; 5 | height: 100%; 6 | 7 | .pane { 8 | position: relative; 9 | display: flex; 10 | flex: 1; 11 | 12 | .runWrapper { 13 | position: absolute; 14 | top: 0; 15 | right: 0; 16 | } 17 | } 18 | } 19 | 20 | .card { 21 | display: flex; 22 | flex-direction: column; 23 | flex: 1; 24 | background-color: #ffffff; 25 | 26 | .cardTitleText { 27 | margin: 10px 24px 0; 28 | font-size: 16px; 29 | font-weight: 500; 30 | color: #000000; 31 | } 32 | 33 | .cardBody { 34 | padding: 10px 24px; 35 | height: 100%; 36 | overflow: scroll; 37 | } 38 | } 39 | 40 | :global { 41 | .ant-tabs { 42 | height: 100%; 43 | } 44 | 45 | .ant-tabs-content-holder { 46 | height: 100%; 47 | } 48 | 49 | .ant-tabs-content { 50 | height: 100%; 51 | overflow: scroll; 52 | } 53 | 54 | .sc-bdVaJa.cjjWdp { 55 | opacity: 1; 56 | background-color: #efefef; 57 | 58 | &:hover { 59 | border-color: transparent; 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /packages/core/src/components/codeRun/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useCallback } from 'react'; 2 | 3 | import styles from './index.module.less'; 4 | 5 | import { Button } from 'antd'; 6 | import { Graph } from '@antv/x6'; 7 | import Console from '../console'; 8 | import InputPanel from './inputPanel'; 9 | import JsonView from 'react-json-view'; 10 | import { executeScript } from '../../utils'; 11 | import { PlayCircleFilled, LoadingOutlined } from '@ant-design/icons'; 12 | import { compileForOnline } from '@imove/compile-code'; 13 | import { toSelectedCellsJSON } from '../../utils/flowChartUtils'; 14 | import SplitPane from 'react-split-pane-v2'; 15 | 16 | const Pane = SplitPane.Pane; 17 | 18 | const defaultInput = { 19 | pipe: {}, 20 | context: {}, 21 | payload: {}, 22 | config: {}, 23 | }; 24 | 25 | interface ICardProps { 26 | title: string; 27 | } 28 | 29 | const Card: React.FC = (props) => { 30 | const { title } = props; 31 | 32 | return ( 33 |
34 |
{title}
35 |
{props.children}
36 |
37 | ); 38 | }; 39 | 40 | interface ICodeRunProps { 41 | flowChart: Graph; 42 | } 43 | 44 | function isJson(obj: any) { 45 | var t = typeof obj; 46 | return ['boolean', 'number', 'string', 'symbol', 'function'].indexOf(t) == -1; 47 | } 48 | 49 | const CodeRun: React.FC = (props) => { 50 | const { flowChart } = props; 51 | const [isRunning, setIsRunning] = useState(false); 52 | const [input, setInput] = useState(defaultInput); 53 | const [output, setOutput] = useState({}); 54 | 55 | useEffect(() => { 56 | // NOTE: listen the event that iMove online exec ends 57 | const handler = (data: any) => { 58 | setIsRunning(false); 59 | // console.dir(data) 60 | 61 | if (isJson(data.detail) == true) { 62 | setOutput(data.detail || {}); 63 | // console.dir( data.detail) 64 | } 65 | }; 66 | window.addEventListener('iMoveOnlineExecEnds', handler); 67 | return () => { 68 | window.removeEventListener('iMoveOnlineExecEnds', handler); 69 | }; 70 | }, []); 71 | 72 | const onClickRun = useCallback(() => { 73 | setIsRunning(true); 74 | const selectedCelssJson = toSelectedCellsJSON(flowChart); 75 | const compiledCode = compileForOnline(selectedCelssJson, input); 76 | executeScript(compiledCode); 77 | }, [flowChart, input]); 78 | 79 | const onChangeInput = useCallback((val: any) => { 80 | setInput(val); 81 | }, []); 82 | 83 | return ( 84 |
85 | 86 | 87 | 88 | 89 |
90 | {isRunning ? ( 91 | 94 | ) : ( 95 | 98 | )} 99 |
100 | 101 | 102 | 103 |
104 | 105 | 106 | 114 | 115 | 116 |
117 |
118 | 119 | 120 | 121 |
122 |
123 | ); 124 | }; 125 | 126 | export default CodeRun; 127 | -------------------------------------------------------------------------------- /packages/core/src/components/codeRun/inputPanel/index.module.less: -------------------------------------------------------------------------------- 1 | .itemHeader { 2 | margin-bottom: 8px; 3 | 4 | .itemTitleText { 5 | font-size: 15px; 6 | font-weight: 500; 7 | } 8 | 9 | .itemDescText { 10 | margin-left: 5px; 11 | font-size: 12px; 12 | color: #999; 13 | } 14 | } 15 | 16 | :global { 17 | .ant-tabs-nav { 18 | margin-bottom: 5px; 19 | } 20 | 21 | .ant-form-item { 22 | margin-bottom: 8px; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/core/src/components/colorPicker/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | 3 | import { SketchPicker, ColorResult } from 'react-color'; 4 | 5 | interface IProps { 6 | color: string; 7 | onChangeComplete: (color: string) => void; 8 | } 9 | 10 | const ColorPicker: React.FC = (props) => { 11 | const { color, onChangeComplete: changeCb } = props; 12 | const [curColor, setCurColor] = useState(color); 13 | 14 | // life 15 | useEffect(() => setCurColor(color), [color]); 16 | 17 | // events 18 | const onChange = (color: ColorResult): void => setCurColor(color.hex); 19 | const onChangeComplete = (color: ColorResult): void => changeCb(color.hex); 20 | 21 | return ( 22 | 27 | ); 28 | }; 29 | 30 | export default ColorPicker; 31 | -------------------------------------------------------------------------------- /packages/core/src/components/console/index.module.less: -------------------------------------------------------------------------------- 1 | .container { 2 | width: 100%; 3 | height: 100%; 4 | overflow: scroll; 5 | 6 | :global { 7 | .ant-tabs-nav { 8 | margin-bottom: 0; 9 | } 10 | } 11 | 12 | .logPanel { 13 | height: 100%; 14 | background: #272823; 15 | 16 | .logLine { 17 | display: flex; 18 | flex-direction: row; 19 | position: relative; 20 | margin-top: -1px; 21 | padding-left: 10px; 22 | font-size: 13px; 23 | border: 1px solid transparent; 24 | 25 | &:first-child { 26 | margin-top: 0; 27 | } 28 | 29 | .logIcon { 30 | padding-top: 0.6rem; 31 | width: 1rem; 32 | height: 1rem; 33 | } 34 | 35 | .logContent { 36 | margin-left: 8px; 37 | min-height: 18px; 38 | padding: 0.4rem 1.5rem 0.4rem 0px; 39 | word-break: break-all; 40 | 41 | :global { 42 | .react-json-view { 43 | display: inline-block; 44 | vertical-align: top; 45 | } 46 | } 47 | } 48 | } 49 | } 50 | 51 | .toolBar { 52 | display: flex; 53 | flex-direction: row; 54 | 55 | .levels { 56 | margin-left: 4px; 57 | width: 150px; 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /packages/core/src/components/miniMapSimpleNode/index.tsx: -------------------------------------------------------------------------------- 1 | import { NodeView } from '@antv/x6'; 2 | 3 | class MiniMapSimpleNode extends NodeView { 4 | protected renderMarkup() { 5 | return this.renderJSONMarkup({ 6 | tagName: 'rect', 7 | selector: 'body', 8 | }); 9 | } 10 | 11 | protected renderPorts() { 12 | return null; 13 | } 14 | 15 | update() { 16 | super.update({ 17 | body: { 18 | refWidth: '100%', 19 | refHeight: '100%', 20 | fill: '#D9D9D9', 21 | }, 22 | }); 23 | } 24 | } 25 | 26 | export default MiniMapSimpleNode; 27 | -------------------------------------------------------------------------------- /packages/core/src/components/schemaForm/index.module.less: -------------------------------------------------------------------------------- 1 | .btnWrap{ 2 | width:100%; 3 | display:flex; 4 | flex-direction: row; 5 | justify-content:flex-end; 6 | .btn{ 7 | margin-top: 20px; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/core/src/components/xIcon/index.less: -------------------------------------------------------------------------------- 1 | #icon-select-all, 2 | #icon-align-top, 3 | #icon-align-left, 4 | #icon-align-vertical-center, 5 | #icon-align-horizontal-center, 6 | #icon-align-bottom, 7 | #icon-align-right { 8 | path { 9 | fill: currentColor; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/core/src/components/xIcon/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createFromIconfontCN } from '@ant-design/icons'; 3 | import './index.less'; 4 | 5 | interface IconFontProps { 6 | type: string; 7 | className?: string; 8 | } 9 | 10 | const XIcon: React.SFC = createFromIconfontCN({ 11 | scriptUrl: '//at.alicdn.com/t/font_2024452_r7nz0cw949.js', 12 | }); 13 | 14 | export default XIcon; 15 | -------------------------------------------------------------------------------- /packages/core/src/global.index.less: -------------------------------------------------------------------------------- 1 | .react-json-view { 2 | .collapsed-icon, .expanded-icon { 3 | svg { 4 | vertical-align: middle !important; 5 | } 6 | } 7 | } 8 | 9 | .x6-node { 10 | 11 | .x6-port-body { 12 | visibility: hidden; 13 | } 14 | 15 | &:hover { 16 | .x6-port-body { 17 | visibility: visible; 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /packages/core/src/hooks/useClickAway.ts: -------------------------------------------------------------------------------- 1 | import { useRef, useEffect, MutableRefObject } from 'react'; 2 | 3 | type EventType = MouseEvent | TouchEvent; 4 | type TargetElement = HTMLElement | Element | Document | Window; 5 | type BasicTarget = 6 | | (() => T | null) 7 | | T 8 | | null 9 | | MutableRefObject; 10 | 11 | const defaultEvent = 'click'; 12 | 13 | const getTargetElement = ( 14 | target: BasicTarget, 15 | defaultElement?: TargetElement, 16 | ): TargetElement | undefined | null => { 17 | if (!target) { 18 | return defaultElement; 19 | } 20 | 21 | let targetElement: TargetElement | undefined | null; 22 | if (typeof target === 'function') { 23 | targetElement = target(); 24 | } else if ('current' in target) { 25 | targetElement = target.current; 26 | } else { 27 | targetElement = target; 28 | } 29 | 30 | return targetElement; 31 | }; 32 | 33 | const useClickAway = ( 34 | onClickAway: (event: EventType) => void, 35 | target: BasicTarget | BasicTarget[], 36 | eventName: string = defaultEvent, 37 | ): void => { 38 | const onClickAwayRef = useRef(onClickAway); 39 | onClickAwayRef.current = onClickAway; 40 | useEffect(() => { 41 | const handler = (event: any) => { 42 | const targets = Array.isArray(target) ? target : [target]; 43 | if ( 44 | targets.some((targetItem) => { 45 | const targetElement = getTargetElement(targetItem) as HTMLElement; 46 | return !targetElement || targetElement?.contains(event.target); 47 | }) 48 | ) { 49 | return; 50 | } 51 | onClickAwayRef.current(event); 52 | }; 53 | document.addEventListener(eventName, handler); 54 | 55 | return () => { 56 | document.removeEventListener(eventName, handler); 57 | }; 58 | }, [target, eventName]); 59 | }; 60 | 61 | export default useClickAway; 62 | -------------------------------------------------------------------------------- /packages/core/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | import './global.index.less'; 4 | import '@antv/x6/dist/x6.css'; 5 | 6 | import { Graph } from '@antv/x6'; 7 | import Layout from './mods/layout'; 8 | import Header from './mods/header'; 9 | import SideBar from './mods/sideBar'; 10 | import ToolBar from './mods/toolBar'; 11 | import FlowChart from './mods/flowChart'; 12 | import SettingBar from './mods/settingBar'; 13 | 14 | interface IProps { 15 | onSave: (data: { nodes: any; edges: any }) => void; 16 | } 17 | 18 | const Core: React.FC = (props) => { 19 | const { onSave } = props; 20 | const [flowChart, setFlowChart] = useState(); 21 | const onFlowChartReady = (flowChart: Graph): void => setFlowChart(flowChart); 22 | return ( 23 | 30 | 31 | 32 | ); 33 | }; 34 | 35 | export default Core; 36 | -------------------------------------------------------------------------------- /packages/core/src/mods/flowChart/codeEditorModal/index.module.less: -------------------------------------------------------------------------------- 1 | .depsInfoModalContent { 2 | padding-top: 20px; 3 | } 4 | 5 | .modal { 6 | :global { 7 | .ant-modal-body { 8 | padding: 0; 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/core/src/mods/flowChart/codeEditorModal/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | 3 | import 'antd/es/modal/style'; 4 | import styles from './index.module.less'; 5 | 6 | import { Graph } from '@antv/x6'; 7 | import { Button, Modal, message } from 'antd'; 8 | import JsonView from 'react-json-view'; 9 | import { safeParse } from '../../../utils'; 10 | import analyzeDeps from '../../../utils/analyzeDeps'; 11 | import CodeEditor from '../../../components/codeEditor'; 12 | 13 | interface IProps { 14 | title?: string; 15 | flowChart: Graph; 16 | } 17 | 18 | const CodeEditModal: React.FC = (props) => { 19 | const { title = '编辑代码', flowChart } = props; 20 | const [code, setCode] = useState(''); 21 | const [visible, setVisible] = useState(false); 22 | 23 | const updateNodeCode = (code: string): void => { 24 | const cell = flowChart.getSelectedCells()[0]; 25 | const { code: oldCode, dependencies } = cell.getData(); 26 | if (code === oldCode) { 27 | return; 28 | } 29 | 30 | cell.setData({ code }); 31 | message.success('代码保存成功', 1); 32 | const excludeDeps = safeParse(dependencies); 33 | analyzeDeps(code, Object.keys(excludeDeps)).then((deps): void => { 34 | if (Object.keys(deps).length > 0) { 35 | Modal.info({ 36 | title: '检测到您的代码有新依赖,已为您自动更新', 37 | content: ( 38 |
39 | 47 |
48 | ), 49 | onOk() { 50 | const newDeps = { ...excludeDeps, ...deps }; 51 | cell.setData({ 52 | code, 53 | dependencies: JSON.stringify(newDeps, null, 2), 54 | }); 55 | // NOTE: notify basic panel to update dependency 56 | flowChart.trigger('settingBar.basicPanel:forceUpdate'); 57 | }, 58 | }); 59 | } 60 | }); 61 | }; 62 | 63 | // life 64 | useEffect(() => { 65 | const handler = () => setVisible(true); 66 | flowChart.on('graph:editCode', handler); 67 | return () => { 68 | flowChart.off('graph:editCode', handler); 69 | }; 70 | }, []); 71 | useEffect(() => { 72 | if (visible) { 73 | const cell = flowChart.getSelectedCells()[0]; 74 | const { code } = cell.getData() || {}; 75 | setCode(code); 76 | } else { 77 | setCode(''); 78 | } 79 | }, [visible]); 80 | 81 | // events 82 | const onOk = (): void => { 83 | setVisible(false); 84 | updateNodeCode(code); 85 | }; 86 | const onCancel = (): void => { 87 | setVisible(false); 88 | }; 89 | const onRunCode = (): void => { 90 | updateNodeCode(code); 91 | flowChart.trigger('graph:runCode'); 92 | }; 93 | const onChangeCode = (ev: any, newCode: string | undefined = ''): void => { 94 | setCode(newCode); 95 | }; 96 | const onSaveCode = (newCode: string) => { 97 | updateNodeCode(newCode); 98 | }; 99 | 100 | return ( 101 | 109 | 取消 110 | , 111 | , 114 | , 117 | ]} 118 | > 119 | 126 | 127 | ); 128 | }; 129 | 130 | export default CodeEditModal; 131 | -------------------------------------------------------------------------------- /packages/core/src/mods/flowChart/codeRunModal/index.module.less: -------------------------------------------------------------------------------- 1 | .modal { 2 | :global { 3 | .ant-modal-body { 4 | padding: 0; 5 | height: 653px; 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /packages/core/src/mods/flowChart/codeRunModal/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useCallback } from 'react'; 2 | 3 | import styles from './index.module.less'; 4 | 5 | import { Modal } from 'antd'; 6 | import { Graph } from '@antv/x6'; 7 | import CodeRun from '../../../components/codeRun'; 8 | 9 | interface IEditModalProps { 10 | title?: string; 11 | flowChart: Graph; 12 | } 13 | 14 | const CodeRunModal: React.FC = (props): JSX.Element => { 15 | const { title = '执行代码', flowChart } = props; 16 | const [visible, setVisible] = useState(false); 17 | 18 | useEffect(() => { 19 | const handler = () => setVisible(true); 20 | flowChart.on('graph:runCode', handler); 21 | return () => { 22 | flowChart.off('graph:runCode', handler); 23 | }; 24 | }, [flowChart]); 25 | 26 | // events 27 | const onClose = useCallback((): void => { 28 | setVisible(false); 29 | }, []); 30 | 31 | return ( 32 | 40 | 41 | 42 | ); 43 | }; 44 | 45 | export default CodeRunModal; 46 | -------------------------------------------------------------------------------- /packages/core/src/mods/flowChart/contextMenu/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useCallback } from 'react'; 2 | 3 | import styles from '../index.module.less'; 4 | 5 | import { Menu } from 'antd'; 6 | import { Graph } from '@antv/x6'; 7 | import useClickAway from '../../../hooks/useClickAway'; 8 | import { nodeMenuConfig, blankMenuConfig } from './menuConfig'; 9 | 10 | interface IProps { 11 | x: number; 12 | y: number; 13 | scene: string; 14 | visible: boolean; 15 | flowChart: Graph; 16 | } 17 | 18 | interface IMenuConfig { 19 | key: string; 20 | title: string; 21 | icon?: React.ReactElement; 22 | children?: IMenuConfig[]; 23 | showDividerBehind?: boolean; 24 | disabled?: boolean | ((flowChart: Graph) => boolean); 25 | handler: (flowChart: Graph) => void; 26 | } 27 | 28 | const menuConfigMap: { [scene: string]: IMenuConfig[] } = { 29 | node: nodeMenuConfig, 30 | blank: blankMenuConfig, 31 | }; 32 | 33 | const FlowChartContextMenu: React.FC = (props) => { 34 | const menuRef = useRef(null); 35 | const { x, y, scene, visible, flowChart } = props; 36 | const menuConfig = menuConfigMap[scene]; 37 | 38 | useClickAway(() => onClickAway(), menuRef); 39 | 40 | const onClickAway = useCallback( 41 | () => flowChart.trigger('graph:hideContextMenu'), 42 | [flowChart], 43 | ); 44 | const onClickMenu = useCallback( 45 | ({ key }) => { 46 | const handlerMap = Helper.makeMenuHandlerMap(menuConfig); 47 | const handler = handlerMap[key]; 48 | if (handler) { 49 | onClickAway(); 50 | handler(flowChart); 51 | } 52 | }, 53 | [flowChart, menuConfig], 54 | ); 55 | 56 | return !visible ? null : ( 57 |
62 | 63 | {Helper.makeMenuContent(flowChart, menuConfig)} 64 | 65 |
66 | ); 67 | }; 68 | 69 | const Helper = { 70 | makeMenuHandlerMap(config: IMenuConfig[]) { 71 | const queue = config.slice(0); 72 | const handlerMap: { [key: string]: (flowChart: Graph) => void } = {}; 73 | while (queue.length > 0) { 74 | const { key, handler, children } = queue.pop() as IMenuConfig; 75 | if (children && children.length > 0) { 76 | queue.push(...children); 77 | } else { 78 | handlerMap[key] = handler; 79 | } 80 | } 81 | return handlerMap; 82 | }, 83 | makeMenuContent(flowChart: Graph, menuConfig: IMenuConfig[]) { 84 | const loop = (config: IMenuConfig[]) => { 85 | return config.map((item) => { 86 | let content = null; 87 | let { 88 | key, 89 | title, 90 | icon, 91 | children, 92 | disabled = false, 93 | showDividerBehind, 94 | } = item; 95 | if (typeof disabled === 'function') { 96 | disabled = disabled(flowChart); 97 | } 98 | if (children && children.length > 0) { 99 | content = ( 100 | 106 | {loop(children)} 107 | 108 | ); 109 | } else { 110 | content = ( 111 | 112 | {title} 113 | 114 | ); 115 | } 116 | return [content, showDividerBehind && ]; 117 | }); 118 | }; 119 | return loop(menuConfig); 120 | }, 121 | }; 122 | 123 | export default FlowChartContextMenu; 124 | -------------------------------------------------------------------------------- /packages/core/src/mods/flowChart/contextMenu/menuConfig/blank.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Graph } from '@antv/x6'; 4 | import XIcon from '../../../../components/xIcon'; 5 | import shortcuts from '../../../../common/shortcuts'; 6 | import { SnippetsOutlined } from '@ant-design/icons'; 7 | 8 | const blankMenuConfig = [ 9 | { 10 | key: 'selectAll', 11 | title: '全选', 12 | icon: , 13 | handler: shortcuts.selectAll.handler, 14 | }, 15 | { 16 | key: 'paste', 17 | title: '粘贴', 18 | icon: , 19 | disabled: (flowChart: Graph) => flowChart.isClipboardEmpty(), 20 | handler: shortcuts.paste.handler, 21 | }, 22 | ]; 23 | 24 | export default blankMenuConfig; 25 | -------------------------------------------------------------------------------- /packages/core/src/mods/flowChart/contextMenu/menuConfig/index.ts: -------------------------------------------------------------------------------- 1 | import nodeMenuConfig from './node'; 2 | import blankMenuConfig from './blank'; 3 | 4 | export { nodeMenuConfig, blankMenuConfig }; 5 | -------------------------------------------------------------------------------- /packages/core/src/mods/flowChart/contextMenu/menuConfig/node.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { 4 | CopyOutlined, 5 | EditOutlined, 6 | CodeOutlined, 7 | FormOutlined, 8 | DeleteOutlined, 9 | } from '@ant-design/icons'; 10 | 11 | import { Graph } from '@antv/x6'; 12 | import XIcon from '../../../../components/xIcon'; 13 | import shortcuts from '../../../../common/shortcuts'; 14 | import { getSelectedNodes } from '../../../../utils/flowChartUtils'; 15 | 16 | const nodeMenuConfig = [ 17 | { 18 | key: 'copy', 19 | title: '复制', 20 | icon: , 21 | handler: shortcuts.copy.handler, 22 | }, 23 | { 24 | key: 'delete', 25 | title: '删除', 26 | icon: , 27 | handler: shortcuts.delete.handler, 28 | }, 29 | { 30 | key: 'rename', 31 | title: '编辑文本', 32 | icon: , 33 | showDividerBehind: true, 34 | handler() { 35 | // TODO 36 | }, 37 | }, 38 | { 39 | key: 'bringToTop', 40 | title: '置于顶层', 41 | icon: , 42 | handler: shortcuts.bringToTop.handler, 43 | }, 44 | { 45 | key: 'bringToBack', 46 | title: '置于底层', 47 | icon: , 48 | showDividerBehind: true, 49 | handler: shortcuts.bringToBack.handler, 50 | }, 51 | { 52 | key: 'editCode', 53 | title: '编辑代码', 54 | icon: , 55 | disabled(flowChart: Graph) { 56 | return getSelectedNodes(flowChart).length !== 1; 57 | }, 58 | handler(flowChart: Graph) { 59 | flowChart.trigger('graph:editCode'); 60 | }, 61 | }, 62 | { 63 | key: 'executeCode', 64 | title: '执行代码', 65 | icon: , 66 | disabled(flowChart: Graph) { 67 | return getSelectedNodes(flowChart).length !== 1; 68 | }, 69 | handler(flowChart: Graph) { 70 | flowChart.trigger('graph:runCode'); 71 | }, 72 | }, 73 | ]; 74 | 75 | export default nodeMenuConfig; 76 | -------------------------------------------------------------------------------- /packages/core/src/mods/flowChart/index.module.less: -------------------------------------------------------------------------------- 1 | .container { 2 | 3 | position: relative; 4 | display: flex; 5 | flex: 1; 6 | width: 100%; 7 | height: 100%; 8 | 9 | .flowChart { 10 | width: 100%; 11 | height: 100%; 12 | } 13 | 14 | .miniMap { 15 | position: absolute; 16 | right: 40px; 17 | bottom: 40px; 18 | 19 | :global { 20 | .x6-widget-minimap-viewport { 21 | border: 1px solid #E0E0E0; 22 | } 23 | } 24 | } 25 | 26 | .contextMenu { 27 | z-index: 1003; 28 | position: fixed; 29 | min-width: 200px; 30 | box-shadow: 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 9px 28px 8px rgba(0, 0, 0, 0.05) !important; 31 | } 32 | } 33 | 34 | // add global for x6 35 | :global { 36 | .x6-widget-transform{ 37 | border-color:rgba(0, 0, 0, 0.3); 38 | border-style: dashed; 39 | background-color: rgba(0, 0, 0, 0.14); 40 | border-radius: 0; 41 | padding: 6px; 42 | margin: -7px 0 0 -7px; 43 | .x6-widget-transform-resize{ 44 | width: 6px; 45 | height: 6px; 46 | border-radius: 0; 47 | border-color:rgba(0, 0, 0, 0.3); 48 | // border: 0 none; 49 | box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.15); 50 | } 51 | .x6-widget-transform-cursor-nw{ 52 | top: -6px; 53 | left: -6px; 54 | } 55 | .x6-widget-transform-cursor-n{ 56 | margin-left: -3px; 57 | top: -6px; 58 | } 59 | .x6-widget-transform-cursor-ne{ 60 | top: -6px; 61 | right: -6px; 62 | } 63 | .x6-widget-transform-cursor-e{ 64 | margin-top: -3px; 65 | right: -6px; 66 | } 67 | .x6-widget-transform-cursor-se{ 68 | right: -6px; 69 | bottom: -6px; 70 | } 71 | .x6-widget-transform-cursor-s{ 72 | margin-left: -3px; 73 | bottom: -6px; 74 | } 75 | .x6-widget-transform-cursor-sw{ 76 | left: -6px; 77 | bottom: -6px; 78 | } 79 | .x6-widget-transform-cursor-w{ 80 | left: -6px; 81 | margin-top: -3px; 82 | } 83 | } 84 | } -------------------------------------------------------------------------------- /packages/core/src/mods/flowChart/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useState, useEffect } from 'react'; 2 | 3 | import styles from './index.module.less'; 4 | 5 | import { Graph } from '@antv/x6'; 6 | import { queryGraph } from '../../api'; 7 | import { parseQuery } from '../../utils'; 8 | import createFlowChart from './createFlowChart'; 9 | import CodeRunModal from './codeRunModal'; 10 | import CodeEditorModal from './codeEditorModal'; 11 | import FlowChartContextMenu from './contextMenu'; 12 | 13 | interface IProps { 14 | onReady: (graph: Graph) => void; 15 | } 16 | 17 | interface IMenuInfo { 18 | x: number; 19 | y: number; 20 | scene: string; 21 | visible: boolean; 22 | } 23 | 24 | const defaultMenuInfo = { 25 | x: 0, 26 | y: 0, 27 | scene: 'blank', 28 | visible: false, 29 | }; 30 | 31 | const FlowChart: React.FC = (props) => { 32 | const { onReady } = props; 33 | const wrapperRef = useRef(null); 34 | const graphRef = useRef(null); 35 | const miniMapRef = useRef(null); 36 | const [flowChart, setFlowChart] = useState(); 37 | const [contextMenuInfo, setContextMenuInfo] = useState( 38 | defaultMenuInfo, 39 | ); 40 | 41 | useEffect(() => { 42 | if (graphRef.current && miniMapRef.current) { 43 | const flowChart = createFlowChart(graphRef.current, miniMapRef.current); 44 | onReady(flowChart); 45 | fetchData(flowChart); 46 | setFlowChart(flowChart); 47 | } 48 | }, []); 49 | 50 | // resize flowChart's size when window size changes 51 | useEffect(() => { 52 | const handler = () => { 53 | requestAnimationFrame(() => { 54 | if (flowChart && wrapperRef && wrapperRef.current) { 55 | const width = wrapperRef.current.clientWidth; 56 | const height = wrapperRef.current.clientHeight; 57 | flowChart.resize(width, height); 58 | } 59 | }); 60 | }; 61 | window.addEventListener('resize', handler); 62 | return () => { 63 | window.removeEventListener('resize', handler); 64 | }; 65 | }, [flowChart, wrapperRef]); 66 | 67 | // NOTE: listen toggling context menu event 68 | useEffect(() => { 69 | const showHandler = (info: IMenuInfo) => { 70 | flowChart?.lockScroller(); 71 | setContextMenuInfo({ ...info, visible: true }); 72 | }; 73 | const hideHandler = () => { 74 | flowChart?.unlockScroller(); 75 | setContextMenuInfo({ ...contextMenuInfo, visible: false }); 76 | }; 77 | if (flowChart) { 78 | flowChart.on('graph:showContextMenu', showHandler); 79 | flowChart.on('graph:hideContextMenu', hideHandler); 80 | } 81 | return () => { 82 | if (flowChart) { 83 | flowChart.off('graph:showContextMenu', showHandler); 84 | flowChart.off('graph:hideContextMenu', hideHandler); 85 | } 86 | }; 87 | }, [flowChart]); 88 | 89 | const fetchData = (flowChart: Graph) => { 90 | const { projectId } = parseQuery(); 91 | queryGraph(projectId as string) 92 | .then((res) => { 93 | const { data: dsl } = res; 94 | flowChart.fromJSON(dsl); 95 | }) 96 | .catch((error) => { 97 | console.log('query graph data failed, the error is:', error); 98 | }); 99 | }; 100 | 101 | return ( 102 |
103 |
104 |
105 | {flowChart && } 106 | {flowChart && } 107 | {flowChart && ( 108 | 109 | )} 110 |
111 | ); 112 | }; 113 | 114 | export default FlowChart; 115 | -------------------------------------------------------------------------------- /packages/core/src/mods/flowChart/registerServerStorage.ts: -------------------------------------------------------------------------------- 1 | import { Graph } from '@antv/x6'; 2 | import merge from 'lodash.merge'; 3 | import { parseQuery } from '../../utils'; 4 | import { modifyGraph, ActionType, IModifyGraphAction } from '../../api'; 5 | 6 | const { projectId } = parseQuery(); 7 | const memQueue: IModifyGraphAction[] = []; 8 | 9 | const validate = (type: string, data: any) => { 10 | if (type === 'node') { 11 | return true; 12 | } else if (type === 'edge') { 13 | const { source, target } = data; 14 | return source.cell && target.cell; 15 | } else { 16 | return false; 17 | } 18 | }; 19 | 20 | const enqueue = (cellType: string, actionType: ActionType, data: any) => { 21 | if (!validate(cellType, data)) { 22 | return; 23 | } 24 | 25 | const foundIndex = memQueue.findIndex( 26 | (item) => 27 | item.type === cellType && 28 | item.actionType === actionType && 29 | item.data.id === data.id, 30 | ); 31 | if (foundIndex > -1) { 32 | const deleted = memQueue.splice(foundIndex, 1)[0]; 33 | merge(deleted.data, data); 34 | } 35 | memQueue.push({ type: cellType, actionType, data }); 36 | }; 37 | 38 | let modifyActionTimer = -1; 39 | const save = ( 40 | flowChart: Graph, 41 | cellType: string, 42 | actionType: ActionType, 43 | data: any, 44 | ) => { 45 | enqueue(cellType, actionType, data); 46 | clearTimeout(modifyActionTimer); 47 | modifyActionTimer = window.setTimeout(() => { 48 | const pushedActions = memQueue.slice(0); 49 | if (pushedActions.length > 0) { 50 | flowChart.trigger('graph:change:modify'); 51 | modifyGraph(projectId, memQueue) 52 | .then((res) => { 53 | memQueue.splice(0, pushedActions.length); 54 | flowChart.trigger('graph:modified', { success: true }); 55 | }) 56 | .catch((error) => { 57 | flowChart.trigger('graph:modified', { success: true, error: error }); 58 | }); 59 | } 60 | }, 100); 61 | }; 62 | 63 | type ActionEventMap = { [key: string]: string[] }; 64 | const nodeActionEventMap: ActionEventMap = { 65 | [ActionType.create]: ['node:added'], 66 | [ActionType.remove]: ['node:removed'], 67 | [ActionType.update]: [ 68 | 'node:moved', 69 | 'node:resized', 70 | 'node:rotated', 71 | 'node:change:ports', 72 | 'node:change:attrs', 73 | 'node:change:data', 74 | 'node:change:zIndex', 75 | ], 76 | }; 77 | 78 | const edgeActionEventMap: ActionEventMap = { 79 | [ActionType.create]: ['edge:connected'], 80 | [ActionType.remove]: ['edge:removed'], 81 | [ActionType.update]: ['edge:moved'], 82 | }; 83 | 84 | export const registerServerStorage = (flowChart: Graph) => { 85 | Object.keys(nodeActionEventMap).forEach((actionType) => { 86 | const events = nodeActionEventMap[actionType]; 87 | events.forEach((event) => { 88 | flowChart.on(event, (args: any) => { 89 | save(flowChart, 'node', actionType as ActionType, args.node.toJSON()); 90 | }); 91 | }); 92 | }); 93 | 94 | Object.keys(edgeActionEventMap).forEach((actionType) => { 95 | const events = edgeActionEventMap[actionType]; 96 | events.forEach((event) => { 97 | flowChart.on(event, (args: any) => { 98 | console.log('edge event:', event, 'args:', args); 99 | save(flowChart, 'edge', actionType as ActionType, args.edge.toJSON()); 100 | }); 101 | }); 102 | }); 103 | }; 104 | -------------------------------------------------------------------------------- /packages/core/src/mods/header/configuration/editModal.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react'; 2 | import 'antd/es/form/style'; 3 | import 'antd/es/modal/style'; 4 | import 'antd/es/input/style'; 5 | import styles from './index.module.less'; 6 | 7 | import { Input, Form, Modal } from 'antd'; 8 | import { FormInstance } from 'antd/lib/form'; 9 | import { ILocalConfig, getLocalConfig, updateLocalConfig } from '../../../api'; 10 | 11 | const PORT_REGEX = /^\d{1,5}$/; 12 | const IP_REGEX = /^(localhost)|(((2(5[0-5]|[0-4]\d))|[01]?\d{1,2})(\.((2(5[0-5]|[0-4]\d))|[01]?\d{1,2})){3})$/; 13 | const NPM_REGISTRY_REGEX = /^https?:\/\//; // 简单 URL 检查 14 | 15 | const makeRules = (regex: RegExp, errTip: string) => { 16 | return [ 17 | { required: true, message: '不能为空!' }, 18 | { 19 | validator: (_: any, val: string) => 20 | regex.test(val) ? Promise.resolve() : Promise.reject(errTip), 21 | }, 22 | ]; 23 | }; 24 | 25 | interface IEditorModalProps { 26 | visible: boolean; 27 | onOk: () => void; 28 | onCancel: () => void; 29 | } 30 | 31 | const EditModal: React.FC = (props: IEditorModalProps) => { 32 | const { visible, onOk, onCancel } = props; 33 | const formRef = useRef(null); 34 | 35 | // events 36 | const onClickOk = (): void => { 37 | if (formRef.current) { 38 | formRef.current 39 | .validateFields() 40 | .then((formValues) => { 41 | updateLocalConfig(formValues as ILocalConfig); 42 | onOk(); 43 | }) 44 | .catch((error) => { 45 | console.log(error.message); 46 | }); 47 | } 48 | }; 49 | 50 | return ( 51 | 60 |
66 | 71 | 72 | 73 | 78 | 79 | 80 | 85 | 86 | 87 |
88 |
89 | ); 90 | }; 91 | 92 | export default EditModal; 93 | -------------------------------------------------------------------------------- /packages/core/src/mods/header/configuration/index.module.less: -------------------------------------------------------------------------------- 1 | .editModal { 2 | 3 | width: 450px; 4 | 5 | :global { 6 | .ant-form-item:last-child { 7 | margin-bottom: 0; 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/core/src/mods/header/configuration/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useCallback } from 'react'; 2 | import { Button } from 'antd'; 3 | import { SettingOutlined } from '@ant-design/icons'; 4 | import EditModal from './editModal'; 5 | 6 | interface IProps { 7 | confirmToSync: () => Promise; 8 | } 9 | 10 | const Configuration: React.FC = (props: IProps) => { 11 | const [visible, setVisible] = useState(false); 12 | 13 | // events 14 | const onOpenEditModal = useCallback(() => { 15 | setVisible(true); 16 | }, [setVisible]); 17 | const onCloseEditModal = useCallback(() => { 18 | setVisible(false); 19 | }, []); 20 | const onOk = useCallback(() => { 21 | onCloseEditModal(); 22 | props.confirmToSync(); 23 | }, [onCloseEditModal]); 24 | 25 | return ( 26 |
27 | 30 | 31 |
32 | ); 33 | }; 34 | 35 | export default Configuration; 36 | -------------------------------------------------------------------------------- /packages/core/src/mods/header/connectStatus/index.module.less: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | flex-direction: row; 4 | align-items: center; 5 | margin-left: 20px; 6 | } 7 | -------------------------------------------------------------------------------- /packages/core/src/mods/header/connectStatus/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | 3 | import 'antd/es/tag/style'; 4 | import { Tag } from 'antd'; 5 | import styles from './index.module.less'; 6 | 7 | export enum Status { 8 | connected = 'success', 9 | disconnected = 'error', 10 | } 11 | 12 | interface IProps { 13 | // flowChart: Graph; 14 | status: Status; 15 | projectName: string; 16 | syncLocal: () => Promise; 17 | confirmToSync: () => Promise; 18 | } 19 | 20 | const PULSE_RATE = 10 * 1000; // try to connect to local servet per 10 seconds 21 | 22 | const ConnectStatus: React.FC = (props: IProps) => { 23 | const { status, confirmToSync, syncLocal, projectName } = props; 24 | 25 | // life 26 | useEffect(() => { 27 | confirmToSync(); 28 | const timer = setInterval(syncLocal, PULSE_RATE); 29 | return () => clearInterval(timer); 30 | }, []); 31 | 32 | let tagText = '本地连接失败'; 33 | if (status === Status.connected) { 34 | tagText = [projectName, '本地连接成功'].join(' '); 35 | } 36 | 37 | return ( 38 |
39 | {tagText} 40 |
41 | ); 42 | }; 43 | 44 | export default ConnectStatus; 45 | -------------------------------------------------------------------------------- /packages/core/src/mods/header/export/exportModal.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import 'antd/es/modal/style'; 4 | import styles from './index.module.less'; 5 | 6 | import JSZip from 'jszip'; 7 | import { Modal } from 'antd'; 8 | import { DataUri, Graph } from '@antv/x6'; 9 | import { compileForProject } from '@imove/compile-code'; 10 | 11 | interface IExportModalProps { 12 | flowChart: Graph; 13 | visible: boolean; 14 | onClose: () => void; 15 | } 16 | 17 | const ExportModal: React.FC = (props) => { 18 | const { flowChart, visible, onClose } = props; 19 | const onExportDSL = () => { 20 | const dsl = JSON.stringify(flowChart.toJSON(), null, 2); 21 | const blob = new Blob([dsl], { type: 'text/plain' }); 22 | DataUri.downloadBlob(blob, 'imove.dsl.json'); 23 | }; 24 | const onExportCode = () => { 25 | const zip = new JSZip(); 26 | const dsl = flowChart.toJSON(); 27 | const output = compileForProject(dsl); 28 | Helper.recursiveZip(zip, output); 29 | zip.generateAsync({ type: 'blob' }).then((blob) => { 30 | DataUri.downloadBlob(blob, 'logic.zip'); 31 | }); 32 | }; 33 | const onExportFlowChart = () => { 34 | flowChart.toPNG( 35 | (dataUri: string) => { 36 | DataUri.downloadDataUri(dataUri, 'flowChart.png'); 37 | }, 38 | { padding: 50, ratio: '3.0' }, 39 | ); 40 | }; 41 | 42 | return ( 43 | 51 |
52 |
53 | 57 | DSL 58 |
59 |
60 | 64 | 代码 65 |
66 |
67 | 71 | 流程图 72 |
73 |
74 |
75 | ); 76 | }; 77 | 78 | const Helper = { 79 | recursiveZip(root: JSZip, json: any) { 80 | if (typeof json !== 'object' || json === null) { 81 | return; 82 | } 83 | for (const key in json) { 84 | const val = json[key]; 85 | if (typeof val === 'string') { 86 | root.file(key, val); 87 | } else { 88 | const dir = root.folder(key) as JSZip; 89 | Helper.recursiveZip(dir, val); 90 | } 91 | } 92 | }, 93 | }; 94 | 95 | export default ExportModal; 96 | -------------------------------------------------------------------------------- /packages/core/src/mods/header/export/index.module.less: -------------------------------------------------------------------------------- 1 | .container { 2 | margin-left: 20px; 3 | } 4 | 5 | .modalContent { 6 | 7 | display: flex; 8 | flex-direction: row; 9 | justify-content: space-around; 10 | width: 100%; 11 | padding: 30px 0; 12 | 13 | .downloadWrap { 14 | 15 | display: flex; 16 | flex-direction: column; 17 | justify-content: center; 18 | align-items: center; 19 | padding: 15px 20px 5px; 20 | border: 0.5px solid #EFEFEF; 21 | 22 | &:hover { 23 | cursor: pointer; 24 | background-color: #EFEFEF; 25 | } 26 | 27 | .picPreview { 28 | width: 150px; 29 | height: 150px; 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /packages/core/src/mods/header/export/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | import styles from './index.module.less'; 4 | 5 | import { Button } from 'antd'; 6 | import { Graph } from '@antv/x6'; 7 | import ExportModal from './exportModal'; 8 | 9 | interface IProps { 10 | flowChart: Graph; 11 | } 12 | 13 | const Export: React.FC = (props) => { 14 | const { flowChart } = props; 15 | const [modalVisible, setModalVisible] = useState(false); 16 | 17 | const onOpenModal = () => setModalVisible(true); 18 | const onCloseModal = () => setModalVisible(false); 19 | 20 | return ( 21 |
22 | 25 | 30 |
31 | ); 32 | }; 33 | 34 | export default Export; 35 | -------------------------------------------------------------------------------- /packages/core/src/mods/header/guide/index.module.less: -------------------------------------------------------------------------------- 1 | .guideContainer{ 2 | height:650px; 3 | 4 | .tabPane{ 5 | height:600px; 6 | overflow-y:auto; 7 | 8 | &::-webkit-scrollbar { 9 | width: 0; 10 | } 11 | 12 | img{ 13 | margin:10px 0; 14 | } 15 | 16 | pre{ 17 | color:#c1811d; 18 | font-size:12px; 19 | line-height:13px; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/core/src/mods/header/guide/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | import { Button } from 'antd'; 4 | import GuideModal from './guideModal'; 5 | 6 | const Export: React.FC = (props) => { 7 | const [modalVisible, setModalVisible] = useState(false); 8 | 9 | const onOpenModal = () => setModalVisible(true); 10 | const onCloseModal = () => setModalVisible(false); 11 | 12 | return ( 13 |
14 | 17 | 18 |
19 | ); 20 | }; 21 | 22 | export default Export; 23 | -------------------------------------------------------------------------------- /packages/core/src/mods/header/importDSL/index.module.less: -------------------------------------------------------------------------------- 1 | .container { 2 | margin-left: 20px; 3 | } -------------------------------------------------------------------------------- /packages/core/src/mods/header/importDSL/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | import styles from './index.module.less'; 4 | 5 | import { Graph } from '@antv/x6'; 6 | import { Button, Upload, message } from 'antd'; 7 | 8 | interface IProps { 9 | flowChart: Graph; 10 | } 11 | 12 | const ImportDSL: React.FC = (props) => { 13 | const { flowChart } = props; 14 | const [disabled, setDisabled] = useState(false); 15 | const beforeUpload = (file: any) => { 16 | setDisabled(true); 17 | const reader = new FileReader(); 18 | reader.onload = (evt) => { 19 | setDisabled(false); 20 | if (!evt.target) { 21 | message.error('加载文件失败!'); 22 | } else { 23 | const dsl = evt.target.result as string; 24 | try { 25 | flowChart.fromJSON(JSON.parse(dsl)); 26 | } catch (err) { 27 | message.error('DSL解析失败!'); 28 | } 29 | } 30 | }; 31 | reader.readAsText(file); 32 | return false; 33 | }; 34 | 35 | return ( 36 | 43 | 46 | 47 | ); 48 | }; 49 | 50 | export default ImportDSL; 51 | -------------------------------------------------------------------------------- /packages/core/src/mods/header/index.module.less: -------------------------------------------------------------------------------- 1 | .container { 2 | flex: 1; 3 | display: flex; 4 | flex-direction: row; 5 | justify-content: space-between; 6 | align-items: center; 7 | width: 100%; 8 | height: 100%; 9 | padding: 0 20px; 10 | background-color: #FEFEFE; 11 | } 12 | 13 | .titleText { 14 | font-size: 24px; 15 | font-weight: 500; 16 | } 17 | 18 | .widgets { 19 | display: flex; 20 | flex-direction: row; 21 | } -------------------------------------------------------------------------------- /packages/core/src/mods/header/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useCallback } from 'react'; 2 | 3 | import styles from './index.module.less'; 4 | 5 | import { Graph } from '@antv/x6'; 6 | import { Modal, message } from 'antd'; 7 | import Guide from './guide'; 8 | import Export from './export'; 9 | import ImportDSL from './importDSL'; 10 | import ConnectStatus, { Status } from './connectStatus'; 11 | import Configuration from './configuration'; 12 | import { localConnect } from '../../api'; 13 | 14 | interface IProps { 15 | flowChart: Graph; 16 | } 17 | 18 | const Header: React.FC = (props: IProps) => { 19 | const { flowChart } = props; 20 | const [projectName, setProjectName] = useState(''); 21 | const [status, setStatus] = useState(Status.disconnected); 22 | 23 | // network 24 | const syncLocal = useCallback(() => { 25 | return localConnect() 26 | .then((res) => res.json()) 27 | .then((data = {}) => { 28 | setStatus(Status.connected); 29 | setProjectName(data.projectName); 30 | return data; 31 | }) 32 | .catch((error) => { 33 | console.log(error); 34 | setStatus(Status.disconnected); 35 | console.log('connect local failed, the error is:', error.message); 36 | }); 37 | }, [setStatus, setProjectName]); 38 | 39 | const confirmToSync = useCallback(() => { 40 | return syncLocal().then((data) => { 41 | const { dsl } = data || {}; 42 | if (dsl) { 43 | Modal.confirm({ 44 | title: '本地连接成功,是否将数据同步至当前项目?', 45 | onOk() { 46 | try { 47 | flowChart.fromJSON(dsl); 48 | } catch (error) { 49 | message.error('同步失败!'); 50 | } 51 | }, 52 | }); 53 | } 54 | }); 55 | }, [syncLocal, flowChart]); 56 | 57 | return ( 58 |
59 | 60 | iMove 61 | 62 |
63 | 64 | 65 | 66 | 72 | 73 |
74 |
75 | ); 76 | }; 77 | 78 | export default Header; 79 | -------------------------------------------------------------------------------- /packages/core/src/mods/layout/index.module.less: -------------------------------------------------------------------------------- 1 | .container { 2 | 3 | display: flex; 4 | flex-direction: column; 5 | width: 100%; 6 | height: 100%; 7 | 8 | .header { 9 | width: 100%; 10 | height: 48px; 11 | border-bottom: 1px solid #D9D9D9; 12 | } 13 | 14 | .toolBar { 15 | height: 43px; 16 | } 17 | 18 | .sideBar { 19 | border-right: 1px solid #D9D9D9; 20 | } 21 | 22 | .settingBar { 23 | width: 350px; 24 | height: 100%; 25 | border-left: 1px solid #D9D9D9; 26 | } 27 | } -------------------------------------------------------------------------------- /packages/core/src/mods/layout/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import styles from './index.module.less'; 4 | 5 | import { Graph } from '@antv/x6'; 6 | import SplitPane from 'react-split-pane-v2'; 7 | 8 | const Pane = SplitPane.Pane; 9 | 10 | interface ISubComponentProps { 11 | flowChart: Graph; 12 | } 13 | 14 | interface IProps { 15 | flowChart: Graph | undefined; 16 | Header: React.FC; 17 | SideBar: React.FC; 18 | ToolBar: React.FC; 19 | SettingBar: React.FC; 20 | } 21 | 22 | const Layout: React.FC = (props) => { 23 | const { flowChart, Header, SideBar, ToolBar, SettingBar } = props; 24 | 25 | let header, sideBar, toolBar, settingBar; 26 | if (flowChart) { 27 | header =
; 28 | sideBar = ; 29 | toolBar = ; 30 | settingBar = ; 31 | } 32 | 33 | return ( 34 |
35 |
{header}
36 |
{toolBar}
37 | 38 | 44 | {sideBar} 45 | 46 | 47 | {props.children} 48 | 54 | {settingBar} 55 | 56 | 57 | 58 |
59 | ); 60 | }; 61 | 62 | export default Layout; 63 | -------------------------------------------------------------------------------- /packages/core/src/mods/settingBar/components/checkbox/index.module.less: -------------------------------------------------------------------------------- 1 | .container { 2 | 3 | margin-top: 20px; 4 | 5 | .titleText { 6 | font-size: 14px; 7 | margin-bottom: 10px; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/core/src/mods/settingBar/components/checkbox/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import 'antd/es/checkbox/style'; 4 | import styles from './index.module.less'; 5 | 6 | import { Checkbox as AntdCheckbox } from 'antd'; 7 | import { CheckboxChangeEvent } from 'antd/lib/checkbox'; 8 | 9 | interface IProps { 10 | value: any; 11 | name: string; 12 | title: string; 13 | disabled?: boolean; 14 | description?: string; 15 | onValueChange: (val: boolean) => void; 16 | } 17 | 18 | const Checkbox: React.FC = (props) => { 19 | const { name, title, value, disabled, onValueChange } = props; 20 | const onChange = (evt: CheckboxChangeEvent): void => { 21 | if (evt.target.checked !== value) { 22 | onValueChange(evt.target.checked); 23 | } 24 | }; 25 | return ( 26 |
27 |

{title}

28 | 34 |
35 | ); 36 | }; 37 | 38 | export default Checkbox; 39 | -------------------------------------------------------------------------------- /packages/core/src/mods/settingBar/components/input/index.module.less: -------------------------------------------------------------------------------- 1 | .titleText { 2 | font-size: 14px; 3 | width:150px; 4 | } 5 | -------------------------------------------------------------------------------- /packages/core/src/mods/settingBar/components/input/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import 'antd/es/input/style'; 4 | import styles from './index.module.less'; 5 | 6 | import { Input as AntdInput } from 'antd'; 7 | 8 | interface IProps { 9 | value: any; 10 | name: string; 11 | title: string; 12 | disabled?: boolean; 13 | description?: string; 14 | onValueChange: (val: string) => void; 15 | } 16 | 17 | const Input: React.FC = (props) => { 18 | const { name, title, value, disabled, onValueChange } = props; 19 | const onChange = (evt: React.ChangeEvent): void => { 20 | if (evt.target.value !== value) { 21 | onValueChange(evt.target.value); 22 | } 23 | }; 24 | return ( 25 |
26 |

{title}

27 | 33 |
34 | ); 35 | }; 36 | 37 | export default Input; 38 | -------------------------------------------------------------------------------- /packages/core/src/mods/settingBar/components/json/index.less: -------------------------------------------------------------------------------- 1 | .ant-card-bordered{ 2 | border:none; 3 | } 4 | .ant-card-head{ 5 | border:none; 6 | height:35px; 7 | } 8 | -------------------------------------------------------------------------------- /packages/core/src/mods/settingBar/components/json/index.module.less: -------------------------------------------------------------------------------- 1 | .container { 2 | margin-top: 20px; 3 | 4 | .header{ 5 | display:flex; 6 | flex-direction: row; 7 | justify-content:space-between; 8 | 9 | .titleText { 10 | font-size: 14px; 11 | margin-bottom: 10px; 12 | } 13 | } 14 | } 15 | 16 | .editModal { 17 | .tabPane{ 18 | .form{ 19 | height:600px; 20 | overflow-y: auto; 21 | margin:0 20px; 22 | 23 | :global { 24 | .fr-wrapper { 25 | position: relative; 26 | } 27 | } 28 | } 29 | .configInput{ 30 | height: 600px; 31 | } 32 | } 33 | 34 | :global { 35 | .ant-modal-body { 36 | padding: 0; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/core/src/mods/settingBar/components/json/json.ts: -------------------------------------------------------------------------------- 1 | const compData = [ 2 | { 3 | text: 'Input', 4 | name: 'input', 5 | schema: { 6 | title: 'Input', 7 | type: 'string', 8 | description: '输入框', 9 | }, 10 | }, 11 | { 12 | text: 'Textarea', 13 | name: 'textarea', 14 | schema: { 15 | title: 'Textarea', 16 | type: 'string', 17 | format: 'textarea', 18 | description: '文本编辑框', 19 | }, 20 | }, 21 | { 22 | text: 'Switch', 23 | name: 'allBoolean', 24 | schema: { 25 | title: 'Switch', 26 | type: 'boolean', 27 | 'ui:widget': 'switch', 28 | description: '开关控制', 29 | }, 30 | }, 31 | { 32 | text: 'Select', 33 | name: 'select', 34 | schema: { 35 | title: 'Select', 36 | type: 'string', 37 | enum: ['a', 'b', 'c'], 38 | enumNames: ['早', '中', '晚'], 39 | description: '下拉单选', 40 | }, 41 | }, 42 | { 43 | text: 'MultiSelect', 44 | name: 'multiSelect', 45 | schema: { 46 | title: 'MultiSelect', 47 | description: '下拉多选', 48 | type: 'array', 49 | items: { 50 | type: 'string', 51 | }, 52 | enum: ['A', 'B', 'C', 'D'], 53 | enumNames: ['1', '2', '3', '4'], 54 | 'ui:widget': 'multiSelect', 55 | }, 56 | }, 57 | { 58 | text: 'Checkbox', 59 | name: 'checkbox', 60 | schema: { 61 | title: 'Checkbox', 62 | description: '点击多选', 63 | type: 'array', 64 | items: { 65 | type: 'string', 66 | }, 67 | enum: ['A', 'B', 'C', 'D'], 68 | enumNames: ['1', '2', '3', '4'], 69 | }, 70 | }, 71 | { 72 | text: 'TimePicker', 73 | name: 'timeSelect', 74 | schema: { 75 | title: 'TimePicker', 76 | type: 'string', 77 | format: 'time', 78 | description: '时间选择', 79 | }, 80 | }, 81 | { 82 | text: 'DatePicker', 83 | name: 'dateSelect', 84 | schema: { 85 | title: 'DatePicker', 86 | type: 'string', 87 | format: 'date', 88 | description: '日期选择', 89 | }, 90 | }, 91 | { 92 | text: 'DateRange', 93 | name: 'dateRangeSelect', 94 | schema: { 95 | title: 'DateRange', 96 | type: 'range', 97 | format: 'date', 98 | description: '日期范围选择', 99 | }, 100 | }, 101 | ]; 102 | 103 | export { compData }; 104 | -------------------------------------------------------------------------------- /packages/core/src/mods/settingBar/index.module.less: -------------------------------------------------------------------------------- 1 | .container { 2 | 3 | display: flex; 4 | flex-direction: column; 5 | width: 100%; 6 | height: 100%; 7 | 8 | &.center { 9 | justify-content: center; 10 | align-items: center; 11 | } 12 | 13 | :global { 14 | 15 | .ant-tabs-nav { 16 | margin-bottom: 0; 17 | } 18 | 19 | .ant-tabs-nav-list { 20 | display: flex; 21 | flex: 1; 22 | } 23 | 24 | .ant-tabs-tab { 25 | display: flex; 26 | flex: 1; 27 | justify-content: center; 28 | align-items: center; 29 | padding: 10px 0; 30 | background-color: #FAFAFA; 31 | border-bottom: 1px solid #D9D9D9; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/core/src/mods/settingBar/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useReducer } from 'react'; 2 | 3 | import 'antd/es/tabs/style'; 4 | import 'antd/es/empty/style'; 5 | import styles from './index.module.less'; 6 | 7 | import { Graph } from '@antv/x6'; 8 | import { Empty, Tabs } from 'antd'; 9 | import Basic from './mods/basic'; 10 | // import TestCase from './mods/testCase'; 11 | 12 | const { TabPane } = Tabs; 13 | 14 | interface IProps { 15 | flowChart: Graph; 16 | } 17 | 18 | const SettingBar: React.FC = (props) => { 19 | const { flowChart } = props; 20 | const forceUpdate = useReducer((n) => n + 1, 0)[1]; 21 | 22 | useEffect(() => { 23 | flowChart.on('settingBar:forceUpdate', forceUpdate); 24 | return () => { 25 | flowChart.off('settingBar:forceUpdate'); 26 | }; 27 | }, []); 28 | 29 | const nodes = flowChart.getSelectedCells().filter((v) => v.shape !== 'edge'); 30 | if (nodes.length === 1) { 31 | return ( 32 |
33 | 43 | 44 | 45 | 46 | {/* 47 | 48 | */} 49 | 50 |
51 | ); 52 | } else { 53 | return ( 54 |
55 | 59 |
60 | ); 61 | } 62 | }; 63 | 64 | export default SettingBar; 65 | -------------------------------------------------------------------------------- /packages/core/src/mods/settingBar/mods/basic/index.module.less: -------------------------------------------------------------------------------- 1 | .container { 2 | height: 900px; 3 | overflow-y: auto; 4 | 5 | &::-webkit-scrollbar { 6 | display: none; 7 | } 8 | 9 | .input{ 10 | margin-top:20px; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/core/src/mods/settingBar/mods/basic/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | 3 | import styles from './index.module.less'; 4 | 5 | import { Card } from 'antd'; 6 | import { Cell, Graph } from '@antv/x6'; 7 | import Json from '../../components/json'; 8 | import Input from '../../components/input'; 9 | 10 | interface IProps { 11 | selectedCell: Cell; 12 | flowChart: Graph; 13 | } 14 | 15 | interface IBasicData { 16 | label: string; 17 | trigger?: string; 18 | dependencies: string; 19 | configSchema: string; 20 | } 21 | 22 | const Basic: React.FC = (props) => { 23 | const { selectedCell, flowChart } = props; 24 | const [data, setData] = useState(selectedCell.getData()); 25 | const { label, trigger, dependencies, configSchema } = data || {}; 26 | 27 | // life 28 | useEffect(() => { 29 | setData(selectedCell.getData()); 30 | }, [selectedCell]); 31 | useEffect(() => { 32 | const handler = () => setData(selectedCell.getData()); 33 | flowChart.on('settingBar.basicPanel:forceUpdate', handler); 34 | return () => { 35 | flowChart.off('settingBar.basicPanel:forceUpdate', handler); 36 | }; 37 | }, [selectedCell]); 38 | 39 | // events 40 | const batchUpdate = (newData: { [key: string]: any }): void => { 41 | selectedCell.setData(newData); 42 | setData(Object.assign({}, data, newData)); 43 | }; 44 | const commonChange = (key: string, val: string): void => { 45 | batchUpdate({ [key]: val }); 46 | }; 47 | const onChangeLabel = (val: string): void => { 48 | commonChange('label', val); 49 | selectedCell.setAttrs({ label: { text: val } }); 50 | }; 51 | const onChangeConfigSchema = (val: string): void => { 52 | commonChange('configSchema', val); 53 | }; 54 | const onChangeTrigger = (val: string): void => { 55 | commonChange('trigger', val); 56 | }; 57 | const onChangeDependencies = (val: string): void => { 58 | commonChange('dependencies', val); 59 | }; 60 | 61 | return ( 62 |
63 | 64 | 70 | {selectedCell.shape === 'imove-start' && ( 71 |
72 | 78 |
79 | )} 80 |
81 | 88 | 96 |
97 | ); 98 | }; 99 | 100 | export default Basic; 101 | -------------------------------------------------------------------------------- /packages/core/src/mods/settingBar/mods/testCase/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useCallback } from 'react'; 2 | 3 | import { Modal } from 'antd'; 4 | import { Graph } from '@antv/x6'; 5 | import CodeRun from '../../../../components/codeRun'; 6 | 7 | interface IProps { 8 | flowChart: Graph; 9 | selectedCell: any; 10 | } 11 | 12 | const TestCase: React.FC = (props) => { 13 | const { flowChart } = props; 14 | const [visible, setVisible] = useState(false); 15 | 16 | useEffect(() => { 17 | flowChart.on('settingBar:runCode', showModal); 18 | return () => { 19 | flowChart.off('settingBar:runCode', showModal); 20 | }; 21 | }, [flowChart]); 22 | 23 | const showModal = useCallback(() => setVisible(true), []); 24 | const closeModal = useCallback(() => setVisible(false), []); 25 | 26 | return ( 27 | 28 | ); 29 | }; 30 | 31 | interface IEditModalProps { 32 | visible: boolean; 33 | flowChart: Graph; 34 | onClose: () => void; 35 | } 36 | 37 | const EditModal: React.FC = (props): JSX.Element => { 38 | const { visible, flowChart, onClose } = props; 39 | 40 | return ( 41 | 50 | 51 | 52 | ); 53 | }; 54 | 55 | export default TestCase; 56 | -------------------------------------------------------------------------------- /packages/core/src/mods/sideBar/index.module.less: -------------------------------------------------------------------------------- 1 | .container { 2 | width: 100%; 3 | 4 | :global { 5 | .ant-collapse > .ant-collapse-item > .ant-collapse-header { 6 | padding: 10px 16px 10px 40px; 7 | } 8 | } 9 | 10 | .collapse { 11 | border: 0; 12 | border-radius: 0; 13 | } 14 | } 15 | 16 | .panelContent { 17 | display: flex; 18 | flex-direction: row; 19 | flex-wrap: wrap; 20 | 21 | .cellWrapper { 22 | display: flex; 23 | justify-content: center; 24 | align-items: center; 25 | margin: 8px; 26 | width: 60px; 27 | height: 60px; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/core/src/mods/sideBar/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useMemo } from 'react'; 2 | 3 | import 'antd/es/collapse/style'; 4 | import styles from './index.module.less'; 5 | 6 | import { Collapse } from 'antd'; 7 | import { Addon, Graph, Node } from '@antv/x6'; 8 | import cellMap from '../../common/previewCell'; 9 | 10 | const { Dnd } = Addon; 11 | const { Panel } = Collapse; 12 | const GENERAL_GROUP = { 13 | key: 'general', 14 | name: '通用元件', 15 | cellTypes: ['imove-start', 'imove-branch', 'imove-behavior'], 16 | }; 17 | 18 | interface IGroupItem { 19 | key: string; 20 | name: string; 21 | cellTypes: string[]; 22 | } 23 | 24 | interface ISideBarProps { 25 | flowChart: Graph; 26 | } 27 | 28 | const SideBar: React.FC = (props) => { 29 | const { flowChart } = props; 30 | const [groups, setGroups] = useState([]); 31 | const dnd = useMemo(() => new Dnd({ target: flowChart, scaled: true }), [ 32 | flowChart, 33 | ]); 34 | 35 | // life 36 | useEffect(() => { 37 | // TODO: fetch to get custom group data 38 | setGroups([GENERAL_GROUP]); 39 | }, []); 40 | 41 | return ( 42 |
43 | 47 | {groups.map((group) => ( 48 | 49 | 50 | 51 | ))} 52 | 53 |
54 | ); 55 | }; 56 | 57 | interface IPanelContentProps { 58 | dnd: Addon.Dnd; 59 | cellTypes: string[]; 60 | } 61 | 62 | const PanelContent: React.FC = (props) => { 63 | const { dnd, cellTypes } = props; 64 | const onMouseDown = (evt: any, cellType: string) => { 65 | dnd.start(Node.create({ shape: cellType }), evt); 66 | }; 67 | return ( 68 |
69 | {cellTypes.map((cellType, index) => { 70 | const Component = cellMap[cellType]; 71 | return ( 72 |
73 | onMouseDown(evt, cellType)} /> 74 |
75 | ); 76 | })} 77 |
78 | ); 79 | }; 80 | 81 | export default SideBar; 82 | -------------------------------------------------------------------------------- /packages/core/src/mods/toolBar/index.module.less: -------------------------------------------------------------------------------- 1 | .container { 2 | 3 | display: flex; 4 | flex-direction: row; 5 | justify-content: flex-start; 6 | align-items: center; 7 | width: 100%; 8 | height: 100%; 9 | background-color: #FAFAFA; 10 | border-bottom: 1px solid #D9D9D9; 11 | 12 | .group { 13 | display: flex; 14 | flex-direction: row; 15 | align-items: center; 16 | padding: 0 4px; 17 | height: 100%; 18 | border-right: 1px solid #D9D9D9; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/core/src/mods/toolBar/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useReducer } from 'react'; 2 | 3 | import 'antd/es/tooltip/style'; 4 | import styles from './index.module.less'; 5 | 6 | import { Graph } from '@antv/x6'; 7 | import widgets from './widgets'; 8 | import ModifyStatus from './widgets/modifyStatus'; 9 | 10 | interface IProps { 11 | flowChart: Graph; 12 | } 13 | 14 | const ToolBar: React.FC = (props) => { 15 | const { flowChart } = props; 16 | const forceUpdate = useReducer((n) => n + 1, 0)[1]; 17 | 18 | useEffect(() => { 19 | flowChart.on('toolBar:forceUpdate', forceUpdate); 20 | return () => { 21 | flowChart.off('toolBar:forceUpdate'); 22 | }; 23 | }, []); 24 | 25 | return ( 26 |
27 | {widgets.map((group, index) => ( 28 |
29 | {group.map((ToolItem, index) => { 30 | return ; 31 | })} 32 |
33 | ))} 34 | 35 |
36 | ); 37 | }; 38 | 39 | export default ToolBar; 40 | -------------------------------------------------------------------------------- /packages/core/src/mods/toolBar/widgets/bgColor.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import 'antd/es/menu/style'; 4 | import styles from './index.module.less'; 5 | 6 | import { Graph } from '@antv/x6'; 7 | import { safeGet } from '../../../utils'; 8 | import XIcon from '../../../components/xIcon'; 9 | import ColorPicker from '../../../components/colorPicker'; 10 | import makeDropdownWidget from './common/makeDropdownWidget'; 11 | import { 12 | hasNodeSelected, 13 | getSelectedNodes, 14 | } from '../../../utils/flowChartUtils'; 15 | 16 | interface IProps { 17 | flowChart: Graph; 18 | } 19 | 20 | const options = { 21 | tooltip: '填充颜色', 22 | getCurBgColor(flowChart: Graph) { 23 | let bgColor = '#DDD'; 24 | const nodes = getSelectedNodes(flowChart); 25 | if (!options.disabled(flowChart) && nodes.length > 0) { 26 | bgColor = safeGet(nodes, '0.attrs.body.fill', '#575757'); 27 | } 28 | return bgColor; 29 | }, 30 | getIcon(flowChart: Graph) { 31 | const bgColor = options.getCurBgColor(flowChart); 32 | return ( 33 |
34 | 35 |
39 |
40 | ); 41 | }, 42 | getOverlay(flowChart: Graph, onChange: (data: any) => void) { 43 | const bgColor = options.getCurBgColor(flowChart); 44 | const onChangeComplete = (color: string) => onChange(color); 45 | return ; 46 | }, 47 | handler: (flowChart: Graph, value: any) => { 48 | getSelectedNodes(flowChart).forEach((node) => 49 | node.setAttrs({ body: { fill: value } }), 50 | ); 51 | }, 52 | disabled(flowChart: Graph) { 53 | return !hasNodeSelected(flowChart); 54 | }, 55 | }; 56 | 57 | const BgColor: React.FC = makeDropdownWidget(options); 58 | 59 | export default BgColor; 60 | -------------------------------------------------------------------------------- /packages/core/src/mods/toolBar/widgets/bold.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Graph } from '@antv/x6'; 4 | import { safeGet } from '../../../utils'; 5 | import { BoldOutlined } from '@ant-design/icons'; 6 | import shortcuts from '../../../common/shortcuts'; 7 | import makeBtnWidget from './common/makeBtnWidget'; 8 | 9 | interface IProps { 10 | flowChart: Graph; 11 | } 12 | 13 | const Bold: React.FC = makeBtnWidget({ 14 | tooltip: '加粗', 15 | handler: shortcuts.bold.handler, 16 | getIcon() { 17 | return ; 18 | }, 19 | disabled(flowChart: Graph) { 20 | return flowChart.getSelectedCellCount() === 0; 21 | }, 22 | selected(flowChart: Graph) { 23 | const cells = flowChart.getSelectedCells(); 24 | if (cells.length > 0) { 25 | return safeGet(cells, '0.attrs.label.fontWeight', 'normal') === 'bold'; 26 | } else { 27 | return false; 28 | } 29 | }, 30 | }); 31 | 32 | export default Bold; 33 | -------------------------------------------------------------------------------- /packages/core/src/mods/toolBar/widgets/borderColor.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import 'antd/es/menu/style'; 4 | import styles from './index.module.less'; 5 | 6 | import { Graph } from '@antv/x6'; 7 | import { safeGet } from '../../../utils'; 8 | import { HighlightOutlined } from '@ant-design/icons'; 9 | import ColorPicker from '../../../components/colorPicker'; 10 | import makeDropdownWidget from './common/makeDropdownWidget'; 11 | import { 12 | hasNodeSelected, 13 | getSelectedNodes, 14 | } from '../../../utils/flowChartUtils'; 15 | 16 | interface IProps { 17 | flowChart: Graph; 18 | } 19 | 20 | const options = { 21 | tooltip: '边框颜色', 22 | getCurBorderColor(flowChart: Graph) { 23 | let borderColor = '#DDD'; 24 | const nodes = getSelectedNodes(flowChart); 25 | if (!options.disabled(flowChart) && nodes.length > 0) { 26 | borderColor = safeGet(nodes, '0.attrs.body.stroke', '#333'); 27 | } 28 | return borderColor; 29 | }, 30 | getIcon(flowChart: Graph) { 31 | const borderColor = options.getCurBorderColor(flowChart); 32 | return ( 33 |
34 | 35 |
39 |
40 | ); 41 | }, 42 | getOverlay(flowChart: Graph, onChange: (data: any) => void) { 43 | const borderColor = options.getCurBorderColor(flowChart); 44 | const onChangeComplete = (color: string) => onChange(color); 45 | return ( 46 | 47 | ); 48 | }, 49 | handler: (flowChart: Graph, value: any) => { 50 | getSelectedNodes(flowChart).forEach((node) => 51 | node.setAttrs({ body: { stroke: value } }), 52 | ); 53 | }, 54 | disabled(flowChart: Graph) { 55 | return !hasNodeSelected(flowChart); 56 | }, 57 | }; 58 | 59 | const BorderColor: React.FC = makeDropdownWidget(options); 60 | 61 | export default BorderColor; 62 | -------------------------------------------------------------------------------- /packages/core/src/mods/toolBar/widgets/bringToBack.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Graph } from '@antv/x6'; 4 | import XIcon from '../../../components/xIcon'; 5 | import shortcuts from '../../../common/shortcuts'; 6 | import makeBtnWidget from './common/makeBtnWidget'; 7 | import { hasNodeSelected } from '../../../utils/flowChartUtils'; 8 | 9 | interface IProps { 10 | flowChart: Graph; 11 | } 12 | 13 | const BringToBack: React.FC = makeBtnWidget({ 14 | tooltip: '置于底层', 15 | handler: shortcuts.bringToBack.handler, 16 | getIcon() { 17 | return ; 18 | }, 19 | disabled(flowChart: Graph) { 20 | return !hasNodeSelected(flowChart); 21 | }, 22 | }); 23 | 24 | export default BringToBack; 25 | -------------------------------------------------------------------------------- /packages/core/src/mods/toolBar/widgets/bringToTop.tsx: -------------------------------------------------------------------------------- 1 | // at.alicdn.com/t/font_2024452_s39le1337k9.js 2 | import React from 'react'; 3 | 4 | import { Graph } from '@antv/x6'; 5 | import XIcon from '../../../components/xIcon'; 6 | import shortcuts from '../../../common/shortcuts'; 7 | import makeBtnWidget from './common/makeBtnWidget'; 8 | import { hasNodeSelected } from '../../../utils/flowChartUtils'; 9 | 10 | interface IProps { 11 | flowChart: Graph; 12 | } 13 | 14 | const BringToTop: React.FC = makeBtnWidget({ 15 | tooltip: '置于顶层', 16 | handler: shortcuts.bringToTop.handler, 17 | getIcon() { 18 | return ; 19 | }, 20 | disabled(flowChart: Graph) { 21 | return !hasNodeSelected(flowChart); 22 | }, 23 | }); 24 | 25 | export default BringToTop; 26 | -------------------------------------------------------------------------------- /packages/core/src/mods/toolBar/widgets/common/makeBtnWidget.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement } from 'react'; 2 | 3 | import 'antd/es/tooltip/style'; 4 | import styles from '../index.module.less'; 5 | 6 | import { Tooltip } from 'antd'; 7 | import { Graph } from '@antv/x6'; 8 | 9 | interface IOptions { 10 | tooltip: string; 11 | getIcon: (flowChart: Graph) => ReactElement; 12 | handler: (flowChart: Graph) => void; 13 | disabled?: (flowChart: Graph) => boolean; 14 | selected?: (flowChart: Graph) => boolean; 15 | } 16 | 17 | interface IBtnWidgetProps { 18 | flowChart: Graph; 19 | } 20 | 21 | const makeBtnWidget = (options: IOptions) => { 22 | const Widget: React.FC = (props) => { 23 | const { flowChart } = props; 24 | const { tooltip, getIcon, handler } = options; 25 | const iconWrapperCls = [styles.btnWidget]; 26 | let { disabled = false, selected = false } = options; 27 | if (typeof disabled === 'function') { 28 | disabled = disabled(flowChart); 29 | disabled && iconWrapperCls.push(styles.disabled); 30 | } 31 | if (typeof selected === 'function') { 32 | selected = selected(flowChart); 33 | selected && iconWrapperCls.push(styles.selected); 34 | } 35 | const onClick = (): void => { 36 | if (disabled) return; 37 | handler(flowChart); 38 | flowChart.trigger('toolBar:forceUpdate'); 39 | }; 40 | return ( 41 | 42 |
43 | {getIcon(flowChart)} 44 |
45 |
46 | ); 47 | }; 48 | return Widget; 49 | }; 50 | 51 | export default makeBtnWidget; 52 | -------------------------------------------------------------------------------- /packages/core/src/mods/toolBar/widgets/common/makeDropdownWidget.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement } from 'react'; 2 | 3 | import 'antd/es/tooltip/style'; 4 | import 'antd/es/dropdown/style'; 5 | import styles from '../index.module.less'; 6 | 7 | import { Graph } from '@antv/x6'; 8 | import { Tooltip, Dropdown } from 'antd'; 9 | import { CaretDownOutlined } from '@ant-design/icons'; 10 | 11 | interface IOptions { 12 | tooltip: string; 13 | getIcon: (flowChart: Graph) => ReactElement; 14 | getOverlay: (flowChart: Graph, onChange: (data: any) => void) => ReactElement; 15 | handler: (flowChart: Graph, data: any) => void; 16 | disabled?: (flowChart: Graph) => boolean; 17 | } 18 | 19 | interface IDropdownWidgetProps { 20 | flowChart: Graph; 21 | } 22 | 23 | const makeDropdownWidget = (options: IOptions) => { 24 | const Widget: React.FC = (props) => { 25 | const { flowChart } = props; 26 | const { tooltip, getIcon, getOverlay, handler } = options; 27 | const iconWrapperCls = [styles.btnWidget]; 28 | let { disabled = false } = options; 29 | if (typeof disabled === 'function') { 30 | disabled = disabled(flowChart); 31 | disabled && iconWrapperCls.push(styles.disabled); 32 | } 33 | const onChange = (data: any): void => { 34 | if (disabled) return; 35 | handler(flowChart, data); 36 | flowChart.trigger('toolBar:forceUpdate'); 37 | }; 38 | return ( 39 | 40 | 45 |
46 | {getIcon(flowChart)} 47 |
48 |
49 |
50 | ); 51 | }; 52 | return Widget; 53 | }; 54 | 55 | export default makeDropdownWidget; 56 | -------------------------------------------------------------------------------- /packages/core/src/mods/toolBar/widgets/fitWindow.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Graph } from '@antv/x6'; 4 | import makeBtnWidget from './common/makeBtnWidget'; 5 | import { FullscreenExitOutlined } from '@ant-design/icons'; 6 | 7 | interface IProps { 8 | flowChart: Graph; 9 | } 10 | 11 | const FitWindow: React.FC = makeBtnWidget({ 12 | tooltip: '适配窗口', 13 | getIcon() { 14 | return ; 15 | }, 16 | handler(flowChart: Graph) { 17 | flowChart.zoomToFit({ minScale: 0.5, maxScale: 1 }); 18 | }, 19 | }); 20 | 21 | export default FitWindow; 22 | -------------------------------------------------------------------------------- /packages/core/src/mods/toolBar/widgets/fontSize.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import 'antd/es/menu/style'; 4 | 5 | import { Menu } from 'antd'; 6 | import { Graph } from '@antv/x6'; 7 | import { safeGet } from '../../../utils'; 8 | import makeDropdownWidget from './common/makeDropdownWidget'; 9 | 10 | interface IProps { 11 | flowChart: Graph; 12 | } 13 | 14 | const MenuItem = Menu.Item; 15 | const FONT_SIZE_SET = [9, 10, 11, 12, 13, 14, 15, 16, 19, 22, 24, 29, 32]; 16 | 17 | const FontSize: React.FC = makeDropdownWidget({ 18 | tooltip: '字号', 19 | getIcon(flowChart: Graph) { 20 | let fontSize = 14; 21 | const cells = flowChart.getSelectedCells(); 22 | if (cells.length > 0) { 23 | fontSize = safeGet(cells, '0.attrs.label.fontSize', 14); 24 | } 25 | return {fontSize}px; 26 | }, 27 | getOverlay(flowChart: Graph, onChange: (data: any) => void) { 28 | return ( 29 | onChange(args.key)}> 30 | {FONT_SIZE_SET.map((fontSize) => ( 31 | 32 | {fontSize} px 33 | 34 | ))} 35 | 36 | ); 37 | }, 38 | handler: (flowChart: Graph, value: any) => { 39 | flowChart 40 | .getSelectedCells() 41 | .forEach((cell) => cell.setAttrs({ label: { fontSize: value } })); 42 | }, 43 | disabled(flowChart: Graph) { 44 | return flowChart.getSelectedCellCount() === 0; 45 | }, 46 | }); 47 | 48 | export default FontSize; 49 | -------------------------------------------------------------------------------- /packages/core/src/mods/toolBar/widgets/horizontalAlign.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement } from 'react'; 2 | 3 | import 'antd/es/menu/style'; 4 | 5 | import { Menu } from 'antd'; 6 | import { Graph } from '@antv/x6'; 7 | import { safeGet } from '../../../utils'; 8 | import makeDropdownWidget from './common/makeDropdownWidget'; 9 | import { 10 | hasNodeSelected, 11 | getSelectedNodes, 12 | } from '../../../utils/flowChartUtils'; 13 | import { 14 | AlignLeftOutlined, 15 | AlignCenterOutlined, 16 | AlignRightOutlined, 17 | } from '@ant-design/icons'; 18 | 19 | interface IProps { 20 | flowChart: Graph; 21 | } 22 | 23 | interface AlignItem { 24 | text: string; 25 | icon: ReactElement; 26 | attrs: { 27 | refX: number; 28 | refX2?: number; 29 | textAnchor: string; 30 | align: { 31 | horizontal: string; 32 | }; 33 | }; 34 | } 35 | 36 | const MenuItem = Menu.Item; 37 | const ALIGN_MAP: { [key: string]: AlignItem } = { 38 | left: { 39 | text: '左对齐', 40 | icon: , 41 | attrs: { 42 | refX: 0, 43 | refX2: 5, 44 | textAnchor: 'start', 45 | align: { 46 | horizontal: 'left', 47 | }, 48 | }, 49 | }, 50 | center: { 51 | text: '居中对齐', 52 | icon: , 53 | attrs: { 54 | refX: 0.5, 55 | refX2: 0, 56 | textAnchor: 'middle', 57 | align: { 58 | horizontal: 'center', 59 | }, 60 | }, 61 | }, 62 | right: { 63 | text: '右对齐', 64 | icon: , 65 | attrs: { 66 | refX: 0.99, 67 | refX2: -5, 68 | textAnchor: 'end', 69 | align: { 70 | horizontal: 'right', 71 | }, 72 | }, 73 | }, 74 | }; 75 | 76 | const HorizontalAlign: React.FC = makeDropdownWidget({ 77 | tooltip: '水平对齐', 78 | getIcon(flowChart: Graph) { 79 | let alignType = 'center'; 80 | const nodes = getSelectedNodes(flowChart); 81 | if (nodes.length > 0) { 82 | alignType = safeGet(nodes, '0.attrs.label.align.horizontal', 'center'); 83 | } 84 | return ALIGN_MAP[alignType].icon; 85 | }, 86 | getOverlay(flowChart: Graph, onChange: (data: any) => void) { 87 | return ( 88 | onChange(ALIGN_MAP[key].attrs)}> 89 | {Object.keys(ALIGN_MAP).map((alignType) => ( 90 | 91 | {ALIGN_MAP[alignType].icon} 92 | {ALIGN_MAP[alignType].text} 93 | 94 | ))} 95 | 96 | ); 97 | }, 98 | handler: (flowChart: Graph, value: any) => { 99 | getSelectedNodes(flowChart).forEach((node) => 100 | node.setAttrs({ label: value }), 101 | ); 102 | }, 103 | disabled(flowChart: Graph) { 104 | return !hasNodeSelected(flowChart); 105 | }, 106 | }); 107 | 108 | export default HorizontalAlign; 109 | -------------------------------------------------------------------------------- /packages/core/src/mods/toolBar/widgets/index.module.less: -------------------------------------------------------------------------------- 1 | .btnWidget { 2 | 3 | box-sizing: border-box; 4 | display: flex; 5 | justify-content: center; 6 | align-items: center; 7 | padding: 0 8px; 8 | min-width: 34px; 9 | height: 34px; 10 | font-size: 16px; 11 | border-radius: 2px; 12 | border: 0.5px solid transparent; 13 | 14 | .caret { 15 | margin-left: 4px; 16 | font-size: 10px; 17 | } 18 | 19 | &:hover { 20 | cursor: pointer; 21 | border: 0.5px solid #CCC; 22 | } 23 | 24 | &.selected { 25 | border: 0.5px solid #CCC; 26 | box-shadow: inset 0 1px 2px rgba(0,0,0,.1); 27 | background-color: #EEE; 28 | background-image: linear-gradient(top,#eee,#e0e0e0); 29 | } 30 | 31 | &.disabled { 32 | color: #CCC; 33 | } 34 | 35 | &.disabled:hover { 36 | cursor: default; 37 | border: 0.5px solid transparent; 38 | } 39 | } 40 | 41 | .zoomContainer { 42 | 43 | display: flex; 44 | flex-direction: row; 45 | align-items: center; 46 | 47 | .zoomText { 48 | width: 45px; 49 | text-align: center; 50 | } 51 | } 52 | 53 | .modifyStatusContainer { 54 | margin-left: 8px; 55 | } 56 | 57 | .bgColorContainer, .textColorContainer, .borderColorContainer { 58 | 59 | position: relative; 60 | display: flex; 61 | flex-direction: column; 62 | justify-content: center; 63 | 64 | .fillIcon { 65 | position: relative; 66 | top: -2px; 67 | font-size: 15px; 68 | } 69 | 70 | .textIcon { 71 | position: relative; 72 | top: -2px; 73 | font-size: 15px; 74 | } 75 | 76 | .borderColorIcon { 77 | position: relative; 78 | top: -2px; 79 | font-size: 15px; 80 | } 81 | 82 | .colorPreview { 83 | position: absolute; 84 | bottom: 0; 85 | left: 0; 86 | right: 0; 87 | height: 1.6px; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /packages/core/src/mods/toolBar/widgets/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Graph } from '@antv/x6'; 4 | 5 | import Save from './save'; 6 | import Undo from './undo'; 7 | import Redo from './redo'; 8 | import Zoom from './zoom'; 9 | import Bold from './bold'; 10 | import Italic from './italic'; 11 | import BgColor from './bgColor'; 12 | import FontSize from './fontSize'; 13 | import TextColor from './textColor'; 14 | import LineStyle from './lineStyle'; 15 | import Underline from './underline'; 16 | import NodeAlign from './nodeAlign'; 17 | import FitWindow from './fitWindow'; 18 | import BringToTop from './bringToTop'; 19 | import BringToBack from './bringToBack'; 20 | import BorderColor from './borderColor'; 21 | import VerticalAlign from './verticalAlign'; 22 | import HorizontalAlign from './horizontalAlign'; 23 | 24 | interface IProps { 25 | flowChart: Graph; 26 | } 27 | 28 | const tools: React.FC[][] = [ 29 | [Save, FitWindow, Undo, Redo], 30 | [Zoom], 31 | [FontSize, Bold, Italic, Underline], 32 | [TextColor, BgColor, BorderColor, LineStyle], 33 | [HorizontalAlign, VerticalAlign], 34 | [NodeAlign, BringToTop, BringToBack], 35 | ]; 36 | 37 | export default tools; 38 | -------------------------------------------------------------------------------- /packages/core/src/mods/toolBar/widgets/italic.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Graph } from '@antv/x6'; 4 | import { safeGet } from '../../../utils'; 5 | import makeBtnWidget from './common/makeBtnWidget'; 6 | import { ItalicOutlined } from '@ant-design/icons'; 7 | import shortcuts from '../../../common/shortcuts'; 8 | 9 | interface IProps { 10 | flowChart: Graph; 11 | } 12 | 13 | const Italic: React.FC = makeBtnWidget({ 14 | tooltip: '斜体', 15 | handler: shortcuts.italic.handler, 16 | getIcon() { 17 | return ; 18 | }, 19 | disabled(flowChart: Graph) { 20 | return flowChart.getSelectedCellCount() === 0; 21 | }, 22 | selected(flowChart: Graph) { 23 | const cells = flowChart.getSelectedCells(); 24 | if (cells.length > 0) { 25 | return safeGet(cells, '0.attrs.label.fontStyle', 'normal') === 'italic'; 26 | } else { 27 | return false; 28 | } 29 | }, 30 | }); 31 | 32 | export default Italic; 33 | -------------------------------------------------------------------------------- /packages/core/src/mods/toolBar/widgets/lineStyle.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement } from 'react'; 2 | 3 | import 'antd/es/menu/style'; 4 | 5 | import { Menu } from 'antd'; 6 | import { Graph } from '@antv/x6'; 7 | import { safeGet } from '../../../utils'; 8 | import XIcon from '../../../components/xIcon'; 9 | import makeDropdownWidget from './common/makeDropdownWidget'; 10 | import { 11 | hasEdgeSelected, 12 | getSelectedEdges, 13 | } from '../../../utils/flowChartUtils'; 14 | 15 | interface IProps { 16 | flowChart: Graph; 17 | } 18 | 19 | interface LineStyleItem { 20 | text: string; 21 | icon: ReactElement; 22 | attrs: { 23 | type: string; 24 | strokeDasharray: string; 25 | }; 26 | } 27 | 28 | const MenuItem = Menu.Item; 29 | const LINE_STYLE_MAP: { [key: string]: LineStyleItem } = { 30 | straight: { 31 | text: '直线', 32 | icon: , 33 | attrs: { 34 | type: 'straight', 35 | strokeDasharray: '5, 0', 36 | }, 37 | }, 38 | dashed: { 39 | text: '虚线', 40 | icon: , 41 | attrs: { 42 | type: 'dashed', 43 | strokeDasharray: '5, 5', 44 | }, 45 | }, 46 | }; 47 | 48 | const LineStyle: React.FC = makeDropdownWidget({ 49 | tooltip: '线条样式', 50 | getIcon(flowChart: Graph) { 51 | let lineType = 'straight'; 52 | const edges = getSelectedEdges(flowChart); 53 | if (edges.length > 0) { 54 | lineType = safeGet(edges, '0.attrs.line.type', 'straight'); 55 | } 56 | return LINE_STYLE_MAP[lineType].icon; 57 | }, 58 | getOverlay(flowChart: Graph, onChange: (data: any) => void) { 59 | return ( 60 | onChange(LINE_STYLE_MAP[key].attrs)}> 61 | {Object.keys(LINE_STYLE_MAP).map((lineType: string) => ( 62 | 63 | {LINE_STYLE_MAP[lineType].icon} 64 | {LINE_STYLE_MAP[lineType].text} 65 | 66 | ))} 67 | 68 | ); 69 | }, 70 | handler: (flowChart: Graph, value: any) => { 71 | getSelectedEdges(flowChart).forEach((edge) => 72 | edge.setAttrs({ line: value }), 73 | ); 74 | }, 75 | disabled(flowChart: Graph) { 76 | return !hasEdgeSelected(flowChart); 77 | }, 78 | }); 79 | 80 | export default LineStyle; 81 | -------------------------------------------------------------------------------- /packages/core/src/mods/toolBar/widgets/modifyStatus.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | 3 | import styles from './index.module.less'; 4 | 5 | import { Graph } from '@antv/x6'; 6 | 7 | enum Status { 8 | pending = 0, 9 | syncing = 1, 10 | successful = 2, 11 | failed = 3, 12 | } 13 | 14 | interface IProps { 15 | flowChart: Graph; 16 | } 17 | 18 | const statusMap = { 19 | [Status.pending]: { 20 | color: '', 21 | text: '', 22 | }, 23 | [Status.syncing]: { 24 | color: '#999', 25 | text: '正在保存...', 26 | }, 27 | [Status.successful]: { 28 | color: '#999', 29 | text: '所有更改已保存', 30 | }, 31 | [Status.failed]: { 32 | color: '#EC5B56', 33 | text: '同步失败,进入离线模式', 34 | }, 35 | }; 36 | 37 | const ModifyStatus: React.FC = (props) => { 38 | const { flowChart } = props; 39 | const [status, setStatus] = useState(Status.pending); 40 | 41 | useEffect(() => { 42 | flowChart.on('graph:change:modify', () => { 43 | setStatus(Status.syncing); 44 | }); 45 | flowChart.on('graph:modified', (res: any) => { 46 | const { success } = res; 47 | if (success) { 48 | setStatus(Status.successful); 49 | } else { 50 | setStatus(Status.failed); 51 | } 52 | }); 53 | return () => { 54 | flowChart.off('graph:change:modify'); 55 | flowChart.off('graph:modified'); 56 | }; 57 | }, []); 58 | 59 | const { color, text } = statusMap[status]; 60 | return status === Status.pending ? null : ( 61 |
62 | {text} 63 |
64 | ); 65 | }; 66 | 67 | export default ModifyStatus; 68 | -------------------------------------------------------------------------------- /packages/core/src/mods/toolBar/widgets/redo.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Graph } from '@antv/x6'; 4 | import { RedoOutlined } from '@ant-design/icons'; 5 | import shortcuts from '../../../common/shortcuts'; 6 | import makeBtnWidget from './common/makeBtnWidget'; 7 | 8 | interface IProps { 9 | flowChart: Graph; 10 | } 11 | 12 | const Save: React.FC = makeBtnWidget({ 13 | tooltip: '重做', 14 | handler: shortcuts.redo.handler, 15 | getIcon() { 16 | return ; 17 | }, 18 | disabled(flowChart: Graph) { 19 | return !flowChart.canRedo(); 20 | }, 21 | }); 22 | 23 | export default Save; 24 | -------------------------------------------------------------------------------- /packages/core/src/mods/toolBar/widgets/save.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Graph } from '@antv/x6'; 4 | import { SaveOutlined } from '@ant-design/icons'; 5 | import shortcuts from '../../../common/shortcuts'; 6 | import makeBtnWidget from './common/makeBtnWidget'; 7 | 8 | interface IProps { 9 | flowChart: Graph; 10 | } 11 | 12 | const Save: React.FC = makeBtnWidget({ 13 | tooltip: '保存', 14 | handler: shortcuts.save.handler, 15 | getIcon() { 16 | return ; 17 | }, 18 | }); 19 | 20 | export default Save; 21 | -------------------------------------------------------------------------------- /packages/core/src/mods/toolBar/widgets/textColor.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import 'antd/es/menu/style'; 4 | import styles from './index.module.less'; 5 | 6 | import { Graph } from '@antv/x6'; 7 | import { safeGet } from '../../../utils'; 8 | import XIcon from '../../../components/xIcon'; 9 | import ColorPicker from '../../../components/colorPicker'; 10 | import makeDropdownWidget from './common/makeDropdownWidget'; 11 | import { 12 | hasNodeSelected, 13 | getSelectedNodes, 14 | } from '../../../utils/flowChartUtils'; 15 | 16 | interface IProps { 17 | flowChart: Graph; 18 | } 19 | 20 | const options = { 21 | tooltip: '文本颜色', 22 | getCurTextColor(flowChart: Graph) { 23 | let textColor = '#DDD'; 24 | const nodes = getSelectedNodes(flowChart); 25 | if (nodes.length > 0) { 26 | textColor = safeGet(nodes, '0.attrs.label.fill', '#575757'); 27 | } 28 | return textColor; 29 | }, 30 | getIcon(flowChart: Graph) { 31 | const textColor = options.getCurTextColor(flowChart); 32 | return ( 33 |
34 | 35 |
39 |
40 | ); 41 | }, 42 | getOverlay(flowChart: Graph, onChange: (data: any) => void) { 43 | const textColor = options.getCurTextColor(flowChart); 44 | const onChangeComplete = (color: string) => onChange(color); 45 | return ( 46 | 47 | ); 48 | }, 49 | handler: (flowChart: Graph, value: any) => { 50 | flowChart 51 | .getSelectedCells() 52 | .forEach((node) => node.setAttrs({ label: { fill: value } })); 53 | }, 54 | disabled(flowChart: Graph) { 55 | return !hasNodeSelected(flowChart); 56 | }, 57 | }; 58 | 59 | const TextColor: React.FC = makeDropdownWidget(options); 60 | 61 | export default TextColor; 62 | -------------------------------------------------------------------------------- /packages/core/src/mods/toolBar/widgets/underline.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Graph } from '@antv/x6'; 4 | import { safeGet } from '../../../utils'; 5 | import shortcuts from '../../../common/shortcuts'; 6 | import makeBtnWidget from './common/makeBtnWidget'; 7 | import { UnderlineOutlined } from '@ant-design/icons'; 8 | 9 | interface IProps { 10 | flowChart: Graph; 11 | } 12 | 13 | const Underline: React.FC = makeBtnWidget({ 14 | tooltip: '下划线', 15 | handler: shortcuts.underline.handler, 16 | getIcon() { 17 | return ; 18 | }, 19 | disabled(flowChart: Graph) { 20 | return flowChart.getSelectedCellCount() === 0; 21 | }, 22 | selected(flowChart: Graph) { 23 | const cells = flowChart.getSelectedCells(); 24 | if (cells.length > 0) { 25 | return ( 26 | safeGet(cells, '0.attrs.label.textDecoration', 'none') === 'underline' 27 | ); 28 | } else { 29 | return false; 30 | } 31 | }, 32 | }); 33 | 34 | export default Underline; 35 | -------------------------------------------------------------------------------- /packages/core/src/mods/toolBar/widgets/undo.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { Graph } from '@antv/x6'; 4 | import { UndoOutlined } from '@ant-design/icons'; 5 | import shortcuts from '../../../common/shortcuts'; 6 | import makeBtnWidget from './common/makeBtnWidget'; 7 | 8 | interface IProps { 9 | flowChart: Graph; 10 | } 11 | 12 | const Save: React.FC = makeBtnWidget({ 13 | tooltip: '撤销', 14 | handler: shortcuts.undo.handler, 15 | getIcon() { 16 | return ; 17 | }, 18 | disabled(flowChart: Graph) { 19 | return !flowChart.canUndo(); 20 | }, 21 | }); 22 | 23 | export default Save; 24 | -------------------------------------------------------------------------------- /packages/core/src/mods/toolBar/widgets/verticalAlign.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement } from 'react'; 2 | 3 | import 'antd/es/menu/style'; 4 | 5 | import { Menu } from 'antd'; 6 | import { Graph } from '@antv/x6'; 7 | import { safeGet } from '../../../utils'; 8 | import makeDropdownWidget from './common/makeDropdownWidget'; 9 | import { 10 | hasNodeSelected, 11 | getSelectedNodes, 12 | } from '../../../utils/flowChartUtils'; 13 | import { 14 | VerticalAlignTopOutlined, 15 | VerticalAlignMiddleOutlined, 16 | VerticalAlignBottomOutlined, 17 | } from '@ant-design/icons'; 18 | 19 | interface IProps { 20 | flowChart: Graph; 21 | } 22 | 23 | interface AlignItem { 24 | text: string; 25 | icon: ReactElement; 26 | attrs: { 27 | refY: number; 28 | refY2?: number; 29 | textVerticalAlign: string; 30 | align: { 31 | vertical: string; 32 | }; 33 | }; 34 | } 35 | 36 | const MenuItem = Menu.Item; 37 | const ALIGN_MAP: { [key: string]: AlignItem } = { 38 | top: { 39 | text: '上对齐', 40 | icon: , 41 | attrs: { 42 | refY: 0, 43 | refY2: 10, 44 | textVerticalAlign: 'start', 45 | align: { 46 | vertical: 'top', 47 | }, 48 | }, 49 | }, 50 | center: { 51 | text: '居中对齐', 52 | icon: , 53 | attrs: { 54 | refY: 0.5, 55 | refY2: 0, 56 | textVerticalAlign: 'middle', 57 | align: { 58 | vertical: 'center', 59 | }, 60 | }, 61 | }, 62 | bottom: { 63 | text: '下对齐', 64 | icon: , 65 | attrs: { 66 | refY: 0.99, 67 | refY2: -10, 68 | textVerticalAlign: 'end', 69 | align: { 70 | vertical: 'bottom', 71 | }, 72 | }, 73 | }, 74 | }; 75 | 76 | const VerticalAlign: React.FC = makeDropdownWidget({ 77 | tooltip: '垂直对齐', 78 | getIcon(flowChart: Graph) { 79 | let alignType = 'center'; 80 | const nodes = getSelectedNodes(flowChart); 81 | if (nodes.length > 0) { 82 | alignType = safeGet(nodes, '0.attrs.label.align.vertical', 'center'); 83 | } 84 | return ALIGN_MAP[alignType].icon; 85 | }, 86 | getOverlay(flowChart: Graph, onChange: (data: any) => void) { 87 | return ( 88 | onChange(ALIGN_MAP[key].attrs)}> 89 | {Object.keys(ALIGN_MAP).map((alignType: string) => ( 90 | 91 | {ALIGN_MAP[alignType].icon} 92 | {ALIGN_MAP[alignType].text} 93 | 94 | ))} 95 | 96 | ); 97 | }, 98 | handler: (flowChart: Graph, value: any) => { 99 | getSelectedNodes(flowChart).forEach((node) => 100 | node.setAttrs({ label: value }), 101 | ); 102 | }, 103 | disabled(flowChart: Graph) { 104 | return !hasNodeSelected(flowChart); 105 | }, 106 | }); 107 | 108 | export default VerticalAlign; 109 | -------------------------------------------------------------------------------- /packages/core/src/mods/toolBar/widgets/zoom.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | 3 | import styles from './index.module.less'; 4 | 5 | import { Graph } from '@antv/x6'; 6 | import shortcuts from '../../../common/shortcuts'; 7 | import makeBtnWidget from './common/makeBtnWidget'; 8 | import { ZoomOutOutlined, ZoomInOutlined } from '@ant-design/icons'; 9 | 10 | interface IProps { 11 | flowChart: Graph; 12 | } 13 | 14 | const MIN_ZOOM = 0.5; 15 | const MAX_ZOOM = 1.5; 16 | 17 | const ZoomOut: React.FC = makeBtnWidget({ 18 | tooltip: '缩小', 19 | handler: shortcuts.zoomOut.handler, 20 | getIcon() { 21 | return ; 22 | }, 23 | disabled(flowChart: Graph) { 24 | return flowChart.zoom() <= MIN_ZOOM; 25 | }, 26 | }); 27 | 28 | const ZoomIn: React.FC = makeBtnWidget({ 29 | tooltip: '放大', 30 | handler: shortcuts.zoomIn.handler, 31 | getIcon() { 32 | return ; 33 | }, 34 | disabled(flowChart: Graph) { 35 | return flowChart.zoom() >= MAX_ZOOM; 36 | }, 37 | }); 38 | 39 | const Zoom: React.FC = (props) => { 40 | const { flowChart } = props; 41 | const [scale, setScale] = useState(flowChart.zoom()); 42 | useEffect(() => { 43 | flowChart.on('scale', () => { 44 | setScale(flowChart.zoom()); 45 | }); 46 | }, [flowChart]); 47 | return ( 48 |
49 | 50 | {Helper.scaleFormatter(scale)} 51 | 52 |
53 | ); 54 | }; 55 | 56 | const Helper = { 57 | scaleFormatter(scale: number): string { 58 | return (scale * 100).toFixed(0) + '%'; 59 | }, 60 | }; 61 | 62 | export default Zoom; 63 | -------------------------------------------------------------------------------- /packages/core/src/typings.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.module.css' { 2 | const classes: { [key: string]: string }; 3 | export default classes; 4 | } 5 | 6 | declare module '*.less' { 7 | const classes: { [key: string]: string }; 8 | export default classes; 9 | } 10 | 11 | declare module 'fr-generator' { 12 | import React from 'react'; 13 | export interface FRProps { 14 | settings?: object; 15 | } 16 | class FormRender extends React.Component {} 17 | export default FRGenerator; 18 | } 19 | -------------------------------------------------------------------------------- /packages/core/src/utils/analyzeDeps.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { safeGet } from './index'; 3 | import { getLocalConfig } from '../api'; 4 | 5 | const regex = /import\s([\s\S]*?)\sfrom\s('|")((@\w[\w\.\-]+\/)?(\w[\w\.\-]+))(\/[\w\.\-]+)*\2/gm; 6 | // (1 ) (2 )(3(4 ) (5 ))(6 ) 7 | 8 | const extractPkgs = (code: string, excludePkgs?: string[]): string[] => { 9 | let matchRet = null; 10 | const pkgNames: string[] = []; 11 | while ((matchRet = regex.exec(code)) != null) { 12 | const pkgName = matchRet[3]; 13 | if (excludePkgs?.indexOf(pkgName) === -1) { 14 | pkgNames.push(pkgName); 15 | } 16 | } 17 | return pkgNames; 18 | }; 19 | 20 | const getPkgLatestVersion = (pkg: string): Promise => { 21 | const localConfig = getLocalConfig(); 22 | return axios 23 | .get(`${localConfig.npmRegistry}/${pkg}`) 24 | .then((res: any) => { 25 | return [pkg, safeGet(res, 'data.dist-tags.latest', '*')]; 26 | }) 27 | .catch((err) => { 28 | console.log(`get package ${pkg} info failed, the error is:`, err); 29 | return [pkg, '*']; 30 | }); 31 | }; 32 | 33 | const analyzeDeps = ( 34 | code: string, 35 | excludePkgs?: string[], 36 | ): Promise<{ [pkgName: string]: string }> => { 37 | const pkgs = extractPkgs(code, excludePkgs); 38 | return Promise.all(pkgs.map((pkg) => getPkgLatestVersion(pkg))) 39 | .then((pkgResults) => { 40 | const map: any = {}; 41 | pkgResults.forEach(([pkg, version]) => { 42 | map[pkg] = version; 43 | }); 44 | return map; 45 | }) 46 | .catch((err) => { 47 | console.log('analyze deps failed, the error is:', err); 48 | }); 49 | }; 50 | 51 | export default analyzeDeps; 52 | -------------------------------------------------------------------------------- /packages/core/src/utils/flowChartUtils.ts: -------------------------------------------------------------------------------- 1 | import { Cell, Graph } from '@antv/x6'; 2 | 3 | export const hasCellSelected = (flowChart: Graph): boolean => { 4 | return flowChart.getSelectedCellCount() > 0; 5 | }; 6 | 7 | export const hasNodeSelected = (flowChart: Graph): boolean => { 8 | return ( 9 | flowChart.getSelectedCells().filter((cell: Cell) => cell.isNode()).length > 10 | 0 11 | ); 12 | }; 13 | 14 | export const hasEdgeSelected = (flowChart: Graph): boolean => { 15 | return ( 16 | flowChart.getSelectedCells().filter((cell: Cell) => cell.isEdge()).length > 17 | 0 18 | ); 19 | }; 20 | 21 | export const getSelectedNodes = (flowChart: Graph): Cell[] => { 22 | return flowChart.getSelectedCells().filter((cell: Cell) => cell.isNode()); 23 | }; 24 | 25 | export const getSelectedEdges = (flowChart: Graph): Cell[] => { 26 | return flowChart.getSelectedCells().filter((cell: Cell) => cell.isEdge()); 27 | }; 28 | 29 | export const toSelectedCellsJSON = ( 30 | flowChart: Graph, 31 | ): { cells: Cell.Properties[] } => { 32 | const json = flowChart.toJSON(); 33 | const selectedCells = flowChart.getSelectedCells(); 34 | return { 35 | cells: json.cells.filter((cell) => 36 | selectedCells.find((o) => o.id === cell.id), 37 | ), 38 | }; 39 | }; 40 | -------------------------------------------------------------------------------- /packages/core/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { parse } from 'query-string'; 2 | 3 | export const safeParse = (json: string): Record => { 4 | try { 5 | return JSON.parse(json); 6 | } catch (error) { 7 | return {}; 8 | } 9 | }; 10 | 11 | export const safeGet = (obj: any, keyChain: string, defaultVal?: any): any => { 12 | if (typeof obj !== 'object' || obj === null) { 13 | return defaultVal; 14 | } 15 | 16 | let val = obj; 17 | const keys = keyChain.split('.'); 18 | for (const key of keys) { 19 | if (val[key] === undefined) { 20 | return defaultVal; 21 | } else { 22 | val = val[key]; 23 | } 24 | } 25 | 26 | return val; 27 | }; 28 | 29 | const parseConfig = { 30 | skipNull: true, 31 | skipEmptyString: true, 32 | parseNumbers: false, 33 | parseBooleans: false, 34 | }; 35 | 36 | export const parseQuery = (): { [key: string]: any } => { 37 | return parse(location.search, parseConfig); 38 | }; 39 | 40 | export const executeScript = (code: string, type = 'module') => { 41 | const script = document.createElement('script'); 42 | script.type = type; 43 | script.text = code; 44 | document.body.appendChild(script); 45 | }; 46 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig", 3 | "compilerOptions": { 4 | "target": "ES2015", 5 | "outDir": "dist/types", 6 | "jsx": "react", 7 | "esModuleInterop": true 8 | }, 9 | "include": ["./src"], 10 | "exclude": ["node_modules"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/plugin-store/.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /packages/plugin-store/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/i5ting/imove/0529e8127132380196ae14ea5f505b332dfcf8a6/packages/plugin-store/README.md -------------------------------------------------------------------------------- /packages/plugin-store/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@imove/plugin-store", 3 | "version": "0.0.3", 4 | "description": "iMove plugin that supports persisting data", 5 | "keywords": [ 6 | "imove", 7 | "plugin", 8 | "store" 9 | ], 10 | "author": "smallstonesk <575913857@qq.com>", 11 | "homepage": "https://github.com/suanmei/iMove#readme", 12 | "license": "MIT", 13 | "main": "dist/core.common.js", 14 | "module": "dist/core.esm.js", 15 | "types": "dist/types/index.d.ts", 16 | "directories": { 17 | "dist": "dist" 18 | }, 19 | "files": [ 20 | "dist" 21 | ], 22 | "publishConfig": { 23 | "access": "public", 24 | "registry": "http://registry.npmjs.org/" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/suanmei/iMove.git" 29 | }, 30 | "scripts": { 31 | "prepublishOnly": "node scripts/prepublish.js", 32 | "declare-type": "tsc --emitDeclarationOnly", 33 | "build": "rollup -c & npm run declare-type", 34 | "watch": "watch \"npm run build\" ./src" 35 | }, 36 | "bugs": { 37 | "url": "https://github.com/suanmei/iMove/issues" 38 | }, 39 | "devDependencies": {}, 40 | "dependencies": {} 41 | } 42 | -------------------------------------------------------------------------------- /packages/plugin-store/rollup.config.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import rollupBaseConfig from '../../rollup.config'; 3 | import typescript from 'rollup-plugin-typescript'; 4 | import pkg from './package.json'; 5 | 6 | export default Object.assign(rollupBaseConfig, { 7 | input: path.join(__dirname, './src/index.ts'), 8 | output: [ 9 | { 10 | file: pkg.main, 11 | format: 'cjs', 12 | }, 13 | { 14 | file: pkg.module, 15 | format: 'es', 16 | }, 17 | ], 18 | plugins: [ 19 | typescript(), 20 | ] 21 | }); 22 | -------------------------------------------------------------------------------- /packages/plugin-store/scripts/prepublish.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const path = require('path'); 4 | const prePublish = require('../../../scripts/prepublish'); 5 | 6 | prePublish('@imove/plugin-store', path.join(__dirname, '../')); 7 | -------------------------------------------------------------------------------- /packages/plugin-store/src/index.ts: -------------------------------------------------------------------------------- 1 | const storeSymbol = Symbol('store'); 2 | 3 | const plugin = (logic: any) => { 4 | return { 5 | ctxCreated(ctx: { [key: string]: any }) { 6 | if (!logic[storeSymbol]) { 7 | logic[storeSymbol] = {}; 8 | } 9 | ctx.store = { 10 | set(key: string, val: any) { 11 | logic[storeSymbol][key] = val; 12 | }, 13 | get(key: string) { 14 | return logic[storeSymbol][key]; 15 | }, 16 | remove(key: string) { 17 | delete logic[storeSymbol][key]; 18 | }, 19 | }; 20 | }, 21 | }; 22 | }; 23 | 24 | export default plugin; 25 | -------------------------------------------------------------------------------- /packages/plugin-store/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig", 3 | "compilerOptions": { 4 | "outDir": "dist/types" 5 | }, 6 | "include": ["./src", "./types"] 7 | } 8 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from '@rollup/plugin-node-resolve'; 2 | import commonjs from '@rollup/plugin-commonjs'; 3 | import strip from '@rollup/plugin-strip'; 4 | import json from '@rollup/plugin-json'; 5 | import { sizeSnapshot } from 'rollup-plugin-size-snapshot'; 6 | 7 | const input = './src/index.tsx'; 8 | const extensions = ['.js', '.jsx', '.ts', '.tsx']; 9 | 10 | export default { 11 | input, 12 | plugins: [ 13 | resolve({ extensions, preferBuiltins: true }), 14 | commonjs(), 15 | json(), 16 | strip({ debugger: true }), 17 | sizeSnapshot({ printInfo: false }), 18 | ], 19 | }; 20 | -------------------------------------------------------------------------------- /sandbox.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": "node", 3 | "container": { 4 | "port": 8000 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /scripts/prepublish.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const childProcess = require('child_process'); 4 | const ora = require('ora'); 5 | 6 | const timeout = 1000 * 60 * 2; 7 | const buildCommand = `npm run build`; 8 | 9 | function prePublish(packageAlias, packagePath) { 10 | const spinner = ora(`Building "${packageAlias}" library`).start(); 11 | 12 | try { 13 | childProcess.execSync(buildCommand, { timeout, cwd: packagePath }); 14 | } catch (error) { 15 | spinner.fail(`Could not finish building "${packageAlias}" library`); 16 | process.exit(); 17 | } 18 | 19 | spinner.succeed(`Finished building "${packageAlias}" library`); 20 | } 21 | 22 | module.exports = prePublish; 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "commonjs", 5 | "allowJs": false, 6 | "jsx": "react", 7 | "declaration": true, 8 | "types": [], 9 | "strict": true, 10 | "resolveJsonModule": true, 11 | "experimentalDecorators": true, 12 | "skipLibCheck": true 13 | } 14 | } --------------------------------------------------------------------------------