├── .commitlintrc.js ├── .editorconfig ├── .eslintrc.js ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── bug_report_zh.yml │ ├── config.yml │ ├── feature_request.yml │ └── feature_request_chinese.yml ├── PULL_REQUEST_TEMPLATE.md ├── issue-branch.yml └── workflows │ ├── apps │ └── wip.yml │ ├── config │ ├── label-commands.yml │ ├── labels.yml │ ├── needs-more-info.yml │ ├── pr-label-branch-name.yml │ ├── pr-label-file-paths.yml │ └── slash-commands.yml │ ├── create-issue-branch.yml │ ├── deploy-docs.yml │ ├── label-commands.yml │ ├── lock.yml │ ├── needs-more-info.yml │ ├── potential-duplicates.yml │ ├── pr-label-branch-name.yml │ ├── pr-label-file-paths.yml │ ├── pr-label-patch-size.yml │ ├── pr-label-status-dummy.yml │ ├── pr-label-status.yml │ ├── pr-label-title-body.yml │ ├── rebase.yml │ ├── release.yml │ ├── slash-commands.yml │ ├── stale.yml │ ├── sync-labels.yml │ ├── update-authors.yml │ ├── update-contributors.yml │ ├── update-license.yml │ └── welcome.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .lintstagedrc.js ├── .npmrc ├── .prettierignore ├── .prettierrc.js ├── .stylelintignore ├── .stylelintrc.js ├── .vscode ├── extensions.json └── settings.json ├── AUTHORS ├── CODE_OF_CONDUCT.md ├── CODE_OF_CONDUCT.zh-Hans.md ├── CONTRIBUTING.md ├── CONTRIBUTING.zh-Hans.md ├── CONTRIBUTORS.svg ├── LEGAL.md ├── LICENSE ├── README.md ├── README.zh-Hans.md ├── SECURITY.md ├── SECURITY.zh-Hans.md ├── apps └── basic │ ├── config │ ├── config.ts │ └── routes.ts │ ├── package.json │ ├── public │ └── favicon.ico │ ├── src │ └── pages │ │ ├── basic │ │ ├── index.less │ │ ├── index.tsx │ │ ├── json.tsx │ │ ├── shape.tsx │ │ └── tools.tsx │ │ ├── dag │ │ ├── config-drawer │ │ │ └── index.tsx │ │ ├── connect.tsx │ │ ├── dnd │ │ │ ├── dnd.less │ │ │ ├── dnd.tsx │ │ │ └── search │ │ │ │ ├── index.less │ │ │ │ └── index.tsx │ │ ├── index.less │ │ ├── index.tsx │ │ ├── node.less │ │ ├── node.tsx │ │ ├── shape.tsx │ │ └── toolbar │ │ │ └── index.tsx │ │ ├── diff │ │ └── index.tsx │ │ ├── dnd │ │ ├── dnd.tsx │ │ ├── index.less │ │ └── index.tsx │ │ ├── drawing │ │ ├── draw.tsx │ │ ├── index.less │ │ └── index.tsx │ │ ├── flow │ │ ├── connector.tsx │ │ ├── dnd.tsx │ │ ├── edge.tsx │ │ ├── index.less │ │ ├── index.tsx │ │ ├── keyboard.tsx │ │ └── node.tsx │ │ └── group │ │ ├── ContextMenu │ │ ├── index.less │ │ └── index.tsx │ │ ├── GroupNode │ │ ├── index.less │ │ └── index.tsx │ │ ├── NormalNode │ │ ├── index.less │ │ └── index.tsx │ │ ├── const.ts │ │ ├── dagreLayout.ts │ │ ├── index.less │ │ ├── index.tsx │ │ ├── tools.tsx │ │ ├── type.ts │ │ └── utils.ts │ ├── tsconfig.json │ └── typings.d.ts ├── nx.json ├── package.json ├── packages ├── core │ ├── README.en-US.md │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── project.json │ ├── rollup.config.js │ ├── src │ │ ├── components │ │ │ ├── Background.tsx │ │ │ ├── Clipboard.tsx │ │ │ ├── Control.tsx │ │ │ ├── Graph.tsx │ │ │ ├── Grid.tsx │ │ │ ├── History.tsx │ │ │ ├── Minimap.tsx │ │ │ ├── Snapline.tsx │ │ │ ├── State.tsx │ │ │ ├── Transform.tsx │ │ │ ├── Wrapper.tsx │ │ │ ├── XFlow.tsx │ │ │ └── index.ts │ │ ├── context │ │ │ ├── GraphContext.tsx │ │ │ ├── StoreContext.tsx │ │ │ └── index.ts │ │ ├── hooks │ │ │ ├── index.ts │ │ │ ├── useClipboard.ts │ │ │ ├── useDnd.ts │ │ │ ├── useExport.ts │ │ │ ├── useGraphEvent.ts │ │ │ ├── useGraphInstance.ts │ │ │ ├── useGraphStore.ts │ │ │ ├── useHistory.ts │ │ │ ├── useKeyboard.ts │ │ │ ├── useLatest.ts │ │ │ └── useLoaded.ts │ │ ├── index.ts │ │ ├── store │ │ │ └── index.ts │ │ ├── styles │ │ │ └── index.less │ │ ├── types │ │ │ └── index.ts │ │ └── util │ │ │ ├── algorithm.ts │ │ │ ├── index.ts │ │ │ └── object.ts │ ├── tsconfig.json │ └── tsup.config.js └── diff │ ├── README.en-US.md │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── project.json │ ├── src │ ├── components │ │ ├── DiffGraph │ │ │ ├── index.tsx │ │ │ └── tool.tsx │ │ └── index.ts │ ├── index.ts │ ├── styles │ │ └── index.less │ ├── types │ │ └── index.ts │ └── util │ │ └── index.ts │ ├── tsconfig.json │ └── tsup.config.js ├── pnpm-lock.yaml ├── pnpm-workspace.yaml └── tooling ├── eslint ├── index.js └── package.json ├── jest ├── config │ ├── base.js │ └── react.js ├── jest.ts ├── package.json └── react-testing-library.ts ├── stylelint ├── index.js └── package.json ├── tsconfig ├── mana.json ├── package.json ├── tsconfig.json └── umi.json └── tsup ├── index.d.ts ├── index.js └── package.json /.commitlintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-conventional'] }; 2 | -------------------------------------------------------------------------------- /.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 | [Makefile] 13 | indent_style = tab 14 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: [require.resolve('@antv/config-eslint')], 4 | ignorePatterns: ['vendor/**/'], 5 | rules: { 6 | '@typescript-eslint/no-empty-function': 'off', 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf 2 | vendor/**/* -text 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: '🐞 Bug Report' 2 | description: Report a reproducible error or regression 3 | body: 4 | - type: markdown 5 | attributes: 6 | value: | 7 | Thank you for reporting an issue :pray:. 8 | 9 | This issue tracker is for reporting bugs found in XFlow (https://github.com/antvis/XFlow) 10 | If you have a question about how to achieve something and are struggling, please post a question 11 | inside of XFlow's Discussion's tab: https://github.com/antvis/XFlow/discussions 12 | 13 | Before submitting a new bug/issue, please check the links below to see if there is a solution or question posted there already: 14 | - XFlow's Issue's tab: https://github.com/antvis/XFlow/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc 15 | - XFlow's Closed issues tab: https://github.com/antvis/XFlow/issues?q=is%3Aissue+sort%3Aupdated-desc+is%3Aclosed 16 | - XFlow Discussion's tab: https://github.com/antvis/XFlow/discussions 17 | 18 | The more information you fill in, the better the community can help you. 19 | - type: textarea 20 | id: description 21 | attributes: 22 | label: Describe the bug 23 | description: Provide a clear and concise description of the challenge you are running into. 24 | validations: 25 | required: true 26 | - type: input 27 | id: link 28 | attributes: 29 | label: Your Example Website or App 30 | description: | 31 | Which website or app were you using when the bug happened? 32 | Note: 33 | - Your bug will may get fixed much faster if we can run your code and it doesn't have dependencies other than the XFlow npm package. 34 | - To create a shareable code example you can use Stackblitz (https://stackblitz.com/) or CodeSandbox (https://codesandbox.io/s/new). Please no localhost URLs. 35 | - Please read these tips for providing a minimal example: https://stackoverflow.com/help/mcve. 36 | placeholder: | 37 | e.g. Stackblitz, Code Sandbox app url 38 | validations: 39 | required: true 40 | - type: textarea 41 | id: steps 42 | attributes: 43 | label: Steps to Reproduce the Bug or Issue 44 | description: Describe the steps we have to take to reproduce the behavior. 45 | placeholder: | 46 | 1. Go to '...' 47 | 2. Click on '....' 48 | 3. Scroll down to '....' 49 | 4. See error 50 | validations: 51 | required: true 52 | - type: textarea 53 | id: expected 54 | attributes: 55 | label: Expected behavior 56 | description: Provide a clear and concise description of what you expected to happen. 57 | placeholder: | 58 | As a user, I expected ___ behavior but i am seeing ___ 59 | validations: 60 | required: true 61 | - type: textarea 62 | id: screenshots_or_videos 63 | attributes: 64 | label: Screenshots or Videos 65 | description: | 66 | If applicable, add screenshots or a video to help explain your problem. 67 | For more information on the supported file image/file types and the file size limits, please refer 68 | to the following link: https://docs.github.com/en/github/writing-on-github/working-with-advanced-formatting/attaching-files 69 | placeholder: | 70 | You can drag your video or image files inside of this editor ↓ 71 | - type: textarea 72 | id: platform 73 | attributes: 74 | label: Platform 75 | value: | 76 | - OS: [e.g. macOS, Windows, Linux] 77 | - Browser: [e.g. Chrome, Safari, Firefox] 78 | - Version: [e.g. 91.1] 79 | validations: 80 | required: true 81 | - type: textarea 82 | id: additional 83 | attributes: 84 | label: Additional context 85 | description: Add any other context about the problem here. 86 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report_zh.yml: -------------------------------------------------------------------------------- 1 | name: '🐞 提交 Bug Issue' 2 | description: 创建一个新的 issue,如果你的 issue 不符合规范,它将会被自动关闭。 3 | body: 4 | - type: markdown 5 | attributes: 6 | value: | 7 | 在提交新 issue 之前,先通过以下链接查看有没有类似的 bug 或者建议: 8 | - [XFlow Issues](https://github.com/antvis/XFlow/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc) 9 | - [XFlow Closed Issues](https://github.com/antvis/XFlow/issues?q=is%3Aissue+sort%3Aupdated-desc+is%3Aclosed) 10 | - [XFlow Discussions](https://github.com/antvis/XFlow/discussions) 11 | - type: textarea 12 | id: description 13 | attributes: 14 | label: 问题描述 15 | description: 简洁清晰地描述你遇到的问题。 16 | validations: 17 | required: true 18 | - type: input 19 | id: link 20 | attributes: 21 | label: 重现链接 22 | description: | 23 | 可以使用 CodeSandbox(https://codesandbox.io/s/new) 或者 StackBlitz(https://stackblitz.com/) 重现你的问题。 24 | placeholder: | 25 | 示例: CodeSandBox 或者 StackBlitz URL 26 | validations: 27 | required: true 28 | - type: textarea 29 | id: steps 30 | attributes: 31 | label: 重现步骤 32 | description: 简洁清晰的重现步骤能够帮助我们更迅速地定位问题所在。 33 | placeholder: | 34 | 1.进入页面... 35 | 2.点击.... 36 | 3.查看错误.... 37 | validations: 38 | required: true 39 | - type: textarea 40 | id: expected 41 | attributes: 42 | label: 预期行为 43 | description: 描述你期望的结果以及实际的结果。 44 | placeholder: | 45 | 我期望看到...,但我看到了... 46 | validations: 47 | required: true 48 | - type: textarea 49 | id: platform 50 | attributes: 51 | label: 平台 52 | value: | 53 | - 操作系统: [macOS, Windows, Linux, React Native ...] 54 | - 网页浏览器: [Google Chrome, Safari, Firefox] 55 | - XFlow 版本: [1.28.2 ... ] 56 | validations: 57 | required: true 58 | - type: textarea 59 | id: screenshots_or_videos 60 | attributes: 61 | label: 屏幕截图或视频(可选) 62 | description: 可以添加屏幕截图或视频帮助你解释问题。 63 | placeholder: | 64 | 可以将你的图片或者视频拖拽到此处↓ 65 | - type: textarea 66 | id: additional 67 | attributes: 68 | label: 补充说明(可选) 69 | description: 比如:遇到这个 bug 的业务场景、上下文。 70 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: 💬 Questions & Answers 4 | url: https://github.com/antvis/XFlow/discussions/categories/q-a 5 | about: Please ask and answer questions here → 6 | - name: 💬 问题和答案 7 | url: https://github.com/antvis/XFlow/discussions/categories/q-a 8 | about: 在此提问并在此处分享您的答案→ 9 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: 💡 Feature Request 2 | description: Create a feature request for XFlow 3 | labels: ["template: story"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: Thanks for taking the time to file a feature request! Please fill out this form as completely as possible. 8 | - type: markdown 9 | attributes: 10 | value: 'Feature requests will be converted to the GitHub Discussions "💡 Ideas" section (https://github.com/antvis/XFlow/discussions/categories/ideas)' 11 | - type: textarea 12 | attributes: 13 | label: Describe the feature you'd like to request 14 | description: A clear and concise description of what you want and what your use case is. The more context & description you provide the team the better it will help us understand your problem and save time. 15 | validations: 16 | required: true 17 | - type: textarea 18 | id: describe_solution 19 | attributes: 20 | label: Describe the solution you'd like 21 | description: A clear and concise description of what you want to happen. 22 | validations: 23 | required: true 24 | - type: textarea 25 | id: describe_alternatives 26 | attributes: 27 | label: Describe alternatives you've considered 28 | description: A clear and concise description of any alternative solutions or features you've considered. 29 | validations: 30 | required: true 31 | - type: input 32 | id: code_link 33 | attributes: 34 | label: Your Example Website or App 35 | description: | 36 | Which website or app were you using when the bug happened? 37 | Note: 38 | - Your bug will may get fixed much faster if we can run your code and it doesn't have dependencies other than the G6 npm package. 39 | - To create a shareable code example you can use Stackblitz (https://stackblitz.com/) or CodeSandbox (https://codesandbox.io/s/new). Please no localhost URLs. 40 | - Please read these tips for providing a minimal example: https://stackoverflow.com/help/mcve. 41 | placeholder: | 42 | e.g. Stackblitz, Code Sandbox app url 43 | - type: textarea 44 | id: screenshots_or_videos 45 | attributes: 46 | label: Screenshots or Videos 47 | description: | 48 | If applicable, add screenshots or a video to help explain your problem. 49 | Please do share examples (links, image, video etc) from other libraries or frameworks if you think that will help capture your idea. 50 | For more information on the supported file image/file types and the file size limits, please refer 51 | to the following link: https://docs.github.com/en/github/writing-on-github/working-with-advanced-formatting/attaching-files 52 | placeholder: | 53 | You can drag your video or image files inside of this editor ↓ 54 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request_chinese.yml: -------------------------------------------------------------------------------- 1 | name: 💡 功能请求 2 | description: 为XFlow创建一个功能请求 3 | labels: ["template: story"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: 谢谢你花时间提出功能请求! 请尽可能完整地填写此表 8 | - type: markdown 9 | attributes: 10 | value: 功能请求将被转换到GitHub讨论的"💡想法 "部分(https://github.com/antvis/XFlow/discussions/categories/ideas)' 11 | - type: textarea 12 | id: describe_feature_request 13 | attributes: 14 | label: 描述你想申请的功能 15 | description: 简明扼要地描述你想要什么,你的用例是什么。你给团队提供的背景和描述越多,就越能帮助我们理解你的问题并节省时间。 16 | validations: 17 | required: true 18 | - type: textarea 19 | id: describe_solution 20 | attributes: 21 | label: 描述你想要的解决方案 22 | description: 简明扼要地描述你希望发生的事情。 23 | validations: 24 | required: true 25 | - type: textarea 26 | id: describe_alternatives 27 | attributes: 28 | label: 描述你所考虑的替代方案 29 | description: 简明扼要地描述你所考虑的任何替代方案或功能。 30 | validations: 31 | required: true 32 | - type: input 33 | id: code_link 34 | attributes: 35 | label: 你的网站或应用程序实例 36 | description: | 37 | 当错误发生时,你在使用哪个网站或应用程序? 38 | 注意。 39 | - 如果我们能够运行你的代码,并且它没有G6 npm包以外的依赖,你的错误可能会更快得到修复。 40 | - 要创建一个可共享的代码示例,你可以使用Stackblitz(https://stackblitz.com/)或CodeSandbox(https://codesandbox.io/s/new)。请不要使用localhost的URL。 41 | - 请阅读这些关于提供一个最小的例子的提示: https://stackoverflow.com/help/mcve。 42 | placeholder: | 43 | 例如: Stackblitz、Code Sandbox应用程序的网址 44 | - type: textarea 45 | id: screenshots_or_videos 46 | attributes: 47 | label: 屏幕截图或视频 48 | description: | 49 | 如果适用,请添加屏幕截图或视频以帮助解释你的问题。 50 | 如果你认为其他库或框架的例子(链接、图片、视频等)有助于捕捉你的想法,请分享。 51 | 关于支持的文件图像/文件类型和文件大小限制的更多信息,请参考 52 | 以下链接: https://docs.github.com/en/github/writing-on-github/working-with-advanced-formatting/attaching-files 53 | placeholder: | 54 | 你可以把你的视频或图片文件拖到这个编辑器里面去 ↓ 55 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ### Description 4 | 5 | 6 | 7 | ### Motivation and Context 8 | 9 | 10 | 11 | 12 | 13 | 14 | ### Types of changes 15 | 16 | 17 | 18 | - [ ] Bug fix (non-breaking change which fixes an issue) 19 | - [ ] New feature (non-breaking change which adds functionality) 20 | - [ ] Breaking change (fix or feature that would cause existing functionality to change) 21 | - [ ] Enhancement (changes that improvement of current feature or performance) 22 | - [ ] Refactoring (changes that neither fixes a bug nor adds a feature) 23 | - [ ] Test Case (changes that add missing tests or correct existing tests) 24 | - [ ] Code style optimization (changes that do not affect the meaning of the code) 25 | - [ ] Docs (changes that only update documentation) 26 | - [ ] Chore (changes that don't modify src or test files) 27 | 28 | ### Self Check before Merge 29 | 30 | 31 | 32 | 33 | - [ ] My code follows the code style of this project. 34 | - [ ] My change requires a change to the documentation. 35 | - [ ] I have updated the documentation accordingly. 36 | - [ ] I have read the [**CONTRIBUTING**](https://github.com/antvis/x6/blob/master/CONTRIBUTING.md) document. 37 | - [ ] I have added tests to cover my changes. 38 | - [ ] All new and existing tests passed. 39 | -------------------------------------------------------------------------------- /.github/issue-branch.yml: -------------------------------------------------------------------------------- 1 | mode: chatops 2 | branchName: short 3 | branches: 4 | - label: bug 5 | prefix: fix/ 6 | - label: 7 | - feature 8 | - enhancement 9 | prefix: feat/ 10 | - label: 11 | - doc 12 | - document 13 | - documentation 14 | prefix: doc/ 15 | - label: '*' 16 | prefix: issue/ 17 | - label: question 18 | skip: true 19 | experimental: 20 | branchNameArgument: true 21 | commentMessage: | 22 | @${issue.user.login} 23 | 24 | Branch [${branchName}](${repository.html_url}/tree/${branchName}) was created for this issue. We will investigate into the matter and get back to you as soon as possible. 25 | 26 | 感谢反馈问题或提供改进建议,我们创建了 [${branchName}](${repository.html_url}/tree/${branchName}) 分支来处理这个问题,你可以继续跟进后续的解决方案。 27 | -------------------------------------------------------------------------------- /.github/workflows/apps/wip.yml: -------------------------------------------------------------------------------- 1 | locations: 2 | - title 3 | - label 4 | - commit 5 | 6 | terms: 7 | - wip 8 | - rfc 9 | - work in progress 10 | - work-in-progress 11 | - do not merge 12 | - do-not-merge 13 | - 🚧 14 | -------------------------------------------------------------------------------- /.github/workflows/config/label-commands.yml: -------------------------------------------------------------------------------- 1 | heated: 2 | lock: true 3 | lockReason: too heated 4 | comment: The thread has been temporarily locked. 5 | 6 | -heated: 7 | unlock: true 8 | 9 | issues: 10 | feature: 11 | close: true 12 | comment: | 13 | :wave: @{{ author }}, please use our idea board to request new features. 14 | -wontfix: 15 | open: true 16 | 17 | needs-more-info: 18 | close: true 19 | comment: | 20 | @{{ author }} 21 | 22 | 为了能够进行高效沟通,我们对 issue 有一定的格式要求,你的 issue 因为无复现步骤或可复现仓库而被自动关闭,提供之后会被 REOPEN。 23 | 24 | 25 | In order to communicate effectively, we have a certain format requirement for the issue, your issue is automatically closed because there is no recurring step or reproducible warehouse, and will be REOPEN after the offer. 26 | 27 | -needs-more-info: 28 | open: true 29 | -------------------------------------------------------------------------------- /.github/workflows/config/labels.yml: -------------------------------------------------------------------------------- 1 | - name: bug 2 | color: ee0701 3 | description: Something isn't working. 4 | - name: chore 5 | color: fef2c0 6 | description: Changes to the build process or auxiliary tools and libraries such as documentation generation. 7 | - name: doc 8 | color: d4c5f9 9 | description: Improvements or additions to documentation. 10 | - name: documentation 11 | color: d4c5f9 12 | description: Improvements or additions to documentation. 13 | - name: duplicate 14 | color: fbca04 15 | description: This issue or pull request already exists. 16 | - name: potential-duplicate 17 | color: fbca04 18 | description: This issue or pull request may duplicated with others. 19 | - name: enhancement 20 | color: 5ebeff 21 | description: New feature or request. 22 | - name: good first issue 23 | color: 7057ff 24 | description: Good for newcomers. 25 | - name: help wanted 26 | color: 0e8a16 27 | description: An issue that could be handled by anyone, even new members of the community. 28 | - name: invalid 29 | color: d73a4a 30 | description: This doesn't seem right. 31 | - name: question 32 | color: d4c5f9 33 | description: Issues that are just questions. 34 | - name: refactor 35 | color: fbca04 36 | description: A code change that neither fixes a bug nor adds a feature. 37 | - name: stale 38 | color: eeeeee 39 | description: Issue that may be closed soon due to the original author not responding any more. 40 | - name: style 41 | color: d4c5f9 42 | description: Changes that do not affect the meaning of the code. 43 | - name: Need Reproduce 44 | color: fbca04 45 | description: Need reproduce infomations. 46 | - name: needs-more-info 47 | color: fbca04 48 | description: PRs/Issues with either the default title or a blank body. 49 | - name: no-issue-activity 50 | color: fbca04 51 | description: 52 | - name: 'todo :spiral_notepad:' 53 | color: fbca04 54 | description: 55 | - name: weekly-digest 56 | color: 7057ff 57 | description: 58 | - name: WIP 59 | color: eeeeee 60 | description: The PR is a WIP. 61 | - name: wontfix 62 | color: eeeeee 63 | description: The issue will not be fixed or otherwise handled. When applied, the issue should be closed. 64 | 65 | # PR types 66 | - name: PR(fix) 67 | color: ee0701 68 | description: ':bug: Bug Fix' 69 | - name: PR(feature) 70 | color: 5ebeff 71 | description: ':tada: New feature' 72 | - name: PR(enhancement) 73 | color: 5ebeff 74 | description: ':rocket: New feature or request' 75 | - name: PR(chore) 76 | color: fef2c0 77 | description: ':turtle: Chore' 78 | - name: PR(dependency) 79 | color: b4a8d1 80 | description: ':shamrock: Updates about dependencies' 81 | - name: PR(documentation) 82 | color: d4c5f9 83 | description: ':book: Improvements or additions to documentation' 84 | - name: PR(refactor) 85 | color: fbca04 86 | description: ':100: A code change that neither fixes a bug nor adds a feature' 87 | - name: PR(test) 88 | color: e9f4dc 89 | description: ':white_check_mark: Adding missing tests' 90 | - name: PR(internal) 91 | color: 2739db 92 | description: ':house: Internal' 93 | - name: PR(breaking) 94 | color: fbca04 95 | description: ':boom: Breaking Change' 96 | 97 | # PR status 98 | - name: 'PR: draft' 99 | color: eeeeee 100 | description: PR is draft. 101 | - name: 'PR: unreviewed' 102 | color: fbca04 103 | description: PR does not have any reviews. 104 | - name: 'PR: reviewed-changes-requested' 105 | color: fbca04 106 | description: PR has reviewed and got Change request event. 107 | - name: 'PR: partially-approved' 108 | color: c2e2a2 109 | description: PR has reviewd and got Approve from one of the reviewers. 110 | - name: 'PR: reviewed-approved' 111 | color: 0e8a16 112 | description: PR has reviewed and got Approve from everyone. 113 | - name: 'PR: merged' 114 | color: 662daf 115 | description: PR has merged. 116 | 117 | # PR size 118 | - name: size/XS 119 | color: 91ca55 120 | description: Denotes a PR that changes 0-9 lines, ignoring generated files. 121 | - name: size/S 122 | color: c2e2a2 123 | description: Denotes a PR that changes 10-29 lines, ignoring generated files. 124 | - name: size/M 125 | color: e9f4dc 126 | description: Denotes a PR that changes 30-99 lines, ignoring generated files. 127 | - name: size/L 128 | color: fef6d7 129 | description: Denotes a PR that changes 100-499 lines, ignoring generated files. 130 | - name: size/XL 131 | color: fef2c0 132 | description: Denotes a PR that changes 500-999 lines, ignoring generated files. 133 | - name: size/XXL 134 | color: fbca04 135 | description: Denotes a PR that changes 1000+ lines, ignoring generated files. 136 | 137 | # PR changed packages 138 | - name: pkg:xflow 139 | color: eeeeee 140 | description: Denotes a PR that changes packages/xflow 141 | - name: pkg:xflow-core 142 | color: eeeeee 143 | description: Denotes a PR that changes packages/xflow-core 144 | - name: pkg:xflow-hook 145 | color: eeeeee 146 | description: Denotes a PR that changes packages/xflow-hook 147 | - name: pkg:xflow-extension 148 | color: eeeeee 149 | description: Denotes a PR that changes packages/xflow-extension 150 | - name: pkg:docs 151 | color: eeeeee 152 | description: Denotes a PR that changes packages/xflow-docs 153 | 154 | # PR release status 155 | - name: released 156 | color: 0e8a16 157 | description: PR has released 158 | - name: released on @beta 159 | color: 0e8a16 160 | description: PR has released on @beta 161 | -------------------------------------------------------------------------------- /.github/workflows/config/needs-more-info.yml: -------------------------------------------------------------------------------- 1 | # common config 2 | # ------------- 3 | 4 | # chenck issue and PR template 5 | checkTemplate: true 6 | # minimum title length required 7 | miniTitleLength: 8 8 | # add label to trigger a label command, see ./label-commands.yml 9 | labelToAdd: needs-more-info 10 | # reactions to add 11 | reactions: 12 | - '-1' 13 | - confused 14 | 15 | 16 | # config for issues 17 | # ----------------- 18 | issue: 19 | badTitles: 20 | - update 21 | - updates 22 | - test 23 | - issue 24 | - debug 25 | - demo 26 | badTitleComment: '' 27 | badBodyComment: '' 28 | 29 | 30 | # config for PRs 31 | # -------------- 32 | pullRequest: 33 | badTitles: 34 | - update 35 | - updates 36 | - test 37 | badTitleComment: | 38 | @{{ author }} Please provide us with more info about this pull request title. 39 | 40 | badBodyComment: | 41 | @{{ author }} Please provide us with more info about this pull request. 42 | -------------------------------------------------------------------------------- /.github/workflows/config/pr-label-branch-name.yml: -------------------------------------------------------------------------------- 1 | # https://github.com/marketplace/actions/PR-labeler 2 | 3 | PR(fix): ['fix/*', 'bug/*'] 4 | PR(chore): [chore/*] 5 | PR(test): ['testing/*', 'test/*'] 6 | PR(feature): ['feature/*', 'feat/*'] 7 | PR(breaking): ['breaking/*', 'break/*'] 8 | PR(internal): ['internal/*', 'inter/*'] 9 | PR(documentation): ['documentation/*', 'document/*', 'doc/*','docs/*'] 10 | PR(enhancement): ['enhancement/*', 'enhance/*'] 11 | PR(dependency): ['dependency/*', 'dep/*', 'deps/*'] 12 | PR(refactor): ['refactoring/*', 'refactor/*'] 13 | -------------------------------------------------------------------------------- /.github/workflows/config/pr-label-file-paths.yml: -------------------------------------------------------------------------------- 1 | pkg:xflow: 2 | - packages/xflow/**/* 3 | pkg:xflow-core: 4 | - packages/xflow-core/**/* 5 | pkg:xflow-hook: 6 | - packages/xflow-hook/**/* 7 | pkg:xflow-extension: 8 | - packages/xflow-extension/**/* 9 | pkg:docs: 10 | - packages/xflow-docs/**/* 11 | -------------------------------------------------------------------------------- /.github/workflows/config/slash-commands.yml: -------------------------------------------------------------------------------- 1 | heated: 2 | lock: true 3 | lockReason: too heated 4 | comment: The thread has been temporarily locked. 5 | 6 | spam: 7 | lock: true 8 | lockReason: spam 9 | comment: The thread has been temporarily locked. 10 | 11 | unlock: 12 | unlock: true 13 | 14 | handover: 15 | assign: 16 | - '-*' 17 | - '{{ input }}' 18 | 19 | assign: 20 | assign: '{{ input }}' 21 | 22 | issues: 23 | pin: 24 | pin: true 25 | 26 | unpin: 27 | unpin: true 28 | -------------------------------------------------------------------------------- /.github/workflows/create-issue-branch.yml: -------------------------------------------------------------------------------- 1 | name: 🚧 Create Issue Branch 2 | on: 3 | issue_comment: 4 | types: [created] 5 | jobs: 6 | cib: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - uses: wow-actions/use-app-token@v1 11 | with: 12 | app_id: ${{ secrets.APP_ID }} 13 | private_key: ${{ secrets.PRIVATE_KEY }} 14 | env_name: bot_token 15 | - uses: robvanderleek/create-issue-branch@main 16 | env: 17 | GITHUB_TOKEN: ${{ env.bot_token }} 18 | -------------------------------------------------------------------------------- /.github/workflows/deploy-docs.yml: -------------------------------------------------------------------------------- 1 | name: github pages 2 | on: 3 | push: 4 | branches: 5 | - master 6 | env: 7 | CI: true 8 | PNPM_CACHE_FOLDER: .pnpm-store 9 | jobs: 10 | version: 11 | timeout-minutes: 15 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: checkout code repository 15 | uses: actions/checkout@v2 16 | with: 17 | fetch-depth: 0 18 | - name: setup node.js 19 | uses: actions/setup-node@v2 20 | with: 21 | node-version: 14 22 | - name: install pnpm 23 | run: npm i pnpm@latest -g 24 | - name: setup pnpm config 25 | run: pnpm config set store-dir $PNPM_CACHE_FOLDER 26 | - name: install dependencies 27 | run: pnpm install 28 | - name: build docs 29 | run: pnpm run docs:build --filter xflow-docs 30 | - name: Deploy 31 | uses: peaceiris/actions-gh-pages@v3 32 | with: 33 | github_token: ${{ secrets.GITHUB_TOKEN }} 34 | publish_dir: ./packages/xflow-docs/dist 35 | -------------------------------------------------------------------------------- /.github/workflows/label-commands.yml: -------------------------------------------------------------------------------- 1 | name: 👾 Label Commands 2 | on: 3 | pull_request_target: 4 | types: [labeled, unlabeled] 5 | issues: 6 | types: [labeled, unlabeled] 7 | jobs: 8 | cmd: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: wow-actions/use-app-token@v1 12 | with: 13 | app_id: ${{ secrets.APP_ID }} 14 | private_key: ${{ secrets.PRIVATE_KEY }} 15 | env_name: bot_token 16 | - uses: wow-actions/label-commands@v1 17 | with: 18 | GITHUB_TOKEN: ${{ env.bot_token }} 19 | CONFIG_FILE: .github/workflows/config/label-commands.yml 20 | -------------------------------------------------------------------------------- /.github/workflows/lock.yml: -------------------------------------------------------------------------------- 1 | name: ⛔️ Lock Threads 2 | on: 3 | schedule: 4 | - cron: 0 0 * * * 5 | jobs: 6 | lock: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: wow-actions/use-app-token@v1 10 | with: 11 | app_id: ${{ secrets.APP_ID }} 12 | private_key: ${{ secrets.PRIVATE_KEY }} 13 | env_name: bot_token 14 | - uses: dessant/lock-threads@v2 15 | with: 16 | github-token: ${{ env.bot_token }} 17 | issue-lock-inactive-days: 365 18 | issue-lock-comment: | 19 | This thread has been automatically locked because it has not had recent activity. 20 | 21 | Please open a new issue for related bugs and link to relevant comments in this thread. 22 | process-only: issues 23 | -------------------------------------------------------------------------------- /.github/workflows/needs-more-info.yml: -------------------------------------------------------------------------------- 1 | name: 🚨 Needs More Info 2 | on: 3 | pull_request_target: 4 | types: [opened] 5 | issues: 6 | types: [opened] 7 | jobs: 8 | evaluate: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: wow-actions/use-app-token@v1 12 | with: 13 | app_id: ${{ secrets.APP_ID }} 14 | private_key: ${{ secrets.PRIVATE_KEY }} 15 | env_name: bot_token 16 | - uses: wow-actions/needs-more-info@v1 17 | with: 18 | GITHUB_TOKEN: ${{ env.bot_token }} 19 | CONFIG_FILE: .github/workflows/config/needs-more-info.yml 20 | 21 | -------------------------------------------------------------------------------- /.github/workflows/potential-duplicates.yml: -------------------------------------------------------------------------------- 1 | name: 🆖 Potential Duplicates 2 | on: 3 | issues: 4 | types: [opened, edited] 5 | jobs: 6 | run: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: wow-actions/use-app-token@v1 10 | with: 11 | app_id: ${{ secrets.APP_ID }} 12 | private_key: ${{ secrets.PRIVATE_KEY }} 13 | env_name: bot_token 14 | - uses: wow-actions/potential-duplicates@v1 15 | with: 16 | GITHUB_TOKEN: ${{ env.bot_token }} 17 | -------------------------------------------------------------------------------- /.github/workflows/pr-label-branch-name.yml: -------------------------------------------------------------------------------- 1 | name: 🏷️ Label(Branch Name) 2 | on: 3 | pull_request_target: 4 | types: [opened] 5 | jobs: 6 | label: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: wow-actions/use-app-token@v1 10 | with: 11 | app_id: ${{ secrets.APP_ID }} 12 | private_key: ${{ secrets.PRIVATE_KEY }} 13 | env_name: bot_token 14 | - uses: TimonVS/pr-labeler-action@v3 15 | with: 16 | configuration-path: .github/workflows/config/pr-label-branch-name.yml 17 | env: 18 | GITHUB_TOKEN: ${{ env.bot_token }} 19 | -------------------------------------------------------------------------------- /.github/workflows/pr-label-file-paths.yml: -------------------------------------------------------------------------------- 1 | name: 🏷️ Label(File Paths) 2 | on: pull_request_target 3 | jobs: 4 | label: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: wow-actions/use-app-token@v1 8 | with: 9 | app_id: ${{ secrets.APP_ID }} 10 | private_key: ${{ secrets.PRIVATE_KEY }} 11 | env_name: bot_token 12 | - uses: actions/labeler@v2 13 | with: 14 | repo-token: ${{ env.bot_token }} 15 | configuration-path: .github/workflows/config/pr-label-file-paths.yml 16 | -------------------------------------------------------------------------------- /.github/workflows/pr-label-patch-size.yml: -------------------------------------------------------------------------------- 1 | name: 🏷️ Label(Patch Size) 2 | on: pull_request_target 3 | jobs: 4 | label: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v2 8 | - uses: wow-actions/use-app-token@v1 9 | with: 10 | app_id: ${{ secrets.APP_ID }} 11 | private_key: ${{ secrets.PRIVATE_KEY }} 12 | env_name: bot_token 13 | - uses: pascalgn/size-label-action@v0.4.3 14 | env: 15 | GITHUB_TOKEN: ${{ env.bot_token }} 16 | IGNORED: "!.gitignore\nyarn.lock" 17 | -------------------------------------------------------------------------------- /.github/workflows/pr-label-status-dummy.yml: -------------------------------------------------------------------------------- 1 | name: 🏷️ Label(Status) Dummy 2 | on: 3 | pull_request_review: 4 | types: [submitted, dismissed] 5 | jobs: 6 | dummy: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - run: echo "this is a dummy workflow that triggers a workflow_run; it's necessary because otherwise the repo secrets will not be in scope for externally forked pull requests" 10 | -------------------------------------------------------------------------------- /.github/workflows/pr-label-status.yml: -------------------------------------------------------------------------------- 1 | name: 🏷️ Label(Status) 2 | on: 3 | pull_request_target: 4 | types: [opened, closed, edited, reopened, synchronize, ready_for_review] 5 | workflow_run: 6 | workflows: ['🏷️ Label(Status) Dummy'] # the workflow in step 1 7 | types: [requested] 8 | jobs: 9 | triage: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: wow-actions/use-app-token@v1 13 | with: 14 | app_id: ${{ secrets.APP_ID }} 15 | private_key: ${{ secrets.PRIVATE_KEY }} 16 | env_name: bot_token 17 | - uses: wow-actions/pr-triage@v1 18 | with: 19 | GITHUB_TOKEN: ${{ env.bot_token }} 20 | WORKFLOW-ID: ${{ github.event.workflow_run.id }} 21 | -------------------------------------------------------------------------------- /.github/workflows/pr-label-title-body.yml: -------------------------------------------------------------------------------- 1 | # Github action for automatically adding label or setting assignee when a new 2 | # Issue or PR is opened. https://github.com/marketplace/actions/issue-labeler 3 | 4 | name: 🏷️ Label(Title and Body) 5 | on: 6 | issues: 7 | types: [opened] 8 | pull_request_target: 9 | types: [opened] 10 | jobs: 11 | label: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: wow-actions/use-app-token@v1 15 | with: 16 | app_id: ${{ secrets.APP_ID }} 17 | private_key: ${{ secrets.PRIVATE_KEY }} 18 | env_name: bot_token 19 | - uses: Naturalclar/issue-action@v2.0.1 20 | with: 21 | title-or-body: title 22 | github-token: ${{ env.bot_token }} 23 | parameters: > 24 | [ 25 | { 26 | "keywords": ["bug", "error"], 27 | "labels": ["bug"] 28 | }, 29 | { 30 | "keywords": ["help", "guidance"], 31 | "labels": ["help-wanted"] 32 | } 33 | ] 34 | -------------------------------------------------------------------------------- /.github/workflows/rebase.yml: -------------------------------------------------------------------------------- 1 | name: 🎉 Rebase 2 | on: 3 | issue_comment: 4 | types: [created] 5 | jobs: 6 | rebase: 7 | if: github.event.issue.pull_request != '' && contains(github.event.comment.body, '/rebase') 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@master 11 | with: 12 | fetch-depth: 0 13 | - uses: wow-actions/use-app-token@v1 14 | with: 15 | app_id: ${{ secrets.APP_ID }} 16 | private_key: ${{ secrets.PRIVATE_KEY }} 17 | env_name: bot_token 18 | - uses: cirrus-actions/rebase@master 19 | env: 20 | GITHUB_TOKEN: ${{ env.bot_token }} 21 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Changesets 2 | on: 3 | push: 4 | branches: 5 | - master 6 | env: 7 | CI: true 8 | PNPM_CACHE_FOLDER: .pnpm-store 9 | jobs: 10 | version: 11 | timeout-minutes: 15 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: checkout code repository 15 | uses: actions/checkout@v2 16 | with: 17 | fetch-depth: 0 18 | - name: setup node.js 19 | uses: actions/setup-node@v2 20 | with: 21 | node-version: 14 22 | - name: install pnpm 23 | run: npm i pnpm@latest -g 24 | - name: Setup npmrc 25 | run: | 26 | cat << EOF > "$HOME/.npmrc" 27 | email=${NPM_EMAIL} 28 | //registry.npmjs.org/:_authToken=$NPM_TOKEN 29 | EOF 30 | env: 31 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 32 | NPM_EMAIL: ${{ secrets.NPM_EMAIL }} 33 | - name: setup pnpm config 34 | run: pnpm config set store-dir $PNPM_CACHE_FOLDER 35 | - name: install dependencies 36 | run: pnpm install 37 | - name: create publish versions 38 | uses: changesets/action@v1 39 | with: 40 | version: pnpm ci:version 41 | commit: 'chore: release versions' 42 | title: 'chore: release versions' 43 | env: 44 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 45 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 46 | - name: publish to npm 47 | run: pnpm run release 48 | -------------------------------------------------------------------------------- /.github/workflows/slash-commands.yml: -------------------------------------------------------------------------------- 1 | name: 🔱 Slash Commands 2 | on: 3 | issue_comment: 4 | types: [created] 5 | jobs: 6 | cmd: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: wow-actions/use-app-token@v1 10 | with: 11 | app_id: ${{ secrets.APP_ID }} 12 | private_key: ${{ secrets.PRIVATE_KEY }} 13 | env_name: bot_token 14 | - uses: wow-actions/slash-commands@v1 15 | with: 16 | GITHUB_TOKEN: ${{ env.bot_token }} 17 | CONFIG_FILE: .github/workflows/config/slash-commands.yml 18 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: 👻 Stale 2 | on: 3 | schedule: 4 | - cron: "0 0 * * *" 5 | jobs: 6 | stale: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: wow-actions/use-app-token@v1 10 | with: 11 | app_id: ${{ secrets.APP_ID }} 12 | private_key: ${{ secrets.PRIVATE_KEY }} 13 | env_name: bot_token 14 | - uses: actions/stale@v3 15 | with: 16 | repo-token: ${{ env.bot_token }} 17 | stale-issue-message: | 18 | Hiya! 19 | 20 | This issue has gone quiet. Spooky quiet. 👻 21 | 22 | We get a lot of issues, so we currently close issues after 60 days of inactivity. It’s been at least 20 days since the last update here. If we missed this issue or if you want to keep it open, please reply here. You can also add the label "not-stale" to keep this issue open! 23 | 24 | As a friendly reminder: the best way to see this issue, or any other, fixed is to open a Pull Request. Check out [contribute](https://github.com/antvis/X6/blob/master/CONTRIBUTING.md) for more information about opening PRs, triaging issues, and contributing! 25 | 26 | Thanks for being a part of the AntV community! 💪💯 27 | 28 | close-issue-message: | 29 | Hey again! 30 | 31 | It’s been 60 days since anything happened on this issue, so our friendly neighborhood robot (that’s me!) is going to close it. Please keep in mind that I’m only a robot 🤖, so if I’ve closed this issue in error, I’m `HUMAN_EMOTION_SORRY`. Please feel free to comment on this issue or create a new one if you need anything else. 32 | 33 | As a friendly reminder: the best way to see this issue, or any other, fixed is to open a Pull Request. Check out [contribute](https://github.com/antvis/X6/blob/master/CONTRIBUTING.md) for more information about opening PRs, triaging issues, and contributing! 34 | 35 | Thanks again for being part of the AntV community! 💪💯 36 | 37 | stale-pr-message: | 38 | Hiya! 39 | 40 | This PR has gone quiet. Spooky quiet. 👻 41 | 42 | We get a lot of PRs, so we currently close PRs after 60 days of inactivity. It’s been at least 20 days since the last update here. If we missed this PR or if you want to keep it open, please reply here. You can also add the label "not-stale" to keep this PR open! 43 | 44 | Thanks for being a part of the AntV community! 💪💯 45 | 46 | close-pr-message: | 47 | Hey again! 48 | 49 | It’s been 60 days since anything happened on this PR, so our friendly neighborhood robot (that’s me!) is going to close it. Please keep in mind that I’m only a robot 🤖, so if I’ve closed this PR in error, I’m `HUMAN_EMOTION_SORRY`. Please feel free to comment on this PR or create a new one if you need anything else. 50 | 51 | Thanks again for being part of the AntV community! 💪💯 52 | 53 | days-before-stale: 20 54 | days-before-close: 40 55 | stale-issue-label: 'stale' 56 | exempt-issue-label: 'not-stale,awaiting-approval,work-in-progress' 57 | stale-pr-label: 'stale' 58 | exempt-pr-label: 'not-stale,awaiting-approval,work-in-progress' 59 | -------------------------------------------------------------------------------- /.github/workflows/sync-labels.yml: -------------------------------------------------------------------------------- 1 | name: 🔄 Sync Labels 2 | on: 3 | push: 4 | branches: 5 | - master 6 | paths: 7 | - .github/workflows/config/labels.yml 8 | jobs: 9 | sync: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: wow-actions/use-app-token@v1 14 | with: 15 | app_id: ${{ secrets.APP_ID }} 16 | private_key: ${{ secrets.PRIVATE_KEY }} 17 | env_name: bot_token 18 | - uses: micnncim/action-label-syncer@v1 19 | env: 20 | GITHUB_TOKEN: ${{ env.bot_token }} 21 | with: 22 | manifest: .github/workflows/config/labels.yml 23 | -------------------------------------------------------------------------------- /.github/workflows/update-authors.yml: -------------------------------------------------------------------------------- 1 | name: 🎗 Update Authors 2 | on: 3 | push: 4 | branches: 5 | - master 6 | - alpha 7 | - beta 8 | jobs: 9 | authors: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | with: 14 | fetch-depth: 0 15 | - uses: wow-actions/use-app-token@v1 16 | with: 17 | app_id: ${{ secrets.APP_ID }} 18 | private_key: ${{ secrets.PRIVATE_KEY }} 19 | env_name: bot_token 20 | - uses: wow-actions/update-authors@v1 21 | with: 22 | GITHUB_TOKEN: ${{ env.bot_token }} 23 | bots: false 24 | -------------------------------------------------------------------------------- /.github/workflows/update-contributors.yml: -------------------------------------------------------------------------------- 1 | name: 🤝 Update Contributors 2 | on: 3 | schedule: 4 | - cron: '0 1 * * *' 5 | push: 6 | branches: 7 | - master 8 | - alpha 9 | - beta 10 | jobs: 11 | contributors: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: wow-actions/use-app-token@v1 15 | with: 16 | app_id: ${{ secrets.APP_ID }} 17 | private_key: ${{ secrets.PRIVATE_KEY }} 18 | env_name: bot_token 19 | - uses: wow-actions/contributors-list@v1 20 | with: 21 | GITHUB_TOKEN: ${{ env.bot_token }} 22 | excludeUsers: semantic-release-bot ImgBotApp 23 | -------------------------------------------------------------------------------- /.github/workflows/update-license.yml: -------------------------------------------------------------------------------- 1 | name: 🔑 Update License 2 | on: 3 | schedule: 4 | - cron: '0 3 1 1 *' # At 03:00 on day-of-month 1 in January. 5 | jobs: 6 | update: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | with: 11 | fetch-depth: 0 12 | - uses: wow-actions/use-app-token@v1 13 | with: 14 | app_id: ${{ secrets.APP_ID }} 15 | private_key: ${{ secrets.PRIVATE_KEY }} 16 | env_name: bot_token 17 | - uses: FantasticFiasco/action-update-license-year@v2 18 | with: 19 | token: ${{ env.bot_token }} 20 | -------------------------------------------------------------------------------- /.github/workflows/welcome.yml: -------------------------------------------------------------------------------- 1 | name: 👋 Welcome 2 | on: 3 | pull_request_target: 4 | types: [opened, closed] 5 | issues: 6 | types: [opened] 7 | jobs: 8 | welcome: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: wow-actions/use-app-token@v1 12 | with: 13 | app_id: ${{ secrets.APP_ID }} 14 | private_key: ${{ secrets.PRIVATE_KEY }} 15 | env_name: bot_token 16 | - uses: wow-actions/welcome@v1 17 | with: 18 | GITHUB_TOKEN: ${{ env.bot_token }} 19 | FIRST_ISSUE: | 20 | 👋 @{{ author }} 21 | 22 | Thanks for opening your first issue here! If you're reporting a 🐞 bug, please make sure you include steps to reproduce it. 23 | 24 | To help make it easier for us to investigate your issue, please follow the [contributing guidelines](https://github.com/antvis/X6/blob/master/CONTRIBUTING.md). 25 | 26 | We get a lot of issues on this repo, so please be patient and we will get back to you as soon as we can. 27 | 28 | FIRST_PR: | 29 | 👋 @{{ author }} 30 | 31 | 💖 Thanks for opening this pull request! 💖 32 | 33 | Please follow the [contributing guidelines](https://github.com/antvis/X6/blob/master/CONTRIBUTING.md). And we use [semantic commit messages](https://github.com/antvis/X6/blob/master/CONTRIBUTING.md#commit-message-format) to streamline the release process. 34 | 35 | Examples of commit messages with semantic prefixes: 36 | - `fix: don't overwrite prevent_default if default wasn't prevented` 37 | - `feat: add graph.scale() method` 38 | - `docs: graph.getShortestPath is now available` 39 | 40 | Things that will help get your PR across the finish line: 41 | - Follow the TypeScript, JavaScript, CSS and React coding style. 42 | - Run `npm run lint` locally to catch formatting errors earlier. 43 | - Document any user-facing changes you've made. 44 | - Include tests when adding/changing behavior. 45 | - Include screenshots and animated GIFs whenever possible. 46 | 47 | We get a lot of pull requests on this repo, so please be patient and we will get back to you as soon as we can. 48 | 49 | 50 | FIRST_PR_MERGED: | 51 | 👋 @{{ author }} 52 | 53 | Congrats on merging your first pull request! 🎉🎉🎉 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | .pnp 6 | .pnp.js 7 | 8 | # testing 9 | coverage 10 | 11 | # next.js 12 | .next/ 13 | out/ 14 | build 15 | 16 | dist 17 | es 18 | lib 19 | 20 | # misc 21 | .DS_Store 22 | *.pem 23 | 24 | # debug 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | .pnpm-debug.log* 29 | 30 | # local env files 31 | .env.local 32 | .env.development.local 33 | .env.test.local 34 | .env.production.local 35 | 36 | # turbo 37 | .turbo 38 | 39 | # Umi 40 | .umi*/ 41 | .dumi*/ 42 | 43 | # macOS 44 | .DS_Store 45 | 46 | .env 47 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | pnpm exec commitlint --edit ${1} 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | # 1) Validate committer email 5 | 6 | EMAIL=$(git config user.email) 7 | 8 | if [[ ! $EMAIL =~ ^.*(@antgroup.com|@alipay.com|@antfinancial-corp.com|@antfin-inc.com|@alibaba-inc.com|@antfin.com)$ ]]; 9 | then 10 | :; 11 | else 12 | echo "You are using a company email address."; 13 | echo "Please use your personal or GitHub email address instead."; 14 | echo ""; 15 | echo "To configure your email for this repository, run:"; 16 | echo ""; 17 | echo " git config user.email 'your.email@example.org'"; 18 | echo ""; 19 | exit 1; 20 | fi; 21 | 22 | # 2) Lint staged files 23 | 24 | pnpm exec lint-staged 25 | -------------------------------------------------------------------------------- /.lintstagedrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | '*.{html,js,jsx,ts,tsx,css,less,json,md}': ['prettier --check --ignore-unknown'], 3 | '*.{css,less}': ['stylelint --allow-empty-input'], 4 | '*.{js,jsx,ts,tsx}': ['eslint'], 5 | // FIXME: disabling type checking until we are ready 6 | // also this doesn't work with tsx 7 | /** 8 | * 9 | * @param {string} filenames 10 | * @returns {string} 11 | */ 12 | // '*.{ts,tsx}': (filenames) => 13 | // ['tsc', '--skipLibCheck', '--noEmit', ...filenames].join(' '), 14 | }; 15 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | # Using npmmirror.com instead of taobao.org to avoid extra redirects in lockfile 2 | # see https://github.com/pnpm/pnpm/issues/5769#issuecomment-1345283717 3 | # registry=https://registry.npmjs.org 4 | registry=https://registry.npmmirror.com 5 | 6 | # PNPM 7 | strict-peer-dependencies=false 8 | save-workspace-protocol=rolling 9 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | pnpm-lock.yaml 2 | dist 3 | vendor 4 | .github 5 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 88, 3 | singleQuote: true, 4 | trailingComma: 'all', 5 | proseWrap: 'always', 6 | }; 7 | -------------------------------------------------------------------------------- /.stylelintignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antvis/XFlow/4228133c32e543ffba8c4cf80218e4eea17adef5/.stylelintignore -------------------------------------------------------------------------------- /.stylelintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [require.resolve('@antv/config-stylelint')], 3 | }; 4 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. 3 | // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp 4 | // List of extensions which should be recommended for users of this workspace. 5 | "recommendations": [ 6 | "dbaeumer.vscode-eslint", 7 | "esbenp.prettier-vscode", 8 | "stylelint.vscode-stylelint", 9 | "amour1688.ts-in-markdown", 10 | "streetsidesoftware.code-spell-checker" 11 | ], 12 | // List of extensions recommended by VS Code that should not be recommended for users of this workspace. 13 | "unwantedRecommendations": [] 14 | } 15 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | ".vscode/*.json": "jsonc", 4 | "turbo.json": "jsonc", 5 | "nx.json": "jsonc", 6 | "tsconfig.*.json": "jsonc", 7 | "tsconfig.json": "jsonc" 8 | }, 9 | "search.exclude": { 10 | "**/node_modules": true, 11 | "**/dist": true, 12 | "**/coverage": true 13 | }, 14 | "editor.insertSpaces": true, 15 | "[typescript]": { 16 | "editor.tabSize": 2, 17 | "editor.defaultFormatter": "esbenp.prettier-vscode" 18 | }, 19 | "[typescriptreact]": { 20 | "editor.tabSize": 2, 21 | "editor.defaultFormatter": "esbenp.prettier-vscode" 22 | }, 23 | "[javascript]": { 24 | "editor.tabSize": 2, 25 | "editor.defaultFormatter": "esbenp.prettier-vscode" 26 | }, 27 | "[json]": { 28 | "editor.tabSize": 2, 29 | "editor.defaultFormatter": "esbenp.prettier-vscode" 30 | }, 31 | "[jsonc]": { 32 | "editor.tabSize": 2, 33 | "editor.defaultFormatter": "esbenp.prettier-vscode" 34 | }, 35 | "typescript.tsdk": "node_modules/typescript/lib", 36 | "files.insertFinalNewline": true, 37 | "editor.rulers": [88], 38 | "editor.formatOnSave": true, 39 | "editor.codeActionsOnSave": { 40 | "source.fixAll.eslint": "explicit", 41 | "source.fixAll.stylelint": "explicit" 42 | }, 43 | "[markdown]": { 44 | "editor.quickSuggestions": { 45 | "other": true, 46 | "comments": true, 47 | "strings": true 48 | } 49 | }, 50 | "cSpell.allowCompoundWords": true, 51 | "cSpell.enabled": true, 52 | "cSpell.words": [ 53 | "ahooks", 54 | "ahooksjs", 55 | "antd", 56 | "antv", 57 | "dumi", 58 | "dumirc", 59 | "esbuild", 60 | "favicons", 61 | "hljs", 62 | "immer", 63 | "isequal", 64 | "lcov", 65 | "lucide", 66 | "mfsu", 67 | "nocheck", 68 | "npmrc", 69 | "pannable", 70 | "scroller", 71 | "svgr", 72 | "tippyjs", 73 | "tsup", 74 | "umijs", 75 | "umirc", 76 | "xflow", 77 | "zustand" 78 | ] 79 | } 80 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Ariel-Lau <35189693+Ariel-Lau@users.noreply.github.com> 2 | Clifford Fajardo 3 | Draco 4 | Indigo-w <59961969+Indigo-w@users.noreply.github.com> 5 | James Tsang 6 | Jay Lu <517642043@qq.com> 7 | Jeepeng 8 | Jie <30657053+liuzhijie1@users.noreply.github.com> 9 | Jinke Li 10 | Joel Alan <31396322+lxfu1@users.noreply.github.com> 11 | Join <32259371+lyp000119@users.noreply.github.com> 12 | Lyn <47809781+lyn-boyu@users.noreply.github.com> 13 | MisterBoole <19159159+MisterBoole@users.noreply.github.com> 14 | MrMengJ <2646973632@qq.com> 15 | OctoberRain <42743672+october-rain@users.noreply.github.com> 16 | Tony Wu <93302820+tonywu6@users.noreply.github.com> 17 | Wenjun Xu <906626481@qq.com> 18 | Xinhui <120797887@qq.com> 19 | Xinhui 20 | Yao Yuan 21 | _ 22 | arthur657834 23 | bigdadel 24 | boyu.zlj 25 | bubkoo 26 | cwtuan 27 | dadel <497718909@qq.com> 28 | gujinyue1010 29 | leeper 30 | lxfu1 31 | lyn-boyu 32 | lyn-boyu 33 | marcccc 34 | newbyvector 35 | promise6512 <86681329+promise6512@users.noreply.github.com> 36 | vector 37 | wjqsummer <52412389+wjqsummer@users.noreply.github.com> 38 | yunyi 39 | zhou9411 40 | 你捉不到的this <2228429150@qq.com> 41 | 冯伟 42 | 小耀 43 | 小耀 44 | 文瑀 45 | 文瑀 46 | 简单爱一次 47 | 茶布多 <410374308@qq.com> 48 | 顾金跃 <361143552@qq.com> -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and 6 | maintainers pledge to making participation in our project and our community a 7 | harassment-free experience for everyone, regardless of age, body size, disability, 8 | ethnicity, sex characteristics, gender identity and expression, level of experience, 9 | education, socio-economic status, nationality, personal appearance, race, religion, or 10 | sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment include: 15 | 16 | - Using welcoming and inclusive language 17 | - Being respectful of differing viewpoints and experiences 18 | - Gracefully accepting constructive criticism 19 | - Focusing on what is best for the community 20 | - Showing empathy towards other community members 21 | 22 | Examples of unacceptable behavior by participants include: 23 | 24 | - The use of sexualized language or imagery and unwelcome sexual attention or advances 25 | - Trolling, insulting/derogatory comments, and personal or political attacks 26 | - Public or private harassment 27 | - Publishing others' private information, such as a physical or electronic address, 28 | without explicit permission 29 | - Other conduct which could reasonably be considered inappropriate in a professional 30 | setting 31 | 32 | ## Our Responsibilities 33 | 34 | Project maintainers are responsible for clarifying the standards of acceptable behavior 35 | and are expected to take appropriate and fair corrective action in response to any 36 | instances of unacceptable behavior. 37 | 38 | Project maintainers have the right and responsibility to remove, edit, or reject 39 | comments, commits, code, wiki edits, issues, and other contributions that are not 40 | aligned to this Code of Conduct, or to ban temporarily or permanently any contributor 41 | for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 42 | 43 | ## Scope 44 | 45 | This Code of Conduct applies both within project spaces and in public spaces when an 46 | individual is representing the project or its community. Examples of representing a 47 | project or community include using an official project e-mail address, posting via an 48 | official social media account, or acting as an appointed representative at an online or 49 | offline event. Representation of a project may be further defined and clarified by 50 | project maintainers. 51 | 52 | ## Enforcement 53 | 54 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by 55 | contacting the project team at . All complaints will be reviewed and investigated 56 | and will result in a response that is deemed necessary and appropriate to the 57 | circumstances. The project team is obligated to maintain confidentiality with regard to 58 | the reporter of an incident. Further details of specific enforcement policies may be 59 | posted separately. 60 | 61 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may 62 | face temporary or permanent repercussions as determined by other members of the 63 | project's leadership. 64 | 65 | ## Attribution 66 | 67 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 68 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 69 | 70 | [homepage]: https://www.contributor-covenant.org 71 | 72 | For answers to common questions about this code of conduct, see 73 | https://www.contributor-covenant.org/faq 74 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.zh-Hans.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and 6 | maintainers pledge to making participation in our project and our community a 7 | harassment-free experience for everyone, regardless of age, body size, disability, 8 | ethnicity, sex characteristics, gender identity and expression, level of experience, 9 | education, socio-economic status, nationality, personal appearance, race, religion, or 10 | sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment include: 15 | 16 | - Using welcoming and inclusive language 17 | - Being respectful of differing viewpoints and experiences 18 | - Gracefully accepting constructive criticism 19 | - Focusing on what is best for the community 20 | - Showing empathy towards other community members 21 | 22 | Examples of unacceptable behavior by participants include: 23 | 24 | - The use of sexualized language or imagery and unwelcome sexual attention or advances 25 | - Trolling, insulting/derogatory comments, and personal or political attacks 26 | - Public or private harassment 27 | - Publishing others' private information, such as a physical or electronic address, 28 | without explicit permission 29 | - Other conduct which could reasonably be considered inappropriate in a professional 30 | setting 31 | 32 | ## Our Responsibilities 33 | 34 | Project maintainers are responsible for clarifying the standards of acceptable behavior 35 | and are expected to take appropriate and fair corrective action in response to any 36 | instances of unacceptable behavior. 37 | 38 | Project maintainers have the right and responsibility to remove, edit, or reject 39 | comments, commits, code, wiki edits, issues, and other contributions that are not 40 | aligned to this Code of Conduct, or to ban temporarily or permanently any contributor 41 | for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 42 | 43 | ## Scope 44 | 45 | This Code of Conduct applies both within project spaces and in public spaces when an 46 | individual is representing the project or its community. Examples of representing a 47 | project or community include using an official project e-mail address, posting via an 48 | official social media account, or acting as an appointed representative at an online or 49 | offline event. Representation of a project may be further defined and clarified by 50 | project maintainers. 51 | 52 | ## Enforcement 53 | 54 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by 55 | contacting the project team at . All complaints will be reviewed and investigated 56 | and will result in a response that is deemed necessary and appropriate to the 57 | circumstances. The project team is obligated to maintain confidentiality with regard to 58 | the reporter of an incident. Further details of specific enforcement policies may be 59 | posted separately. 60 | 61 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may 62 | face temporary or permanent repercussions as determined by other members of the 63 | project's leadership. 64 | 65 | ## Attribution 66 | 67 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 68 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 69 | 70 | [homepage]: https://www.contributor-covenant.org 71 | 72 | For answers to common questions about this code of conduct, see 73 | https://www.contributor-covenant.org/faq 74 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | English (US) | [简体中文](CONTRIBUTING.zh-Hans.md) 2 | 3 | # Contributing 4 | 5 | ## Development 6 | 7 | Make sure you have [pnpm](https://pnpm.io/installation) and 8 | [Nx](https://nx.dev/getting-started/installation#installing-nx-globally) installed globally. 9 | 10 | ```bash 11 | npm -g install pnpm nx 12 | npm -g exec pnpm setup 13 | ``` 14 | 15 | ### First time setup 16 | 17 | ```bash 18 | pnpm bootstrap 19 | ``` 20 | 21 | > This will install deps with `pnpm install`, ant then setup all packages with 22 | > `nx run-many setup`. 23 | 24 | ### Starting the dev server 25 | 26 | ```bash 27 | pnpm dev 28 | ``` 29 | 30 | The example app will be available at http://localhost:8000 by default. 31 | 32 | ### Formatting 33 | 34 | ```bash 35 | pnpm fix 36 | # run ESLint/Stylelint/Prettier and attempt to fix found issues 37 | ``` 38 | 39 | ### Linting and testing 40 | 41 | ```bash 42 | pnpm lint 43 | pnpm test 44 | ``` 45 | 46 | ### Building 47 | 48 | ```bash 49 | pnpm build 50 | ``` 51 | 52 | You may then serve the built app with `pnpm serve`. 53 | 54 | ### Per-package operations 55 | 56 | [https://pnpm.io/filtering](https://pnpm.io/filtering) 57 | 58 | Use either the package name (the `"name"` field in `package.json`) or the **relative** 59 | path to the package's folder (must be prefixed with `./`) to specify the package(s) on 60 | which the command should be run. 61 | 62 | ```bash 63 | pnpm --filter [...] 64 | 65 | pnpm --filter web add react react-dom 66 | pnpm --filter ./apps/web add react react-dom 67 | # add react and react-dom as dependencies to the package named "web" 68 | # whose folder is located at ./apps/web 69 | 70 | pnpm --filter "@scope/*" run clean 71 | pnpm --filter "./packages/*" run clean 72 | # use glob to select multiple packages, the pattern must be quoted 73 | ``` 74 | 75 | #### Adding dependencies 76 | 77 | [https://pnpm.io/cli/add](https://pnpm.io/cli/add) 78 | 79 | `add` for normal dependencies `dependencies` 80 | 81 | ```bash 82 | pnpm --filter add [dependency ...] 83 | # pnpm --filter web add react react-dom 84 | ``` 85 | 86 | `add -D` for development dependencies `devDependencies` 87 | 88 | ```bash 89 | pnpm --filter add -D [dependency ...] 90 | # pnpm --filter web add -D jest 91 | ``` 92 | 93 | `add --save-peer` for peer dependencies `peerDependencies` 94 | 95 | ```bash 96 | pnpm --filter add --save-peer [dependency ...] 97 | # pnpm --filter ui add --save-peer react "monaco-editor@^0.31.0" 98 | ``` 99 | 100 | 💡 for adding workspace packages as dependencies, use the same command as above, but 101 | append `--workspace` 102 | 103 | ```bash 104 | pnpm --filter add [--save-dev|--save-peer] [dependency ...] --workspace 105 | # pnpm --filter web add -D eslint-config-project --workspace 106 | ``` 107 | -------------------------------------------------------------------------------- /CONTRIBUTING.zh-Hans.md: -------------------------------------------------------------------------------- 1 | [English (US)](CONTRIBUTING.md) | 简体中文 2 | 3 | # 贡献指南 4 | 5 | ## 开发 6 | 7 | 请确保有安装 [pnpm](https://pnpm.io/installation) 和 8 | [Nx](https://turbo.build/repo/docs/installing) 9 | 10 | ```bash 11 | npm -g install pnpm nx 12 | npm -g exec pnpm setup 13 | ``` 14 | 15 | ### 首次运行 16 | 17 | ```bash 18 | pnpm bootstrap 19 | ``` 20 | 21 | > 这会 `pnpm install` 安装依赖,然后 `nx run-many --target=setup` 初始化所有包。 22 | 23 | ### 启动开发服务器 24 | 25 | ```bash 26 | pnpm dev 27 | ``` 28 | 29 | 默认在 http://localhost:8000 启动 30 | 31 | ### 代码格式化 32 | 33 | ```bash 34 | pnpm fix 35 | # 运行 ESLint/Stylelint/Prettier 并且尝试自动修正问题 36 | ``` 37 | 38 | ### 检查及测试 39 | 40 | ```bash 41 | pnpm lint 42 | pnpm test 43 | ``` 44 | 45 | ### 构建 46 | 47 | ```bash 48 | pnpm build 49 | ``` 50 | 51 | 构建产物可以通过 `pnpm serve` 命令进行预览。 52 | 53 | ### 对单个 package 进行操作 54 | 55 | [https://pnpm.io/filtering](https://pnpm.io/filtering) 56 | 57 | 使用 package 的名称(`package.json` 中的 `"name"` 字段)或者 **相对路径**(必须以 `./` 58 | 开头)来指定要操作的 package. 59 | 60 | ```bash 61 | pnpm --filter [...] 62 | 63 | pnpm --filter web add react react-dom 64 | pnpm --filter ./apps/web add react react-dom 65 | # 将 react 和 react-dom 作为 dependencies 加到名为 web 的 package 中,其路径为 ./apps/web 66 | 67 | pnpm --filter "@scope/*" run clean 68 | pnpm --filter "./packages/*" run clean 69 | # 使用 glob 来选择多个 package,表达式必须使用双引号包裹 70 | ``` 71 | 72 | #### 安装新依赖 73 | 74 | [https://pnpm.io/cli/add](https://pnpm.io/cli/add) 75 | 76 | 使用以下命令: 77 | 78 | `add` 安装普通依赖 `dependencies` 79 | 80 | ```bash 81 | pnpm --filter add [dependency ...] 82 | # pnpm --filter web add react react-dom 83 | ``` 84 | 85 | `add -D` 安装开发依赖 `devDependencies` 86 | 87 | ```bash 88 | pnpm --filter add -D [dependency ...] 89 | # pnpm --filter web add -D jest 90 | ``` 91 | 92 | `add --save-peer` 安装同伴依赖 `peerDependencies` 93 | 94 | ```bash 95 | pnpm --filter add --save-peer [dependency ...] 96 | # pnpm --filter ui add --save-peer react "monaco-editor@^0.31.0" 97 | ``` 98 | 99 | 💡 将内部 package 作为依赖,请使用与上面相同的命令并在命令末尾加上 `--workspace` 100 | 101 | ```bash 102 | pnpm --filter add [--save-dev|--save-peer] [dependency ...] --workspace 103 | # pnpm --filter web add -D eslint-config-project --workspace 104 | ``` 105 | 106 | ## 协作 107 | 108 | ### 提交信息 109 | 110 | 我们使用 111 | [Angular 的提交规范](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#commit-message-format)。 112 | 113 | 标题的格式是 `type: subject`: 114 | 115 | - `type` 一个标签,表明这是一个什么提交(涉及什么样的变化) 116 | - `subject` 提交内容的一句话描述 117 | - 使用英文的祈使句(这个提交会做什么);首字母小写;不使用句号 118 | - (我们暂时不使用 `scope`) 119 | 120 | 常用的 `type`: 121 | 122 | - `fix` 这是一个修复缺陷的提交 123 | - `feat` 这是一个添加新功能的提交 124 | - `refactor` 这是一个对现有功能进行重构的提交 125 | - `docs` 这个提交会更新文档(README/注释/...) 126 | - `ci` 这个提交会对 CI 造成变化(改变了 ESLint 规则/升级了测试工具/更新了 GitHub 127 | Actions...) 128 | - `chore` 其它不满足以上描述的变化(比如常规的依赖更新) 129 | 130 | **如果你发现你的提交同时满足多个标签,你的提交需要被拆分成多个。** 131 | 132 | 示例: 133 | 134 | ``` 135 | feat: add ahooks 136 | ci: update tooling config 137 | refactor: remove useless ide-scql 138 | docs: make issues/PR templates bilingual 139 | ``` 140 | 141 | ### 来源分支 142 | 143 | 分支命名采用和提交信息相似的规范。格式是 `type/subject`,其中 `subject` 使用 144 | `kebab-case` (全小写,使用 - 作为连字符),**分支名不需要加入你的名字。** 145 | -------------------------------------------------------------------------------- /LEGAL.md: -------------------------------------------------------------------------------- 1 | Legal Disclaimer 2 | 3 | Within this source code, the comments in Chinese shall be the original, governing 4 | version. Any comment in other languages are for reference only. In the event of any 5 | conflict between the Chinese language version comments and other language version 6 | comments, the Chinese language version shall prevail. 7 | 8 | 法律免责声明 9 | 10 | 关于代码注释部分,中文注释为官方版本,其它语言注释仅做参考。中文注释可能与其它语言注释存 11 | 在不一致,当中文注释与其它语言注释存在不一致时,请以中文注释为准。 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-2024 Alipay.inc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | English (US) | [简体中文](README.zh-Hans.md) 2 | 3 |

React component for building interactive diagrams

4 | 5 | ## Features 6 | 7 | - 🌱   Easy-to-use: Provides a more appropriate way to use React components. 8 | - 🚀   Unified state management: Service data and graph data can be managed in a 9 | unified manner. 10 | - 🧲   Supports multi-graph mode: Each graph component has a separate state and graph 11 | instance. 12 | - 💯   Out of the box features: There are a lot of diagram components out of the box. 13 | 14 | ## Installation 15 | 16 | ```shell 17 | # npm 18 | $ npm install @antv/xflow --save 19 | 20 | # yarn 21 | $ yarn add @antv/xflow 22 | 23 | # pnpm 24 | $ pnpm add @antv/xflow 25 | ``` 26 | 27 | ## Usage 28 | 29 | ```tsx 30 | const Page = () => { 31 | return ( 32 | 33 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | ); 54 | }; 55 | ``` 56 | 57 | ## Documentation 58 | 59 | The documentation for XFlow 2.0 is still being developed urgently, so if you want to 60 | know how to use it, you can refer to the 61 | [code examples](https://github.com/antvis/XFlow/tree/2.0/apps/basic/src/pages). 62 | 63 | ## Development 64 | 65 | ```shell 66 | $ pnpm bootstrap 67 | $ pnpm dev 68 | ``` 69 | 70 | ## Contributing 71 | 72 | To become a contributor, please follow our [contributing guide](/CONTRIBUTING.md). If 73 | you are an active contributor, you can apply to be a outside collaborator. 74 | 75 | 76 | Contributors 77 | 78 | 79 | ## License 80 | 81 | The scripts and documentation in this project are released under the 82 | [MIT License](/LICENSE). 83 | -------------------------------------------------------------------------------- /README.zh-Hans.md: -------------------------------------------------------------------------------- 1 | [English (US)](README.md) | 简体中文 2 | 3 |

用于构建图编辑应用的 React 组件

4 | 5 | ## 特性 6 | 7 | - 🌱   简单的使用方式: 提供了更贴合 React 组件的使用方式。 8 | - 🚀   统一的状态管理: 业务数据和图数据可以统一管理。 9 | - 🧲   支持多画布模式: 每个画布组件拥有单独的状态和实例。 10 | - 💯   开箱即用的配套功能: 提供大量的开箱即用的图编辑组件。 | 11 | 12 | ## 安装 13 | 14 | ```shell 15 | # npm 16 | $ npm install @antv/xflow --save 17 | 18 | # yarn 19 | $ yarn add @antv/xflow 20 | 21 | # pnpm 22 | $ pnpm add @antv/xflow 23 | ``` 24 | 25 | ## 示例 26 | 27 | ```tsx 28 | const Page = () => { 29 | return ( 30 | 31 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | ); 52 | }; 53 | ``` 54 | 55 | ## 文档 56 | 57 | XFlow 2.0 的文档还在紧急开发中,如果你想了解使用方式,可以参 58 | 考[代码示例](https://github.com/antvis/XFlow/tree/2.0/apps/basic/src/pages)。 59 | 60 | ## 本地开发 61 | 62 | ```shell 63 | $ pnpm bootstrap 64 | $ pnpm dev 65 | ``` 66 | 67 | ## 参与共建 68 | 69 | 如果希望参与到 XFlow 的开发中,请遵从我们的[贡献指南](/CONTRIBUTING.zh-CN.md)。如果你贡 70 | 献度足够活跃,你可以申请成为社区协作者。 71 | 72 | 73 | Contributors 74 | 75 | 76 | ## 开源协议 77 | 78 | 该项目的代码和文档基于 [MIT License](/LICENSE) 开源协议。 79 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | TODO: 4 | -------------------------------------------------------------------------------- /SECURITY.zh-Hans.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | TODO: 4 | -------------------------------------------------------------------------------- /apps/basic/config/config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'umi'; 2 | 3 | import { routes } from './routes'; 4 | 5 | export default defineConfig({ 6 | routes, 7 | npmClient: 'pnpm', 8 | https: false, 9 | svgr: {}, 10 | title: 'XFlow Basic', 11 | favicons: ['/favicon.ico'], 12 | mfsu: false, 13 | esbuildMinifyIIFE: true, 14 | }); 15 | -------------------------------------------------------------------------------- /apps/basic/config/routes.ts: -------------------------------------------------------------------------------- 1 | export const routes = [ 2 | { path: '/', redirect: '/basic' }, 3 | { path: '/basic', component: '@/pages/basic' }, 4 | { path: '/dnd', component: '@/pages/dnd' }, 5 | { path: '/dag', component: '@/pages/dag' }, 6 | { path: '/diff', component: '@/pages/diff' }, 7 | { path: '/flow', component: '@/pages/flow' }, 8 | { path: '/group', component: '@/pages/group' }, 9 | { path: '/drawing', component: '@/pages/drawing' }, 10 | ]; 11 | -------------------------------------------------------------------------------- /apps/basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "dev": "umi dev", 5 | "build": "umi build", 6 | "postinstall": "umi setup", 7 | "setup": "umi setup", 8 | "start": "npm run dev" 9 | }, 10 | "dependencies": { 11 | "@antv/xflow": "workspace:*", 12 | "@antv/xflow-diff": "workspace:*", 13 | "@antv/layout": "^0.3.2", 14 | "@antv/x6-react-components": "^2.0.8", 15 | "highlight.js": "^10.7.3", 16 | "umi": "^4.0.64", 17 | "@ant-design/icons": "^5.2.6", 18 | "classnames": "^2.3.2", 19 | "lodash": "^4.17.15", 20 | "@types/lodash": "4.14.186" 21 | }, 22 | "devDependencies": { 23 | "@types/highlight.js": "^9.12.4", 24 | "@types/react": "^18.0.0", 25 | "@types/react-dom": "^18.0.0" 26 | }, 27 | "peerDependencies": { 28 | "antd": "^5.0.0", 29 | "react": "^18.0.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /apps/basic/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antvis/XFlow/4228133c32e543ffba8c4cf80218e4eea17adef5/apps/basic/public/favicon.ico -------------------------------------------------------------------------------- /apps/basic/src/pages/basic/index.less: -------------------------------------------------------------------------------- 1 | .page { 2 | width: 1200px; 3 | margin: 100px auto; 4 | 5 | .container { 6 | width: 1200px; 7 | height: 600px; 8 | margin-bottom: 50px; 9 | border-radius: 5px; 10 | 11 | .tools { 12 | height: 80px; 13 | 14 | button { 15 | margin-right: 12px; 16 | margin-bottom: 8px; 17 | } 18 | } 19 | 20 | .content { 21 | display: flex; 22 | height: calc(100% - 80px); 23 | box-shadow: 0 12px 5px -10px rgba(0, 0, 0, 0.1), 0 0 4px 0 rgba(0, 0, 0, 0.1); 24 | 25 | .json-code { 26 | width: 400px; 27 | flex-shrink: 0; 28 | height: 100%; 29 | overflow: auto; 30 | border-left: 1px solid #ccc; 31 | 32 | pre { 33 | margin: 0; 34 | } 35 | } 36 | } 37 | } 38 | 39 | .react-node { 40 | width: 100%; 41 | height: 100%; 42 | border: 1px solid #8f8f8f; 43 | border-radius: 6px; 44 | display: flex; 45 | align-items: center; 46 | justify-content: center; 47 | } 48 | 49 | :global(.x6-node-selected) { 50 | .react-node { 51 | border: 1px solid #1890ff; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /apps/basic/src/pages/basic/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | XFlow, 3 | XFlowGraph, 4 | Grid, 5 | Background, 6 | Clipboard, 7 | History, 8 | Minimap, 9 | Snapline, 10 | Transform, 11 | } from '@antv/xflow'; 12 | 13 | import styles from './index.less'; 14 | import { JSONCode } from './json'; 15 | import { ToolsButton } from './tools'; 16 | 17 | const Page = () => { 18 | return ( 19 |
20 |
21 | 22 | 23 |
24 | 38 | 39 | 40 | 41 | 42 | 43 | 44 |
45 |
46 |
47 |
48 | 49 | 50 |
51 | 62 | 63 | 64 |
65 |
66 |
67 |
68 | ); 69 | }; 70 | 71 | export default Page; 72 | -------------------------------------------------------------------------------- /apps/basic/src/pages/basic/json.tsx: -------------------------------------------------------------------------------- 1 | import { useGraphStore } from '@antv/xflow'; 2 | import hljs from 'highlight.js/lib/core'; 3 | import json from 'highlight.js/lib/languages/json'; 4 | import { useEffect, useRef } from 'react'; 5 | 6 | import 'highlight.js/styles/github.css'; 7 | import styles from './index.less'; 8 | 9 | hljs.registerLanguage('json', json); 10 | 11 | const JSONCode = () => { 12 | const ref = useRef(null); 13 | const nodes = useGraphStore((state) => state.nodes); 14 | const edges = useGraphStore((state) => state.edges); 15 | 16 | const parse = () => { 17 | if (ref.current) { 18 | ref.current.innerText = JSON.stringify({ nodes, edges }, null, 2); 19 | hljs.highlightBlock(ref.current); 20 | } 21 | }; 22 | 23 | useEffect(() => { 24 | parse(); 25 | // eslint-disable-next-line react-hooks/exhaustive-deps 26 | }, [nodes, edges]); 27 | 28 | return ( 29 |
30 |
31 |         
32 |       
33 |
34 | ); 35 | }; 36 | 37 | export { JSONCode }; 38 | -------------------------------------------------------------------------------- /apps/basic/src/pages/basic/shape.tsx: -------------------------------------------------------------------------------- 1 | import { register } from '@antv/xflow'; 2 | import type { Node } from '@antv/xflow'; 3 | 4 | import styles from './index.less'; 5 | 6 | const REACT_NODE = 'react-node'; 7 | 8 | const NodeComponent = ({ node }: { node: Node }) => { 9 | const data = node.getData(); 10 | 11 | return ( 12 |
13 | {`${data.animal.name}-${data.animal.age}`} 14 |
15 | ); 16 | }; 17 | 18 | register({ 19 | shape: REACT_NODE, 20 | component: NodeComponent, 21 | width: 100, 22 | height: 40, 23 | effect: ['data'], // re-render when data changes 24 | }); 25 | 26 | export { REACT_NODE }; 27 | -------------------------------------------------------------------------------- /apps/basic/src/pages/dag/config-drawer/index.tsx: -------------------------------------------------------------------------------- 1 | import { useGraphStore, useGraphEvent } from '@antv/xflow'; 2 | import { Drawer, Space, Button, Form, Input } from 'antd'; 3 | import { useState } from 'react'; 4 | 5 | interface NodeData { 6 | id: string; 7 | label?: string; 8 | status?: 'default' | 'running' | 'success' | 'failed'; 9 | } 10 | const ConfigDrawer = () => { 11 | const [form] = Form.useForm(); 12 | const updateNode = useGraphStore((state) => state.updateNode); 13 | const [open, setOpen] = useState(false); 14 | const [nodeData, setNodeData] = useState(); 15 | 16 | const onClose = () => { 17 | setOpen(false); 18 | form.resetFields(); 19 | }; 20 | 21 | const onSave = () => { 22 | form.validateFields().then(({ label }) => { 23 | updateNode(nodeData?.id as string, { 24 | data: { 25 | ...nodeData, 26 | label, 27 | }, 28 | }); 29 | onClose(); 30 | }); 31 | }; 32 | 33 | useGraphEvent('node:click', ({ node }) => { 34 | const { data, id } = node; 35 | setOpen(true); 36 | setNodeData({ ...data, id }); 37 | form.setFieldsValue(data); 38 | }); 39 | 40 | useGraphEvent('blank:click', () => { 41 | onClose(); 42 | }); 43 | 44 | return ( 45 | 54 | 55 | 58 | 59 | } 60 | > 61 |
62 | 67 | 68 | 69 |
70 |
71 | ); 72 | }; 73 | 74 | export { ConfigDrawer }; 75 | -------------------------------------------------------------------------------- /apps/basic/src/pages/dag/connect.tsx: -------------------------------------------------------------------------------- 1 | import { useGraphEvent, useGraphStore } from '@antv/xflow'; 2 | 3 | const Connect = () => { 4 | const updateEdge = useGraphStore((state) => state.updateEdge); 5 | useGraphEvent('edge:connected', ({ edge }) => { 6 | updateEdge(edge.id, { 7 | animated: false, 8 | }); 9 | }); 10 | return null; 11 | }; 12 | 13 | export { Connect }; 14 | -------------------------------------------------------------------------------- /apps/basic/src/pages/dag/dnd/dnd.less: -------------------------------------------------------------------------------- 1 | .components { 2 | width: 240px; 3 | height: 100%; 4 | flex-shrink: 0; 5 | border-right: 1px solid #d4d7da; 6 | background-color: #f7f8fa; 7 | 8 | .action { 9 | padding: 8px 12px; 10 | 11 | .search { 12 | height: 28px; 13 | margin: 0; 14 | 15 | &:hover { 16 | border-color: #0068fa; 17 | } 18 | } 19 | 20 | :global(.ant-input-affix-wrapper-focused) { 21 | border-color: #0068fa; 22 | } 23 | } 24 | 25 | .tree { 26 | overflow: auto; 27 | height: 100%; 28 | padding: 0 4px; 29 | background-color: #f7f8fa; 30 | 31 | :global { 32 | .ant-tree-treenode { 33 | &:hover { 34 | background-color: unset; 35 | } 36 | 37 | .ant-tree-node-content-wrapper-normal { 38 | &:hover { 39 | background-color: #f7f8fa; 40 | } 41 | } 42 | } 43 | 44 | .ant-tree-indent, 45 | .ant-tree-switcher-noop { 46 | display: none; 47 | } 48 | 49 | .ant-tree-node-selected { 50 | background-color: unset !important; 51 | } 52 | 53 | .ant-tree-treenode-selected::before { 54 | background: transparent !important; 55 | } 56 | 57 | .ant-tree-switcher-icon { 58 | color: rgb(135 136 137 / 100%); 59 | } 60 | } 61 | 62 | .dir { 63 | margin-left: -4px; 64 | color: rgb(0 10 26 / 68%); 65 | font-size: 12px; 66 | } 67 | 68 | .node { 69 | display: flex; 70 | width: 200px; 71 | height: 32px; 72 | align-items: center; 73 | padding: 0 8px; 74 | border: 1px solid #d4d7da; 75 | border-radius: 4px; 76 | margin: 0 4px; 77 | background-color: #fff; 78 | color: rgb(0 10 26 / 68%); 79 | font-size: 12px; 80 | 81 | &:hover { 82 | border: 1px solid #0068fa; 83 | background-color: #fff; 84 | box-shadow: 0 0 0 2px rgb(0 104 250 / 15%); 85 | 86 | .nodeDragHolder { 87 | display: block; 88 | } 89 | } 90 | 91 | .nodeTitle { 92 | flex: 1; 93 | } 94 | 95 | .nodeDragHolder { 96 | display: none; 97 | width: 10px; 98 | } 99 | 100 | .icon { 101 | margin-right: 8px; 102 | color: #bbbcbc; 103 | } 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /apps/basic/src/pages/dag/dnd/dnd.tsx: -------------------------------------------------------------------------------- 1 | import { DatabaseFilled, HolderOutlined } from '@ant-design/icons'; 2 | import { useDnd } from '@antv/xflow'; 3 | import { Popover, Tree } from 'antd'; 4 | import React from 'react'; 5 | 6 | import { DAG_NODE } from '../shape'; 7 | 8 | import styles from './dnd.less'; 9 | import SearchInput from './search'; 10 | 11 | const { DirectoryTree } = Tree; 12 | 13 | type ComponentTreeItem = { 14 | category: string; 15 | docString: string; 16 | isLeaf: boolean; 17 | key: string; 18 | title: string; 19 | children?: ComponentTreeItem[]; 20 | ports?: { 21 | id: string; 22 | group: string; 23 | }[]; 24 | }; 25 | 26 | const componentTreeData = [ 27 | { 28 | category: '', 29 | docString: '', 30 | isLeaf: false, 31 | key: '分组一', 32 | title: '分组一', 33 | children: [ 34 | { 35 | category: '分组一', 36 | docString: '读数据', 37 | isLeaf: true, 38 | key: '1', 39 | title: '读数据', 40 | ports: [ 41 | { 42 | id: '1-1', 43 | group: 'bottom', 44 | }, 45 | ], 46 | }, 47 | { 48 | category: '分组一', 49 | docString: '逻辑回归', 50 | isLeaf: true, 51 | key: '2', 52 | title: '逻辑回归', 53 | ports: [ 54 | { 55 | id: '2-1', 56 | group: 'top', 57 | }, 58 | { 59 | id: '2-2', 60 | group: 'bottom', 61 | }, 62 | { 63 | id: '2-3', 64 | group: 'bottom', 65 | }, 66 | ], 67 | }, 68 | ], 69 | }, 70 | { 71 | category: '', 72 | docString: '', 73 | isLeaf: false, 74 | key: '分组二', 75 | title: '分组二', 76 | children: [ 77 | { 78 | category: '分组二', 79 | docString: '模型预测', 80 | isLeaf: true, 81 | key: '3', 82 | title: '模型预测', 83 | ports: [ 84 | { 85 | id: '3-1', 86 | group: 'top', 87 | }, 88 | { 89 | id: '3-2', 90 | group: 'bottom', 91 | }, 92 | ], 93 | }, 94 | { 95 | category: '分组二', 96 | docString: '读取参数', 97 | isLeaf: true, 98 | key: '4', 99 | title: '读取参数', 100 | ports: [ 101 | { 102 | id: '4-1', 103 | group: 'top', 104 | }, 105 | { 106 | id: '4-2', 107 | group: 'bottom', 108 | }, 109 | ], 110 | }, 111 | ], 112 | }, 113 | ]; 114 | 115 | const Dnd = () => { 116 | let id = 0; 117 | const { startDrag } = useDnd(); 118 | 119 | const handleMouseDown = ( 120 | e: React.MouseEvent, 121 | item: ComponentTreeItem, 122 | ) => { 123 | id += 1; 124 | startDrag( 125 | { 126 | id: id.toString(), 127 | shape: DAG_NODE, 128 | data: { 129 | id: id.toString(), 130 | label: item.title, 131 | status: 'default', 132 | }, 133 | ports: item.ports, 134 | }, 135 | e, 136 | ); 137 | }; 138 | 139 | const [searchComponents, setSearchComponents] = React.useState( 140 | [], 141 | ); 142 | 143 | const handleSearchComponent = (keyword?: string) => { 144 | if (!keyword) { 145 | setSearchComponents([]); 146 | return; 147 | } 148 | const searchResult = componentTreeData.flatMap((group) => 149 | group.children.filter((child) => 150 | child.title.toLowerCase().includes(keyword.toLowerCase()), 151 | ), 152 | ); 153 | setSearchComponents(searchResult); 154 | }; 155 | 156 | const treeNodeRender = (treeNode: ComponentTreeItem) => { 157 | const { isLeaf, docString, title } = treeNode; 158 | if (isLeaf) { 159 | return ( 160 | 170 | {docString} 171 | 172 | } 173 | placement="right" 174 | > 175 |
handleMouseDown(e, treeNode)} 178 | > 179 |
180 | 181 | 182 | 183 | {title} 184 |
185 |
186 | 187 |
188 |
189 |
190 | ); 191 | } else { 192 | return {title}; 193 | } 194 | }; 195 | 196 | return ( 197 |
198 |
199 | handleSearchComponent(key)} 203 | > 204 |
205 | {componentTreeData.length && ( 206 | treeNodeRender(node)} 212 | treeData={searchComponents.length ? searchComponents : componentTreeData} 213 | > 214 | )} 215 |
216 | ); 217 | }; 218 | 219 | export { Dnd }; 220 | -------------------------------------------------------------------------------- /apps/basic/src/pages/dag/dnd/search/index.less: -------------------------------------------------------------------------------- 1 | .search { 2 | width: 100%; 3 | height: 24px; 4 | border-radius: 5px; 5 | margin: 4px 8px; 6 | background-color: white; 7 | color: rgb(0 0 0 / 25%); 8 | 9 | .searchIcon { 10 | color: rgb(0 0 0 / 45%); 11 | font-size: 16px; 12 | } 13 | 14 | :global(.ant-input) { 15 | font-size: 12px; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /apps/basic/src/pages/dag/dnd/search/index.tsx: -------------------------------------------------------------------------------- 1 | import { SearchOutlined } from '@ant-design/icons'; 2 | import { Input } from 'antd'; 3 | import classnames from 'classnames'; 4 | import { useRef, useState } from 'react'; 5 | 6 | import styles from './index.less'; 7 | 8 | interface IProps { 9 | className?: string; 10 | placeholder: string; 11 | onSearch: (key: string) => void; 12 | } 13 | 14 | export const SearchInput = (props: IProps) => { 15 | const inputRef = useRef(null); 16 | const searchTimeout = useRef(0); 17 | const [value, setValue] = useState(''); 18 | 19 | const handleSearch = (evt: React.FocusEvent) => { 20 | const key = evt.target.value; 21 | setValue(key); 22 | if (searchTimeout.current) clearTimeout(searchTimeout.current); 23 | searchTimeout.current = window.setTimeout(() => { 24 | props.onSearch(key); 25 | searchTimeout.current = 0; 26 | }, 200); 27 | }; 28 | 29 | return ( 30 | } 34 | allowClear 35 | value={value} 36 | onChange={handleSearch} 37 | size="small" 38 | placeholder={props.placeholder} 39 | /> 40 | ); 41 | }; 42 | 43 | export default SearchInput; 44 | -------------------------------------------------------------------------------- /apps/basic/src/pages/dag/index.less: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | } 4 | 5 | .page { 6 | overflow: hidden; 7 | width: 100%; 8 | height: 100%; 9 | box-sizing: border-box; 10 | 11 | .container { 12 | display: flex; 13 | overflow: hidden; 14 | width: 100%; 15 | height: 100vh; 16 | box-sizing: border-box; 17 | 18 | .left { 19 | display: flex; 20 | width: 240px; 21 | height: 100%; 22 | flex-direction: column; 23 | background-color: #f7f8fa; 24 | 25 | .leftTop { 26 | display: flex; 27 | width: 100%; 28 | height: 42px; 29 | box-sizing: border-box; 30 | flex-shrink: 0; 31 | align-items: center; 32 | justify-content: center; 33 | padding: 0 16px; 34 | border-bottom: 1px solid #eaebed; 35 | background: #f7f8fa; 36 | } 37 | } 38 | 39 | .center { 40 | position: relative; 41 | width: calc(100% - 240px); 42 | height: 100%; 43 | outline: none; 44 | 45 | .toolbar { 46 | display: flex; 47 | width: 100%; 48 | height: 42px; 49 | box-sizing: border-box; 50 | align-items: center; 51 | justify-content: space-between; 52 | padding: 0 16px; 53 | background-color: #f6f8fa; 54 | } 55 | 56 | .graph { 57 | position: relative; 58 | width: 100%; 59 | height: calc(100% - 42px); 60 | 61 | .controlTool { 62 | position: absolute; 63 | right: 24px; 64 | bottom: 24px; 65 | } 66 | } 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /apps/basic/src/pages/dag/index.tsx: -------------------------------------------------------------------------------- 1 | import { XFlow, XFlowGraph, Clipboard, Control } from '@antv/xflow'; 2 | 3 | import { ConfigDrawer } from './config-drawer'; 4 | import { Connect } from './connect'; 5 | import { Dnd } from './dnd/dnd'; 6 | import styles from './index.less'; 7 | import { InitShape } from './node'; 8 | import { DAG_EDGE, DAG_CONNECTOR } from './shape'; 9 | import { Toolbar } from './toolbar'; 10 | 11 | const Page = () => { 12 | return ( 13 | 14 |
15 |
16 |
17 |
算子组件库
18 | 19 |
20 |
21 |
22 | 23 |
24 |
25 | 45 | 46 | 47 | 48 |
49 | 52 |
53 |
54 |
55 |
56 | 57 |
58 |
59 | ); 60 | }; 61 | 62 | export default Page; 63 | -------------------------------------------------------------------------------- /apps/basic/src/pages/dag/node.less: -------------------------------------------------------------------------------- 1 | .node { 2 | display: flex; 3 | width: 100%; 4 | height: 100%; 5 | align-items: center; 6 | border: 1px solid #c2c8d5; 7 | border-radius: 4px; 8 | border-left: 4px solid #5f95ff; 9 | background-color: #fff; 10 | box-shadow: 0 2px 5px 1px rgb(0 0 0 / 6%); 11 | } 12 | 13 | .node img { 14 | width: 20px; 15 | height: 20px; 16 | flex-shrink: 0; 17 | margin-left: 8px; 18 | } 19 | 20 | .node .label { 21 | display: inline-block; 22 | width: 104px; 23 | flex-shrink: 0; 24 | margin-left: 8px; 25 | color: #666; 26 | font-size: 12px; 27 | } 28 | 29 | .node .status { 30 | height: 20px; 31 | flex-shrink: 0; 32 | } 33 | 34 | .node.success { 35 | border-left: 4px solid #52c41a; 36 | } 37 | 38 | .node.failed { 39 | border-left: 4px solid #ff4d4f; 40 | } 41 | 42 | .node.running .status img { 43 | animation: spin 1s linear infinite; 44 | } 45 | 46 | .x6-node-selected .node { 47 | border-color: #1890ff; 48 | border-radius: 2px; 49 | box-shadow: 0 0 0 4px #d4e8fe; 50 | } 51 | 52 | .x6-node-selected .node.success { 53 | border-color: #52c41a; 54 | border-radius: 2px; 55 | box-shadow: 0 0 0 4px #ccecc0; 56 | } 57 | 58 | .x6-node-selected .node.failed { 59 | border-color: #ff4d4f; 60 | border-radius: 2px; 61 | box-shadow: 0 0 0 4px #fedcdc; 62 | } 63 | 64 | .x6-edge:hover path:nth-child(2) { 65 | stroke: #1890ff; 66 | stroke-width: 1px; 67 | } 68 | 69 | .x6-edge-selected path:nth-child(2) { 70 | stroke: #1890ff; 71 | stroke-width: 1.5px !important; 72 | } 73 | 74 | @keyframes running-line { 75 | to { 76 | stroke-dashoffset: -1000; 77 | } 78 | } 79 | 80 | @keyframes spin { 81 | from { 82 | transform: rotate(0deg); 83 | } 84 | 85 | to { 86 | transform: rotate(360deg); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /apps/basic/src/pages/dag/node.tsx: -------------------------------------------------------------------------------- 1 | import { useGraphStore } from '@antv/xflow'; 2 | import { useCallback, useEffect } from 'react'; 3 | 4 | import { DAG_EDGE, DAG_NODE } from './shape'; 5 | 6 | const InitShape = () => { 7 | const addNodes = useGraphStore((state) => state.addNodes); 8 | const addEdges = useGraphStore((state) => state.addEdges); 9 | const updateNode = useGraphStore((state) => state.updateNode); 10 | const updateEdge = useGraphStore((state) => state.updateEdge); 11 | 12 | const initEdge = useCallback(() => { 13 | addEdges([ 14 | { 15 | id: 'initEdge1', 16 | shape: DAG_EDGE, 17 | source: { 18 | cell: 'initNode1', 19 | port: 'initNode1-1', 20 | }, 21 | target: { 22 | cell: 'initNode2', 23 | port: 'initNode2-1', 24 | }, 25 | animated: true, 26 | }, 27 | { 28 | id: 'initEdge2', 29 | shape: DAG_EDGE, 30 | source: { 31 | cell: 'initNode2', 32 | port: 'initNode2-2', 33 | }, 34 | target: { 35 | cell: 'initNode3', 36 | port: 'initNode3-1', 37 | }, 38 | animated: true, 39 | }, 40 | { 41 | id: 'initEdge3', 42 | shape: DAG_EDGE, 43 | source: { 44 | cell: 'initNode2', 45 | port: 'initNode2-3', 46 | }, 47 | target: { 48 | cell: 'initNode4', 49 | port: 'initNode4-1', 50 | }, 51 | animated: true, 52 | }, 53 | ]); 54 | }, [addEdges]); 55 | 56 | const addNodeInit = useCallback(() => { 57 | addNodes([ 58 | { 59 | id: 'initNode1', 60 | shape: DAG_NODE, 61 | x: 490, 62 | y: 200, 63 | data: { 64 | label: '读数据', 65 | status: 'success', 66 | }, 67 | ports: [ 68 | { 69 | id: 'initNode1-1', 70 | group: 'bottom', 71 | }, 72 | ], 73 | }, 74 | { 75 | id: 'initNode2', 76 | shape: DAG_NODE, 77 | x: 490, 78 | y: 350, 79 | data: { 80 | label: '逻辑回归', 81 | status: 'running', 82 | }, 83 | ports: [ 84 | { 85 | id: 'initNode2-1', 86 | group: 'top', 87 | }, 88 | { 89 | id: 'initNode2-2', 90 | group: 'bottom', 91 | }, 92 | { 93 | id: 'initNode2-3', 94 | group: 'bottom', 95 | }, 96 | ], 97 | }, 98 | { 99 | id: 'initNode3', 100 | shape: DAG_NODE, 101 | x: 320, 102 | y: 500, 103 | data: { 104 | label: '模型预测', 105 | status: 'running', 106 | }, 107 | ports: [ 108 | { 109 | id: 'initNode3-1', 110 | group: 'top', 111 | }, 112 | { 113 | id: 'initNode3-2', 114 | group: 'bottom', 115 | }, 116 | ], 117 | }, 118 | { 119 | id: 'initNode4', 120 | shape: DAG_NODE, 121 | x: 670, 122 | y: 500, 123 | data: { 124 | label: '读取参数', 125 | status: 'running', 126 | }, 127 | ports: [ 128 | { 129 | id: 'initNode4-1', 130 | group: 'top', 131 | }, 132 | { 133 | id: 'initNode4-2', 134 | group: 'bottom', 135 | }, 136 | ], 137 | }, 138 | ]); 139 | setTimeout(() => { 140 | updateNode('initNode2', { 141 | data: { 142 | status: 'success', 143 | }, 144 | }); 145 | updateEdge('initEdge1', { 146 | animated: false, 147 | }); 148 | }, 1000); 149 | setTimeout(() => { 150 | updateNode('initNode4', { 151 | data: { 152 | status: 'success', 153 | }, 154 | }); 155 | updateNode('initNode3', { 156 | data: { 157 | status: 'failed', 158 | }, 159 | }); 160 | updateEdge('initEdge2', { 161 | animated: false, 162 | }); 163 | updateEdge('initEdge3', { 164 | animated: false, 165 | }); 166 | }, 2000); 167 | }, [addNodes, updateNode, updateEdge]); 168 | 169 | useEffect(() => { 170 | addNodeInit(); 171 | initEdge(); 172 | }, [addNodeInit, initEdge]); 173 | 174 | return null; 175 | }; 176 | 177 | export { InitShape }; 178 | -------------------------------------------------------------------------------- /apps/basic/src/pages/dag/shape.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | CopyOutlined, 3 | DeleteOutlined, 4 | EditOutlined, 5 | PlayCircleOutlined, 6 | } from '@ant-design/icons'; 7 | import { Dropdown, Menu } from '@antv/x6-react-components'; 8 | import { register, Graph, Path, XFlow } from '@antv/xflow'; 9 | import type { Node } from '@antv/xflow'; 10 | import { Modal, Input } from 'antd'; 11 | import { useEffect, useState } from 'react'; 12 | import './node.less'; 13 | import '@antv/x6-react-components/dist/index.css'; 14 | 15 | const { Item: MenuItem, Divider } = Menu; 16 | 17 | const DAG_NODE = 'dag-node'; 18 | const DAG_EDGE = 'dag-edge'; 19 | const DAG_CONNECTOR = 'dag-connector'; 20 | interface NodeStatus { 21 | id: string; 22 | status: 'default' | 'success' | 'failed' | 'running'; 23 | label?: string; 24 | } 25 | const image = { 26 | logo: 'https://gw.alipayobjects.com/mdn/rms_43231b/afts/img/A*evDjT5vjkX0AAAAAAAAAAAAAARQnAQ', 27 | success: 28 | 'https://gw.alipayobjects.com/mdn/rms_43231b/afts/img/A*6l60T6h8TTQAAAAAAAAAAAAAARQnAQ', 29 | failed: 30 | 'https://gw.alipayobjects.com/mdn/rms_43231b/afts/img/A*SEISQ6My-HoAAAAAAAAAAAAAARQnAQ', 31 | running: 32 | 'https://gw.alipayobjects.com/mdn/rms_43231b/afts/img/A*t8fURKfgSOgAAAAAAAAAAAAAARQnAQ', 33 | }; 34 | 35 | const AlgoNode = ({ node }: { node: Node }) => { 36 | const data = node?.getData() as NodeStatus; 37 | const { label, status = 'default' } = data || {}; 38 | const [open, setOpen] = useState(false); 39 | const [value, setValue] = useState(); 40 | 41 | useEffect(() => { 42 | setValue(label); 43 | }, [label]); 44 | 45 | const onMenuItemClick = (key: string) => { 46 | const graph = node?.model?.graph; 47 | if (!graph) { 48 | return; 49 | } 50 | switch (key) { 51 | case 'delete': 52 | node.remove(); 53 | break; 54 | case 'exec': 55 | node.setData({ 56 | ...node.data, 57 | status: 'running', 58 | }); 59 | setTimeout(() => { 60 | node.setData({ 61 | ...node.data, 62 | status: 'success', 63 | }); 64 | }, 2000); 65 | break; 66 | case 'copy': 67 | graph.copy([graph.getCellById(node.id)]); 68 | break; 69 | case 'paste': 70 | graph.paste(); 71 | break; 72 | case 'rename': 73 | setOpen(true); 74 | break; 75 | default: 76 | break; 77 | } 78 | }; 79 | 80 | const menu = ( 81 | onMenuItemClick(key)}> 82 | } text="重命名" /> 83 | } text="复制" /> 84 | } text="粘贴" /> 85 | } text="删除" /> 86 | 87 | } text="执行节点" /> 88 | 89 | ); 90 | 91 | return ( 92 | 93 | setOpen(false)} 99 | onOk={() => { 100 | node.setData({ 101 | ...node.data, 102 | label: value, 103 | }); 104 | setOpen(false); 105 | }} 106 | > 107 | setValue(e.target.value)} /> 108 | 109 | 114 |
115 | logo 116 | {label} 117 | 118 | {status === 'success' && success} 119 | {status === 'failed' && failed} 120 | {status === 'running' && running} 121 | 122 |
123 |
124 |
125 | ); 126 | }; 127 | 128 | register({ 129 | shape: DAG_NODE, 130 | width: 180, 131 | height: 36, 132 | component: AlgoNode, 133 | effect: ['data'], 134 | ports: { 135 | groups: { 136 | top: { 137 | position: 'top', 138 | attrs: { 139 | circle: { 140 | r: 4, 141 | magnet: true, 142 | stroke: '#C2C8D5', 143 | strokeWidth: 1, 144 | fill: '#fff', 145 | }, 146 | }, 147 | }, 148 | bottom: { 149 | position: 'bottom', 150 | attrs: { 151 | circle: { 152 | r: 4, 153 | magnet: true, 154 | stroke: '#C2C8D5', 155 | strokeWidth: 1, 156 | fill: '#fff', 157 | }, 158 | }, 159 | }, 160 | }, 161 | }, 162 | }); 163 | 164 | Graph.registerConnector( 165 | DAG_CONNECTOR, 166 | (s, e) => { 167 | const offset = 4; 168 | const deltaY = Math.abs(e.y - s.y); 169 | const control = Math.floor((deltaY / 3) * 2); 170 | 171 | const v1 = { x: s.x, y: s.y + offset + control }; 172 | const v2 = { x: e.x, y: e.y - offset - control }; 173 | 174 | return Path.normalize( 175 | `M ${s.x} ${s.y} 176 | L ${s.x} ${s.y + offset} 177 | C ${v1.x} ${v1.y} ${v2.x} ${v2.y} ${e.x} ${e.y - offset} 178 | L ${e.x} ${e.y} 179 | `, 180 | ); 181 | }, 182 | true, 183 | ); 184 | 185 | Graph.registerEdge( 186 | DAG_EDGE, 187 | { 188 | inherit: 'edge', 189 | attrs: { 190 | line: { 191 | stroke: '#C2C8D5', 192 | strokeWidth: 1, 193 | targetMarker: null, 194 | }, 195 | }, 196 | zIndex: -1, 197 | }, 198 | true, 199 | ); 200 | 201 | export { DAG_NODE, DAG_EDGE, DAG_CONNECTOR }; 202 | -------------------------------------------------------------------------------- /apps/basic/src/pages/dag/toolbar/index.tsx: -------------------------------------------------------------------------------- 1 | import { PlayCircleOutlined, CopyOutlined } from '@ant-design/icons'; 2 | import type { Edge, NodeOptions, Node } from '@antv/xflow'; 3 | import { 4 | useGraphInstance, 5 | useClipboard, 6 | useGraphEvent, 7 | useGraphStore, 8 | useKeyboard, 9 | } from '@antv/xflow'; 10 | import { Button, Space } from 'antd'; 11 | 12 | const Toolbar = () => { 13 | const graph = useGraphInstance(); 14 | const { copy, paste } = useClipboard(); 15 | const nodes = useGraphStore((state) => state.nodes); 16 | const updateNode = useGraphStore((state) => state.updateNode); 17 | const updateEdge = useGraphStore((state) => state.updateEdge); 18 | const removeNodes = useGraphStore((state) => state.removeNodes); 19 | 20 | useKeyboard('ctrl+c', () => onCopy()); 21 | 22 | useKeyboard('ctrl+v', () => onPaste()); 23 | 24 | useKeyboard('backspace', () => { 25 | const selected = nodes.filter((node) => node.selected); 26 | const ids: string[] = selected.map((node) => node.id || ''); 27 | removeNodes(ids); 28 | }); 29 | 30 | useGraphEvent('node:change:data', ({ node }) => { 31 | if (graph) { 32 | const edges = graph.getIncomingEdges(node); 33 | const { status } = node.data; 34 | edges?.forEach((edge: Edge) => { 35 | if (status === 'running') { 36 | updateEdge(edge.id, { 37 | animated: true, 38 | }); 39 | } else { 40 | updateEdge(edge.id, { 41 | animated: false, 42 | }); 43 | } 44 | }); 45 | } 46 | }); 47 | 48 | const handleExecute = () => { 49 | if (graph) { 50 | nodes.forEach((node: Node | NodeOptions, index: number) => { 51 | const edges = graph.getOutgoingEdges(node as Node); 52 | updateNode(node.id!, { 53 | data: { 54 | ...node.data, 55 | status: 'running', 56 | }, 57 | }); 58 | 59 | setTimeout(() => { 60 | updateNode(node.id!, { 61 | data: { 62 | ...node.data, 63 | status: edges 64 | ? 'success' 65 | : Number(node.id!.slice(-1)) % 2 !== 0 66 | ? 'success' 67 | : 'failed', 68 | }, 69 | }); 70 | }, 1000 * index + 1); 71 | }); 72 | } 73 | }; 74 | 75 | const onCopy = () => { 76 | const selected = nodes.filter((node) => node.selected); 77 | const ids: string[] = selected.map((node) => node.id || ''); 78 | copy(ids); 79 | }; 80 | 81 | const onPaste = () => { 82 | paste(); 83 | }; 84 | 85 | return ( 86 | 87 | 96 | 100 | 104 | 105 | ); 106 | }; 107 | 108 | export { Toolbar }; 109 | -------------------------------------------------------------------------------- /apps/basic/src/pages/dnd/dnd.tsx: -------------------------------------------------------------------------------- 1 | import { useDnd } from '@antv/xflow'; 2 | 3 | import styles from './index.less'; 4 | 5 | const Dnd = () => { 6 | const { startDrag } = useDnd(); 7 | const list = ['node1', 'node2', 'node3', 'node4', 'node5', 'node6', 'node7']; 8 | 9 | const handleMouseDown = (e: React.MouseEvent, item: string) => { 10 | startDrag( 11 | { 12 | id: item, 13 | shape: 'rect', 14 | width: 192, 15 | height: 32, 16 | attrs: { 17 | body: { 18 | stroke: '#D9DADD', 19 | strokeWidth: 1, 20 | }, 21 | }, 22 | label: item, 23 | }, 24 | e, 25 | ); 26 | }; 27 | 28 | return ( 29 |
30 |
    31 | {list.map((item) => ( 32 |
  • handleMouseDown(e, item)}> 33 | {item} 34 |
  • 35 | ))} 36 |
37 |
38 | ); 39 | }; 40 | 41 | export { Dnd }; 42 | -------------------------------------------------------------------------------- /apps/basic/src/pages/dnd/index.less: -------------------------------------------------------------------------------- 1 | .page { 2 | width: 1200px; 3 | margin: 100px auto; 4 | 5 | .container { 6 | display: flex; 7 | width: 1200px; 8 | height: 600px; 9 | border-radius: 5px; 10 | box-shadow: 0 12px 5px -10px rgba(0, 0, 0, 0.1), 0 0 4px 0 rgba(0, 0, 0, 0.1); 11 | } 12 | 13 | .dnd { 14 | width: 200px; 15 | flex-shrink: 0; 16 | border-right: 1px solid #ccc; 17 | 18 | ul { 19 | list-style: none; 20 | padding: 0; 21 | margin: 0; 22 | 23 | li { 24 | margin: 0 4px; 25 | padding: 5px 10px; 26 | cursor: move; 27 | 28 | &:hover { 29 | background-color: #fff; 30 | border: 1px solid #d9dadd; 31 | box-shadow: 0 1px 3px #0000000f; 32 | } 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /apps/basic/src/pages/dnd/index.tsx: -------------------------------------------------------------------------------- 1 | import { XFlow, XFlowGraph, Background } from '@antv/xflow'; 2 | 3 | import { Dnd } from './dnd'; 4 | import styles from './index.less'; 5 | 6 | const Page = () => { 7 | return ( 8 |
9 |
10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 | ); 18 | }; 19 | 20 | export default Page; 21 | -------------------------------------------------------------------------------- /apps/basic/src/pages/drawing/index.less: -------------------------------------------------------------------------------- 1 | .page { 2 | width: 1200px; 3 | margin: 100px auto; 4 | 5 | .content { 6 | width: 800px; 7 | height: 500px; 8 | margin-top: 20px; 9 | box-shadow: 0 12px 5px -10px rgba(0, 0, 0, 0.1), 0 0 4px 0 rgba(0, 0, 0, 0.1); 10 | border-radius: 5px; 11 | } 12 | 13 | :global { 14 | .preview-shape { 15 | stroke: green; 16 | fill: none; 17 | stroke-width: 2; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /apps/basic/src/pages/drawing/index.tsx: -------------------------------------------------------------------------------- 1 | import { XFlow, XFlowGraph, Background, Grid } from '@antv/xflow'; 2 | import { Radio } from 'antd'; 3 | import { useState } from 'react'; 4 | 5 | import Draw from './draw'; 6 | import styles from './index.less'; 7 | 8 | const options = [ 9 | { label: 'Pointer', value: 'pointer' }, 10 | { label: 'Line', value: 'line' }, 11 | { label: 'Rectangle', value: 'rectangle' }, 12 | { label: 'Ellipse', value: 'ellipse' }, 13 | { label: 'Free Draw', value: 'free' }, 14 | ]; 15 | 16 | const Page = () => { 17 | const [type, setType] = useState('pointer'); 18 | 19 | return ( 20 |
21 | 22 | setType(e.target.value)} 25 | value={type} 26 | optionType="button" 27 | buttonStyle="solid" 28 | /> 29 |
30 | 31 | 32 | 38 |
39 | 40 |
41 |
42 | ); 43 | }; 44 | 45 | export default Page; 46 | -------------------------------------------------------------------------------- /apps/basic/src/pages/flow/connector.tsx: -------------------------------------------------------------------------------- 1 | import { useGraphEvent, useGraphInstance } from '@antv/xflow'; 2 | 3 | const Connector = () => { 4 | const graph = useGraphInstance(); 5 | 6 | useGraphEvent('node:mouseenter', () => { 7 | const ports = graph?.container.querySelectorAll( 8 | '.x6-port-body', 9 | ) as NodeListOf; 10 | showPorts(ports, true); 11 | }); 12 | 13 | useGraphEvent('node:mouseleave', () => { 14 | const ports = graph?.container.querySelectorAll( 15 | '.x6-port-body', 16 | ) as NodeListOf; 17 | showPorts(ports, false); 18 | }); 19 | 20 | function showPorts(ports: NodeListOf, show: boolean) { 21 | for (let i = 0, len = ports.length; i < len; i = i + 1) { 22 | ports[i].style.visibility = show ? 'visible' : 'hidden'; 23 | } 24 | } 25 | return null; 26 | }; 27 | 28 | export { Connector }; 29 | -------------------------------------------------------------------------------- /apps/basic/src/pages/flow/dnd.tsx: -------------------------------------------------------------------------------- 1 | import type { NodeOptions } from '@antv/xflow'; 2 | import { useDnd } from '@antv/xflow'; 3 | 4 | import styles from './index.less'; 5 | import { 6 | CUSTOMPROCESSNODE, 7 | CUSTOMCOURSENODE, 8 | CUSTOMVERIFYNODE, 9 | BASICFLOWNODE, 10 | } from './node'; 11 | 12 | const Dnd = () => { 13 | const { startDrag } = useDnd(); 14 | 15 | const handleMouseDown = ( 16 | e: React.MouseEvent, 17 | item: { type: string; label: string; node: NodeOptions }, 18 | ) => { 19 | startDrag(item.node, e); 20 | }; 21 | 22 | return ( 23 |
24 |
    25 | {list.map((item) => ( 26 |
    30 | handleMouseDown( 31 | e, 32 | item as { type: string; label: string; node: NodeOptions }, 33 | ) 34 | } 35 | > 36 | {item.label} 37 |
    38 | ))} 39 |
40 |
41 | ); 42 | }; 43 | 44 | export { Dnd }; 45 | 46 | const list = [ 47 | { 48 | type: 'Start', 49 | label: '开始', 50 | node: { 51 | shape: BASICFLOWNODE, 52 | label: '开始', 53 | width: 80, 54 | height: 50, 55 | attrs: { 56 | body: { 57 | rx: 25, 58 | ry: 25, 59 | strokeWidth: 2, 60 | stroke: 'rgb(255, 149, 128)', 61 | fill: 'rgb(255, 149, 128)', 62 | }, 63 | }, 64 | }, 65 | }, 66 | { 67 | type: 'Process', 68 | label: '过程', 69 | node: { 70 | shape: CUSTOMCOURSENODE, 71 | label: '过程', 72 | width: 100, 73 | height: 60, 74 | }, 75 | }, 76 | { 77 | type: 'No', 78 | label: 'No', 79 | node: { 80 | shape: CUSTOMVERIFYNODE, 81 | label: 'No', 82 | width: 36, 83 | height: 36, 84 | }, 85 | }, 86 | { 87 | type: 'Yes', 88 | label: 'Yes', 89 | node: { 90 | shape: CUSTOMVERIFYNODE, 91 | label: 'Yes', 92 | width: 42, 93 | height: 42, 94 | }, 95 | }, 96 | { 97 | type: 'No', 98 | label: 'Ok', 99 | node: { 100 | shape: CUSTOMVERIFYNODE, 101 | label: 'Ok', 102 | width: 36, 103 | height: 36, 104 | }, 105 | }, 106 | { 107 | type: 'NotOk', 108 | label: 'NotOk', 109 | node: { 110 | shape: CUSTOMVERIFYNODE, 111 | label: 'Not Ok', 112 | width: 66, 113 | height: 37, 114 | }, 115 | }, 116 | { 117 | type: 'Polygon', 118 | label: '决策', 119 | node: { 120 | shape: CUSTOMPROCESSNODE, 121 | label: '决策', 122 | }, 123 | }, 124 | ]; 125 | -------------------------------------------------------------------------------- /apps/basic/src/pages/flow/edge.tsx: -------------------------------------------------------------------------------- 1 | import { Graph, useGraphStore } from '@antv/xflow'; 2 | import { useCallback, useEffect } from 'react'; 3 | 4 | const CUSTOM_EDGE = 'custom-edge'; 5 | 6 | const InitEdge = () => { 7 | const addEdges = useGraphStore((state) => state.addEdges); 8 | 9 | const initEdge = useCallback(() => { 10 | addEdges([ 11 | { 12 | shape: CUSTOM_EDGE, 13 | source: 'initNode1', 14 | target: 'initNode2', 15 | }, 16 | { 17 | shape: CUSTOM_EDGE, 18 | source: 'initNode2', 19 | target: 'initNode3', 20 | }, 21 | { 22 | shape: CUSTOM_EDGE, 23 | source: 'initNode3', 24 | target: 'initNode4', 25 | }, 26 | { 27 | shape: CUSTOM_EDGE, 28 | source: 'initNode4', 29 | target: 'initNode5', 30 | }, 31 | { 32 | shape: CUSTOM_EDGE, 33 | source: 'initNode5', 34 | target: 'initNode6', 35 | }, 36 | { 37 | shape: CUSTOM_EDGE, 38 | source: 'initNode6', 39 | target: 'initNode7', 40 | }, 41 | { 42 | shape: CUSTOM_EDGE, 43 | source: 'initNode7', 44 | target: 'initNode8', 45 | }, 46 | { 47 | shape: CUSTOM_EDGE, 48 | source: { 49 | cell: 'initNode8', 50 | port: 'group4', 51 | }, 52 | target: { 53 | cell: 'initNode5', 54 | port: 'group2', 55 | }, 56 | }, 57 | { 58 | shape: CUSTOM_EDGE, 59 | source: 'initNode6', 60 | target: 'initNode9', 61 | }, 62 | { 63 | shape: CUSTOM_EDGE, 64 | source: 'initNode9', 65 | target: 'initNode10', 66 | }, 67 | { 68 | shape: CUSTOM_EDGE, 69 | source: 'initNode10', 70 | target: { 71 | cell: 'initNode11', 72 | port: 'group1', 73 | }, 74 | }, 75 | { 76 | shape: CUSTOM_EDGE, 77 | source: 'initNode10', 78 | target: 'initNode12', 79 | }, 80 | { 81 | shape: CUSTOM_EDGE, 82 | source: { 83 | cell: 'initNode12', 84 | port: 'group3', 85 | }, 86 | target: { 87 | cell: 'initNode13', 88 | port: 'group1', 89 | }, 90 | }, 91 | { 92 | shape: CUSTOM_EDGE, 93 | source: { 94 | cell: 'initNode11', 95 | port: 'group2', 96 | }, 97 | target: { 98 | cell: 'initNode13', 99 | port: 'group4', 100 | }, 101 | }, 102 | { 103 | shape: CUSTOM_EDGE, 104 | source: 'initNode13', 105 | target: 'initNode14', 106 | }, 107 | { 108 | shape: CUSTOM_EDGE, 109 | source: 'initNode14', 110 | target: 'initNode15', 111 | }, 112 | ]); 113 | }, [addEdges]); 114 | 115 | useEffect(() => { 116 | initEdge(); 117 | }, [initEdge]); 118 | 119 | return null; 120 | }; 121 | 122 | Graph.registerEdge(CUSTOM_EDGE, { 123 | attrs: { 124 | line: { 125 | stroke: 'rgb(72, 203, 164)', 126 | strokeWidth: 2, 127 | targetMarker: { 128 | name: 'block', 129 | width: 14, 130 | height: 10, 131 | }, 132 | }, 133 | }, 134 | }); 135 | 136 | export { InitEdge, CUSTOM_EDGE }; 137 | -------------------------------------------------------------------------------- /apps/basic/src/pages/flow/index.less: -------------------------------------------------------------------------------- 1 | .page { 2 | margin: 10px auto; 3 | 4 | .container { 5 | display: flex; 6 | width: 1200px; 7 | height: 100vh; 8 | border-radius: 5px; 9 | box-shadow: 0 12px 5px -10px rgb(0 0 0 / 10%), 0 0 4px 0 rgb(0 0 0 / 10%); 10 | } 11 | 12 | .dnd { 13 | overflow: auto; 14 | width: 200px; 15 | flex-shrink: 0; 16 | border-right: 1px solid #ccc; 17 | 18 | ul { 19 | display: flex; 20 | flex-wrap: wrap; 21 | padding: 0; 22 | margin: 0; 23 | color: #fff; 24 | cursor: move; 25 | list-style: none; 26 | text-align: center; 27 | 28 | div { 29 | margin: 0 4px; 30 | margin: 16px; 31 | } 32 | 33 | .dndItemStart { 34 | width: 80px; 35 | height: 50px; 36 | border: 1px solid rgb(255 149 128); 37 | border-radius: 25px; 38 | background-color: rgb(255 149 128); 39 | line-height: 50px; 40 | } 41 | 42 | .dndItemProcess { 43 | width: 100px; 44 | height: 60px; 45 | border: 1px solid rgb(74 123 203); 46 | background-color: rgb(74 123 203); 47 | line-height: 60px; 48 | } 49 | 50 | .dndItemNo { 51 | width: 36px; 52 | height: 36px; 53 | border: 1px solid rgb(72 203 164); 54 | border-radius: 5px; 55 | background-color: rgb(72 203 164); 56 | line-height: 36px; 57 | } 58 | 59 | .dndItemYes { 60 | width: 38px; 61 | height: 38px; 62 | border: 1px solid rgb(72 203 164); 63 | border-radius: 5px; 64 | background-color: rgb(72 203 164); 65 | line-height: 38px; 66 | } 67 | 68 | .dndItemNotOk { 69 | width: 66px; 70 | height: 37px; 71 | border: 1px solid rgb(72 203 164); 72 | border-radius: 5px; 73 | background-color: rgb(72 203 164); 74 | line-height: 37px; 75 | } 76 | 77 | .dndItemPolygon { 78 | position: relative; 79 | width: 140px; 80 | height: 60px; 81 | background-color: rgb(128 170 255); 82 | clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%); 83 | transition: 1s clip-path; 84 | 85 | span { 86 | position: absolute; 87 | top: 20px; 88 | left: 54px; 89 | color: #fff; 90 | font-size: 14px; 91 | } 92 | } 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /apps/basic/src/pages/flow/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | XFlow, 3 | XFlowGraph, 4 | Grid, 5 | History, 6 | Clipboard, 7 | Transform, 8 | Snapline, 9 | Minimap, 10 | } from '@antv/xflow'; 11 | 12 | import { Connector } from './connector'; 13 | import { Dnd } from './dnd'; 14 | import { InitEdge } from './edge'; 15 | import styles from './index.less'; 16 | import { Keyboard } from './keyboard'; 17 | import { InitNode } from './node'; 18 | 19 | const Page = () => { 20 | return ( 21 |
22 |
23 | 24 | 25 | 73 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 |
99 |
100 | ); 101 | }; 102 | 103 | export default Page; 104 | -------------------------------------------------------------------------------- /apps/basic/src/pages/flow/keyboard.tsx: -------------------------------------------------------------------------------- 1 | import { useKeyboard, useHistory, useClipboard, useGraphStore } from '@antv/xflow'; 2 | import { useCallback } from 'react'; 3 | 4 | const Keyboard = () => { 5 | const { copy, paste, cut } = useClipboard(); 6 | const { undo, redo } = useHistory(); 7 | const nodes = useGraphStore((state) => state.nodes); 8 | const edges = useGraphStore((state) => state.edges); 9 | const updateEdge = useGraphStore((state) => state.updateEdge); 10 | const updateNode = useGraphStore((state) => state.updateNode); 11 | 12 | const removeNodes = useGraphStore((state) => state.removeNodes); 13 | const removeEdges = useGraphStore((state) => state.removeEdges); 14 | 15 | const selectNodeIds = useCallback(() => { 16 | const nodeSelected = nodes.filter((node) => node.selected); 17 | const nodeIds: string[] = nodeSelected.map((node) => node.id!); 18 | return nodeIds; 19 | }, [nodes]); 20 | 21 | const selectEdgeIds = useCallback(() => { 22 | const edgesSelect = edges.filter((edge) => edge.selected); 23 | const edgeIds: string[] = edgesSelect.map((edge) => edge.id!); 24 | return edgeIds; 25 | }, [edges]); 26 | 27 | const selectShapeIds = () => { 28 | return [...selectEdgeIds(), ...selectNodeIds()]; 29 | }; 30 | 31 | useKeyboard(['meta+c', 'ctrl+c'], () => { 32 | copy(selectShapeIds()); 33 | }); 34 | 35 | useKeyboard(['meta+v', 'ctrl+v'], () => { 36 | paste({ offset: 48 }); 37 | }); 38 | 39 | useKeyboard(['meta+x', 'ctrl+x'], () => { 40 | cut(selectShapeIds()); 41 | }); 42 | 43 | useKeyboard('backspace', () => { 44 | removeNodes(selectNodeIds()); 45 | removeEdges(selectEdgeIds()); 46 | }); 47 | 48 | useKeyboard(['meta+z', 'ctrl+z'], () => { 49 | undo(); 50 | }); 51 | useKeyboard(['meta+shift+z', 'ctrl+shift+z'], () => { 52 | redo(); 53 | }); 54 | 55 | useKeyboard(['meta+a', 'ctrl+a'], () => { 56 | nodes.map((node) => updateNode(node.id!, { selected: true })); 57 | edges.map((edge) => updateEdge(edge.id, { selected: true })); 58 | }); 59 | 60 | return null; 61 | }; 62 | 63 | export { Keyboard }; 64 | -------------------------------------------------------------------------------- /apps/basic/src/pages/group/ContextMenu/index.less: -------------------------------------------------------------------------------- 1 | .context-menu-wrapper { 2 | position: absolute; 3 | } 4 | -------------------------------------------------------------------------------- /apps/basic/src/pages/group/ContextMenu/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 节点的右键菜单 3 | */ 4 | 5 | import { Menu } from '@antv/x6-react-components'; 6 | import type { Cell } from '@antv/xflow'; 7 | // 不引入组件样式的话,节点右键菜单样式会有问题 8 | import '@antv/x6-react-components/es/menu/style/index.css'; 9 | 10 | import { useGraphEvent } from '@antv/xflow'; 11 | import React, { useState, useMemo, useCallback } from 'react'; 12 | import ReactDOM from 'react-dom'; 13 | 14 | import { Utils } from '../utils'; 15 | 16 | import styles from './index.less'; 17 | 18 | // https://g6.antv.antgroup.com/manual/advanced/coordinate-system 19 | // 获取到位置后将 dom 节点挂到 body 上定位,否则可能 dom 展示位位置和鼠标点击的位置有偏差 20 | const contextMenuRoot = document.createElement('div'); 21 | document.body.appendChild(contextMenuRoot); 22 | 23 | interface IProps { 24 | // 重命名群组 25 | renameCombineGroup: ( 26 | cell: Cell, 27 | confirmPosition: { confirmX: number; confirmY: number }, 28 | ) => void; 29 | // 解散群组 30 | removeCombineGroup: ( 31 | cell: Cell, 32 | confirmPosition: { confirmX: number; confirmY: number }, 33 | ) => void; 34 | } 35 | const ContextMenu = (props: IProps) => { 36 | const { renameCombineGroup, removeCombineGroup } = props; 37 | const [contextMenuPosition, setContextMenuPosition] = useState<{ 38 | contextMenuX: number; 39 | contextMenuY: number; 40 | } | null>(null); 41 | // 当前操作的 cell 42 | const [currentCell, setCurrnetCell] = useState(); 43 | 44 | // 取消右键菜单 45 | const cancelCellContextMenu = () => { 46 | setCurrnetCell(null); 47 | setContextMenuPosition(null); 48 | }; 49 | 50 | useGraphEvent('cell:contextmenu', ({ e, x, y, cell, view }) => { 51 | if (cell.isNode()) { 52 | const nodeType = cell?.getData()?.originData?.conceptType; 53 | // 除组合节点内部的普通子节点外,其它节点右键都会有操作 54 | if (!(cell.getParent() && !Utils.isCombineGroup(nodeType))) { 55 | setCurrnetCell(cell); 56 | setContextMenuPosition({ contextMenuX: e.pageX, contextMenuY: e.pageY }); 57 | } 58 | } else { 59 | setCurrnetCell(cell); 60 | setContextMenuPosition({ contextMenuX: e.pageX, contextMenuY: e.pageY }); 61 | } 62 | }); 63 | 64 | // 取消右键菜单 65 | useGraphEvent('blank:click', () => { 66 | setCurrnetCell(null); 67 | setContextMenuPosition(null); 68 | }); 69 | 70 | useGraphEvent('cell:click', () => { 71 | setCurrnetCell(null); 72 | setContextMenuPosition(null); 73 | }); 74 | 75 | // 解散组 76 | const handleRemoveCombineGroup = useCallback(() => { 77 | removeCombineGroup(currentCell as Cell, { 78 | confirmX: contextMenuPosition?.contextMenuX || 0, 79 | confirmY: contextMenuPosition?.contextMenuY || 0, 80 | }); 81 | cancelCellContextMenu(); 82 | }, [currentCell, contextMenuPosition, removeCombineGroup]); 83 | 84 | // 重命名组 85 | const handleRenameCombineGroup = useCallback(() => { 86 | renameCombineGroup(currentCell as Cell, { 87 | confirmX: contextMenuPosition?.contextMenuX || 0, 88 | confirmY: contextMenuPosition?.contextMenuY || 0, 89 | }); 90 | cancelCellContextMenu(); 91 | }, [currentCell, contextMenuPosition, renameCombineGroup]); 92 | 93 | // 获取右键菜单操作 94 | const rightMenuItem = useMemo(() => { 95 | let menuItem: { 96 | key: string; 97 | dom: React.ReactNode; 98 | }[] = []; 99 | if ( 100 | currentCell?.isNode() && 101 | Utils.isCombineGroup(currentCell?.getData()?.nodeType) 102 | ) { 103 | menuItem = [ 104 | { 105 | key: 'removeCombineGroup', 106 | dom: ( 107 | 108 | 解除组 109 | 110 | ), 111 | }, 112 | { 113 | key: 'renameCombineGroup', 114 | dom: ( 115 | 116 | 重命名组 117 | 118 | ), 119 | }, 120 | ]; 121 | } 122 | return menuItem; 123 | }, [currentCell, handleRemoveCombineGroup, handleRenameCombineGroup]); 124 | 125 | return ReactDOM.createPortal( 126 | currentCell?.isNode() && 127 | Utils.isCombineGroup(currentCell?.getData()?.nodeType) && 128 | contextMenuPosition ? ( 129 |
136 | {rightMenuItem.map((item) => item.dom)} 137 |
138 | ) : null, 139 | contextMenuRoot, 140 | ); 141 | }; 142 | 143 | export default ContextMenu; 144 | -------------------------------------------------------------------------------- /apps/basic/src/pages/group/GroupNode/index.less: -------------------------------------------------------------------------------- 1 | .group-wrap { 2 | .fold-group-wrap { 3 | display: flex; 4 | flex-direction: column; 5 | align-items: center; 6 | } 7 | 8 | .group-name { 9 | margin-top: 6px; 10 | text-align: center; 11 | cursor: pointer; 12 | } 13 | 14 | .group-name-fold { 15 | width: 100px; 16 | } 17 | 18 | .fold-icon { 19 | position: absolute; 20 | top: 98%; 21 | left: 50%; 22 | width: 16px; 23 | height: 16px; 24 | background-color: rgb(245, 247, 251); 25 | transform: translate(-50%, -50%); 26 | cursor: pointer; 27 | } 28 | 29 | .fold-style { 30 | position: relative; 31 | width: 36px; 32 | height: 36px; 33 | border: 2px solid #227eff; 34 | border-radius: 2px; 35 | background-color: #fff; 36 | } 37 | 38 | .children-number { 39 | position: absolute; 40 | top: -30%; 41 | left: 80%; 42 | width: 20px; 43 | height: 20px; 44 | line-height: 20px; 45 | text-align: center; 46 | border: 1px solid #fff; 47 | border-radius: 50%; 48 | color: #227eff; 49 | background-color: #dde7f9; 50 | } 51 | 52 | .unfold-style { 53 | position: relative; 54 | padding: 12px; 55 | border: 1px solid rgba(#227eff, 0.4); 56 | border-radius: 8px; 57 | background-color: rgba(#227eff, 0.08); 58 | 59 | .unfold-icon { 60 | position: absolute; 61 | top: 98%; 62 | left: 50%; 63 | width: 16px; 64 | height: 16px; 65 | background-color: rgb(245, 247, 251); 66 | transform: translate(-50%, -50%); 67 | cursor: pointer; 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /apps/basic/src/pages/group/GroupNode/index.tsx: -------------------------------------------------------------------------------- 1 | import { PlusCircleOutlined, MinusCircleOutlined } from '@ant-design/icons'; 2 | import type { Node } from '@antv/xflow'; 3 | import cx from 'classnames'; 4 | 5 | import type { INodeData } from '../type'; 6 | 7 | import styles from './index.less'; 8 | import React from 'react'; 9 | 10 | const GroupNode = ({ node }: { node: Node }) => { 11 | const { width, height } = node.getBBox(); 12 | const data: INodeData = node.getData(); 13 | const isFold = node.getProp('isFold'); 14 | const { label } = data || {}; 15 | const descendantsNumber = node.getProp('descendantsNumber') || 0; 16 | 17 | const renderNodes = () => { 18 | const foldElment = ( 19 |
20 |
21 |
{descendantsNumber}
22 | {/* event 自定义事件 */} 23 |
e.stopPropagation()} 27 | > 28 | 29 |
30 |
31 |
32 | {label} 33 |
34 |
35 | ); 36 | 37 | const unfoldElment = ( 38 | <> 39 |
43 |
e.stopPropagation()} 48 | > 49 | 50 |
51 |
52 |
{label}
53 | 54 | ); 55 | return ( 56 |
{isFold ? foldElment : unfoldElment}
57 | ); 58 | }; 59 | 60 | return
{renderNodes()}
; 61 | }; 62 | 63 | export default GroupNode; 64 | -------------------------------------------------------------------------------- /apps/basic/src/pages/group/NormalNode/index.less: -------------------------------------------------------------------------------- 1 | .graph-normal-node { 2 | height: 100%; 3 | 4 | .node-wrap { 5 | display: flex; 6 | flex-direction: column; 7 | align-items: center; 8 | } 9 | 10 | .node-styl { 11 | position: relative; 12 | background-color: #227eff; 13 | border-radius: 2px; 14 | } 15 | 16 | .node-text { 17 | width: 100px; 18 | margin-top: 6px; 19 | text-align: center; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /apps/basic/src/pages/group/NormalNode/index.tsx: -------------------------------------------------------------------------------- 1 | import type { Node } from '@antv/xflow'; 2 | import React from 'react'; 3 | import type { INodeData } from '../type'; 4 | 5 | import styles from './index.less'; 6 | 7 | const NormalNode = ({ node }: { node: Node }) => { 8 | const data: INodeData = node.getData(); 9 | const { width, height } = node.getBBox(); 10 | 11 | return ( 12 |
13 |
14 |
15 |
{data?.label}
16 |
17 |
18 | ); 19 | }; 20 | 21 | export default NormalNode; 22 | -------------------------------------------------------------------------------- /apps/basic/src/pages/group/const.ts: -------------------------------------------------------------------------------- 1 | // 群组节点默认的 size:调整大小或影响 Ports 的位置 2 | export const defaultGroupSize = { 3 | width: 36, 4 | height: 36, 5 | }; 6 | 7 | // 群组节点的 padding 8 | export const groupNodePadding = 24; 9 | 10 | // 非群组节点的 size:调整大小或影响 Ports 的位置 11 | export const notGroupNodeSize = { 12 | width: 36, 13 | height: 36, 14 | }; 15 | -------------------------------------------------------------------------------- /apps/basic/src/pages/group/dagreLayout.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description @antv/layout DagreLayout 3 | */ 4 | import { DagreLayout } from '@antv/layout'; 5 | import * as _ from 'lodash'; 6 | 7 | import type { IGraph } from './type'; 8 | 9 | export const layoutDagre = (graphData: IGraph, options?: Record) => { 10 | const antvDagreLayout = new DagreLayout({ 11 | type: 'dagre', 12 | nodesep: 50, 13 | // 布局方向和文档说明不一样:https://g6.antv.antgroup.com/manual/middle/layout/graph-layout#dagre 14 | // LR 是从左到有布局,但是配置之后变成从上到下的布局展示 15 | // TB 是从上到下布局,但是配置之后变成从左到右布局 16 | rankdir: 'TB', 17 | begin: options?.begin || [250, 250], 18 | }); 19 | antvDagreLayout.layout(graphData); 20 | 21 | return { 22 | edges: graphData.edges, 23 | nodes: graphData.nodes, 24 | }; 25 | }; 26 | -------------------------------------------------------------------------------- /apps/basic/src/pages/group/index.less: -------------------------------------------------------------------------------- 1 | .container { 2 | width: 1200px; 3 | height: 600px; 4 | margin: 100px auto; 5 | border-radius: 5px; 6 | 7 | .content { 8 | display: flex; 9 | height: calc(100% - 80px); 10 | box-shadow: 0 12px 5px -10px rgba(0, 0, 0, 0.1), 0 0 4px 0 rgba(0, 0, 0, 0.1); 11 | } 12 | 13 | .tools { 14 | height: 80px; 15 | 16 | button { 17 | margin-right: 12px; 18 | margin-bottom: 8px; 19 | } 20 | } 21 | } 22 | 23 | .edit-popconfirm-wrapper { 24 | position: relative; 25 | width: 500px; 26 | 27 | .group-name-input { 28 | display: flex; 29 | align-items: center; 30 | } 31 | 32 | .confirm-footer { 33 | margin-top: 12px; 34 | margin-left: calc(100% - 135px); 35 | display: flex; 36 | 37 | .cancel-btn { 38 | margin-right: 12px; 39 | } 40 | } 41 | 42 | :global(.ant-input) { 43 | flex: 1; 44 | background-color: #eaeaea; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /apps/basic/src/pages/group/index.tsx: -------------------------------------------------------------------------------- 1 | import { XFlow, XFlowGraph, register } from '@antv/xflow'; 2 | 3 | import GroupNode from './GroupNode'; 4 | import styles from './index.less'; 5 | import NormalNode from './NormalNode'; 6 | import { ToolsButton } from './tools'; 7 | import { NodeRenderKey } from './type'; 8 | import React from 'react'; 9 | 10 | // 注册普通节点 11 | register({ 12 | shape: NodeRenderKey.NORMAL_NODE, 13 | component: NormalNode, 14 | }); 15 | 16 | // 注册组合节点 17 | register({ 18 | shape: NodeRenderKey.GROUP_NODE, 19 | component: GroupNode, 20 | }); 21 | 22 | const Group = () => { 23 | return ( 24 |
25 | 26 | 27 |
28 | { 44 | const cell = view?.cell; 45 | if (cell && cell.isNode()) { 46 | const parent = cell.getParent(); 47 | if (parent) { 48 | return parent.getBBox(); 49 | } 50 | } 51 | return null; 52 | }, 53 | }} 54 | /> 55 |
56 |
57 |
    58 |
  • 添加多个节点
  • 59 |
  • 移动节点会自动检查是否需要成组
  • 60 |
  • 支持节点成组、往组合节点继续添加子节点、解除组、重命名组
  • 61 |
  • 支持组合节点的收起、展开
  • 62 |
63 |
64 |
65 |
66 | ); 67 | }; 68 | 69 | export default Group; 70 | -------------------------------------------------------------------------------- /apps/basic/src/pages/group/type.ts: -------------------------------------------------------------------------------- 1 | import type { Graph, Node } from '@antv/x6'; 2 | import type { PortManager } from '@antv/x6/lib/model/port'; 3 | import type { Attr } from '@antv/x6/lib/registry/attr'; 4 | import type { ReactShape } from '@antv/x6-react-shape'; 5 | 6 | // 组合操作类型枚举 7 | export enum GRAPH_EDIT_OPT_TYPE { 8 | // 合并节点:单个节点合并 9 | MERGE_NODE = 'MERGE_NODE', 10 | // 往组合节点增加节点 11 | ADD_NODE_TO_COMBINE_GROUP = 'ADD_NODE_TO_COMBINE_GROUP', 12 | // 解除组组合(即删除关系) 13 | REMOVE_COMBINE_GROUP = 'REMOVE_COMBINE_GROUP', 14 | // 重命名组合 15 | RENAME_COMBINE_GROUP = 'RENAME_COMBINE_GROUP', 16 | } 17 | 18 | // 节点渲染类型 19 | export enum NodeRenderKey { 20 | NORMAL_NODE = 'normal_node', 21 | GROUP_NODE = 'group_node', 22 | } 23 | 24 | // 节点类型 25 | export enum NodeType { 26 | // 单个节点 27 | SINGLE = 'SINGLE', 28 | // 组合节点 29 | GROUP = 'GROUP', 30 | } 31 | 32 | // 节点 data 原始数据 33 | export type INodeData = { 34 | id: string; 35 | label: string; 36 | nodeType: NodeType; 37 | }; 38 | 39 | // 节点数据模型 40 | export type INode = { 41 | // 节点id 42 | id: string; 43 | width?: number; 44 | height?: number; 45 | x?: number; 46 | y?: number; 47 | ports?: Partial; 48 | zIndex?: number; 49 | shape?: NodeRenderKey; 50 | component?: (this: Graph, node: ReactShape) => React.Component; 51 | // 原始数据 52 | data?: INodeData; 53 | // ==== 以下是非标准属性,获取非标准属性 node.getProp('parentNodes') ==== 54 | // 父节点 55 | parentNodes?: INodeData; 56 | // 子节点组 57 | childrenNodes?: INodeData[]; 58 | // 组合节点所有后代节点的数量:不包括组合节点,便于在组合节点收起时展示数量 59 | descendantsNumber?: number; 60 | // 节点展开/收起 61 | isFold?: boolean; 62 | }; 63 | 64 | // 边类型 65 | export enum EdgeType { 66 | // 线 67 | LINE = 'LINE', 68 | // 带箭头的线 69 | ARROW = 'ARROW', 70 | } 71 | 72 | // 边 data 原始数据 73 | export type IEdgeData = { 74 | // 源节点 75 | source: string; 76 | // 目标节点 77 | target: string; 78 | // 关系类型 79 | edgeType: EdgeType; 80 | }; 81 | 82 | // 边的数据模型 83 | export type IEdge = { 84 | id: string; 85 | // 对于组合与子节点的关系,source 是父节点 86 | source: string; 87 | // 对于组合与子节点的关系,target 是子节点 88 | target: string; 89 | zIndex?: number; 90 | attrs?: Attr.CellAttrs; 91 | data: IEdgeData; 92 | isWrapEdge?: boolean; 93 | }; 94 | 95 | // 图数据类型 96 | export type IGraph = { nodes: INode[]; edges: IEdge[] }; 97 | 98 | /** 99 | * 二次确认弹窗的内容 100 | * 节点移动: 101 | * 合并节点:source 当前移动的节点;target 待合并的节点 102 | * 添加节点:source 当前移动的节点;target 待添加子节点的群组节点 103 | * 重命名群组:source 当前操作的节点;target null 104 | * 解散群组:source 当前操作的节点;target null 105 | */ 106 | export type GRAPH_OPT_CONFIRM_CONTENT = { 107 | source: Node; 108 | target?: Node; 109 | }; 110 | -------------------------------------------------------------------------------- /apps/basic/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./src/.umi/tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /apps/basic/typings.d.ts: -------------------------------------------------------------------------------- 1 | import 'umi/typings'; 2 | -------------------------------------------------------------------------------- /nx.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://cdn.jsdelivr.net/npm/nx@latest/schemas/nx-schema.json", 3 | "tasksRunnerOptions": { 4 | "default": { 5 | "runner": "nx/tasks-runners/default", 6 | "options": { 7 | "parallel": 6, 8 | "cacheableOperations": [ 9 | "setup", 10 | "build", 11 | "lint", 12 | "lint:js", 13 | "lint:css", 14 | "lint:format", 15 | "lint:typing", 16 | "test" 17 | ] 18 | } 19 | } 20 | }, 21 | "targetDefaults": { 22 | "setup": { 23 | "dependsOn": ["^setup"], 24 | "outputs": ["{projectRoot}/dist"] 25 | }, 26 | "build": { 27 | "dependsOn": ["^build"], 28 | "outputs": ["{projectRoot}/dist"] 29 | }, 30 | "lint": { 31 | "executor": "nx:noop", 32 | "dependsOn": ["lint:js", "lint:css", "lint:format" /* "lint:typing" */] 33 | }, 34 | "test": { 35 | "outputs": ["{projectRoot}/coverage"] 36 | }, 37 | "ci": { 38 | "executor": "nx:noop", 39 | "dependsOn": ["lint", "test"] 40 | } 41 | }, 42 | "defaultBase": "main" 43 | } 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "preinstall": "npx only-allow pnpm", 5 | "postinstall": "is-ci || husky install", 6 | "setup": "nx run-many --target=setup", 7 | "dev": "nx run-many --target=dev --parallel=100", 8 | "lint": "nx run-many --target=lint", 9 | "test": "nx run-many --target=test", 10 | "ci": "nx affected --target=lint:js,lint:css,lint:format,test --nx-bail=true", 11 | "build": "nx run-many --target=build --nx-bail=true", 12 | "serve": "nx run-many --target=serve", 13 | "fix": "nx run-many --target=lint:js,lint:css -- --fix; nx run-many --target=lint:format -- --write", 14 | "format-all": "prettier --write '**/src/**/*.{html,js,jsx,ts,tsx,css,less,json}' **/*.md", 15 | "bootstrap": "pnpm install && pnpm run setup", 16 | "clean": "pnpm -s dlx rimraf -g './node_modules' '**/node_modules' './apps/*/dist' './packages/*/dist'" 17 | }, 18 | "devDependencies": { 19 | "@commitlint/cli": "^17.4.4", 20 | "@commitlint/config-conventional": "^17.4.4", 21 | "@antv/config-eslint": "workspace:^", 22 | "@antv/config-stylelint": "workspace:^", 23 | "dotenv-cli": "^7.1.0", 24 | "eslint": "^8.35.0", 25 | "husky": "^8.0.3", 26 | "is-ci": "^3.0.1", 27 | "jest": "^29.5.0", 28 | "lint-staged": "^13.1.2", 29 | "nx": "^15.8.6", 30 | "prettier": "^2.8.4", 31 | "stylelint": "^15.2.0", 32 | "tsup": "^6.7.0", 33 | "typescript": "^4.9.5" 34 | }, 35 | "engines": { 36 | "node": ">=16.19.0" 37 | }, 38 | "packageManager": "pnpm@7.27.0", 39 | "dependencies": { 40 | "@antv/x6-plugin-dnd": "^2.0.5" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/core/README.en-US.md: -------------------------------------------------------------------------------- 1 | English (US) | [简体中文](README.zh-Hans.md) 2 | 3 | # Core 4 | -------------------------------------------------------------------------------- /packages/core/README.md: -------------------------------------------------------------------------------- 1 | [English (US)](README.md) | 简体中文 2 | 3 | # Core 4 | -------------------------------------------------------------------------------- /packages/core/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@antv/testing/config/react'); 2 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@antv/xflow", 3 | "version": "2.1.13", 4 | "description": "", 5 | "main": "dist/index.cjs.js", 6 | "module": "dist/index.esm.js", 7 | "types": "dist/typing/index.d.ts", 8 | "private": false, 9 | "files": [ 10 | "dist", 11 | "src" 12 | ], 13 | "keywords": [ 14 | "xflow", 15 | "x6", 16 | "antv" 17 | ], 18 | "scripts": { 19 | "setup": "tsup src/index.ts", 20 | "build": "tsup src/index.ts && pnpm run build:umd", 21 | "build:umd": "rollup -c", 22 | "dev": "tsup src/index.ts --watch", 23 | "lint:js": "eslint 'src/**/*.{js,jsx,ts,tsx}'", 24 | "lint:css": "stylelint --allow-empty-input 'src/**/*.{css,less}'", 25 | "lint:format": "prettier --check *.md *.json 'src/**/*.{js,jsx,ts,tsx,css,less,md,json}'", 26 | "lint:typing": "tsc --noEmit", 27 | "test": "jest --coverage" 28 | }, 29 | "dependencies": { 30 | "@antv/x6": "^2.15.3", 31 | "@antv/x6-plugin-clipboard": "^2.1.6", 32 | "@antv/x6-plugin-dnd": "^2.1.1", 33 | "@antv/x6-plugin-export": "^2.1.6", 34 | "@antv/x6-plugin-history": "^2.2.4", 35 | "@antv/x6-plugin-keyboard": "^2.2.1", 36 | "@antv/x6-plugin-minimap": "^2.0.6", 37 | "@antv/x6-plugin-scroller": "^2.0.10", 38 | "@antv/x6-plugin-selection": "^2.2.1", 39 | "@antv/x6-plugin-snapline": "^2.1.7", 40 | "@antv/x6-plugin-transform": "^2.1.8", 41 | "@antv/x6-react-shape": "2.0.8", 42 | "@tippyjs/react": "^4.2.6", 43 | "immer": "^10.0.3", 44 | "lucide-react": "^0.292.0", 45 | "tippy.js": "^6.3.7", 46 | "zustand": "^4.4.3", 47 | "classnames": "^2.3.2" 48 | }, 49 | "devDependencies": { 50 | "@antv/config-tsconfig": "workspace:^", 51 | "@antv/config-tsup": "workspace:^", 52 | "@antv/testing": "workspace:^", 53 | "@types/react": "^18.2.37", 54 | "@rollup/plugin-commonjs": "^20.0.0", 55 | "@rollup/plugin-node-resolve": "^13.0.4", 56 | "@rollup/plugin-replace": "^3.0.0", 57 | "@rollup/plugin-typescript": "^8.2.5", 58 | "rollup": "^2.56.3", 59 | "rollup-plugin-auto-external": "^2.0.0", 60 | "rollup-plugin-filesize": "^9.1.1", 61 | "rollup-plugin-postcss": "^4.0.1", 62 | "rollup-plugin-progress": "^1.1.2", 63 | "rollup-plugin-terser": "^7.0.2", 64 | "less": "^4.1.1" 65 | }, 66 | "peerDependencies": { 67 | "react": ">=16.8.6 || >=17.0.0 || >=18.0.0", 68 | "react-dom": ">=16.8.6 || >=17.0.0 || >= 18.0.0" 69 | }, 70 | "bugs": { 71 | "url": "https://github.com/antvis/XFlow/issues" 72 | }, 73 | "repository": { 74 | "type": "git", 75 | "url": "https://github.com/antvis/XFlow.git", 76 | "directory": "packages/ide" 77 | }, 78 | "publishConfig": { 79 | "access": "public" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /packages/core/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://cdn.jsdelivr.net/npm/nx@latest/schemas/project-schema.json", 3 | "targets": { 4 | "lint": {}, 5 | "ci": {} 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/core/rollup.config.js: -------------------------------------------------------------------------------- 1 | import commonjs from '@rollup/plugin-commonjs'; 2 | import resolve from '@rollup/plugin-node-resolve'; 3 | import replace from '@rollup/plugin-replace'; 4 | import typescript from '@rollup/plugin-typescript'; 5 | import filesize from 'rollup-plugin-filesize'; 6 | import postcss from 'rollup-plugin-postcss'; 7 | import { terser } from 'rollup-plugin-terser'; 8 | 9 | export default () => { 10 | return { 11 | input: './src/index.ts', 12 | plugins: [ 13 | typescript({ declaration: false }), 14 | resolve(), 15 | commonjs(), 16 | replace({ 17 | preventAssignment: true, 18 | 'process.env.NODE_ENV': JSON.stringify('production'), 19 | }), 20 | terser(), 21 | filesize(), 22 | postcss(), 23 | ], 24 | output: [ 25 | { 26 | name: 'XFlow', 27 | format: 'umd', 28 | file: 'dist/index.umd.js', 29 | sourcemap: true, 30 | globals: { 31 | react: 'React', 32 | 'react-dom': 'ReactDOM', 33 | }, 34 | }, 35 | ], 36 | external: ['react', 'react-dom'], 37 | }; 38 | }; 39 | -------------------------------------------------------------------------------- /packages/core/src/components/Background.tsx: -------------------------------------------------------------------------------- 1 | import type { Graph } from '@antv/x6'; 2 | import React, { useEffect } from 'react'; 3 | 4 | import { useGraphInstance } from '../hooks/useGraphInstance'; 5 | 6 | const Background = (props: Graph.BackgroundManager.Options) => { 7 | const graph = useGraphInstance(); 8 | 9 | useEffect(() => { 10 | if (graph) { 11 | graph.clearBackground(); 12 | graph.drawBackground(props); 13 | } 14 | }, [graph, props]); 15 | 16 | return null; 17 | }; 18 | 19 | export { Background }; 20 | -------------------------------------------------------------------------------- /packages/core/src/components/Clipboard.tsx: -------------------------------------------------------------------------------- 1 | import { Clipboard as C } from '@antv/x6-plugin-clipboard'; 2 | import React, { useEffect } from 'react'; 3 | 4 | import { useGraphInstance } from '../hooks/useGraphInstance'; 5 | 6 | const Clipboard = (props: Omit) => { 7 | const graph = useGraphInstance(); 8 | 9 | useEffect(() => { 10 | if (graph) { 11 | if (graph.getPlugin('clipboard')) { 12 | graph.disposePlugins('clipboard'); 13 | } 14 | graph.use( 15 | new C({ 16 | enabled: true, 17 | ...props, 18 | }), 19 | ); 20 | } 21 | }, [graph, props]); 22 | 23 | return null; 24 | }; 25 | 26 | export { Clipboard }; 27 | -------------------------------------------------------------------------------- /packages/core/src/components/Control.tsx: -------------------------------------------------------------------------------- 1 | import Tippy from '@tippyjs/react'; 2 | import classNames from 'classnames'; 3 | import { Plus, Minus, Minimize, Dice5 } from 'lucide-react'; 4 | import React, { useEffect, useState } from 'react'; 5 | import 'tippy.js/dist/tippy.css'; 6 | 7 | import { useGraphEvent, useGraphInstance } from '@/hooks'; 8 | 9 | import '../styles/index.less'; 10 | 11 | export enum ControlEnum { 12 | ZoomTo = 'zoomTo', 13 | ZoomIn = 'zoomIn', 14 | ZoomOut = 'zoomOut', 15 | ZoomToFit = 'zoomToFit', 16 | ZoomToOrigin = 'zoomToOrigin', 17 | } 18 | 19 | const dropDownItems = [ 20 | { 21 | key: '1', 22 | label: '50%', 23 | }, 24 | { 25 | key: '2', 26 | label: '75%', 27 | }, 28 | { 29 | key: '3', 30 | label: '100%', 31 | }, 32 | { 33 | key: '4', 34 | label: '125%', 35 | }, 36 | { 37 | key: '5', 38 | label: '150%', 39 | }, 40 | ]; 41 | 42 | const ControlToolMap = { 43 | [ControlEnum.ZoomIn]: { 44 | label: '放大', 45 | icon: , 46 | }, 47 | [ControlEnum.ZoomOut]: { 48 | label: '缩小', 49 | icon: , 50 | }, 51 | [ControlEnum.ZoomTo]: { 52 | label: '缩放至', 53 | icon: , 54 | }, 55 | [ControlEnum.ZoomToFit]: { 56 | label: '自适应窗口大小', 57 | icon: , 58 | }, 59 | [ControlEnum.ZoomToOrigin]: { 60 | label: '实际像素展示', 61 | icon: , 62 | }, 63 | }; 64 | 65 | const ControlActionList = [ 66 | 'zoomTo', 67 | 'zoomIn', 68 | 'zoomOut', 69 | 'zoomToFit', 70 | 'zoomToOrigin', 71 | ] as const; 72 | 73 | type ControlAction = (typeof ControlActionList)[number]; 74 | 75 | interface ControlIProps { 76 | items: ControlAction[]; 77 | direction?: 'horizontal' | 'vertical'; 78 | placement?: 'top' | 'right' | 'bottom' | 'left'; 79 | } 80 | 81 | const Control = (props: ControlIProps) => { 82 | const { items, direction = 'horizontal', placement = 'top' } = props; 83 | const graph = useGraphInstance(); 84 | 85 | const [zoom, setZoom] = useState(1); 86 | 87 | useGraphEvent('scale', ({ sx }) => { 88 | setZoom(sx); 89 | }); 90 | 91 | useEffect(() => { 92 | if (graph) { 93 | setZoom(graph.zoom()); 94 | } 95 | }, [graph, props]); 96 | 97 | const changeZoom = (type: ControlAction, args?: string) => { 98 | if (!graph) return; 99 | const key = parseInt(args || '1', 10); 100 | const zoomNum = (0.25 * (key + 1)) as number; 101 | switch (type) { 102 | case ControlEnum.ZoomIn: 103 | if (zoom < 1.5) { 104 | graph.zoom(0.25); 105 | } 106 | break; 107 | case ControlEnum.ZoomOut: 108 | if (zoom > 0.5) { 109 | graph.zoom(-0.25); 110 | } 111 | break; 112 | case ControlEnum.ZoomToFit: 113 | graph.zoomToFit({ maxScale: 1 }); 114 | break; 115 | case ControlEnum.ZoomToOrigin: 116 | graph.zoomTo(1); 117 | break; 118 | case ControlEnum.ZoomTo: 119 | graph.zoomTo(zoomNum); 120 | break; 121 | default: 122 | break; 123 | } 124 | setZoom(graph.zoom()); 125 | }; 126 | 127 | const isToolButtonEnabled = (type: ControlEnum) => { 128 | if (type == ControlEnum.ZoomIn) { 129 | return zoom < 1.5; 130 | } else if (type === ControlEnum.ZoomOut) { 131 | return zoom > 0.51; 132 | } 133 | return true; 134 | }; 135 | 136 | return ( 137 |
142 | {items.map((tool) => { 143 | if (tool === 'zoomTo') { 144 | return ( 145 | 149 | {dropDownItems.map((item) => { 150 | return ( 151 | 154 | ); 155 | })} 156 |
157 | } 158 | interactive 159 | placement="top" 160 | arrow={false} 161 | theme="light-border" 162 | > 163 | 164 | 165 | ); 166 | } else if (ControlActionList.includes(tool)) { 167 | return ( 168 | 174 | 180 | 181 | ); 182 | } else { 183 | return null; 184 | } 185 | })} 186 |
187 | ); 188 | }; 189 | 190 | export { Control }; 191 | -------------------------------------------------------------------------------- /packages/core/src/components/Graph.tsx: -------------------------------------------------------------------------------- 1 | import { Graph, Options } from '@antv/x6'; 2 | import { Keyboard } from '@antv/x6-plugin-keyboard'; 3 | import { Scroller } from '@antv/x6-plugin-scroller'; 4 | import { Selection } from '@antv/x6-plugin-selection'; 5 | import React, { useContext, useRef, useEffect } from 'react'; 6 | 7 | import { GraphContext } from '../context'; 8 | import type { GraphOptions } from '../types'; 9 | 10 | import { XFlowState } from './State'; 11 | import { Wrapper } from './Wrapper'; 12 | 13 | const XFlowGraph = (props: GraphOptions) => { 14 | const container = useRef(null); 15 | const { 16 | className, 17 | style, 18 | readonly, 19 | virtual, 20 | minScale, 21 | maxScale, 22 | zoomable, 23 | zoomOptions, 24 | pannable, 25 | panOptions, 26 | embedable, 27 | embedOptions, 28 | restrict, 29 | restrictOptions, 30 | connectionOptions, 31 | onPortRendered, 32 | onEdgeLabelRendered, 33 | createCellView, 34 | selectOptions, 35 | keyboardOptions, 36 | scroller, 37 | scrollerOptions, 38 | connectionEdgeOptions, 39 | defaultHighlightOptions, 40 | embedHighlightOptions, 41 | nodeAvailableHighlightOptions, 42 | magnetAvailableHighlightOptions, 43 | magnetAdsorbedHighlightOptions, 44 | } = props; 45 | const { graph, setGraph } = useContext(GraphContext); 46 | 47 | useEffect(() => { 48 | const g = new Graph({ 49 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 50 | container: container.current!, 51 | autoResize: true, 52 | virtual, 53 | scaling: { 54 | min: minScale, 55 | max: maxScale, 56 | }, 57 | connecting: { 58 | ...connectionOptions, 59 | createEdge() { 60 | return this.createEdge({ 61 | shape: 'edge', 62 | ...connectionEdgeOptions, 63 | }); 64 | }, 65 | }, 66 | highlighting: { 67 | default: defaultHighlightOptions, 68 | embedding: embedHighlightOptions, 69 | nodeAvailable: nodeAvailableHighlightOptions, 70 | magnetAvailable: magnetAvailableHighlightOptions, 71 | magnetAdsorbed: magnetAdsorbedHighlightOptions, 72 | }, 73 | onPortRendered, 74 | onEdgeLabelRendered, 75 | createCellView, 76 | }); 77 | 78 | g.use(new Selection({ enabled: true, ...selectOptions })); 79 | g.use(new Keyboard({ enabled: true, ...keyboardOptions })); 80 | 81 | if (scroller) { 82 | g.use(new Scroller({ enabled: true, ...scrollerOptions })); 83 | } 84 | 85 | setGraph(g); 86 | 87 | return () => { 88 | if (g) { 89 | g.dispose(); 90 | setGraph(null); 91 | } 92 | }; 93 | // eslint-disable-next-line react-hooks/exhaustive-deps 94 | }, []); 95 | 96 | useEffect(() => { 97 | if (graph) { 98 | if (readonly) { 99 | graph.options.interacting = false; 100 | } else { 101 | graph.options.interacting = { 102 | nodeMovable: (view) => { 103 | const cell = view.cell; 104 | return cell.prop('draggable') !== false; 105 | }, 106 | edgeMovable: (view) => { 107 | const cell = view.cell; 108 | return cell.prop('draggable') !== false; 109 | }, 110 | edgeLabelMovable: (view) => { 111 | const cell = view.cell; 112 | return cell.prop('labelDraggable') === true; 113 | }, 114 | }; 115 | } 116 | } 117 | }, [graph, readonly]); 118 | 119 | useEffect(() => { 120 | if (graph) { 121 | if (zoomable) { 122 | graph.enableMouseWheel(); 123 | graph.options.mousewheel = { 124 | ...Options.defaults.mousewheel, 125 | ...zoomOptions, 126 | enabled: true, 127 | }; 128 | } else { 129 | graph.disableMouseWheel(); 130 | } 131 | } 132 | }, [graph, zoomable, zoomOptions]); 133 | 134 | useEffect(() => { 135 | if (graph) { 136 | if (pannable) { 137 | graph.options.panning = { 138 | ...Options.defaults.panning, 139 | enabled: true, 140 | ...panOptions, 141 | }; 142 | graph.enablePanning(); 143 | } else { 144 | graph.disablePanning(); 145 | } 146 | } 147 | }, [graph, pannable, panOptions]); 148 | 149 | useEffect(() => { 150 | if (graph) { 151 | if (embedable) { 152 | graph.options.embedding = { 153 | ...Options.defaults.embedding, 154 | enabled: true, 155 | validate: () => true, 156 | ...embedOptions, 157 | }; 158 | } else { 159 | graph.options.embedding = { enabled: false, validate: () => false }; 160 | } 161 | } 162 | }, [graph, embedable, embedOptions]); 163 | 164 | useEffect(() => { 165 | if (graph) { 166 | if (restrict) { 167 | graph.options.translating = { 168 | restrict: restrictOptions ? restrictOptions.bound : restrict, 169 | }; 170 | } else { 171 | graph.options.translating = { restrict: false }; 172 | } 173 | } 174 | }, [graph, restrict, restrictOptions]); 175 | 176 | return ( 177 |
178 |
179 | 180 | 187 | 188 |
189 | ); 190 | }; 191 | 192 | export { XFlowGraph }; 193 | -------------------------------------------------------------------------------- /packages/core/src/components/Grid.tsx: -------------------------------------------------------------------------------- 1 | import { type Registry } from '@antv/x6'; 2 | import React, { useEffect } from 'react'; 3 | 4 | import { useGraphInstance } from '../hooks/useGraphInstance'; 5 | 6 | type GridTypes = keyof Registry.Grid.Presets; 7 | interface GridProps { 8 | visible?: boolean; 9 | size?: number; 10 | type: T; 11 | options: Registry.Grid.OptionsMap[T]; 12 | } 13 | 14 | const Grid = (props: GridProps) => { 15 | const graph = useGraphInstance(); 16 | const { visible, size, type, options } = props; 17 | 18 | useEffect(() => { 19 | if (graph) { 20 | graph.clearGrid(); 21 | graph.drawGrid({ 22 | type, 23 | args: options, 24 | }); 25 | graph.showGrid(); 26 | } 27 | }, [graph, type, options]); 28 | 29 | useEffect(() => { 30 | if (graph) { 31 | if (visible === false) { 32 | graph.hideGrid(); 33 | } else { 34 | graph.showGrid(); 35 | } 36 | } 37 | }, [graph, visible]); 38 | 39 | useEffect(() => { 40 | if (graph) { 41 | if (size) { 42 | graph.setGridSize(size); 43 | } 44 | } 45 | }, [graph, size]); 46 | 47 | return null; 48 | }; 49 | 50 | export { Grid }; 51 | -------------------------------------------------------------------------------- /packages/core/src/components/History.tsx: -------------------------------------------------------------------------------- 1 | import { History as H } from '@antv/x6-plugin-history'; 2 | import React, { useEffect } from 'react'; 3 | 4 | import { useGraphInstance } from '../hooks/useGraphInstance'; 5 | 6 | const History = (props: Omit) => { 7 | const graph = useGraphInstance(); 8 | 9 | useEffect(() => { 10 | if (graph) { 11 | if (graph.getPlugin('history')) { 12 | graph.disposePlugins('history'); 13 | } 14 | graph.use( 15 | new H({ 16 | enabled: true, 17 | ...props, 18 | }), 19 | ); 20 | } 21 | }, [graph, props]); 22 | 23 | return null; 24 | }; 25 | 26 | export { History }; 27 | -------------------------------------------------------------------------------- /packages/core/src/components/Minimap.tsx: -------------------------------------------------------------------------------- 1 | import { NodeView } from '@antv/x6'; 2 | import { MiniMap as M } from '@antv/x6-plugin-minimap'; 3 | import React, { useEffect, useRef } from 'react'; 4 | 5 | import { useGraphInstance } from '../hooks/useGraphInstance'; 6 | 7 | type IProps = Partial> & { 8 | style?: React.CSSProperties; 9 | className?: string; 10 | simple?: boolean; 11 | simpleNodeBackground?: string; 12 | }; 13 | 14 | class SimpleNodeView extends NodeView { 15 | static nodeBackground = '#8f8f8f'; 16 | 17 | protected renderMarkup() { 18 | const tag = this.cell.shape === 'circle' ? 'circle' : 'rect'; 19 | return this.renderJSONMarkup({ 20 | tagName: tag, 21 | selector: 'body', 22 | }); 23 | } 24 | 25 | update() { 26 | super.update({ 27 | body: { 28 | refWidth: '100%', 29 | refHeight: '100%', 30 | fill: SimpleNodeView.nodeBackground, 31 | }, 32 | }); 33 | } 34 | } 35 | 36 | const Minimap = (props: IProps) => { 37 | const { style, className, simple, simpleNodeBackground, ...others } = props; 38 | const ref = useRef(null); 39 | const graph = useGraphInstance(); 40 | 41 | useEffect(() => { 42 | if (graph && ref.current) { 43 | if (graph.getPlugin('minimap')) { 44 | graph.disposePlugins('minimap'); 45 | } 46 | SimpleNodeView.nodeBackground = 47 | simpleNodeBackground || SimpleNodeView.nodeBackground; 48 | graph.use( 49 | new M({ 50 | container: ref.current, 51 | width: 200, 52 | height: 160, 53 | padding: 10, 54 | graphOptions: simple 55 | ? { 56 | createCellView(cell) { 57 | if (cell.isEdge()) { 58 | return null; 59 | } 60 | if (cell.isNode()) { 61 | return SimpleNodeView; 62 | } 63 | return undefined; 64 | }, 65 | } 66 | : undefined, 67 | ...others, 68 | }), 69 | ); 70 | } 71 | // eslint-disable-next-line react-hooks/exhaustive-deps 72 | }, [graph, others]); 73 | 74 | return
; 75 | }; 76 | 77 | export { Minimap }; 78 | -------------------------------------------------------------------------------- /packages/core/src/components/Snapline.tsx: -------------------------------------------------------------------------------- 1 | import { Snapline as S } from '@antv/x6-plugin-snapline'; 2 | import React, { useEffect } from 'react'; 3 | 4 | import { useGraphInstance } from '../hooks/useGraphInstance'; 5 | 6 | const Snapline = (props: Omit) => { 7 | const graph = useGraphInstance(); 8 | 9 | useEffect(() => { 10 | if (graph) { 11 | if (graph.getPlugin('snapline')) { 12 | graph.disposePlugins('snapline'); 13 | } 14 | graph.use( 15 | new S({ 16 | enabled: true, 17 | ...props, 18 | }), 19 | ); 20 | } 21 | }, [graph, props]); 22 | 23 | return null; 24 | }; 25 | 26 | export { Snapline }; 27 | -------------------------------------------------------------------------------- /packages/core/src/components/Transform.tsx: -------------------------------------------------------------------------------- 1 | import { Transform as T } from '@antv/x6-plugin-transform'; 2 | import React, { useEffect } from 'react'; 3 | 4 | import { useGraphInstance } from '../hooks/useGraphInstance'; 5 | 6 | type IProps = { 7 | resizing?: T.Options['resizing']; 8 | rotating?: T.Options['rotating']; 9 | }; 10 | 11 | const Transform = (props: IProps) => { 12 | const graph = useGraphInstance(); 13 | const { resizing, rotating } = props; 14 | 15 | const parseOptions = (options: T.Options['resizing'] | T.Options['rotating']) => { 16 | if (typeof options === 'boolean') { 17 | return options; 18 | } 19 | if (typeof options === 'object') { 20 | return { 21 | enabled: true, 22 | ...options, 23 | }; 24 | } 25 | 26 | return false; 27 | }; 28 | 29 | useEffect(() => { 30 | if (graph) { 31 | if (graph.getPlugin('transform')) { 32 | graph.disposePlugins('transform'); 33 | } 34 | 35 | graph.use( 36 | new T({ 37 | resizing: parseOptions(resizing), 38 | rotating: parseOptions(rotating), 39 | }), 40 | ); 41 | } 42 | }, [graph, resizing, rotating]); 43 | 44 | return null; 45 | }; 46 | 47 | export { Transform }; 48 | -------------------------------------------------------------------------------- /packages/core/src/components/Wrapper.tsx: -------------------------------------------------------------------------------- 1 | import type { FC, PropsWithChildren } from 'react'; 2 | import React from 'react'; 3 | 4 | import { useGraphInstance } from '../hooks/useGraphInstance'; 5 | 6 | const Wrapper: FC = ({ children }) => { 7 | const graph = useGraphInstance(); 8 | 9 | if (graph) { 10 | return <>{children}; 11 | } 12 | 13 | return null; 14 | }; 15 | 16 | export { Wrapper }; 17 | -------------------------------------------------------------------------------- /packages/core/src/components/XFlow.tsx: -------------------------------------------------------------------------------- 1 | import { type PropsWithChildren, type FC } from 'react'; 2 | import React from 'react'; 3 | import { GraphProvider, StoreProvider } from '../context'; 4 | import '../styles/index.less'; 5 | 6 | const XFlow: FC = ({ children }) => { 7 | return ( 8 | 9 | {children} 10 | 11 | ); 12 | }; 13 | 14 | export { XFlow }; 15 | -------------------------------------------------------------------------------- /packages/core/src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Wrapper'; 2 | export * from './XFlow'; 3 | export * from './Graph'; 4 | export * from './Grid'; 5 | export * from './Background'; 6 | export * from './Clipboard'; 7 | export * from './History'; 8 | export * from './Minimap'; 9 | export * from './Snapline'; 10 | export * from './Transform'; 11 | export * from './Control'; 12 | -------------------------------------------------------------------------------- /packages/core/src/context/GraphContext.tsx: -------------------------------------------------------------------------------- 1 | import type { Graph } from '@antv/x6'; 2 | import React, { createContext, useState, type FC, type PropsWithChildren } from 'react'; 3 | 4 | interface GraphContextValue { 5 | graph: Graph | null; 6 | setGraph: (graph: Graph | null) => void; 7 | } 8 | 9 | export const GraphContext = createContext({ 10 | graph: null, 11 | setGraph: () => {}, 12 | }); 13 | 14 | export const GraphProvider: FC = ({ children }) => { 15 | const [graph, setGraph] = useState(null); 16 | 17 | return ( 18 | 19 | {children} 20 | 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /packages/core/src/context/StoreContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useRef, type FC, type PropsWithChildren } from 'react'; 2 | 3 | import { createGraphStore, type GraphStore } from '../store'; 4 | 5 | export const StoreContext = createContext(null); 6 | 7 | export const StoreProvider: FC = ({ children }) => { 8 | const storeRef = useRef(); 9 | if (!storeRef.current) { 10 | storeRef.current = createGraphStore(); 11 | } 12 | return ( 13 | {children} 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /packages/core/src/context/index.ts: -------------------------------------------------------------------------------- 1 | export * from './GraphContext'; 2 | export * from './StoreContext'; 3 | -------------------------------------------------------------------------------- /packages/core/src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useGraphInstance'; 2 | export * from './useGraphStore'; 3 | export * from './useGraphEvent'; 4 | export * from './useDnd'; 5 | export * from './useClipboard'; 6 | export * from './useExport'; 7 | export * from './useHistory'; 8 | export * from './useKeyboard'; 9 | -------------------------------------------------------------------------------- /packages/core/src/hooks/useClipboard.ts: -------------------------------------------------------------------------------- 1 | import type { Clipboard } from '@antv/x6-plugin-clipboard'; 2 | import { useCallback } from 'react'; 3 | 4 | import { useGraphInstance } from './useGraphInstance'; 5 | import { useLoaded } from './useLoaded'; 6 | 7 | export const useClipboard = () => { 8 | const graph = useGraphInstance(); 9 | const { isLoaded } = useLoaded('clipboard'); 10 | 11 | const copy = useCallback( 12 | (ids: string[], copyOptions?: Clipboard.CopyOptions) => { 13 | if (isLoaded() && graph) { 14 | const cells = ids.map((id) => graph?.getCellById(id)).filter(Boolean); 15 | graph.copy(cells, copyOptions); 16 | } 17 | }, 18 | [graph, isLoaded], 19 | ); 20 | 21 | const cut = useCallback( 22 | (ids: string[], cutOptions?: Clipboard.CopyOptions) => { 23 | if (isLoaded() && graph) { 24 | const cells = ids.map((id) => graph?.getCellById(id)).filter(Boolean); 25 | graph.cut(cells, cutOptions); 26 | } 27 | }, 28 | [graph, isLoaded], 29 | ); 30 | 31 | const paste = useCallback( 32 | (pasteOptions?: Clipboard.PasteOptions) => { 33 | if (isLoaded() && graph) { 34 | const cells = graph.paste(pasteOptions); 35 | return cells; 36 | } 37 | return []; 38 | }, 39 | [graph, isLoaded], 40 | ); 41 | 42 | return { copy, cut, paste }; 43 | }; 44 | -------------------------------------------------------------------------------- /packages/core/src/hooks/useDnd.ts: -------------------------------------------------------------------------------- 1 | import { Dnd } from '@antv/x6-plugin-dnd'; 2 | import { useCallback, useEffect, useRef } from 'react'; 3 | 4 | import type { NodeOptions } from '../types'; 5 | 6 | import { useGraphInstance } from './useGraphInstance'; 7 | 8 | export const useDnd = ( 9 | options?: Omit, 10 | ) => { 11 | const graph = useGraphInstance(); 12 | const ref = useRef(); 13 | 14 | useEffect(() => { 15 | if (graph && !ref.current) { 16 | ref.current = new Dnd({ 17 | target: graph, 18 | getDragNode: (node) => node.clone({ keepId: true }), 19 | getDropNode: (node) => node.clone({ keepId: true }), 20 | ...options, 21 | }); 22 | } 23 | // eslint-disable-next-line react-hooks/exhaustive-deps 24 | }, [graph]); 25 | 26 | const startDrag = useCallback( 27 | (n: NodeOptions, e: React.MouseEvent) => { 28 | if (graph && ref.current) { 29 | e.persist(); 30 | ref.current.start(graph.createNode(n), e.nativeEvent); 31 | } 32 | }, 33 | [graph], 34 | ); 35 | 36 | return { startDrag }; 37 | }; 38 | -------------------------------------------------------------------------------- /packages/core/src/hooks/useExport.ts: -------------------------------------------------------------------------------- 1 | import { Export } from '@antv/x6-plugin-export'; 2 | import { useCallback } from 'react'; 3 | 4 | import { useGraphInstance } from './useGraphInstance'; 5 | import { useLoaded } from './useLoaded'; 6 | 7 | export const useExport = () => { 8 | const graph = useGraphInstance(); 9 | const { isLoaded } = useLoaded('export'); 10 | 11 | const ensure = useCallback(() => { 12 | return isLoaded(() => { 13 | graph?.use(new Export()); 14 | return true; 15 | }); 16 | }, [graph, isLoaded]); 17 | 18 | const exportPNG = useCallback( 19 | (fileName = 'chart', options: Export.ToImageOptions = {}) => { 20 | if (ensure() && graph) { 21 | graph.exportPNG(fileName, options); 22 | } 23 | }, 24 | [graph, ensure], 25 | ); 26 | 27 | const exportJPEG = useCallback( 28 | (fileName = 'chart', options: Export.ToImageOptions = {}) => { 29 | if (ensure() && graph) { 30 | graph.exportJPEG(fileName, options); 31 | } 32 | }, 33 | [graph, ensure], 34 | ); 35 | 36 | const exportSVG = useCallback( 37 | (fileName = 'chart', options: Export.ToSVGOptions = {}) => { 38 | if (ensure() && graph) { 39 | graph.exportSVG(fileName, options); 40 | } 41 | }, 42 | [graph, ensure], 43 | ); 44 | 45 | return { exportPNG, exportJPEG, exportSVG }; 46 | }; 47 | -------------------------------------------------------------------------------- /packages/core/src/hooks/useGraphEvent.ts: -------------------------------------------------------------------------------- 1 | import type { EventArgs } from '@antv/x6'; 2 | import { useEffect } from 'react'; 3 | 4 | import { useGraphInstance } from './useGraphInstance'; 5 | import { useLatest } from './useLatest'; 6 | 7 | export const useGraphEvent = ( 8 | name: T, 9 | callback: (args: EventArgs[T]) => void, 10 | ) => { 11 | const cbRef = useLatest(callback); 12 | const graph = useGraphInstance(); 13 | 14 | useEffect(() => { 15 | if (graph) { 16 | cbRef.current = callback; 17 | graph.on(name, (args: EventArgs[T]) => { 18 | cbRef.current(args); 19 | }); 20 | } 21 | 22 | return () => { 23 | if (graph && cbRef.current) { 24 | graph.off(name, cbRef.current); 25 | } 26 | }; 27 | // eslint-disable-next-line react-hooks/exhaustive-deps 28 | }, [graph]); 29 | }; 30 | -------------------------------------------------------------------------------- /packages/core/src/hooks/useGraphInstance.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | 3 | import { GraphContext } from '../context/GraphContext'; 4 | 5 | export const useGraphInstance = () => { 6 | const { graph } = useContext(GraphContext); 7 | 8 | return graph; 9 | }; 10 | -------------------------------------------------------------------------------- /packages/core/src/hooks/useGraphStore.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import { useStore } from 'zustand'; 3 | 4 | import { StoreContext } from '../context'; 5 | import type { State, Actions } from '../store'; 6 | 7 | export const useGraphStore = (selector: (state: State & Actions) => T) => { 8 | const store = useContext(StoreContext); 9 | if (!store) { 10 | throw new Error('can only be get inside the xflow component.'); 11 | } 12 | return useStore(store, selector); 13 | }; 14 | -------------------------------------------------------------------------------- /packages/core/src/hooks/useHistory.ts: -------------------------------------------------------------------------------- 1 | import type { KeyValue } from '@antv/x6'; 2 | import { useCallback, useState } from 'react'; 3 | 4 | import { useGraphEvent } from './useGraphEvent'; 5 | import { useGraphInstance } from './useGraphInstance'; 6 | import { useLoaded } from './useLoaded'; 7 | 8 | export const useHistory = () => { 9 | const graph = useGraphInstance(); 10 | const { isLoaded } = useLoaded('history'); 11 | const [canUndo, setCanUndo] = useState(false); 12 | const [canRedo, setCanRedo] = useState(false); 13 | 14 | const undo = useCallback( 15 | (options?: KeyValue) => { 16 | if (isLoaded() && graph) { 17 | return graph.undo(options); 18 | } 19 | return null; 20 | }, 21 | [graph, isLoaded], 22 | ); 23 | 24 | const redo = useCallback( 25 | (options?: KeyValue) => { 26 | if (isLoaded() && graph) { 27 | return graph.redo(options); 28 | } 29 | return null; 30 | }, 31 | [graph, isLoaded], 32 | ); 33 | 34 | useGraphEvent('history:change', () => { 35 | if (graph) { 36 | setCanUndo(graph.canUndo()); 37 | setCanRedo(graph.canRedo()); 38 | } 39 | }); 40 | 41 | return { undo, redo, canUndo, canRedo }; 42 | }; 43 | -------------------------------------------------------------------------------- /packages/core/src/hooks/useKeyboard.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | 3 | import { useGraphInstance } from './useGraphInstance'; 4 | import { useLatest } from './useLatest'; 5 | 6 | export const useKeyboard = ( 7 | key: string | string[], 8 | callback: (e: KeyboardEvent) => void, 9 | action?: 'keypress' | 'keydown' | 'keyup', 10 | ) => { 11 | const cbRef = useLatest(callback); 12 | const graph = useGraphInstance(); 13 | 14 | useEffect(() => { 15 | if (graph) { 16 | cbRef.current = callback; 17 | graph.bindKey( 18 | key, 19 | (e) => { 20 | cbRef.current(e); 21 | }, 22 | action, 23 | ); 24 | } 25 | 26 | return () => { 27 | if (graph) { 28 | graph.unbindKey(key); 29 | } 30 | }; 31 | // eslint-disable-next-line react-hooks/exhaustive-deps 32 | }, [graph]); 33 | }; 34 | -------------------------------------------------------------------------------- /packages/core/src/hooks/useLatest.ts: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react'; 2 | 3 | function useLatest(value: T) { 4 | const ref = useRef(value); 5 | ref.current = value; 6 | 7 | return ref; 8 | } 9 | 10 | export { useLatest }; 11 | -------------------------------------------------------------------------------- /packages/core/src/hooks/useLoaded.ts: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | 3 | import { useGraphInstance } from './useGraphInstance'; 4 | 5 | export const useLoaded = (name: string) => { 6 | const graph = useGraphInstance(); 7 | 8 | const isLoaded = useCallback( 9 | (cb?: () => void) => { 10 | if (!graph) { 11 | console.warn('graph can only be get inside the xflow component.'); 12 | return false; 13 | } 14 | const plugin = graph.getPlugin(name); 15 | if (!plugin) { 16 | if (cb) { 17 | return cb(); 18 | } 19 | console.warn(`${name} is not loaded, please use ${name} component first.`); 20 | return false; 21 | } 22 | return true; 23 | }, 24 | [graph, name], 25 | ); 26 | 27 | return { 28 | isLoaded, 29 | }; 30 | }; 31 | -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Graph, 3 | Point, 4 | Rectangle, 5 | Line, 6 | Path, 7 | Polyline, 8 | Dom, 9 | Vector, 10 | Markup, 11 | } from '@antv/x6'; 12 | 13 | export * from './components'; 14 | export * from './hooks'; 15 | export * from './util'; 16 | export * from './types'; 17 | 18 | export * from '@antv/x6-react-shape'; 19 | export { Graph, Point, Rectangle, Line, Path, Polyline, Dom, Vector, Markup }; 20 | -------------------------------------------------------------------------------- /packages/core/src/styles/index.less: -------------------------------------------------------------------------------- 1 | @keyframes animated-line { 2 | to { 3 | stroke-dashoffset: -1000; 4 | } 5 | } 6 | 7 | // selection 8 | .x6-node-selected rect { 9 | stroke: #239edd; 10 | } 11 | 12 | .x6-edge-selected { 13 | path:nth-child(2) { 14 | stroke: #239edd; 15 | stroke-width: 1.5px; 16 | } 17 | } 18 | 19 | // snapline 20 | .x6-widget-snapline-horizontal, 21 | .x6-widget-snapline-vertical { 22 | stroke: #239edd; 23 | } 24 | 25 | // transform 26 | .x6-widget-transform { 27 | margin: -1px 0 0 -1px; 28 | padding: 0px; 29 | border: 1px solid #239edd; 30 | } 31 | .x6-widget-transform > div { 32 | border: 1px solid #239edd; 33 | } 34 | .x6-widget-transform > div:hover { 35 | background-color: #3dafe4; 36 | } 37 | .x6-widget-transform-active-handle { 38 | background-color: #3dafe4; 39 | } 40 | .x6-widget-transform-resize { 41 | border-radius: 0; 42 | } 43 | 44 | // control style 45 | .toolButton { 46 | display: flex; 47 | flex-direction: row; 48 | align-items: center; 49 | justify-content: center; 50 | border: 1px solid #e6e8eb; 51 | border-radius: 6px; 52 | 53 | button { 54 | min-width: 36px !important; 55 | height: 36px; 56 | border: 1px solid #fff; 57 | border-radius: 4px; 58 | background-color: #fff; 59 | outline: none; 60 | user-select: none; 61 | touch-action: manipulation; 62 | cursor: pointer; 63 | display: flex; 64 | align-items: center; 65 | justify-content: center; 66 | color: rgb(0, 0, 0); 67 | 68 | &:disabled { 69 | background-color: rgba(0, 0, 0, 0.04); 70 | color: rgba(0, 0, 0, 0.25); 71 | } 72 | &:hover { 73 | border: 1px solid #4096ff; 74 | } 75 | } 76 | 77 | .dropDownBtn { 78 | width: 100%; 79 | padding: 0; 80 | } 81 | 82 | .tippy-box { 83 | border: none !important; 84 | box-shadow: 0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 3px 6px -4px rgba(0, 0, 0, 0.12), 85 | 0 9px 28px 8px rgba(0, 0, 0, 0.05) !important; 86 | background-color: #ffffff; 87 | } 88 | 89 | .tippy-content { 90 | width: 60px; 91 | padding: 4px !important; 92 | } 93 | 94 | .tippyBtnContent { 95 | display: flex; 96 | flex-direction: column; 97 | align-items: center; 98 | 99 | button { 100 | width: 100%; 101 | min-width: 36px !important; 102 | 103 | &:hover { 104 | border: none !important; 105 | background-color: rgba(0, 0, 0, 0.04); 106 | } 107 | } 108 | } 109 | } 110 | 111 | .toolbuttonVertical { 112 | flex-direction: column; 113 | 114 | button { 115 | min-width: 40px !important; 116 | } 117 | } 118 | // control style end 119 | -------------------------------------------------------------------------------- /packages/core/src/types/index.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | Node, 3 | Edge, 4 | Cell, 5 | Graph, 6 | Options, 7 | Rectangle, 8 | CellView, 9 | Markup, 10 | } from '@antv/x6'; 11 | import type { Scroller } from '@antv/x6-plugin-scroller'; 12 | import type { Selection } from '@antv/x6-plugin-selection'; 13 | 14 | export type { Node, Edge, Cell }; 15 | export interface NodeOptions extends Node.Metadata { 16 | selected?: boolean; 17 | draggable?: boolean; 18 | } 19 | 20 | export interface EdgeOptions extends Edge.Metadata { 21 | selected?: boolean; 22 | draggable?: boolean; 23 | labelDraggable?: boolean; 24 | animated?: boolean; 25 | } 26 | 27 | export type GraphModel = { nodes: NodeOptions[]; edges: EdgeOptions[] }; 28 | 29 | export interface OnEdgeLabelRenderedArgs { 30 | edge: Edge; 31 | label: Edge.Label; 32 | container: Element; 33 | selectors: Markup.Selectors; 34 | } 35 | export interface GraphOptions { 36 | // graph 37 | className?: string; 38 | style?: React.CSSProperties; 39 | readonly?: boolean; 40 | virtual?: boolean; 41 | minScale?: number; 42 | maxScale?: number; 43 | zoomable?: boolean; 44 | zoomOptions?: Omit; 45 | pannable?: boolean; 46 | panOptions?: Omit; 47 | centerView?: boolean; 48 | centerViewOptions?: Graph.TransformManager.CenterOptions; 49 | fitView?: boolean; 50 | fitViewOptions?: Graph.TransformManager.ScaleContentToFitOptions; 51 | scroller?: boolean; 52 | scrollerOptions?: Omit; 53 | 54 | // node & edge 55 | connectionEdgeOptions?: Partial; 56 | onPortRendered?: (args: Options.OnPortRenderedArgs) => void; 57 | onEdgeLabelRendered?: ( 58 | args: Options.OnEdgeLabelRenderedArgs, 59 | ) => undefined | ((args: OnEdgeLabelRenderedArgs) => void); 60 | 61 | // cell 62 | createCellView?: ( 63 | this: Graph, 64 | cell: Cell, 65 | ) => typeof CellView | (new (...args: any[]) => CellView) | null | undefined; 66 | 67 | // interactive 68 | embedable?: boolean; 69 | embedOptions?: Partial>; 70 | restrict?: boolean; 71 | restrictOptions?: { 72 | bound: 73 | | Rectangle.RectangleLike 74 | | ((arg: CellView | null) => Rectangle.RectangleLike | null); 75 | }; 76 | connectionOptions?: Partial>; 77 | selectOptions?: Omit; 78 | keyboardOptions?: { 79 | global?: boolean; 80 | format?: (key: string) => string; 81 | guard?: (e: KeyboardEvent) => boolean; 82 | }; 83 | 84 | // highlight 85 | defaultHighlightOptions?: Graph.HighlightManager.Options; 86 | embedHighlightOptions?: Graph.HighlightManager.Options; 87 | nodeAvailableHighlightOptions?: Graph.HighlightManager.Options; 88 | magnetAvailableHighlightOptions?: Graph.HighlightManager.Options; 89 | magnetAdsorbedHighlightOptions?: Graph.HighlightManager.Options; 90 | } 91 | -------------------------------------------------------------------------------- /packages/core/src/util/algorithm.ts: -------------------------------------------------------------------------------- 1 | import type { Graph } from '@antv/x6'; 2 | 3 | export const getSuperGraph = (graph: Graph, nodeId: string, nodeIds: string[] = []) => { 4 | nodeIds.push(nodeId); 5 | const incomingEdges = graph.getIncomingEdges(nodeId); 6 | incomingEdges?.forEach((edge) => { 7 | const source = edge.getSourceCellId(); 8 | getSuperGraph(graph, source, nodeIds); 9 | }); 10 | return nodeIds; 11 | }; 12 | 13 | export const getSubGraph = (graph: Graph, nodeId: string, nodeIds: string[] = []) => { 14 | nodeIds.push(nodeId); 15 | const outgoingEdges = graph.getOutgoingEdges(nodeId); 16 | outgoingEdges?.forEach((edge) => { 17 | const target = edge.getTargetCellId(); 18 | getSubGraph(graph, target, nodeIds); 19 | }); 20 | return nodeIds; 21 | }; 22 | -------------------------------------------------------------------------------- /packages/core/src/util/index.ts: -------------------------------------------------------------------------------- 1 | export * from './algorithm'; 2 | export * from './object'; 3 | -------------------------------------------------------------------------------- /packages/core/src/util/object.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-prototype-builtins */ 2 | /* eslint-disable @typescript-eslint/no-unused-vars */ 3 | /* eslint-disable @typescript-eslint/ban-types */ 4 | /* eslint-disable @typescript-eslint/no-explicit-any */ 5 | import { ObjectExt } from '@antv/x6'; 6 | 7 | type mpObj = { [k in keyof T | string | number | symbol]: any }; 8 | 9 | export function apply( 10 | target: mpObj, 11 | patchItem: mpObj, 12 | ): Partial & Partial; 13 | export function apply(target: mpObj, patchItem: mpObj): R; 14 | export function apply(target: mpObj, patchItem: mpObj): {}; 15 | export function apply(target: mpObj, patchItem: null): null; 16 | export function apply(target: mpObj, patchItem: string): string; 17 | export function apply(target: mpObj, patchItem: number): number; 18 | export function apply(target: mpObj, patchItem: undefined): undefined; 19 | export function apply(target: mpObj, patchItem: R[]): R[]; 20 | 21 | export function apply(target: any, patchItem: any): any { 22 | /** 23 | * If the patch is anything other than an object, 24 | * the result will always be to replace 25 | * the entire target with the entire patch. 26 | */ 27 | if (typeof patchItem !== 'object' || Array.isArray(patchItem) || !patchItem) { 28 | return JSON.parse(JSON.stringify(patchItem)); //return new instance of variable 29 | } 30 | 31 | if ( 32 | typeof patchItem === 'object' && 33 | patchItem.toJSON !== undefined && 34 | typeof patchItem.toJSON === 'function' 35 | ) { 36 | return patchItem.toJSON(); 37 | } 38 | /** Also, it is not possible to 39 | * patch part of a target that is not an object, 40 | * such as to replace just some of the values in an array. 41 | */ 42 | let targetResult = target; 43 | if (typeof target !== 'object') { 44 | //Target is empty/not an object, so basically becomes patch, minus any null valued sections (becomes {} + patch) 45 | targetResult = { ...patchItem }; 46 | } 47 | 48 | Object.keys(patchItem).forEach((k) => { 49 | if (!targetResult.hasOwnProperty(k)) targetResult[k] = patchItem[k]; //This ensure the key exists and TS can't throw a wobbly over an undefined key 50 | if (patchItem[k] === null || patchItem[k] === undefined) { 51 | delete targetResult[k]; 52 | } else if (targetResult[k] !== null && targetResult[k] !== undefined) { 53 | targetResult[k] = apply(targetResult[k], patchItem[k]); 54 | } 55 | }); 56 | return targetResult; 57 | } 58 | 59 | export function flatten(obj: any, delim = '/', stop?: (val: any) => boolean) { 60 | const ret: { [key: string]: any } = {}; 61 | 62 | Object.keys(obj).forEach((key) => { 63 | const val = obj[key]; 64 | let deep = ObjectExt.isPlainObject(val); 65 | if (deep && stop && stop(val)) { 66 | deep = false; 67 | } 68 | 69 | if (deep) { 70 | const flatObject = flatten(val, delim, stop); 71 | Object.keys(flatObject).forEach((flatKey) => { 72 | ret[key + delim + flatKey] = flatObject[flatKey]; 73 | }); 74 | } else { 75 | ret[key] = val; 76 | } 77 | }); 78 | 79 | // eslint-disable-next-line no-restricted-syntax 80 | for (const key in obj) { 81 | if (!Object.prototype.hasOwnProperty.call(obj, key)) { 82 | continue; 83 | } 84 | } 85 | 86 | return ret; 87 | } 88 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@antv/config-tsconfig/mana.json", 3 | "compilerOptions": { 4 | "declarationDir": "./dist/typing", 5 | "baseUrl": ".", 6 | "paths": { 7 | "@/*": ["./src/*"] 8 | } 9 | }, 10 | "include": ["./src/**/*"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/core/tsup.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@antv/config-tsup'); 2 | -------------------------------------------------------------------------------- /packages/diff/README.en-US.md: -------------------------------------------------------------------------------- 1 | English (US) | [简体中文](README.zh-Hans.md) 2 | 3 | # Diff 4 | -------------------------------------------------------------------------------- /packages/diff/README.md: -------------------------------------------------------------------------------- 1 | [English (US)](README.md) | 简体中文 2 | 3 | # Diff 4 | -------------------------------------------------------------------------------- /packages/diff/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@antv/testing/config/react'); 2 | -------------------------------------------------------------------------------- /packages/diff/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@antv/xflow-diff", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "dist/index.cjs.js", 6 | "module": "dist/index.esm.js", 7 | "types": "dist/typing/index.d.ts", 8 | "private": false, 9 | "files": [ 10 | "dist", 11 | "src" 12 | ], 13 | "keywords": [ 14 | "xflow", 15 | "x6", 16 | "antv" 17 | ], 18 | "scripts": { 19 | "setup": "tsup src/index.ts", 20 | "build": "tsup src/index.ts", 21 | "dev": "tsup src/index.ts --watch", 22 | "lint:js": "eslint 'src/**/*.{js,jsx,ts,tsx}'", 23 | "lint:css": "stylelint --allow-empty-input 'src/**/*.{css,less}'", 24 | "lint:format": "prettier --check *.md *.json 'src/**/*.{js,jsx,ts,tsx,css,less,md,json}'", 25 | "lint:typing": "tsc --noEmit", 26 | "test": "jest --coverage" 27 | }, 28 | "dependencies": { 29 | "@antv/xflow": "workspace:^" 30 | }, 31 | "devDependencies": { 32 | "@antv/config-tsconfig": "workspace:^", 33 | "@antv/config-tsup": "workspace:^", 34 | "@antv/testing": "workspace:^", 35 | "@types/react": "^18.0.28" 36 | }, 37 | "peerDependencies": { 38 | "antd": ">= 3.26.19 || >=5.0.0", 39 | "react": ">=16.8.6 || >=17.0.0 || >=18.0.0", 40 | "react-dom": ">=16.8.6 || >=17.0.0 || >= 18.0.0" 41 | }, 42 | "bugs": { 43 | "url": "https://github.com/antvis/XFlow/issues" 44 | }, 45 | "repository": { 46 | "type": "git", 47 | "url": "https://github.com/antvis/XFlow.git", 48 | "directory": "packages/diff" 49 | }, 50 | "publishConfig": { 51 | "access": "public" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /packages/diff/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://cdn.jsdelivr.net/npm/nx@latest/schemas/project-schema.json", 3 | "targets": { 4 | "lint": {}, 5 | "ci": {} 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/diff/src/components/DiffGraph/index.tsx: -------------------------------------------------------------------------------- 1 | import type { EdgeOptions, NodeOptions, useGraphInstance } from '@antv/xflow'; 2 | import { XFlow, XFlowGraph } from '@antv/xflow'; 3 | import type { FC } from 'react'; 4 | import React, { useEffect, useState } from 'react'; 5 | 6 | import type { DiffGraphOptions } from '@/types'; 7 | import { compare, syncGraph } from '@/util'; 8 | 9 | import '../../styles/index.less'; 10 | import Tool from './tool'; 11 | 12 | const DiffGraph: FC = (props) => { 13 | const { 14 | originalData, // 变更前数据 15 | currentData, // 变更后数据 16 | addColor = '#50a14f', // 新增节点的颜色 17 | addExtAttr, 18 | delColor = '#ff7875', // 删除节点的颜色 19 | delExtAttr, 20 | changeColor = '#ffc069', // 变更节点的颜色 21 | changeExtAttr, 22 | graphOptions, 23 | } = props; 24 | 25 | const [originalDataWithDiffInfo, setOriginalDataWithDiffInfo] = useState<{ 26 | nodes: NodeOptions[]; 27 | edges: EdgeOptions[]; 28 | }>({ nodes: [], edges: [] }); 29 | const [currentDataWithDiffInfo, setCurrentDataWithDiffInfo] = useState<{ 30 | nodes: NodeOptions[]; 31 | edges: EdgeOptions[]; 32 | }>({ nodes: [], edges: [] }); 33 | const [status, setStatus] = useState<'init' | 'computing' | 'done'>('init'); 34 | const [graphs, setGraphs] = useState[]>([]); 35 | 36 | useEffect(() => { 37 | // 获取 diff 信息,注入 attr 38 | setStatus('computing'); 39 | const { 40 | originalDataWithDiffInfo: originalDataWithDiffInfoRe, 41 | currentDataWithDiffInfo: currentDataWithDiffInfoRe, 42 | } = compare( 43 | originalData, 44 | currentData, 45 | addColor, 46 | delColor, 47 | changeColor, 48 | addExtAttr, 49 | delExtAttr, 50 | changeExtAttr, 51 | ); 52 | 53 | setOriginalDataWithDiffInfo(originalDataWithDiffInfoRe); 54 | setCurrentDataWithDiffInfo(currentDataWithDiffInfoRe); 55 | setStatus('done'); 56 | }, []); // eslint-disable-line 57 | 58 | useEffect(() => { 59 | // 关联双图的缩放和移动 60 | if (graphs.length === 2) { 61 | syncGraph(graphs[0], graphs[1]); 62 | } 63 | }, [graphs]); 64 | 65 | const addGraph = (graph: ReturnType) => { 66 | setGraphs((pre) => { 67 | return [...pre, graph]; 68 | }); 69 | }; 70 | 71 | return ( 72 |
73 | {/* 左图 */} 74 | {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */} 75 | {/* @ts-ignore */} 76 | 77 | {status === 'done' && ( 78 | 79 | )} 80 | 93 | 94 | 95 | {/* 右图 */} 96 | {/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */} 97 | {/* @ts-ignore */} 98 | 99 | {status === 'done' && ( 100 | 101 | )} 102 | 115 | 116 |
117 | ); 118 | }; 119 | 120 | export { DiffGraph }; 121 | -------------------------------------------------------------------------------- /packages/diff/src/components/DiffGraph/tool.tsx: -------------------------------------------------------------------------------- 1 | import type { EdgeOptions, NodeOptions } from '@antv/xflow'; 2 | import { useGraphInstance, useGraphStore } from '@antv/xflow'; 3 | import type { FC } from 'react'; 4 | import React, { useEffect } from 'react'; 5 | 6 | interface ToolOptions { 7 | data: { 8 | nodes: NodeOptions[]; 9 | edges: EdgeOptions[]; 10 | }; 11 | addGraph: (graph: ReturnType) => void; 12 | } 13 | 14 | const Tool: FC = (props) => { 15 | const { data, addGraph } = props; 16 | const initData = useGraphStore((state) => state.initData); 17 | const graphIns = useGraphInstance(); 18 | 19 | useEffect(() => { 20 | // 初始化数据 21 | initData(data); 22 | 23 | // 上报 graph 实例 24 | addGraph(graphIns); 25 | }, []); // eslint-disable-line 26 | 27 | return null; 28 | }; 29 | 30 | export default Tool; 31 | -------------------------------------------------------------------------------- /packages/diff/src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './DiffGraph'; 2 | -------------------------------------------------------------------------------- /packages/diff/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './components'; 2 | export * from './types'; 3 | -------------------------------------------------------------------------------- /packages/diff/src/styles/index.less: -------------------------------------------------------------------------------- 1 | .xflow-diff { 2 | position: relative; 3 | display: flex; 4 | height: 100%; 5 | 6 | &::after { 7 | position: absolute; 8 | left: 50%; 9 | width: 1px; 10 | height: 100%; 11 | background-color: #333; 12 | content: ''; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/diff/src/types/index.ts: -------------------------------------------------------------------------------- 1 | import type { EdgeOptions, GraphOptions, NodeOptions } from '@antv/xflow'; 2 | 3 | export interface DiffGraphOptions { 4 | originalData: GraphData; 5 | currentData: GraphData; 6 | addColor?: ''; 7 | addExtAttr?: object; 8 | delColor?: ''; 9 | delExtAttr?: object; 10 | changeColor?: ''; 11 | changeExtAttr?: object; 12 | graphOptions?: GraphOptions; 13 | } 14 | 15 | export interface GraphData { 16 | nodes: NodeOptions[]; 17 | edges: EdgeOptions[]; 18 | } 19 | 20 | export interface NodeOptionsWithDiffInfo extends NodeOptions { 21 | diffType?: 'DEL' | 'ADD' | 'CHG' | 'NONE'; 22 | } 23 | export interface EdgeOptionsWithDiffInfo extends EdgeOptions { 24 | diffType?: 'DEL' | 'ADD' | 'CHG' | 'NONE'; 25 | } 26 | 27 | export interface GraphDataWithDiffInfo { 28 | nodes: NodeOptionsWithDiffInfo[]; 29 | edges: EdgeOptionsWithDiffInfo[]; 30 | } 31 | -------------------------------------------------------------------------------- /packages/diff/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@antv/config-tsconfig/mana.json", 3 | "compilerOptions": { 4 | "declarationDir": "./dist/typing", 5 | "baseUrl": ".", 6 | "paths": { 7 | "@/*": ["./src/*"] 8 | }, 9 | "esModuleInterop": true, 10 | "jsx": "react" 11 | }, 12 | "include": ["./src/**/*"] 13 | } 14 | -------------------------------------------------------------------------------- /packages/diff/tsup.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@antv/config-tsup'); 2 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'apps/*' 3 | - 'packages/*' 4 | - 'tooling/*' 5 | -------------------------------------------------------------------------------- /tooling/eslint/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | 'eslint:recommended', 4 | 'plugin:@typescript-eslint/recommended', 5 | 'plugin:react/recommended', 6 | 'prettier', 7 | ], 8 | plugins: ['@typescript-eslint', 'import', 'react', 'react-hooks'], 9 | env: { 10 | node: true, 11 | }, 12 | settings: { 13 | react: { 14 | version: '18', 15 | }, 16 | }, 17 | rules: { 18 | '@typescript-eslint/no-explicit-any': ['warn', { ignoreRestArgs: true }], 19 | '@typescript-eslint/no-shadow': [ 20 | 'warn', 21 | { ignoreTypeValueShadow: true, builtinGlobals: true }, 22 | ], 23 | '@typescript-eslint/consistent-type-imports': 'warn', 24 | 'react/jsx-uses-react': 'off', 25 | 'react/react-in-jsx-scope': 'off', 26 | 'import/newline-after-import': 'warn', 27 | 'react-hooks/exhaustive-deps': 'error', 28 | 'react-hooks/rules-of-hooks': 'error', 29 | 'import/order': [ 30 | 'warn', 31 | { 32 | pathGroups: [ 33 | { 34 | pattern: '@/**', 35 | group: 'internal', 36 | position: 'before', 37 | }, 38 | ], 39 | distinctGroup: false, 40 | groups: [ 41 | 'builtin', 42 | 'external', 43 | 'internal', 44 | 'parent', 45 | 'sibling', 46 | 'index', 47 | 'object', 48 | ], 49 | 'newlines-between': 'always', 50 | alphabetize: { 51 | order: 'asc', 52 | caseInsensitive: true, 53 | }, 54 | }, 55 | ], 56 | }, 57 | overrides: [ 58 | { files: '*.js', rules: { '@typescript-eslint/no-var-requires': 'off' } }, 59 | ], 60 | }; 61 | -------------------------------------------------------------------------------- /tooling/eslint/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@antv/config-eslint", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "dependencies": { 6 | "@typescript-eslint/eslint-plugin": "^5.56.0", 7 | "@typescript-eslint/parser": "^5.56.0", 8 | "eslint-config-prettier": "^8.8.0", 9 | "eslint-plugin-import": "^2.27.5", 10 | "eslint-plugin-react": "^7.32.2", 11 | "eslint-plugin-react-hooks": "^4.6.0" 12 | }, 13 | "private": true, 14 | "publishConfig": { 15 | "access": "public" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tooling/jest/config/base.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | module.exports = { 3 | preset: 'ts-jest', 4 | testEnvironment: 'node', 5 | }; 6 | // TODO: doesn't support tsconfig paths yet 7 | -------------------------------------------------------------------------------- /tooling/jest/config/react.js: -------------------------------------------------------------------------------- 1 | /** @type {import('ts-jest').JestConfigWithTsJest} */ 2 | module.exports = { 3 | ...require('./base'), 4 | testEnvironment: 'jsdom', 5 | moduleNameMapper: { 6 | '\\.(css|less|sass|scss)$': 'identity-obj-proxy', 7 | '\\.(gif|ttf|eot|svg)$': 'identity-obj-proxy', 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /tooling/jest/jest.ts: -------------------------------------------------------------------------------- 1 | import 'jest'; 2 | -------------------------------------------------------------------------------- /tooling/jest/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@antv/testing", 3 | "version": "0.0.0", 4 | "dependencies": { 5 | "@testing-library/jest-dom": "^5.16.5", 6 | "@testing-library/react": "^14.0.0", 7 | "@types/jest": "^29.4.0", 8 | "@types/testing-library__jest-dom": "^5.14.5", 9 | "jest-environment-jsdom": "^29.5.0", 10 | "ts-jest": "^29.0.5" 11 | }, 12 | "private": true, 13 | "publishConfig": { 14 | "access": "public" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tooling/jest/react-testing-library.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | 3 | export * from '@testing-library/react'; 4 | -------------------------------------------------------------------------------- /tooling/stylelint/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | 'stylelint-config-standard', 4 | 'stylelint-config-css-modules', 5 | 'stylelint-config-idiomatic-order', 6 | ], 7 | rules: { 8 | // FIXME: these need to be fixed 9 | 'no-descending-specificity': null, 10 | 'selector-class-pattern': null, 11 | }, 12 | overrides: [ 13 | { 14 | files: ['*.less', '**/*.less'], 15 | customSyntax: 'postcss-less', 16 | }, 17 | ], 18 | }; 19 | -------------------------------------------------------------------------------- /tooling/stylelint/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@antv/config-stylelint", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "dependencies": { 6 | "postcss": "^8.0.0", 7 | "postcss-less": "^6.0.0", 8 | "stylelint-config-css-modules": "^4.2.0", 9 | "stylelint-config-idiomatic-order": "^9.0.0", 10 | "stylelint-config-prettier": "^9.0.5", 11 | "stylelint-config-standard": "^30.0.1", 12 | "stylelint-order": "^6.0.3" 13 | }, 14 | "private": true, 15 | "publishConfig": { 16 | "access": "public" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tooling/tsconfig/mana.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "./tsconfig.json", 4 | "display": "Mana", 5 | "compilerOptions": { 6 | "experimentalDecorators": true, 7 | "declaration": true, 8 | "declarationMap": true, 9 | "sourceMap": true 10 | }, 11 | "exclude": ["node_modules"] 12 | } 13 | -------------------------------------------------------------------------------- /tooling/tsconfig/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@antv/config-tsconfig", 3 | "version": "0.0.0", 4 | "private": true, 5 | "publishConfig": { 6 | "access": "public" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tooling/tsconfig/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "allowSyntheticDefaultImports": true, 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "isolatedModules": true, 8 | "jsx": "react", 9 | "lib": ["ESNext", "DOM", "DOM.Iterable"], 10 | "downlevelIteration": true, 11 | "module": "ESNext", 12 | "moduleResolution": "node", 13 | "noImplicitReturns": true, 14 | "noUnusedLocals": true, 15 | "preserveWatchOutput": true, 16 | "resolveJsonModule": true, 17 | "skipLibCheck": true, 18 | "sourceMap": true, 19 | "strict": true, 20 | "target": "ESNext" 21 | }, 22 | "exclude": ["node_modules"] 23 | } 24 | -------------------------------------------------------------------------------- /tooling/tsconfig/umi.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "./tsconfig.json", 4 | "display": "Umi", 5 | "compilerOptions": { 6 | "sourceMap": true 7 | }, 8 | "exclude": ["node_modules", "dist", "src/.umi*"] 9 | } 10 | -------------------------------------------------------------------------------- /tooling/tsup/index.d.ts: -------------------------------------------------------------------------------- 1 | import type { Options } from 'tsup'; 2 | 3 | declare const configFactory: (options: Options) => Options; 4 | 5 | export = configFactory; 6 | -------------------------------------------------------------------------------- /tooling/tsup/index.js: -------------------------------------------------------------------------------- 1 | const { spawn } = require('child_process'); 2 | 3 | const { yellow } = require('colorette'); 4 | const svgr = require('esbuild-plugin-svgr'); 5 | const { defineConfig } = require('tsup'); 6 | 7 | /** 8 | * 9 | * @param {string} outDir 10 | * @returns 11 | */ 12 | const emitDeclarations = (outDir) => 13 | new Promise((resolve, reject) => { 14 | const timer = `${yellow('TSC')} .d.ts generated in`; 15 | console.time(timer); 16 | console.log(yellow('TSC'), 'Generating .d.ts'); 17 | const proc = spawn( 18 | 'tsc', 19 | [ 20 | '--emitDeclarationOnly', 21 | '--declaration', 22 | '--declarationMap', 23 | '--skipLibCheck', 24 | '--declarationDir', 25 | outDir, 26 | ], 27 | { stdio: ['ignore', 'ignore', 'ignore'] }, 28 | ); 29 | proc.on('exit', () => { 30 | console.timeEnd(timer); 31 | resolve(); 32 | }); 33 | proc.on('error', reject); 34 | }); 35 | 36 | module.exports = defineConfig((overrides) => ({ 37 | outDir: 'dist', 38 | format: ['esm', 'cjs'], 39 | outExtension: ({ format }) => ({ js: `.${format}.js` }), 40 | sourcemap: true, 41 | dts: false, 42 | loader: { 43 | '.less': 'copy', 44 | }, 45 | esbuildPlugins: [svgr()], 46 | onSuccess: () => emitDeclarations('./dist/typing'), 47 | clean: overrides.clean || !overrides.watch, 48 | })); 49 | -------------------------------------------------------------------------------- /tooling/tsup/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@antv/config-tsup", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "index.js", 6 | "types": "index.d.ts", 7 | "publishConfig": { 8 | "access": "public" 9 | }, 10 | "dependencies": { 11 | "@swc/core": "^1.3.42", 12 | "colorette": "^2.0.19", 13 | "esbuild-plugin-svgr": "^1.1.0", 14 | "tsup": "^6.7.0" 15 | } 16 | } 17 | --------------------------------------------------------------------------------