Compare commits

..

71 Commits

Author SHA1 Message Date
wwsun fb26540f27 chore(release): publish
- @music163/tango-context@0.2.7
 - @music163/tango-core@0.2.6
 - @music163/tango-designer@0.5.6
 - @music163/tango-helpers@0.1.8
 - @music163/tango-sandbox@0.1.11
 - @music163/tango-setting-form@0.2.10
 - @music163/tango-ui@0.1.11
2023-11-23 15:56:48 +08:00
Wells 558f2cd0a6 fix: refactor update import specifiers (#68)
* fix: refactor update import declaration

* fix: rename module methods

* fix: update types

* fix: update

---------

Co-authored-by: wwsun <ww.sww@outlook.com>
2023-11-23 15:54:22 +08:00
Wells 8a1234e565 fix: prototype to import declaration (#67)
Co-authored-by: wwsun <ww.sww@outlook.com>
2023-11-22 20:00:32 +08:00
wwsun 630c486eb8 chore(release): publish
- @music163/tango-designer@0.5.5
2023-11-20 17:20:18 +08:00
wwsun 324f450851 docs: update timeline 2023-11-20 17:15:59 +08:00
wwsun d6ffb50e85 fix: pass down sandbox props 2023-11-20 17:05:44 +08:00
青岚 2906efc8fc docs: add seal d2c intro (#63) 2023-11-20 17:02:27 +08:00
wwsun 2259fe77fc chore(release): publish
- @music163/tango-context@0.2.6
 - @music163/tango-core@0.2.5
 - @music163/tango-designer@0.5.4
 - @music163/tango-helpers@0.1.7
 - @music163/tango-sandbox@0.1.10
 - @music163/tango-setting-form@0.2.9
 - @music163/tango-ui@0.1.10
2023-11-20 14:04:52 +08:00
Wells dbbd1dddc7 fix: refactor parse expression (#61)
* fix: update interfaces

* test: update

* fix: test if code has expression wrapper

* fix: parse code to expression node

* fix: update long text style in variableTree

---------

Co-authored-by: wwsun <ww.sww@outlook.com>
2023-11-20 14:01:01 +08:00
wwsun 14e17ebbed chore(release): publish
- @music163/tango-context@0.2.5
 - @music163/tango-core@0.2.4
 - @music163/tango-designer@0.5.3
 - @music163/tango-helpers@0.1.6
 - @music163/tango-sandbox@0.1.9
 - @music163/tango-setting-form@0.2.8
 - @music163/tango-ui@0.1.9
2023-11-16 14:35:52 +08:00
Wells 4a06eb20cb refactor: update workspace and viewModule
* docs: update example

* fix: refactor workspace

* fix: refactor viewModule

* fix: update interfaces

* fix: always switch to the view matched code file

* fix: keep sidebar state

* test: update

---------

Co-authored-by: wwsun <ww.sww@outlook.com>
2023-11-16 14:31:10 +08:00
wwsun 089a482f75 fix: parse variables from view 2023-11-14 11:30:45 +08:00
wwsun afb94d6a30 fix: fix sandbox props 2023-11-14 11:10:55 +08:00
wwsun b9403cfec6 fix: isVariableString 2023-11-07 20:20:07 +08:00
wwsun 6cf673e7f6 fix: allow override existing file via addFile 2023-11-07 17:23:02 +08:00
wwsun 653386dcc0 fix: update types 2023-11-06 19:21:35 +08:00
BoBoooooo 208e275bb1 fix: export isJSXElementById from traverse (#54)
* feat: traverse helper add findJSXParentsById func
* fix: update findJSXParentsById def
* feat: export isJSXElementById from traverse
2023-11-06 11:31:33 +08:00
wwsun 77f4145adf chore(release): publish
- @music163/tango-context@0.2.4
 - @music163/tango-core@0.2.3
 - @music163/tango-designer@0.5.2
 - @music163/tango-helpers@0.1.5
 - @music163/tango-sandbox@0.1.8
 - @music163/tango-setting-form@0.2.7
 - @music163/tango-ui@0.1.8
2023-11-02 15:28:40 +08:00
wwsun 11f166a2a6 fix: update style of components panel 2023-11-02 15:26:13 +08:00
wwsun 0452fdb7f1 chore: fix types 2023-11-02 15:17:22 +08:00
wwsun 0068d3426e docs: update 2023-11-02 11:25:22 +08:00
wwsun c5840f29b7 fix: limit outline tree node icon 2023-11-02 11:25:22 +08:00
wwsun 963bfe1ab8 fix(core): logging drop action 2023-11-02 11:21:02 +08:00
wwsun ae806aee35 fix(core): create store without entry file 2023-11-02 11:21:02 +08:00
BoBoooooo b29e23599b fix: outline-panel tree expand bug & add props (#52)
* fix: outline-panel tree expand bug

* feat: collpase-panel add showBottomBorder prop

* feat: outline-panel add props

* fix: rename onPageNodeSelect to onSelect
2023-11-02 11:10:39 +08:00
wwsun f53ce94c6e fix: refactor workspace onFilesChange 2023-10-31 17:24:18 +08:00
BoBoooooo d775a5003a fix: add onFilesChange to workspace (#50)
* feat: workspace add onFileChange event callback

* fix: rename onFileChange to onFilesChange

* revert: sandbox ts def
2023-10-31 17:05:00 +08:00
wwsun 46d5f59b4c fix: update types 2023-10-31 16:54:47 +08:00
wwsun 0c0d95fe58 chore: up 2023-10-31 16:50:18 +08:00
BoBoooooo 9dd11f7a6d fix: designer model add defaultActiveSidebarPanel (#51)
* feat: designer add component support click mode & support set defaultActivePanel

* revert: remove defaultAddComponentType

* revert: remove addComponentType ts def
2023-10-31 16:49:14 +08:00
BoBoooooo fd2fa943b0 fix: resizable-box support horizontal and vertical dragging (#48) 2023-10-31 15:44:03 +08:00
wwsun 70fc6f8030 fix: optional context config 2023-10-25 11:24:18 +08:00
Jiyu Shao 4f7233bd6c chore: fix tsconfig paths (#44) 2023-10-25 11:22:01 +08:00
wwsun 564d50b168 chore(release): publish
- @music163/tango-context@0.2.3
 - @music163/tango-core@0.2.2
 - @music163/tango-designer@0.5.1
 - @music163/tango-helpers@0.1.4
 - @music163/tango-sandbox@0.1.7
 - @music163/tango-setting-form@0.2.6
 - @music163/tango-ui@0.1.7
2023-10-23 12:56:20 +08:00
wwsun 0b3e632745 fix: update interface 2023-10-23 12:11:30 +08:00
wwsun 3f073eeb43 fix: custom store template 2023-10-23 11:35:31 +08:00
wwsun 6bbbe634ad fix: add listImportSources to viewModule 2023-10-23 11:30:29 +08:00
wwsun 8fefa6344f fix: add url validator 2023-10-23 11:12:20 +08:00
Wells 5ebbb428fb fix: enhance expSetter (#43)
* docs: Update

* fix: register history tool

* fix: add eslint to InputCode

* fix: enhance expSetter

* fix: transform value to expression code

* fix: value change of expSetter

* fix: input code without wrapper

* fix: disable eslint for InputCode by default

---------

Co-authored-by: wwsun <ww.sww@outlook.com>
2023-10-20 16:48:49 +08:00
wwsun c240c1824b chore(release): publish
- @music163/tango-context@0.2.2
 - @music163/tango-designer@0.5.0
 - @music163/tango-setting-form@0.2.5
 - @music163/tango-ui@0.1.6
2023-10-12 15:24:21 +08:00
Wells d63517ecb9 fix: refactor expSetter (#41)
* fix: refactor expSetter

* feat: support pass custom variable list

---------

Co-authored-by: wwsun <ww.sww@outlook.com>
2023-10-11 23:51:27 -05:00
Samson Yang 1a17ee1e94 docs: fix typo (#39)
修改错别字
2023-10-09 21:24:01 -05:00
864907600cc e1bab66819 feat: add umd resources config when modifying dependency (#36)
* feat: mark dependency when it's in both package.json and tango.config.json

* fix: remove mark for internal resources

* feat: add resource config when adding dependency

* feat: reuse add dependency form modal when editing dependency

* fix: tidy up

---------

Co-authored-by: ccloli <8115912+ccloli@users.noreply.github.com>
2023-10-09 05:58:05 -05:00
wwsun 89bcd45728 chore(release): publish
- @music163/tango-designer@0.4.1
 - @music163/tango-setting-form@0.2.4
 - @music163/tango-ui@0.1.5
2023-10-09 18:44:15 +08:00
wwsun e86a6d7cd9 docs: update 2023-10-09 18:41:21 +08:00
wwsun 46b54459ce fix: update tabs style 2023-09-25 19:38:04 +08:00
wwsun 85898dee19 fix: always switch to design mode after preview is on 2023-09-25 16:31:47 +08:00
wwsun e429610606 fix: update export entry 2023-09-25 16:01:04 +08:00
wwsun b6814d6484 chore: update script 2023-09-22 17:55:30 +08:00
wwsun 3d7b58f19a chore(release): publish
- @music163/tango-context@0.2.1
 - @music163/tango-core@0.2.1
 - @music163/tango-designer@0.4.0
 - @music163/tango-sandbox@0.1.6
 - @music163/tango-setting-form@0.2.3
2023-09-22 17:52:12 +08:00
wwsun b6a47acdde chore: add husky 2023-09-22 17:48:54 +08:00
wwsun 226d71c704 fix: remove useless api 2023-09-22 17:48:15 +08:00
wwsun 1c72ee6ac2 feat: add global widget map 2023-09-22 17:37:26 +08:00
wwsun 09a1e2135a fix: add listServiceFunctions 2023-09-22 16:42:38 +08:00
wwsun 7999f604fd chore(release): publish
- @music163/tango-context@0.2.0
 - @music163/tango-core@0.2.0
 - @music163/tango-designer@0.3.0
 - @music163/tango-sandbox@0.1.5
 - @music163/tango-setting-form@0.2.2
2023-09-21 13:59:44 +08:00
Wells a7df29c3de feat: support multiple service modules & refactor workspace
* feat: support multiple service modules

* style: add memo

* fix: refactor module

* fix: refactor workspace

---------

Co-authored-by: wwsun <ww.sww@outlook.com>
2023-09-21 13:57:04 +08:00
wwsun 308db9cac8 chore(release): publish
- @music163/tango-context@0.1.4
 - @music163/tango-core@0.1.4
 - @music163/tango-designer@0.2.1
 - @music163/tango-helpers@0.1.3
 - @music163/tango-sandbox@0.1.4
 - @music163/tango-setting-form@0.2.1
 - @music163/tango-ui@0.1.4
2023-09-18 14:33:20 +08:00
Wells a1dbf785a5 Feat/beta (#30)
* chore: up babel

* Revert "chore: up babel"

This reverts commit a64a2afa82.

* chore: use same typescript

* fix: move all setters to designer

* fix: skip pass class properties

* fix: update setters

* docs: update release blog

* chore: fix deps

* fix: rename folder

---------

Co-authored-by: wwsun <ww.sww@outlook.com>
2023-09-18 14:31:20 +08:00
wwsun 813eb83ebb fix: remove useless codes 2023-09-13 17:42:15 +08:00
wwsun 994db5db55 chore(release): publish
- @music163/tango-context@0.1.3
 - @music163/tango-core@0.1.3
 - @music163/tango-designer@0.2.0
 - @music163/tango-sandbox@0.1.3
 - @music163/tango-setting-form@0.2.0
 - @music163/tango-ui@0.1.3
2023-09-13 12:39:47 +08:00
wwsun 15542d9eb2 fix: refactor types 2023-09-13 12:37:21 +08:00
wwsun 4d809e9afd fix: update sidebar 2023-09-13 12:36:23 +08:00
wwsun b786de2f1a fix: refactor setting formItem 2023-09-13 12:36:23 +08:00
wwsun 8d396ff134 feat: refactor setting-form 2023-09-13 12:36:23 +08:00
wwsun 7e9ab12503 fix: remove some useless setters 2023-09-13 12:36:23 +08:00
wwsun 443ae589f6 fix: update exports 2023-09-13 12:36:23 +08:00
BoBoooooo 12de10b0bc fix: nodeTree defaultExpandAll rerender bug (#24)
* fix: fix: nodeTree defaultExpandAll rerender bug
* chore: tsconfig add tango-ui path
2023-09-13 10:20:09 +08:00
BoBoooooo 19d0a4523b fix(core): optimize find jsxElement parent node way (#22) 2023-09-11 19:09:44 +08:00
wwsun b80f5e4c10 chore(release): publish
- @music163/tango-designer@0.1.3
 - @music163/tango-setting-form@0.1.3
2023-09-07 16:42:45 +08:00
wwsun f9e6537dd6 fix: fix lint errors 2023-09-07 16:26:09 +08:00
wwsun 5ecf444fc3 fix: add WorkspaceView 2023-09-07 16:02:26 +08:00
159 changed files with 8517 additions and 8634 deletions
+4 -1
View File
@@ -2,10 +2,13 @@
"extends": ["eslint-config-ali/typescript/react", "prettier"], "extends": ["eslint-config-ali/typescript/react", "prettier"],
"ignorePatterns": ["**/dist/**/*", "**/lib/**/*", "**/node_modules/**/*", "scripts/**/*"], "ignorePatterns": ["**/dist/**/*", "**/lib/**/*", "**/node_modules/**/*", "scripts/**/*"],
"rules": { "rules": {
"@typescript-eslint/consistent-type-definitions": "off",
"@typescript-eslint/dot-notation": "off", "@typescript-eslint/dot-notation": "off",
"@typescript-eslint/no-unused-vars": "warn", "@typescript-eslint/no-unused-vars": "warn",
"import/no-cycle": "off", "import/no-cycle": "off",
"no-nested-ternary": "off", "no-nested-ternary": "off",
"no-useless-return":"off" "no-useless-return": "off",
"no-param-reassign": "off",
"prefer-destructuring": "off"
} }
} }
+4
View File
@@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
yarn commitlint --edit "$1"
+4
View File
@@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
yarn lint-staged --allow-empty
+14 -6
View File
@@ -5,7 +5,7 @@
<h1 align="center">Tango LowCode Builder</h1> <h1 align="center">Tango LowCode Builder</h1>
<div align="center"> <div align="center">
A source code based low-code builder, developed by NetEase Cloud Music Develop Team. A source code based low-code builder.
[![license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/NetEase/tango/blob/main/LICENSE) [![license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/NetEase/tango/blob/main/LICENSE)
[![NPM version](https://img.shields.io/npm/v/@music163/tango-designer.svg?style=flat-square)](http://npmjs.org/package/@music163/tango-designer) [![NPM version](https://img.shields.io/npm/v/@music163/tango-designer.svg?style=flat-square)](http://npmjs.org/package/@music163/tango-designer)
@@ -79,6 +79,7 @@ yarn start
Join NetEase Tango Community to share your ideas, suggestions, or questions and connect with other users and contributors. Join NetEase Tango Community to share your ideas, suggestions, or questions and connect with other users and contributors.
- Discord: <https://discord.gg/B6hkGTe4Rz> - Discord: <https://discord.gg/B6hkGTe4Rz>
- [Usage Trends](https://npm-compare.com/@music163/tango-helpers,@music163/tango-context,@music163/tango-core,@music163/tango-setting-form,@music163/tango-sandbox,@music163/tango-ui,@music163/tango-designer)
## 🤝 Contributing ## 🤝 Contributing
@@ -89,16 +90,23 @@ Please read the [github contribution guide](https://docs.github.com/en/get-start
- Commit and push your code - Commit and push your code
- Open a Pull Request - Open a Pull Request
## 📈 Usage Trends
[Usage Trends of Tango Packages](https://npm-compare.com/@music163/tango-helpers,@music163/tango-context,@music163/tango-core,@music163/tango-setting-form,@music163/tango-sandbox,@music163/tango-ui,@music163/tango-designer)
## 💗 Acknowledgments ## 💗 Acknowledgments
Thanks to the NetEase Cloud Music Front-end team, Public Technology team, Live Broadcasting Technology team, and all the colleagues who participated in the Tango project. Thanks to the NetEase Cloud Music Front-end team, Public Technology team, Live Broadcasting Technology team, and all the developers who participated in the Tango project.
Thank you to CodeSandbox for providing the [Sandpack](https://sandpack.codesandbox.io/) project, which provides powerful online code execution capabilities for Tango. Thank you to CodeSandbox for providing the [Sandpack](https://sandpack.codesandbox.io/) project, which provides powerful online code execution capabilities for Tango.
## 📣 Product Promotion
![](https://p5.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/31629770956/da9e/3a74/4e00/7c69cf46a713f1b008bd1243b5b1ab1c.png)
Don't waste your time restoring the UI, try the NetEase Cloud Music "Seal D2C" development tool! Easily turn your design into code, support React, RN, Vue, WeChat apps and other multi-end scenarios, what you see is what you get!
Experience "Seal D2C" now:
- I'm a Figma user: <https://www.figma.com/community/plugin/1174548852019950797/seal-figma-to-code-d2c/>
- I'm a MasterGo user: <https://mastergo.com/community/plugin/98956774428196/>
## 📄 License ## 📄 License
This project is licensed under the terms of the [MIT license](./LICENSE) This project is licensed under the terms of the [MIT license](./LICENSE)
+14 -6
View File
@@ -5,7 +5,7 @@
<h1 align="center">Tango 低代码设计器</h1> <h1 align="center">Tango 低代码设计器</h1>
<div align="center"> <div align="center">
一个源码驱动的低代码设计器,来自网易云音乐技术团队 一个源码驱动的低代码设计器框架
[![license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/NetEase/tango/blob/main/LICENSE) [![license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/NetEase/tango/blob/main/LICENSE)
[![NPM version](https://img.shields.io/npm/v/@music163/tango-designer.svg?style=flat-square)](http://npmjs.org/package/@music163/tango-designer) [![NPM version](https://img.shields.io/npm/v/@music163/tango-designer.svg?style=flat-square)](http://npmjs.org/package/@music163/tango-designer)
@@ -79,6 +79,7 @@ yarn start
参与 NetEase Tango 的社区,以分享您的想法、建议或问题,并与其他用户和贡献者建立联系。 参与 NetEase Tango 的社区,以分享您的想法、建议或问题,并与其他用户和贡献者建立联系。
- Discord: <https://discord.gg/B6hkGTe4Rz> - Discord: <https://discord.gg/B6hkGTe4Rz>
- [使用趋势](https://npm-compare.com/@music163/tango-helpers,@music163/tango-context,@music163/tango-core,@music163/tango-setting-form,@music163/tango-sandbox,@music163/tango-ui,@music163/tango-designer)
## 🤝 参与共建 ## 🤝 参与共建
@@ -90,16 +91,23 @@ yarn start
- 合并修改 git rebase master - 合并修改 git rebase master
- 发起 Pull Request - 发起 Pull Request
## 📈 使用量趋势
[Tango 使用量趋势](https://npm-compare.com/@music163/tango-helpers,@music163/tango-context,@music163/tango-core,@music163/tango-setting-form,@music163/tango-sandbox,@music163/tango-ui,@music163/tango-designer)
## 💗 致谢 ## 💗 致谢
感谢网易云音乐公共技术团队,大前端团队,直播技术团队,以及所有参与过 Tango 项目的同学们 感谢网易云音乐公共技术团队,大前端团队,直播技术团队,以及所有参与过 Tango 项目的开发者
感谢 CodeSandbox 提供的 [Sandpack](https://sandpack.codesandbox.io/) 项目,为 Tango 提供了强大的基于浏览器的代码构建与执行能力。 感谢 CodeSandbox 提供的 [Sandpack](https://sandpack.codesandbox.io/) 项目,为 Tango 提供了强大的基于浏览器的代码构建与执行能力。
## 📣 产品推广
![](https://p5.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/31629770956/da9e/3a74/4e00/7c69cf46a713f1b008bd1243b5b1ab1c.png)
不要再浪费时间还原 UI 啦,快来试试网易云音乐「海豹 D2C」研发工具吧!轻松将设计稿转为代码,支持 React、RN、Vue、微信小程序等多端场景,所见即所得!
立即体验「海豹 D2C」:
- 我是 Figma 用户:<https://www.figma.com/community/plugin/1174548852019950797/seal-figma-to-code-d2c/>
- 我是 MasterGo 用户:<https://mastergo.com/community/plugin/98956774428196/>
## 📄 开源协议 ## 📄 开源协议
此项目遵循 [MIT 开源协议](./LICENSE) 此项目遵循 [MIT 开源协议](./LICENSE)
+1 -1
View File
@@ -14,7 +14,7 @@ export default {
], ],
devServer: { devServer: {
host: 'local.netease.com', host: 'local.netease.com',
port: 7007, port: 8008,
https: { https: {
key: path.resolve(__dirname, 'local.netease.com-key.pem'), key: path.resolve(__dirname, 'local.netease.com-key.pem'),
cert: path.resolve(__dirname, 'local.netease.com.pem'), cert: path.resolve(__dirname, 'local.netease.com.pem'),
+1 -1
View File
@@ -10,7 +10,7 @@
], ],
"scripts": { "scripts": {
"build": "COMPRESS=none umi build", "build": "COMPRESS=none umi build",
"start": "HTTPS=1 umi dev" "start": "umi dev"
}, },
"dependencies": { "dependencies": {
"@ant-design/icons": "^4.8.0", "@ant-design/icons": "^4.8.0",
@@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { Row, Switch } from 'antd'; import { Row, Switch } from 'antd';
import { observer } from '@music163/tango-designer'; import { observer } from '@music163/tango-context';
const OtherPanel = observer(({ autoRemove, setAutoRemove }: any) => { const OtherPanel = observer(({ autoRemove, setAutoRemove }: any) => {
const onChange = (checked: boolean) => { const onChange = (checked: boolean) => {
+42 -18
View File
@@ -2,7 +2,7 @@ const packageJson = {
name: 'demo', name: 'demo',
private: true, private: true,
dependencies: { dependencies: {
'@music163/antd': '0.1.5', '@music163/antd': '0.1.6',
'@music163/tango-boot': '0.1.3', '@music163/tango-boot': '0.1.3',
react: '17.0.2', react: '17.0.2',
'react-dom': '17.0.2', 'react-dom': '17.0.2',
@@ -11,59 +11,65 @@ const packageJson = {
}, },
}; };
const tangoJson = { const tangoConfigJson = {
packages: { packages: {
react: { react: {
version: '17.0.2', version: '17.0.2',
library: 'React', library: 'React',
type: 'dependency', type: 'dependency',
resources: ['https://unpkg.com/react@{{version}}/umd/react.development.js'], resources: ['https://cdn.jsdelivr.net/npm/react@{{version}}/umd/react.development.js'],
}, },
'react-dom': { 'react-dom': {
version: '17.0.2', version: '17.0.2',
library: 'ReactDOM', library: 'ReactDOM',
type: 'dependency', type: 'dependency',
resources: ['https://unpkg.com/react-dom@{{version}}/umd/react-dom.development.js'], resources: [
'https://cdn.jsdelivr.net/npm/react-dom@{{version}}/umd/react-dom.development.js',
],
}, },
'react-is': { 'react-is': {
version: '16.13.1', version: '16.13.1',
library: 'ReactIs', library: 'ReactIs',
type: 'dependency', type: 'dependency',
resources: ['https://unpkg.com/react-is@{{version}}/umd/react-is.production.min.js'], resources: [
'https://cdn.jsdelivr.net/npm/react-is@{{version}}/umd/react-is.production.min.js',
],
}, },
'styled-components': { 'styled-components': {
version: '5.3.5', version: '5.3.5',
library: 'styled', library: 'styled',
type: 'dependency', type: 'dependency',
resources: ['https://unpkg.com/styled-components@{{version}}/dist/styled-components.min.js'], resources: [
'https://cdn.jsdelivr.net/npm/styled-components@{{version}}/dist/styled-components.min.js',
],
}, },
moment: { moment: {
version: '2.29.4', version: '2.29.4',
library: 'moment', library: 'moment',
type: 'dependency', type: 'dependency',
resources: ['https://unpkg.com/moment@{{version}}/moment.js'], resources: ['https://cdn.jsdelivr.net/npm/moment@{{version}}/moment.js'],
}, },
'@music163/tango-boot': { '@music163/tango-boot': {
version: '0.1.3', version: '0.2.1',
library: 'TangoBoot', library: 'TangoBoot',
type: 'baseDependency', type: 'baseDependency',
resources: ['https://unpkg.com/@music163/tango-boot@{{version}}/dist/boot.js'], resources: ['https://cdn.jsdelivr.net/npm/@music163/tango-boot@{{version}}/dist/boot.js'],
// resources: ['http://localhost:9001/boot.js'], // resources: ['http://localhost:9001/boot.js'],
description: '云音乐低代码运行时框架', description: '云音乐低代码运行时框架',
}, },
'@music163/antd': { '@music163/antd': {
version: '0.1.5', version: '0.1.7',
library: 'TangoAntd', library: 'TangoAntd',
type: 'baseDependency', type: 'baseDependency',
resources: [ resources: [
'https://unpkg.com/@music163/antd@{{version}}/dist/index.js', 'https://cdn.jsdelivr.net/npm/@music163/antd@{{version}}/dist/index.js',
'https://unpkg.com/antd@4.24.13/dist/antd.css', 'https://cdn.jsdelivr.net/npm/antd@4.24.13/dist/antd.css',
], ],
description: '云音乐低代码中后台应用基础物料', description: '云音乐低代码中后台应用基础物料',
designerResources: [ designerResources: [
'https://unpkg.com/@music163/antd@{{version}}/dist/designer.js', 'https://cdn.jsdelivr.net/npm/@music163/antd@{{version}}/dist/designer.js',
// 'http://localhost:9002/designer.js', // 'http://localhost:9002/designer.js',
'https://unpkg.com/antd@4.24.13/dist/antd.css', 'https://cdn.jsdelivr.net/npm/antd@4.24.13/dist/antd.css',
], ],
}, },
}, },
@@ -145,15 +151,18 @@ import {
Input, Input,
FormilyForm, FormilyForm,
} from "@music163/antd"; } from "@music163/antd";
import { Space } from '@music163/antd';
class App extends React.Component { class App extends React.Component {
render() { render() {
return ( return (
<Page title="页面标题"> <Page title={tango.stores.app.title}>
<Section> <Section title="Section Title">
</Section> </Section>
<Section> <Section>
<Space>
<Button>button</Button> <Button>button</Button>
<Input /> <Input />
</Space>
</Section> </Section>
</Page> </Page>
); );
@@ -167,7 +176,7 @@ import { defineStore } from '@music163/tango-boot';
export default defineStore({ export default defineStore({
title: 'hello world', title: 'Page Title',
array: [1, 2, 3], array: [1, 2, 3],
}, 'app'); }, 'app');
@@ -175,6 +184,7 @@ export default defineStore({
const serviceCode = ` const serviceCode = `
import { defineServices } from '@music163/tango-boot'; import { defineServices } from '@music163/tango-boot';
import './sub';
export default defineServices({ export default defineServices({
get: { get: {
@@ -201,6 +211,18 @@ export default defineServices({
}); });
`; `;
const subServiceCode = `
import { defineServices } from '@music163/tango-boot';
export default defineServices({
list: {
url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/anchor-list-normal',
},
}, {
namespace: 'sub',
});
`;
const lessCode = ` const lessCode = `
body { body {
font-size: 12px; font-size: 12px;
@@ -221,7 +243,7 @@ p {
export const sampleFiles = [ export const sampleFiles = [
{ filename: '/package.json', code: JSON.stringify(packageJson) }, { filename: '/package.json', code: JSON.stringify(packageJson) },
{ filename: '/appJson.json', code: JSON.stringify(appJson) }, { filename: '/appJson.json', code: JSON.stringify(appJson) },
{ filename: '/tango.config.json', code: JSON.stringify(tangoJson) }, { filename: '/tango.config.json', code: JSON.stringify(tangoConfigJson) },
{ filename: '/README.md', code: '# readme' }, { filename: '/README.md', code: '# readme' },
{ filename: '/src/index.less', code: lessCode }, { filename: '/src/index.less', code: lessCode },
{ filename: '/src/style.css', code: cssCode }, { filename: '/src/style.css', code: cssCode },
@@ -232,6 +254,8 @@ export const sampleFiles = [
{ filename: '/src/stores/app.js', code: storeApp }, { filename: '/src/stores/app.js', code: storeApp },
{ filename: '/src/stores/counter.js', code: storeCounter }, { filename: '/src/stores/counter.js', code: storeCounter },
{ filename: '/src/services/index.js', code: serviceCode }, { filename: '/src/services/index.js', code: serviceCode },
{ filename: '/src/services/sub.js', code: subServiceCode },
{ filename: '/src/utils/index.js', code: `export function foo() {}` },
]; ];
export const genDefaultPage = (index: number) => ({ export const genDefaultPage = (index: number) => ({
+9 -9
View File
@@ -8,14 +8,14 @@
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
<script src="https://unpkg.com/react@16.14.0/umd/react.development.js"></script> <script src="https://cdn.jsdelivr.net/npm/react@16.14.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16.14.0/umd/react-dom.development.js"></script> <script src="https://cdn.jsdelivr.net/npm/react-dom@16.14.0/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/react-is@16.13.1/umd/react-is.production.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/react-is@16.13.1/umd/react-is.production.min.js"></script>
<script src="https://unpkg.com/moment/min/moment-with-locales.js"></script> <script src="https://cdn.jsdelivr.net/npm/moment/min/moment-with-locales.js"></script>
<script src="https://unpkg.com/styled-components@5.3.11/dist/styled-components.js"></script> <script src="https://cdn.jsdelivr.net/npm/styled-components@5.3.11/dist/styled-components.js"></script>
<script src="https://unpkg.com/@babel/standalone@7.15.6/babel.js"></script> <script src="https://cdn.jsdelivr.net/npm/@babel/standalone@7.15.6/babel.js"></script>
<script src="https://unpkg.com/prettier@2.6.0/standalone.js"></script> <script src="https://cdn.jsdelivr.net/npm/prettier@2.6.0/standalone.js"></script>
<script src="https://unpkg.com/prettier@2.6.0/parser-babel.js"></script> <script src="https://cdn.jsdelivr.net/npm/prettier@2.6.0/parser-babel.js"></script>
<script src="https://unpkg.com/antd@4.24.13/dist/antd-with-locales.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/antd@4.24.13/dist/antd-with-locales.min.js"></script>
</body> </body>
</html> </html>
+51 -18
View File
@@ -1,4 +1,6 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Box } from 'coral-system';
import { Button, Space } from 'antd';
import { import {
Designer, Designer,
DesignerPanel, DesignerPanel,
@@ -6,18 +8,22 @@ import {
Sidebar, Sidebar,
Toolbar, Toolbar,
WorkspacePanel, WorkspacePanel,
ComponentsPanel, WorkspaceView,
ViewPanel,
CodeEditor, CodeEditor,
Sandbox, Sandbox,
DndQuery, DndQuery,
} from '@music163/tango-designer'; } from '@music163/tango-designer';
import { createEngine, Workspace } from '@music163/tango-core'; import { createEngine, Workspace } from '@music163/tango-core';
import { Logo, ProjectDetail } from './share'; import { Logo, ProjectDetail, bootHelperVariables } from './share';
import { sampleFiles } from '../mock/project'; import { sampleFiles } from '../mock/project';
import './index.less'; import './index.less';
import { Box } from 'coral-system'; import {
import { Button, Space } from 'antd'; ApiOutlined,
AppstoreAddOutlined,
BuildOutlined,
ClusterOutlined,
FunctionOutlined,
} from '@ant-design/icons';
// 1. 实例化工作区 // 1. 实例化工作区
const workspace = new Workspace({ const workspace = new Workspace({
@@ -28,6 +34,7 @@ const workspace = new Workspace({
// 2. 引擎初始化 // 2. 引擎初始化
const engine = createEngine({ const engine = createEngine({
workspace, workspace,
defaultActiveSidebarPanel: 'outline',
}); });
// @ts-ignore // @ts-ignore
@@ -44,7 +51,14 @@ export default function App() {
const [menuLoading, setMenuLoading] = useState(true); const [menuLoading, setMenuLoading] = useState(true);
const [menuData, setMenuData] = useState(false); const [menuData, setMenuData] = useState(false);
return ( return (
<Designer engine={engine} sandboxQuery={sandboxQuery}> <Designer
engine={engine}
config={{
customActionVariables: bootHelperVariables,
customExpressionVariables: bootHelperVariables,
}}
sandboxQuery={sandboxQuery}
>
<DesignerPanel <DesignerPanel
logo={<Logo />} logo={<Logo />}
description={<ProjectDetail />} description={<ProjectDetail />}
@@ -52,10 +66,11 @@ export default function App() {
<Box px="l"> <Box px="l">
<Toolbar> <Toolbar>
<Toolbar.Item key="routeSwitch" placement="left" /> <Toolbar.Item key="routeSwitch" placement="left" />
<Toolbar.Item key="preview" placement="left" />
<Toolbar.Item key="modeSwitch" placement="right" /> <Toolbar.Item key="modeSwitch" placement="right" />
<Toolbar.Item key="togglePanel" placement="right" /> <Toolbar.Item key="togglePanel" placement="right" />
<Toolbar.Separator /> <Toolbar.Separator />
<Toolbar.Item key="extra" placement="right"> <Toolbar.Item placement="right">
<Space> <Space>
<Button type="primary"></Button> <Button type="primary"></Button>
</Space> </Space>
@@ -65,16 +80,34 @@ export default function App() {
} }
> >
<Sidebar> <Sidebar>
<Sidebar.Item key="outline" /> <Sidebar.Item key="outline" label="结构" icon={<BuildOutlined />} />
<Sidebar.Item key="components"> <Sidebar.Item
<ComponentsPanel menuData={menuData as any} loading={menuLoading} /> key="components"
</Sidebar.Item> label="组件"
<Sidebar.Item key="variables" isFloat width={800} /> icon={<AppstoreAddOutlined />}
<Sidebar.Item key="dataSource" isFloat width={800} /> widgetProps={{
<Sidebar.Item key="dependency" isFloat width={800} /> menuData: menuData as any,
loading: menuLoading,
}}
/>
<Sidebar.Item
key="variables"
label="变量"
icon={<FunctionOutlined />}
isFloat
width={800}
/>
<Sidebar.Item key="dataSource" label="接口" icon={<ApiOutlined />} isFloat width={800} />
<Sidebar.Item
key="dependency"
label="依赖"
icon={<ClusterOutlined />}
isFloat
width={800}
/>
</Sidebar> </Sidebar>
<WorkspacePanel> <WorkspacePanel>
<ViewPanel mode="design"> <WorkspaceView mode="design">
<Sandbox <Sandbox
onMessage={(e) => { onMessage={(e) => {
if (e.type === 'done') { if (e.type === 'done') {
@@ -91,10 +124,10 @@ export default function App() {
} }
}} }}
/> />
</ViewPanel> </WorkspaceView>
<ViewPanel mode="code"> <WorkspaceView mode="code">
<CodeEditor /> <CodeEditor />
</ViewPanel> </WorkspaceView>
</WorkspacePanel> </WorkspacePanel>
<SettingPanel /> <SettingPanel />
</DesignerPanel> </DesignerPanel>
+31 -1
View File
@@ -6,6 +6,36 @@ import { registerSetter } from '@music163/tango-designer';
import type { ComponentPrototypeType } from '@music163/tango-helpers'; import type { ComponentPrototypeType } from '@music163/tango-helpers';
import { FooSetter } from '../components'; import { FooSetter } from '../components';
export const bootHelperVariables = [
{
key: '$helpers',
title: '工具函数',
children: [
{
title: 'setStoreValue',
key: '() => tango.setStoreValue("variableName", "variableValue")',
type: 'function',
},
{
title: 'getStoreValue',
key: '() => tango.getStoreValue("variableName")',
type: 'function',
},
{ title: 'openModal', key: '() => tango.openModal("")', type: 'function' },
{ title: 'closeModal', key: '() => tango.closeModal("")', type: 'function' },
{ title: 'navigateTo', key: '() => tango.navigateTo("/")', type: 'function' },
{ title: 'showToast', key: '() => tango.showToast("hello")', type: 'function' },
{ title: 'formatDate', key: '() => tango.formatDate("2022-12-12")', type: 'function' },
{ title: 'formatNumber', key: '() => tango.formatDate(9999)', type: 'function' },
{
title: 'copyToClipboard',
key: '() => tango.copyToClipboard("hello")',
type: 'function',
},
],
},
];
// folder-name // folder-name
// 物料列表定义 // 物料列表定义
const bizToggleButtonPrototype: ComponentPrototypeType = { const bizToggleButtonPrototype: ComponentPrototypeType = {
@@ -133,7 +163,7 @@ export function ProjectDetail() {
return ( return (
<Box display="flex" alignItems="center" columnGap="l"> <Box display="flex" alignItems="center" columnGap="l">
<Box className="ProjectName" fontSize="18px" fontWeight="bold"> <Box className="ProjectName" fontSize="18px" fontWeight="bold">
lc-online-test community-test
</Box> </Box>
<Box className="BranchName" as="code" fontSize="14px"> <Box className="BranchName" as="code" fontSize="14px">
<BranchesOutlined /> feature/list <BranchesOutlined /> feature/list
+70
View File
@@ -0,0 +1,70 @@
import React from 'react';
import { Box } from 'coral-system';
import { CodeSandbox } from '@music163/tango-sandbox';
import { JsonView } from '@music163/tango-ui';
export default {
title: 'CodeSandbox',
};
const entryFilename = '/index.js';
const packageJson = {
name: 'demo',
private: true,
main: entryFilename,
dependencies: {
react: '^17.0.1',
'react-dom': '^17.0.1',
},
};
const entryFile = `
import React from 'react';
import ReactDOM from "react-dom";
import App from './src';
const rootElement = document.getElementById("root");
ReactDOM.render(
<App />,
rootElement
);
`;
const srcAppFile = `
import React from 'react';
export default function App() {
return <div style={{ background: 'yellow', color: 'red' }}>hello world</div>;
}
`;
const files = {
'/package.json': {
code: JSON.stringify(packageJson),
},
[entryFilename]: {
code: entryFile,
},
'/src/index.js': { code: srcAppFile },
};
export function Basic() {
return (
<Box display="flex" height="100vh">
<CodeSandbox
template="create-react-app"
bundlerURL="https://codesandbox.fn.netease.com"
files={files}
entry={entryFilename}
style={{
flex: 1,
}}
/>
<Box width="30%">
<JsonView src={files} />
</Box>
</Box>
);
}
+10 -1
View File
@@ -10,11 +10,20 @@
项目使用的 npm 包依赖信息,用于定义依赖的类型以及外置依赖资源。该配置项为一个 map,key 为对应的 npm 包名,value 为该依赖的配置信息。 项目使用的 npm 包依赖信息,用于定义依赖的类型以及外置依赖资源。该配置项为一个 map,key 为对应的 npm 包名,value 为该依赖的配置信息。
- version 包的版本号,必选
- library UMD 包中的全局变量名,必选
- type 依赖的类型
- baseDependency 基础依赖包
- bizDependency 业务组件依赖包
- dependency 其他依赖包
- resources 外置依赖资源列表
- designerResources 在设计器视图载入的外置资源列表,用来需要区别加载时使用
- description 描述信息
依赖的类型 `type` 可以是 `baseDependency` 即项目的基础依赖,用于获取项目基础物料列表的信息;也可以是 `bizDependency` 即业务组件依赖,用于获取合并到物料面板业务组件菜单中的业务组件列表;也可以是 `depenency` 即项目的其他依赖,只用于提供 external 信息与版本信息等,不会和物料面板产生关系。 依赖的类型 `type` 可以是 `baseDependency` 即项目的基础依赖,用于获取项目基础物料列表的信息;也可以是 `bizDependency` 即业务组件依赖,用于获取合并到物料面板业务组件菜单中的业务组件列表;也可以是 `depenency` 即项目的其他依赖,只用于提供 external 信息与版本信息等,不会和物料面板产生关系。
当配置了外置依赖资源 `resources` 与其对应的全局变量 `library` 时,当这些依赖被 import 时,相关资源不会被打包到 bundle 中,而是在运行时再去外部获取这些扩展依赖,其等效于之前的 `sandbox.externalResources` 与 `sandbox.externals` 配置项。此外,可以将 `resource` 中的版本号替换为 `{{version}}`,其会在平台上构建时替换为版本号。特殊的,可以定义 `designerResources` 用于适配平台的设计器视图。 当配置了外置依赖资源 `resources` 与其对应的全局变量 `library` 时,当这些依赖被 import 时,相关资源不会被打包到 bundle 中,而是在运行时再去外部获取这些扩展依赖,其等效于之前的 `sandbox.externalResources` 与 `sandbox.externals` 配置项。此外,可以将 `resource` 中的版本号替换为 `{{version}}`,其会在平台上构建时替换为版本号。特殊的,可以定义 `designerResources` 用于适配平台的设计器视图。
```json ```json
{ {
"react": { "react": {
+6 -2
View File
@@ -2,10 +2,10 @@ import Link from '@docusaurus/Link';
# 快速开始 # 快速开始
Tango 是一个用于快速构建低代码工具的前端框架借助 Tango 只需要少量代码就可以快速的搭建一个基本的低代码设计器。本文将会帮助你借助 Tango 实现一个基本的低代码可视化设计器 Tango 是一个用于快速构建低代码平台的低代码设计器框架借助 Tango 只需要数行代码就可以完成一个基本的低代码平台前端系统的搭建
:::warning 测试版免责声明 :::warning 测试版免责声明
基于 Tango 低代码引擎的低代码平台已经在网易云音乐内部服务了内部众多业务开发场景。由于开源工作涉及到大量的代码分离工作,当前开源版本为低代码引擎的 alpha 演示版本,API 可能不稳定,不建议在生产环境中使用。我们预计在 2023 年 Q4 发布可以运行在生产环境的正式版。 当前开源版本为低代码引擎的 beta 演示版本,API 可能不稳定,不建议在生产环境中使用。为了保障社区版本能够有效与内部版本解藕,并且项目开源涉及到大量的额外的代码分离工作,我们预计在 2023 年 Q4 发布可以运行在生产环境的正式版。
::: :::
## 环境要求 ## 环境要求
@@ -34,6 +34,10 @@ Tango 是一个用于快速构建低代码工具的前端框架。借助 Tango
本地开发除了需要启动 Tango 的 playground 外,还需要一个可用的沙箱环境来承载设计器。 本地开发除了需要启动 Tango 的 playground 外,还需要一个可用的沙箱环境来承载设计器。
:::tip
沙箱部分来源于 codesandbox,不属于 tango 的核心代码库,我们在原有的 codesandbox 基础上进行了一系列的改造
:::
如果没有可用的在线沙箱,可以通过本地启动一个沙箱来完成开发;如果已有一个在线沙箱,且沙箱与本地的 playground 共用一级域名,请直接跳至第 7 步修改 `bundlerURL` 为在线沙箱的地址。 如果没有可用的在线沙箱,可以通过本地启动一个沙箱来完成开发;如果已有一个在线沙箱,且沙箱与本地的 playground 共用一级域名,请直接跳至第 7 步修改 `bundlerURL` 为在线沙箱的地址。
1. 克隆仓库至本地 1. 克隆仓库至本地
+17 -23
View File
@@ -2,36 +2,30 @@ import Link from '@docusaurus/Link';
# 简介 # 简介
Tango 是一个用于快速构建低代码平台的低代码设计器框架,借助 Tango 只需要数行代码就可以完成一个基本的低代码平台前端系统的搭建。Tango 低代码设计器**直接读取前端项目的源代码,并以源代码为中心,执行和渲染前端视图,并为用户提供低代码可视化搭建能力,用户的搭建操作会转为对代码的修改**。借助 Tango 可以实现 **源码进,源码出**的效果。 Tango 是一个用于快速构建低代码平台的低代码设计器框架,借助 Tango 只需要数行代码就可以完成一个基本的低代码平台前端系统的搭建。Tango 低代码设计器直接读取前端项目的源代码,并以源代码为中心,执行和渲染前端视图,并为用户提供低代码可视化搭建能力,用户的搭建操作会转为对代码的修改。借助 Tango 构建的低代码工具或平台,可以实现 源码进,源码出的效果,无缝与企业内部现有的研发体系进行集成
## 引擎架构概览 <img src="https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/30108735057/7ba9/dced/9ac3/420f6e04b371dd47de06e7d71142560d.gif" alt="preview" />
Tango 低代码引擎在实现上进行了分层解藕,使得上层的低代码平台与底层的低代码引擎可以独立开发和维护,快速集成部署。另一方面,Tango 低代码引擎定义了一套开放的物料生态体系,开发者可以自由的贡献扩展组件配置能力的属性设置器,以及扩展低代码物料的二方三方业务组件。 ## 核心特性
如下图所示: - 经历网易云音乐内网生产环境的实际检验,可灵活集成应用于低代码平台,本地开发工具等
- 基于源码 AST 驱动,无私有 DSL 和协议
- 提供实时出码能力,支持源码进,源码出
- 开箱即用的前端低代码设计器,提供灵活易用的设计器 React 组件
- 使用 TypeScript 开发,提供完整的类型定义文件
<img src="https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/18236990116/aabf/8a16/28ee/f0fd29a84c7aa40bb26383cdef12b88c.png" /> ## 实现原理
## 引擎设计器构成概览 Tango 低代码引擎不依赖私有搭建协议和 DSL,而是直接使用源代码驱动,引擎内部将源码转为 AST,用户的所有的搭建操作转为对 AST 的遍历和修改,进而将 AST 重新生成为代码,将代码同步给在线沙箱执行。具体的过程如下图所示,首先将源代码解析为 AST。用户的拖拉拽等操作则映射为对 AST 的遍历和修改。最后将新的 AST 重新生成代码,交给设计器沙箱去渲染执行。而对 AST 的解析、遍历、修改、生成,则可以借助大量的社区工具,这里我们选择的是 babel!
Tango 低代码引擎设计器用于低成本初始化一个基本的低代码平台,其前端部分主要包括如下几个部分: ![Tango 低代码引擎实现分析](https://p5.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/30578073085/61cd/b2db/e103/9ed9dd334a6679c6ec18a02270efe446.png)
- 设计器主框架:进行外框架初始化,状态管理,拖拽引擎绑定等核心逻辑。 ## 技术架构
- 侧边栏面板:提供可扩展的侧边面板,除了内置通用的面板,用户还可以任意添加自定义的面板。
- 属性设置面板:属性设置器面板,支持用户进行属性配置。
- 运行时沙箱:用于设计器运行时视图的渲染。
- Web IDE:用于进行在线源代码编辑。
<img src="https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/30108642346/b8cf/e86d/ef5a/514d90b722b5d8dc0e18516ed594a07b.png" /> Tango 低代码引擎主要包括 3 个核心组成部分,分别是:
## 基于源代码的引擎内核 - 引擎内核:扶额建立文件,节点模型,提供输入输出能力。
- 拖拽引擎和可视化面板:提供可视化开发能力
- 渲染沙箱:提供源码在浏览器上的编译执行能力。
Tango 低代码引擎不依赖私有搭建协议和 DSL,而是直接使用源代码驱动,引擎内部将源码转为 AST,用户的所有的搭建操作转为对 AST 的遍历和修改,进而将 AST 重新生成为代码,将代码同步给在线沙箱执行。与传统的借助 [Schema 驱动的低代码方案](https://mp.weixin.qq.com/s/yqYey76qLGYPfDtpGkVFfA) 相比,不受到私有 DSL 和协议的限制,能够完美的实现低代码搭建与源码开发的无缝集成。 ![引擎构成](https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/30579167082/1404/27e2/b8e5/0c719ca82494a282080d73adeff7196e.png)
<img src="https://p5.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/13140534982/ee2e/f42c/cc9a/184e2918a011b57d46e6c64a2722fa44.png" />
## 源码进,源码出
由于引擎内核完全基于源代码驱动实现,Tango 低代码引擎能够实现源代码进,源代码出的可视化搭建能力,不提供任何私有的中间产物。从而使得,Tango 构建的线上研发能力可以与团队现有的研发服务(代码托管、构建、部署、CDN)无缝衔接与集成。
<img src="https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/13208022400/b809/b82e/77b0/5e4fe78a8f11c8ed89c9ec9ced43e845.png" />
+15 -2
View File
@@ -80,7 +80,7 @@ const config = {
type: 'docSidebar', type: 'docSidebar',
sidebarId: 'designer', sidebarId: 'designer',
position: 'left', position: 'left',
label: '引擎文档', label: '文档',
}, },
{ {
type: 'docSidebar', type: 'docSidebar',
@@ -137,7 +137,7 @@ const config = {
], ],
}, },
{ {
title: '更多', title: '相关资源',
items: [ items: [
{ {
label: '博客', label: '博客',
@@ -149,6 +149,19 @@ const config = {
}, },
], ],
}, },
{
title: '更多产品',
items: [
{
label: '海豹 D2C - Figma 插件',
to: 'https://www.figma.com/community/plugin/1174548852019950797/seal-figma-to-code-d2c',
},
{
label: '海豹 D2C - MasterGo 插件',
to: 'https://mastergo.com/community/plugin/98956774428196/',
},
],
},
], ],
logo: { logo: {
alt: 'NetEase Cloud Music', alt: 'NetEase Cloud Music',
Binary file not shown.

Before

Width:  |  Height:  |  Size: 297 KiB

After

Width:  |  Height:  |  Size: 279 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

@@ -6,7 +6,7 @@ tags: [netease, tango, open-source]
## 📝 Introduction ## 📝 Introduction
Tango is a low-code designer framework for quickly building low-code platforms. With Tango, you only need a few lines of code to build a basic low-code platform frontend system. The Tango low-code designer directly reads the source code of the frontend project, executes and renders the frontend view based on the source code, and provides users with low-code visual building capabilities. User's building operations will be translated into modifications to the source code. With low-code tools or platforms built using Tango, it is possible to achieve the effect of "source code in, source code out" and seamlessly integrate with the existing R&D system within the enterprise. Tango is a low-code builder framework for quickly building low-code platforms or tools. With Tango, you only need a few lines of code to build a basic low-code builder. The Tango low-code builder reads the source code of the frontend project directly, executes and renders the frontend view based on the source code, and provides users with low-code visual building capabilities. User's building operations will be translated into modifications to the source code. With low-code tools or platforms built using Tango, it is possible to achieve the effect of "source code in, source code out" and seamlessly integrate with the existing development flow within the enterprise.
### ✨ Features ### ✨ Features
@@ -20,11 +20,11 @@ Tango is a low-code designer framework for quickly building low-code platforms.
Due to the fact that the engine kernel is completely based on the source code-driven implementation, Tango low-code engine can achieve the visualization building capability of source code input and source code output, without providing any private intermediate products. If the company already has a complete R&D system (code hosting, building, deployment, CDN), Tango low-code engine can be directly used to integrate with existing services to build a low-code development platform. Due to the fact that the engine kernel is completely based on the source code-driven implementation, Tango low-code engine can achieve the visualization building capability of source code input and source code output, without providing any private intermediate products. If the company already has a complete R&D system (code hosting, building, deployment, CDN), Tango low-code engine can be directly used to integrate with existing services to build a low-code development platform.
<img src="https://p5.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/13140534982/ee2e/f42c/cc9a/184e2918a011b57d46e6c64a2722fa44.png" /> ![engine flow](./engine-flow.png)
### 📄 Code in, Code out ### 📄 Code in, Code out
由于引擎内核完全基于源代码驱动实现,Tango 低代码引擎能够实现源代码进,源代码出的可视化搭建能力,不提供任何私有的中间产物。如果公司内部已经有了一套完善的研发体系(代码托管、构建、部署、CDN),那么可以直接使用 Tango 低代码引擎与现有的服务集成构建低代码开发平台。 Due to the engine kernel being completely driven by source code, the Tango low-code engine can achieve the visual construction capability of source code in and source code out, without providing any private intermediate products. If the company already has a complete development system (code hosting, building, deployment, CDN), then it can directly use the Tango low-code engine to integrate with existing services to build a low-code development platform.
![code in, code out](./codein-codeout.png) ![code in, code out](./codein-codeout.png)
@@ -32,23 +32,24 @@ Due to the fact that the engine kernel is completely based on the source code-dr
Compared to the low-code building solution based on private schema, Tango low-code engine has the following advantages: Compared to the low-code building solution based on private schema, Tango low-code engine has the following advantages:
| Comparison Items | Schema-base low-code solution | Tango (Code-based) | | Comparison Items | Schema-base low-code solution | Tango (Code-based) |
| ---------------- | ----------------------------------------- | ------------------------------------------- | | ------------------------- | -------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| Applicable Scenarios | Targeted vertical building scenarios, such as forms, marketing pages, etc. | 🔥 Applicable to application building scenarios centered on source code | | Applicable Scenarios | Targeted vertical building scenarios, such as forms, marketing pages, etc. | 🔥 Applicable to application building scenarios centered on source code |
| Language Capabilities | Relies on private protocols for extension, inflexible, and difficult to align with programming language capabilities | 🔥 Based directly on JavaScript language, can use all language features, no extensibility issues | | Language Capabilities | Relies on private protocols for extension, inflexible, and difficult to align with programming language capabilities | 🔥 Based directly on JavaScript language, can use all language features, no extensibility issues |
| Development Capability | LowCode | 🔥 LowCode + ProCode | | Development Capability | LowCode | 🔥 LowCode + ProCode |
| Source Code Export | Centered on schema, one-way code output, irreversible | 🔥 Source code-centered, two-way transcoding | | Source Code Export | Centered on schema, one-way code output, irreversible | 🔥 Source code-centered, two-way transcoding |
| Custom Dependencies | Need to be encapsulated according to private protocol extension, high customization cost | 🔥 Existing components can be seamlessly and low-cost integrated | | Custom Dependencies | Need to be encapsulated according to private protocol extension, high customization cost | 🔥 Existing components can be seamlessly and low-cost integrated |
| Integrated R&D Facilities | High customization cost, additional customization required | 🔥 Low-cost integration, can directly reuse existing deployment and publishing capabilities | | Integrated R&D Facilities | High customization cost, additional customization required | 🔥 Low-cost integration, can directly reuse existing deployment and publishing capabilities |
The specific technical architecture is shown in the following figure: The specific technical architecture is shown in the following figure:
## 📐 Architecture ## 📐 Architecture
The Tango low-code engine has been layered and decoupled in implementation, enabling independent development and maintenance of the upper-level low-code platform and the lower-level low-code engine, allowing for fast integration and deployment. In addition, Tango's low-code engine defines an open material ecosystem, allowing developers to freely contribute to extension components and configure property setters that enhance low-code materials. The Tango low-code engine has been layered and decoupled in implementation, enabling independent development and maintenance of the upper-level low-code platform and the lower-level low-code engine, allowing for fast integration and deployment. In addition, Tango's low-code engine defines an open material ecosystem, allowing developers to freely contribute to extension components and configure property setters that enhance low-code materials.
The specific technical architecture is shown in the following diagram. The specific technical architecture is shown in the following diagram.
![low-code engine](./lowocode-engine.png) ![low-code engine](./engine-components.png)
## ⏰ Milestones ## ⏰ Milestones
@@ -58,6 +59,7 @@ Tango low-code engine is the core component of NetEase Cloud Music's internal lo
2. We plan to release the 1.0 Beta version of the low-code engine on September 30th this year. This version will refactor the core implementation for community scenarios, remove some compatibility code we have internally in Cloud Music, and optimize the implementation of the core. 2. We plan to release the 1.0 Beta version of the low-code engine on September 30th this year. This version will refactor the core implementation for community scenarios, remove some compatibility code we have internally in Cloud Music, and optimize the implementation of the core.
3. We plan to release the 1.0 RC version of the low-code engine on October 30th this year. This version will ensure that the core API is basically stable and no longer has BREAKING CHANGE. At the same time, we will provide comprehensive and detailed development guides, deployment documents, and demonstration applications. 3. We plan to release the 1.0 RC version of the low-code engine on October 30th this year. This version will ensure that the core API is basically stable and no longer has BREAKING CHANGE. At the same time, we will provide comprehensive and detailed development guides, deployment documents, and demonstration applications.
4. The official version will be released before the end of Q4 2023, and we will further improve our open source community operation mechanism. 4. The official version will be released before the end of Q4 2023, and we will further improve our open source community operation mechanism.
![milestones](./tango-milestones.png) ![milestones](./tango-milestones.png)
## 🤝 Community ## 🤝 Community
@@ -74,3 +76,5 @@ Welcome everyone to join our community and participate in the open source constr
Thanks to the Netease Cloud Music Public Technical Team, the Front-End Team, the Live Broadcasting Technical Team, and all the colleagues who participated in the Tango project. Thanks to the Netease Cloud Music Public Technical Team, the Front-End Team, the Live Broadcasting Technical Team, and all the colleagues who participated in the Tango project.
Thanks to the Sandpack project provided by CodeSandbox, which provides powerful browser-based code building and execution capabilities for Tango. Thanks to the Sandpack project provided by CodeSandbox, which provides powerful browser-based code building and execution capabilities for Tango.
Tanks to the Babel project, which provides powerful javascript ast parsing and traversing capabilities for Tango.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 259 KiB

+1 -2
View File
@@ -25,8 +25,7 @@
}, },
"devDependencies": { "devDependencies": {
"@docusaurus/module-type-aliases": "2.4.1", "@docusaurus/module-type-aliases": "2.4.1",
"@tsconfig/docusaurus": "^1.0.5", "@tsconfig/docusaurus": "^1.0.5"
"typescript": "^4.7.4"
}, },
"browserslist": { "browserslist": {
"production": [ "production": [
@@ -3,37 +3,37 @@ import clsx from 'clsx';
import styles from './styles.module.css'; import styles from './styles.module.css';
import Translate, { translate } from '@docusaurus/Translate'; import Translate, { translate } from '@docusaurus/Translate';
type FeatureItem = { interface FeatureItem {
title: string; title: string;
Svg: React.ComponentType<React.ComponentProps<'svg'>>; Svg: React.ComponentType<React.ComponentProps<'svg'>>;
description: JSX.Element; description: JSX.Element;
}; }
const FeatureList: FeatureItem[] = [ const FeatureList: FeatureItem[] = [
{ {
title: translate({ id: 'homepage.features.low-code', message: '可视化搭建' }), title: translate({ id: 'homepage.features.code-driven', message: '源码驱动' }),
Svg: require('@site/static/img/undraw_docusaurus_mountain.svg').default, Svg: require('@site/static/img/html-d.svg').default,
description: ( description: (
<Translate id="homepage.features.low-code-content"> <Translate id="homepage.features.code-driven-content">
</Translate> </Translate>
), ),
}, },
{ {
title: translate({ id: 'homepage.features.code-driven', message: '源码驱动' }), title: translate({ id: 'homepage.features.low-code', message: '实时出码' }),
Svg: require('@site/static/img/undraw_docusaurus_tree.svg').default, Svg: require('@site/static/img/digital-content-4.svg').default,
description: ( description: (
<Translate id="homepage.features.code-driven-content"> <Translate id="homepage.features.low-code-content">
使DSL线
</Translate> </Translate>
), ),
}, },
{ {
title: translate({ id: 'homepage.features.outbox', message: '开箱即用' }), title: translate({ id: 'homepage.features.outbox', message: '开箱即用' }),
Svg: require('@site/static/img/undraw_docusaurus_react.svg').default, Svg: require('@site/static/img/cpu-f.svg').default,
description: ( description: (
<Translate id="homepage.features.outbox-content"> <Translate id="homepage.features.outbox-content">
</Translate> </Translate>
), ),
}, },
+2 -2
View File
@@ -24,7 +24,7 @@ const timelines = [
{ {
icon: null, icon: null,
title: '1.0 RC', title: '1.0 RC',
date: '2023.10.30', date: '2023.11.30',
description: translate({ description: translate({
id: 'homepage.timeline.rc', id: 'homepage.timeline.rc',
message: '核心 API 基本稳定,不再发生 BR,发布 1.0 RC 版本。', message: '核心 API 基本稳定,不再发生 BR,发布 1.0 RC 版本。',
@@ -33,7 +33,7 @@ const timelines = [
{ {
icon: null, icon: null,
title: '1.0', title: '1.0',
date: 'Before 2023.12.25', date: 'Before 2023.12.29',
description: translate({ description: translate({
id: 'homepage.timeline.stable', id: 'homepage.timeline.stable',
message: 'API 完全稳定,提供良好的社区支持,可用于生产环境。', message: 'API 完全稳定,提供良好的社区支持,可用于生产环境。',
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 19 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 300" data-imageid="digital-content-4" imageName="Coding" class="illustrations_image" style="width: 261px;"><g id="_87_coding_outline" data-name="#87_coding_outline"><path d="M289.32,90.26V223.54a3.65,3.65,0,0,1-3.65,3.65H130.85a3.65,3.65,0,0,1-3.65-3.65V90.26a3.65,3.65,0,0,1,3.65-3.66H285.67A3.65,3.65,0,0,1,289.32,90.26Z" fill="#2e8555" class="target-color"/><path d="M285.67,228.19H130.85a4.66,4.66,0,0,1-4.65-4.65V90.26a4.66,4.66,0,0,1,4.65-4.66H285.67a4.66,4.66,0,0,1,4.65,4.66V223.54A4.65,4.65,0,0,1,285.67,228.19ZM130.85,87.6a2.66,2.66,0,0,0-2.65,2.66V223.54a2.65,2.65,0,0,0,2.65,2.65H285.67a2.65,2.65,0,0,0,2.65-2.65V90.26a2.66,2.66,0,0,0-2.65-2.66Z" fill="#093f68"/><path d="M272.8,76.47V209.75a3.65,3.65,0,0,1-3.65,3.65H114.33a3.65,3.65,0,0,1-3.65-3.65V76.47a3.65,3.65,0,0,1,3.65-3.66H269.15A3.65,3.65,0,0,1,272.8,76.47Z" fill="#fff"/><path d="M269.15,214.4H114.33a4.65,4.65,0,0,1-4.65-4.65V76.47a4.66,4.66,0,0,1,4.65-4.66H269.15a4.66,4.66,0,0,1,4.65,4.66V209.75A4.65,4.65,0,0,1,269.15,214.4ZM114.33,73.81a2.66,2.66,0,0,0-2.65,2.66V209.75a2.65,2.65,0,0,0,2.65,2.65H269.15a2.65,2.65,0,0,0,2.65-2.65V76.47a2.66,2.66,0,0,0-2.65-2.66Z" fill="#093f68"/><path d="M272.8,76.47V209.75a3.65,3.65,0,0,1-3.65,3.65h-103V72.81h103A3.65,3.65,0,0,1,272.8,76.47Z" fill="#093f68"/><path d="M269.15,214.4h-103a1,1,0,0,1-1-1V72.81a1,1,0,0,1,1-1h103a4.66,4.66,0,0,1,4.65,4.66V209.75A4.65,4.65,0,0,1,269.15,214.4Zm-102-2h102a2.65,2.65,0,0,0,2.65-2.65V76.47a2.66,2.66,0,0,0-2.65-2.66h-102Z" fill="#093f68"/><path d="M272.8,76.47v15H110.68v-15a3.65,3.65,0,0,1,3.65-3.66H269.15A3.65,3.65,0,0,1,272.8,76.47Z" fill="#fff"/><path d="M272.8,92.48H110.68a1,1,0,0,1-1-1v-15a4.66,4.66,0,0,1,4.65-4.66H269.15a4.66,4.66,0,0,1,4.65,4.66v15A1,1,0,0,1,272.8,92.48Zm-161.12-2H271.8v-14a2.66,2.66,0,0,0-2.65-2.66H114.33a2.66,2.66,0,0,0-2.65,2.66Z" fill="#093f68"/><circle cx="123.19" cy="81.63" r="3.38" fill="#f56132"/><path d="M123.19,86a4.38,4.38,0,1,1,4.38-4.38A4.39,4.39,0,0,1,123.19,86Zm0-6.76a2.38,2.38,0,1,0,2.38,2.38A2.39,2.39,0,0,0,123.19,79.25Z" fill="#093f68"/><circle cx="136.84" cy="81.63" r="3.38" fill="#ffbc0e"/><path d="M136.84,86a4.38,4.38,0,1,1,4.37-4.38A4.39,4.39,0,0,1,136.84,86Zm0-6.76a2.38,2.38,0,1,0,2.37,2.38A2.38,2.38,0,0,0,136.84,79.25Z" fill="#093f68"/><circle cx="150.48" cy="81.63" r="3.38" fill="#70cc40"/><path d="M150.48,86a4.38,4.38,0,1,1,4.38-4.38A4.39,4.39,0,0,1,150.48,86Zm0-6.76a2.38,2.38,0,1,0,2.38,2.38A2.39,2.39,0,0,0,150.48,79.25Z" fill="#093f68"/><path d="M148.3,104.19H120.73a1,1,0,0,1,0-2H148.3a1,1,0,0,1,0,2Z" fill="#dfeaef"/><path d="M153,116.59H129.79a1,1,0,0,1,0-2H153a1,1,0,0,1,0,2Z" fill="#dfeaef"/><path d="M156.18,129h-19.3a1,1,0,0,1,0-2h19.3a1,1,0,0,1,0,2Z" fill="#dfeaef"/><path d="M149.88,141.41H125.46a1,1,0,0,1,0-2h24.42a1,1,0,0,1,0,2Z" fill="#dfeaef"/><path d="M143.31,153.81H120.73a1,1,0,0,1,0-2h22.58a1,1,0,0,1,0,2Z" fill="#dfeaef"/><path d="M148.3,166.22h-20a1,1,0,0,1,0-2h20a1,1,0,0,1,0,2Z" fill="#dfeaef"/><path d="M141.41,178.62H120.73a1,1,0,0,1,0-2h20.68a1,1,0,0,1,0,2Z" fill="#dfeaef"/><path d="M148.3,191h-20a1,1,0,0,1,0-2h20a1,1,0,0,1,0,2Z" fill="#dfeaef"/><path d="M193.62,104.19h-16.8a1,1,0,0,1,0-2h16.8a1,1,0,0,1,0,2Z" fill="#fff"/><path d="M216.82,116.59H182.34a1,1,0,0,1,0-2h34.48a1,1,0,0,1,0,2Z" fill="#ffbc0e"/><path d="M208.26,129H182.34a1,1,0,0,1,0-2h25.92a1,1,0,0,1,0,2Z" fill="#ffbc0e"/><path d="M194.58,141.41H179.7a1,1,0,0,1,0-2h14.88a1,1,0,0,1,0,2Z" fill="#fff"/><path d="M200.5,153.81H183.7a1,1,0,0,1,0-2h16.8a1,1,0,0,1,0,2Z" fill="#f56132"/><path d="M209.06,166.22H183.12a1,1,0,0,1,0-2h25.94a1,1,0,0,1,0,2Z" fill="#fff"/><path d="M189.42,178.62h-12.6a1,1,0,0,1,0-2h12.6a1,1,0,0,1,0,2Z" fill="#fff"/><path d="M209.06,191h-27.6a1,1,0,0,1,0-2h27.6a1,1,0,0,1,0,2Z" fill="#70cc40"/><path d="M235.5,191H217.38a1,1,0,0,1,0-2H235.5a1,1,0,0,1,0,2Z" fill="#ffbc0e"/><path d="M229.62,129h-12.8a1,1,0,0,1,0-2h12.8a1,1,0,0,1,0,2Z" fill="#fff"/><path d="M247.87,116.59H223.22a1,1,0,0,1,0-2h24.65a1,1,0,0,1,0,2Z" fill="#2e8555" class="target-color"/><path d="M243.79,129h-7.21a1,1,0,0,1,0-2h7.21a1,1,0,0,1,0,2Z" fill="#f56132"/><path d="M212.66,153.81h-7.2a1,1,0,1,1,0-2h7.2a1,1,0,1,1,0,2Z" fill="#f56132"/><path d="M247.87,153.81H218.58a1,1,0,1,1,0-2h29.29a1,1,0,0,1,0,2Z" fill="#ffbc0e"/><path d="M223.22,166.22h-6.4a1,1,0,0,1,0-2h6.4a1,1,0,0,1,0,2Z" fill="#2e8555" class="target-color"/><path d="M247.87,191h-4.08a1,1,0,0,1,0-2h4.08a1,1,0,0,1,0,2Z" fill="#fff"/></g></svg>

After

Width:  |  Height:  |  Size: 4.4 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 300" data-imageid="html-d" imageName="Javascript" class="illustrations_image" style="width: 261px;"><g id="_181_javascript_outline" data-name="#181_javascript_outline"><rect x="144.72" y="69.25" width="130.1" height="153.64" rx="10.96" fill="#2e8555" class="target-color"/><path d="M263.86,223.89H155.67a12,12,0,0,1-11.95-11.95V80.21a12,12,0,0,1,11.95-12H263.86a12,12,0,0,1,12,12V211.94A12,12,0,0,1,263.86,223.89ZM155.67,70.25a10,10,0,0,0-9.95,10V211.94a10,10,0,0,0,9.95,9.95H263.86a10,10,0,0,0,10-9.95V80.21a10,10,0,0,0-10-10Z" fill="#093f68"/><path d="M257.86,230.75H141.93a7.08,7.08,0,0,1-7.08-7.08V84.75a7.08,7.08,0,0,1,7.08-7.08h93.81l29.2,27.19V223.67A7.08,7.08,0,0,1,257.86,230.75Z" fill="#fff"/><path d="M257.86,231.75H141.93a8.09,8.09,0,0,1-8.08-8.08V84.75a8.09,8.09,0,0,1,8.08-8.08h93.81a1,1,0,0,1,.68.27l29.21,27.19a1,1,0,0,1,.31.73V223.67A8.09,8.09,0,0,1,257.86,231.75ZM141.93,78.67a6.09,6.09,0,0,0-6.08,6.08V223.67a6.09,6.09,0,0,0,6.08,6.08H257.86a6.08,6.08,0,0,0,6.08-6.08V105.3l-28.6-26.63Z" fill="#093f68"/><path d="M235.74,77.67V99.06a5.46,5.46,0,0,0,5.39,5.47l23.81.33Z" fill="#2e8555" class="target-color"/><path d="M264.94,105.86h0l-23.81-.33a6.49,6.49,0,0,1-6.38-6.47V77.67a1,1,0,0,1,1.68-.73l29.21,27.19a1,1,0,0,1,.24,1.1A1,1,0,0,1,264.94,105.86ZM236.74,80V99.06a4.47,4.47,0,0,0,4.4,4.47l21.22.29Z" fill="#093f68"/><path d="M182.76,164.08,164,172.64v.14l18.78,8.56v9.39l-30-13.67v-8.7l30-13.68Z" fill="#093f68"/><path d="M182.76,191.73a.85.85,0,0,1-.41-.09l-30-13.67a1,1,0,0,1-.58-.91v-8.7a1,1,0,0,1,.58-.91l30-13.68a1,1,0,0,1,1.41.91v9.4a1,1,0,0,1-.58.91l-17,7.72,17,7.72a1,1,0,0,1,.58.91v9.39a1,1,0,0,1-.46.85A1,1,0,0,1,182.76,191.73Zm-29-15.31,28,12.76V182l-18.2-8.29a1,1,0,0,1-.58-.91v-.14a1,1,0,0,1,.58-.91l18.2-8.3v-7.19l-28,12.76Zm29-12.34h0Z" fill="#093f68"/><path d="M205,141.08l6.76,2.41L194,197.3l-6.7-2.42Z" fill="#093f68"/><path d="M194,198.3a1,1,0,0,1-.34-.06l-6.7-2.42a1,1,0,0,1-.61-1.25l17.72-53.81a1,1,0,0,1,.51-.58,1,1,0,0,1,.78,0l6.75,2.41a1,1,0,0,1,.62,1.26L195,197.61a1,1,0,0,1-.51.59A1.13,1.13,0,0,1,194,198.3Zm-5.44-4,4.81,1.74,17.15-51.9-4.87-1.75Z" fill="#093f68"/><path d="M216.36,181.34l18.78-8.56v-.14l-18.78-8.56v-9.4l30,13.68v8.7l-30,13.67Z" fill="#093f68"/><path d="M216.36,191.73a1,1,0,0,1-.54-.15,1,1,0,0,1-.46-.85v-9.39a1,1,0,0,1,.58-.91l16.94-7.72L215.94,165a1,1,0,0,1-.58-.91v-9.4a1,1,0,0,1,1.41-.91l30,13.68a1,1,0,0,1,.58.91v8.7a1,1,0,0,1-.58.91l-30,13.67A.85.85,0,0,1,216.36,191.73Zm1-9.75v7.2l28-12.76V169l-28-12.76v7.19l18.2,8.3a1,1,0,0,1,.58.91v.14a1,1,0,0,1-.58.91Z" fill="#093f68"/><rect x="120.98" y="92.53" width="71.35" height="44.25" rx="2.13" fill="#2e8555" class="target-color"/><path d="M190.2,137.78H123.11a3.13,3.13,0,0,1-3.13-3.12v-40a3.14,3.14,0,0,1,3.13-3.13H190.2a3.13,3.13,0,0,1,3.13,3.13v40A3.13,3.13,0,0,1,190.2,137.78ZM123.11,93.53A1.14,1.14,0,0,0,122,94.66v40a1.13,1.13,0,0,0,1.13,1.12H190.2a1.12,1.12,0,0,0,1.13-1.12v-40a1.13,1.13,0,0,0-1.13-1.13Z" fill="#093f68"/><path d="M151.53,122.88a7.29,7.29,0,0,1-1.07,2.59,6,6,0,0,1-2.18,2,7.46,7.46,0,0,1-3.61.77,7.3,7.3,0,0,1-4.6-1.43,6.78,6.78,0,0,1-2.46-3.67l4.64-1.56a2.7,2.7,0,0,0,.78,1.38,2,2,0,0,0,1.4.52,1.69,1.69,0,0,0,1.7-.89,5.49,5.49,0,0,0,.42-2.33V105.87h5.28v14.3A13.07,13.07,0,0,1,151.53,122.88Z" fill="#093f68"/><path d="M167.67,111.52a4.69,4.69,0,0,0-1.77-1.37,5,5,0,0,0-2.1-.51,6.44,6.44,0,0,0-1.06.1,2.81,2.81,0,0,0-1,.35,2.31,2.31,0,0,0-.72.67,1.84,1.84,0,0,0-.29,1.06,1.78,1.78,0,0,0,.23.93,2.06,2.06,0,0,0,.67.64,7,7,0,0,0,1.06.51c.41.15.87.31,1.39.47.73.25,1.5.52,2.3.82a8.49,8.49,0,0,1,2.18,1.18,6.3,6.3,0,0,1,1.62,1.82,5.29,5.29,0,0,1,.65,2.72,7.1,7.1,0,0,1-.69,3.24,6.51,6.51,0,0,1-1.86,2.26,8.14,8.14,0,0,1-2.67,1.32,11.49,11.49,0,0,1-3.1.43,12.59,12.59,0,0,1-4.51-.82A9.59,9.59,0,0,1,154.4,125l3.44-3.5a6.32,6.32,0,0,0,2.11,1.64,5.66,5.66,0,0,0,2.59.66,5.38,5.38,0,0,0,1.14-.12,3.08,3.08,0,0,0,1-.4,2,2,0,0,0,.66-.74,2.28,2.28,0,0,0,.25-1.11,1.71,1.71,0,0,0-.31-1,2.9,2.9,0,0,0-.87-.78A7,7,0,0,0,163,119l-1.92-.63a19.51,19.51,0,0,1-2-.79,7,7,0,0,1-1.78-1.19,5.49,5.49,0,0,1-1.26-1.75,6,6,0,0,1-.47-2.5,6.16,6.16,0,0,1,.73-3.1,6.57,6.57,0,0,1,1.94-2.12,8.49,8.49,0,0,1,2.7-1.21,12.06,12.06,0,0,1,3-.38,11.27,11.27,0,0,1,3.76.67,9.66,9.66,0,0,1,3.36,2Z" fill="#093f68"/><path d="M191.53,101.63H121a1,1,0,0,1,0-2h70.55a1,1,0,0,1,0,2Z" fill="#093f68"/><path d="M131.06,98.11h-5.12a1,1,0,0,1,0-2h5.12a1,1,0,0,1,0,2Z" fill="#093f68"/><path d="M257.68,212.94h-113a1,1,0,1,1,0-2h113a1,1,0,0,1,0,2Z" fill="#dfeaef"/></g></svg>

After

Width:  |  Height:  |  Size: 4.4 KiB

@@ -1,171 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1088" height="687.962" viewBox="0 0 1088 687.962">
<title>Easy to Use</title>
<g id="Group_12" data-name="Group 12" transform="translate(-57 -56)">
<g id="Group_11" data-name="Group 11" transform="translate(57 56)">
<path id="Path_83" data-name="Path 83" d="M1017.81,560.461c-5.27,45.15-16.22,81.4-31.25,110.31-20,38.52-54.21,54.04-84.77,70.28a193.275,193.275,0,0,1-27.46,11.94c-55.61,19.3-117.85,14.18-166.74,3.99a657.282,657.282,0,0,0-104.09-13.16q-14.97-.675-29.97-.67c-15.42.02-293.07,5.29-360.67-131.57-16.69-33.76-28.13-75-32.24-125.27-11.63-142.12,52.29-235.46,134.74-296.47,155.97-115.41,369.76-110.57,523.43,7.88C941.15,276.621,1036.99,396.031,1017.81,560.461Z" transform="translate(-56 -106.019)" fill="#3f3d56"/>
<path id="Path_84" data-name="Path 84" d="M986.56,670.771c-20,38.52-47.21,64.04-77.77,80.28a193.272,193.272,0,0,1-27.46,11.94c-55.61,19.3-117.85,14.18-166.74,3.99a657.3,657.3,0,0,0-104.09-13.16q-14.97-.675-29.97-.67-23.13.03-46.25,1.72c-100.17,7.36-253.82-6.43-321.42-143.29L382,283.981,444.95,445.6l20.09,51.59,55.37-75.98L549,381.981l130.2,149.27,36.8-81.27L970.78,657.9l14.21,11.59Z" transform="translate(-56 -106.019)" fill="#f2f2f2"/>
<path id="Path_85" data-name="Path 85" d="M302,282.962l26-57,36,83-31-60Z" opacity="0.1"/>
<path id="Path_86" data-name="Path 86" d="M610.5,753.821q-14.97-.675-29.97-.67L465.04,497.191Z" transform="translate(-56 -106.019)" opacity="0.1"/>
<path id="Path_87" data-name="Path 87" d="M464.411,315.191,493,292.962l130,150-132-128Z" opacity="0.1"/>
<path id="Path_88" data-name="Path 88" d="M908.79,751.051a193.265,193.265,0,0,1-27.46,11.94L679.2,531.251Z" transform="translate(-56 -106.019)" opacity="0.1"/>
<circle id="Ellipse_11" data-name="Ellipse 11" cx="3" cy="3" r="3" transform="translate(479 98.962)" fill="#f2f2f2"/>
<circle id="Ellipse_12" data-name="Ellipse 12" cx="3" cy="3" r="3" transform="translate(396 201.962)" fill="#f2f2f2"/>
<circle id="Ellipse_13" data-name="Ellipse 13" cx="2" cy="2" r="2" transform="translate(600 220.962)" fill="#f2f2f2"/>
<circle id="Ellipse_14" data-name="Ellipse 14" cx="2" cy="2" r="2" transform="translate(180 265.962)" fill="#f2f2f2"/>
<circle id="Ellipse_15" data-name="Ellipse 15" cx="2" cy="2" r="2" transform="translate(612 96.962)" fill="#f2f2f2"/>
<circle id="Ellipse_16" data-name="Ellipse 16" cx="2" cy="2" r="2" transform="translate(736 192.962)" fill="#f2f2f2"/>
<circle id="Ellipse_17" data-name="Ellipse 17" cx="2" cy="2" r="2" transform="translate(858 344.962)" fill="#f2f2f2"/>
<path id="Path_89" data-name="Path 89" d="M306,121.222h-2.76v-2.76h-1.48v2.76H299V122.7h2.76v2.759h1.48V122.7H306Z" fill="#f2f2f2"/>
<path id="Path_90" data-name="Path 90" d="M848,424.222h-2.76v-2.76h-1.48v2.76H841V425.7h2.76v2.759h1.48V425.7H848Z" fill="#f2f2f2"/>
<path id="Path_91" data-name="Path 91" d="M1144,719.981c0,16.569-243.557,74-544,74s-544-57.431-544-74,243.557,14,544,14S1144,703.413,1144,719.981Z" transform="translate(-56 -106.019)" fill="#3f3d56"/>
<path id="Path_92" data-name="Path 92" d="M1144,719.981c0,16.569-243.557,74-544,74s-544-57.431-544-74,243.557,14,544,14S1144,703.413,1144,719.981Z" transform="translate(-56 -106.019)" opacity="0.1"/>
<ellipse id="Ellipse_18" data-name="Ellipse 18" cx="544" cy="30" rx="544" ry="30" transform="translate(0 583.962)" fill="#3f3d56"/>
<path id="Path_93" data-name="Path 93" d="M624,677.981c0,33.137-14.775,24-33,24s-33,9.137-33-24,33-96,33-96S624,644.844,624,677.981Z" transform="translate(-56 -106.019)" fill="#ff6584"/>
<path id="Path_94" data-name="Path 94" d="M606,690.66c0,15.062-6.716,10.909-15,10.909s-15,4.153-15-10.909,15-43.636,15-43.636S606,675.6,606,690.66Z" transform="translate(-56 -106.019)" opacity="0.1"/>
<rect id="Rectangle_97" data-name="Rectangle 97" width="92" height="18" rx="9" transform="translate(489 604.962)" fill="#2f2e41"/>
<rect id="Rectangle_98" data-name="Rectangle 98" width="92" height="18" rx="9" transform="translate(489 586.962)" fill="#2f2e41"/>
<path id="Path_95" data-name="Path 95" d="M193,596.547c0,55.343,34.719,100.126,77.626,100.126" transform="translate(-56 -106.019)" fill="#3f3d56"/>
<path id="Path_96" data-name="Path 96" d="M270.626,696.673c0-55.965,38.745-101.251,86.626-101.251" transform="translate(-56 -106.019)" fill="#6c63ff"/>
<path id="Path_97" data-name="Path 97" d="M221.125,601.564c0,52.57,22.14,95.109,49.5,95.109" transform="translate(-56 -106.019)" fill="#6c63ff"/>
<path id="Path_98" data-name="Path 98" d="M270.626,696.673c0-71.511,44.783-129.377,100.126-129.377" transform="translate(-56 -106.019)" fill="#3f3d56"/>
<path id="Path_99" data-name="Path 99" d="M254.3,697.379s11.009-.339,14.326-2.7,16.934-5.183,17.757-1.395,16.544,18.844,4.115,18.945-28.879-1.936-32.19-3.953S254.3,697.379,254.3,697.379Z" transform="translate(-56 -106.019)" fill="#a8a8a8"/>
<path id="Path_100" data-name="Path 100" d="M290.716,710.909c-12.429.1-28.879-1.936-32.19-3.953-2.522-1.536-3.527-7.048-3.863-9.591l-.368.014s.7,8.879,4.009,10.9,19.761,4.053,32.19,3.953c3.588-.029,4.827-1.305,4.759-3.2C294.755,710.174,293.386,710.887,290.716,710.909Z" transform="translate(-56 -106.019)" opacity="0.2"/>
<path id="Path_101" data-name="Path 101" d="M777.429,633.081c0,38.029,23.857,68.8,53.341,68.8" transform="translate(-56 -106.019)" fill="#3f3d56"/>
<path id="Path_102" data-name="Path 102" d="M830.769,701.882c0-38.456,26.623-69.575,59.525-69.575" transform="translate(-56 -106.019)" fill="#6c63ff"/>
<path id="Path_103" data-name="Path 103" d="M796.755,636.528c0,36.124,15.213,65.354,34.014,65.354" transform="translate(-56 -106.019)" fill="#6c63ff"/>
<path id="Path_104" data-name="Path 104" d="M830.769,701.882c0-49.139,30.773-88.9,68.8-88.9" transform="translate(-56 -106.019)" fill="#3f3d56"/>
<path id="Path_105" data-name="Path 105" d="M819.548,702.367s7.565-.233,9.844-1.856,11.636-3.562,12.2-.958,11.368,12.949,2.828,13.018-19.844-1.33-22.119-2.716S819.548,702.367,819.548,702.367Z" transform="translate(-56 -106.019)" fill="#a8a8a8"/>
<path id="Path_106" data-name="Path 106" d="M844.574,711.664c-8.54.069-19.844-1.33-22.119-2.716-1.733-1.056-2.423-4.843-2.654-6.59l-.253.01s.479,6.1,2.755,7.487,13.579,2.785,22.119,2.716c2.465-.02,3.317-.9,3.27-2.2C847.349,711.159,846.409,711.649,844.574,711.664Z" transform="translate(-56 -106.019)" opacity="0.2"/>
<path id="Path_107" data-name="Path 107" d="M949.813,724.718s11.36-1.729,14.5-4.591,16.89-7.488,18.217-3.667,19.494,17.447,6.633,19.107-30.153,1.609-33.835-.065S949.813,724.718,949.813,724.718Z" transform="translate(-56 -106.019)" fill="#a8a8a8"/>
<path id="Path_108" data-name="Path 108" d="M989.228,734.173c-12.86,1.659-30.153,1.609-33.835-.065-2.8-1.275-4.535-6.858-5.2-9.45l-.379.061s1.833,9.109,5.516,10.783,20.975,1.725,33.835.065c3.712-.479,4.836-1.956,4.529-3.906C993.319,732.907,991.991,733.817,989.228,734.173Z" transform="translate(-56 -106.019)" opacity="0.2"/>
<path id="Path_109" data-name="Path 109" d="M670.26,723.9s9.587-1.459,12.237-3.875,14.255-6.32,15.374-3.095,16.452,14.725,5.6,16.125-25.448,1.358-28.555-.055S670.26,723.9,670.26,723.9Z" transform="translate(-56 -106.019)" fill="#a8a8a8"/>
<path id="Path_110" data-name="Path 110" d="M703.524,731.875c-10.853,1.4-25.448,1.358-28.555-.055-2.367-1.076-3.827-5.788-4.39-7.976l-.32.051s1.547,7.687,4.655,9.1,17.7,1.456,28.555.055c3.133-.4,4.081-1.651,3.822-3.3C706.977,730.807,705.856,731.575,703.524,731.875Z" transform="translate(-56 -106.019)" opacity="0.2"/>
<path id="Path_111" data-name="Path 111" d="M178.389,719.109s7.463-1.136,9.527-3.016,11.1-4.92,11.969-2.409,12.808,11.463,4.358,12.553-19.811,1.057-22.23-.043S178.389,719.109,178.389,719.109Z" transform="translate(-56 -106.019)" fill="#a8a8a8"/>
<path id="Path_112" data-name="Path 112" d="M204.285,725.321c-8.449,1.09-19.811,1.057-22.23-.043-1.842-.838-2.979-4.506-3.417-6.209l-.249.04s1.2,5.984,3.624,7.085,13.781,1.133,22.23.043c2.439-.315,3.177-1.285,2.976-2.566C206.973,724.489,206.1,725.087,204.285,725.321Z" transform="translate(-56 -106.019)" opacity="0.2"/>
<path id="Path_113" data-name="Path 113" d="M439.7,707.337c0,30.22-42.124,20.873-93.7,20.873s-93.074,9.347-93.074-20.873,42.118-36.793,93.694-36.793S439.7,677.117,439.7,707.337Z" transform="translate(-56 -106.019)" opacity="0.1"/>
<path id="Path_114" data-name="Path 114" d="M439.7,699.9c0,30.22-42.124,20.873-93.7,20.873s-93.074,9.347-93.074-20.873S295.04,663.1,346.616,663.1,439.7,669.676,439.7,699.9Z" transform="translate(-56 -106.019)" fill="#3f3d56"/>
</g>
<g id="docusaurus_keytar" transform="translate(312.271 493.733)">
<path id="Path_40" data-name="Path 40" d="M99,52h91.791V89.153H99Z" transform="translate(5.904 -14.001)" fill="#fff" fill-rule="evenodd"/>
<path id="Path_41" data-name="Path 41" d="M24.855,163.927A21.828,21.828,0,0,1,5.947,153a21.829,21.829,0,0,0,18.908,32.782H46.71V163.927Z" transform="translate(-3 -4.634)" fill="#3ecc5f" fill-rule="evenodd"/>
<path id="Path_42" data-name="Path 42" d="M121.861,61.1l76.514-4.782V45.39A21.854,21.854,0,0,0,176.52,23.535H78.173L75.441,18.8a3.154,3.154,0,0,0-5.464,0l-2.732,4.732L64.513,18.8a3.154,3.154,0,0,0-5.464,0l-2.732,4.732L53.586,18.8a3.154,3.154,0,0,0-5.464,0L45.39,23.535c-.024,0-.046,0-.071,0l-4.526-4.525a3.153,3.153,0,0,0-5.276,1.414l-1.5,5.577-5.674-1.521a3.154,3.154,0,0,0-3.863,3.864L26,34.023l-5.575,1.494a3.155,3.155,0,0,0-1.416,5.278l4.526,4.526c0,.023,0,.046,0,.07L18.8,48.122a3.154,3.154,0,0,0,0,5.464l4.732,2.732L18.8,59.05a3.154,3.154,0,0,0,0,5.464l4.732,2.732L18.8,69.977a3.154,3.154,0,0,0,0,5.464l4.732,2.732L18.8,80.9a3.154,3.154,0,0,0,0,5.464L23.535,89.1,18.8,91.832a3.154,3.154,0,0,0,0,5.464l4.732,2.732L18.8,102.76a3.154,3.154,0,0,0,0,5.464l4.732,2.732L18.8,113.687a3.154,3.154,0,0,0,0,5.464l4.732,2.732L18.8,124.615a3.154,3.154,0,0,0,0,5.464l4.732,2.732L18.8,135.542a3.154,3.154,0,0,0,0,5.464l4.732,2.732L18.8,146.469a3.154,3.154,0,0,0,0,5.464l4.732,2.732L18.8,157.4a3.154,3.154,0,0,0,0,5.464l4.732,2.732L18.8,168.324a3.154,3.154,0,0,0,0,5.464l4.732,2.732A21.854,21.854,0,0,0,45.39,198.375H176.52a21.854,21.854,0,0,0,21.855-21.855V89.1l-76.514-4.782a11.632,11.632,0,0,1,0-23.219" transform="translate(-1.681 -17.226)" fill="#3ecc5f" fill-rule="evenodd"/>
<path id="Path_43" data-name="Path 43" d="M143,186.71h32.782V143H143Z" transform="translate(9.984 -5.561)" fill="#3ecc5f" fill-rule="evenodd"/>
<path id="Path_44" data-name="Path 44" d="M196.71,159.855a5.438,5.438,0,0,0-.7.07c-.042-.164-.081-.329-.127-.493a5.457,5.457,0,1,0-5.4-9.372q-.181-.185-.366-.367a5.454,5.454,0,1,0-9.384-5.4c-.162-.046-.325-.084-.486-.126a5.467,5.467,0,1,0-10.788,0c-.162.042-.325.08-.486.126a5.457,5.457,0,1,0-9.384,5.4,21.843,21.843,0,1,0,36.421,21.02,5.452,5.452,0,1,0,.7-10.858" transform="translate(10.912 -6.025)" fill="#44d860" fill-rule="evenodd"/>
<path id="Path_45" data-name="Path 45" d="M153,124.855h32.782V103H153Z" transform="translate(10.912 -9.271)" fill="#3ecc5f" fill-rule="evenodd"/>
<path id="Path_46" data-name="Path 46" d="M194.855,116.765a2.732,2.732,0,1,0,0-5.464,2.811,2.811,0,0,0-.349.035c-.022-.082-.04-.164-.063-.246a2.733,2.733,0,0,0-1.052-5.253,2.7,2.7,0,0,0-1.648.566q-.09-.093-.184-.184a2.7,2.7,0,0,0,.553-1.633,2.732,2.732,0,0,0-5.245-1.07,10.928,10.928,0,1,0,0,21.031,2.732,2.732,0,0,0,5.245-1.07,2.7,2.7,0,0,0-.553-1.633q.093-.09.184-.184a2.7,2.7,0,0,0,1.648.566,2.732,2.732,0,0,0,1.052-5.253c.023-.081.042-.164.063-.246a2.814,2.814,0,0,0,.349.035" transform="translate(12.767 -9.377)" fill="#44d860" fill-rule="evenodd"/>
<path id="Path_47" data-name="Path 47" d="M65.087,56.891a2.732,2.732,0,0,1-2.732-2.732,8.2,8.2,0,0,0-16.391,0,2.732,2.732,0,0,1-5.464,0,13.659,13.659,0,0,1,27.319,0,2.732,2.732,0,0,1-2.732,2.732" transform="translate(0.478 -15.068)" fill-rule="evenodd"/>
<path id="Path_48" data-name="Path 48" d="M103,191.347h65.565a21.854,21.854,0,0,0,21.855-21.855V93H124.855A21.854,21.854,0,0,0,103,114.855Z" transform="translate(6.275 -10.199)" fill="#ffff50" fill-rule="evenodd"/>
<path id="Path_49" data-name="Path 49" d="M173.216,129.787H118.535a1.093,1.093,0,1,1,0-2.185h54.681a1.093,1.093,0,0,1,0,2.185m0,21.855H118.535a1.093,1.093,0,1,1,0-2.186h54.681a1.093,1.093,0,0,1,0,2.186m0,21.855H118.535a1.093,1.093,0,1,1,0-2.185h54.681a1.093,1.093,0,0,1,0,2.185m0-54.434H118.535a1.093,1.093,0,1,1,0-2.185h54.681a1.093,1.093,0,0,1,0,2.185m0,21.652H118.535a1.093,1.093,0,1,1,0-2.186h54.681a1.093,1.093,0,0,1,0,2.186m0,21.855H118.535a1.093,1.093,0,1,1,0-2.186h54.681a1.093,1.093,0,0,1,0,2.186M189.585,61.611c-.013,0-.024-.007-.037-.005-3.377.115-4.974,3.492-6.384,6.472-1.471,3.114-2.608,5.139-4.473,5.078-2.064-.074-3.244-2.406-4.494-4.874-1.436-2.835-3.075-6.049-6.516-5.929-3.329.114-4.932,3.053-6.346,5.646-1.5,2.762-2.529,4.442-4.5,4.364-2.106-.076-3.225-1.972-4.52-4.167-1.444-2.443-3.112-5.191-6.487-5.1-3.272.113-4.879,2.606-6.3,4.808-1.5,2.328-2.552,3.746-4.551,3.662-2.156-.076-3.27-1.65-4.558-3.472-1.447-2.047-3.077-4.363-6.442-4.251-3.2.109-4.807,2.153-6.224,3.954-1.346,1.709-2.4,3.062-4.621,2.977a1.093,1.093,0,0,0-.079,2.186c3.3.11,4.967-1.967,6.417-3.81,1.286-1.635,2.4-3.045,4.582-3.12,2.1-.09,3.091,1.218,4.584,3.327,1.417,2,3.026,4.277,6.263,4.394,3.391.114,5.022-2.42,6.467-4.663,1.292-2,2.406-3.734,4.535-3.807,1.959-.073,3.026,1.475,4.529,4.022,1.417,2.4,3.023,5.121,6.324,5.241,3.415.118,5.064-2.863,6.5-5.5,1.245-2.282,2.419-4.437,4.5-4.509,1.959-.046,2.981,1.743,4.492,4.732,1.412,2.79,3.013,5.95,6.365,6.071l.185,0c3.348,0,4.937-3.36,6.343-6.331,1.245-2.634,2.423-5.114,4.444-5.216Z" transform="translate(7.109 -13.11)" fill-rule="evenodd"/>
<path id="Path_50" data-name="Path 50" d="M83,186.71h43.71V143H83Z" transform="translate(4.42 -5.561)" fill="#3ecc5f" fill-rule="evenodd"/>
<g id="Group_8" data-name="Group 8" transform="matrix(0.966, -0.259, 0.259, 0.966, 109.327, 91.085)">
<rect id="Rectangle_3" data-name="Rectangle 3" width="92.361" height="36.462" rx="2" transform="translate(0 0)" fill="#d8d8d8"/>
<g id="Group_2" data-name="Group 2" transform="translate(1.531 23.03)">
<rect id="Rectangle_4" data-name="Rectangle 4" width="5.336" height="5.336" rx="1" transform="translate(16.797 0)" fill="#4a4a4a"/>
<rect id="Rectangle_5" data-name="Rectangle 5" width="5.336" height="5.336" rx="1" transform="translate(23.12 0)" fill="#4a4a4a"/>
<rect id="Rectangle_6" data-name="Rectangle 6" width="5.336" height="5.336" rx="1" transform="translate(29.444 0)" fill="#4a4a4a"/>
<rect id="Rectangle_7" data-name="Rectangle 7" width="5.336" height="5.336" rx="1" transform="translate(35.768 0)" fill="#4a4a4a"/>
<rect id="Rectangle_8" data-name="Rectangle 8" width="5.336" height="5.336" rx="1" transform="translate(42.091 0)" fill="#4a4a4a"/>
<rect id="Rectangle_9" data-name="Rectangle 9" width="5.336" height="5.336" rx="1" transform="translate(48.415 0)" fill="#4a4a4a"/>
<rect id="Rectangle_10" data-name="Rectangle 10" width="5.336" height="5.336" rx="1" transform="translate(54.739 0)" fill="#4a4a4a"/>
<rect id="Rectangle_11" data-name="Rectangle 11" width="5.336" height="5.336" rx="1" transform="translate(61.063 0)" fill="#4a4a4a"/>
<rect id="Rectangle_12" data-name="Rectangle 12" width="5.336" height="5.336" rx="1" transform="translate(67.386 0)" fill="#4a4a4a"/>
<path id="Path_51" data-name="Path 51" d="M1.093,0H14.518a1.093,1.093,0,0,1,1.093,1.093V4.243a1.093,1.093,0,0,1-1.093,1.093H1.093A1.093,1.093,0,0,1,0,4.243V1.093A1.093,1.093,0,0,1,1.093,0ZM75,0H88.426a1.093,1.093,0,0,1,1.093,1.093V4.243a1.093,1.093,0,0,1-1.093,1.093H75a1.093,1.093,0,0,1-1.093-1.093V1.093A1.093,1.093,0,0,1,75,0Z" transform="translate(0 0)" fill="#4a4a4a" fill-rule="evenodd"/>
</g>
<g id="Group_3" data-name="Group 3" transform="translate(1.531 10.261)">
<path id="Path_52" data-name="Path 52" d="M1.093,0H6.218A1.093,1.093,0,0,1,7.31,1.093V4.242A1.093,1.093,0,0,1,6.218,5.335H1.093A1.093,1.093,0,0,1,0,4.242V1.093A1.093,1.093,0,0,1,1.093,0Z" transform="translate(0 0)" fill="#4a4a4a" fill-rule="evenodd"/>
<rect id="Rectangle_13" data-name="Rectangle 13" width="5.336" height="5.336" rx="1" transform="translate(8.299 0)" fill="#4a4a4a"/>
<rect id="Rectangle_14" data-name="Rectangle 14" width="5.336" height="5.336" rx="1" transform="translate(14.623 0)" fill="#4a4a4a"/>
<rect id="Rectangle_15" data-name="Rectangle 15" width="5.336" height="5.336" rx="1" transform="translate(20.947 0)" fill="#4a4a4a"/>
<rect id="Rectangle_16" data-name="Rectangle 16" width="5.336" height="5.336" rx="1" transform="translate(27.271 0)" fill="#4a4a4a"/>
<rect id="Rectangle_17" data-name="Rectangle 17" width="5.336" height="5.336" rx="1" transform="translate(33.594 0)" fill="#4a4a4a"/>
<rect id="Rectangle_18" data-name="Rectangle 18" width="5.336" height="5.336" rx="1" transform="translate(39.918 0)" fill="#4a4a4a"/>
<rect id="Rectangle_19" data-name="Rectangle 19" width="5.336" height="5.336" rx="1" transform="translate(46.242 0)" fill="#4a4a4a"/>
<rect id="Rectangle_20" data-name="Rectangle 20" width="5.336" height="5.336" rx="1" transform="translate(52.565 0)" fill="#4a4a4a"/>
<rect id="Rectangle_21" data-name="Rectangle 21" width="5.336" height="5.336" rx="1" transform="translate(58.888 0)" fill="#4a4a4a"/>
<rect id="Rectangle_22" data-name="Rectangle 22" width="5.336" height="5.336" rx="1" transform="translate(65.212 0)" fill="#4a4a4a"/>
<rect id="Rectangle_23" data-name="Rectangle 23" width="5.336" height="5.336" rx="1" transform="translate(71.536 0)" fill="#4a4a4a"/>
<rect id="Rectangle_24" data-name="Rectangle 24" width="5.336" height="5.336" rx="1" transform="translate(77.859 0)" fill="#4a4a4a"/>
<rect id="Rectangle_25" data-name="Rectangle 25" width="5.336" height="5.336" rx="1" transform="translate(84.183 0)" fill="#4a4a4a"/>
</g>
<g id="Group_4" data-name="Group 4" transform="translate(91.05 9.546) rotate(180)">
<path id="Path_53" data-name="Path 53" d="M1.093,0H6.219A1.093,1.093,0,0,1,7.312,1.093v3.15A1.093,1.093,0,0,1,6.219,5.336H1.093A1.093,1.093,0,0,1,0,4.243V1.093A1.093,1.093,0,0,1,1.093,0Z" transform="translate(0 0)" fill="#4a4a4a" fill-rule="evenodd"/>
<rect id="Rectangle_26" data-name="Rectangle 26" width="5.336" height="5.336" rx="1" transform="translate(8.299 0)" fill="#4a4a4a"/>
<rect id="Rectangle_27" data-name="Rectangle 27" width="5.336" height="5.336" rx="1" transform="translate(14.623 0)" fill="#4a4a4a"/>
<rect id="Rectangle_28" data-name="Rectangle 28" width="5.336" height="5.336" rx="1" transform="translate(20.947 0)" fill="#4a4a4a"/>
<rect id="Rectangle_29" data-name="Rectangle 29" width="5.336" height="5.336" rx="1" transform="translate(27.271 0)" fill="#4a4a4a"/>
<rect id="Rectangle_30" data-name="Rectangle 30" width="5.336" height="5.336" rx="1" transform="translate(33.594 0)" fill="#4a4a4a"/>
<rect id="Rectangle_31" data-name="Rectangle 31" width="5.336" height="5.336" rx="1" transform="translate(39.918 0)" fill="#4a4a4a"/>
<rect id="Rectangle_32" data-name="Rectangle 32" width="5.336" height="5.336" rx="1" transform="translate(46.242 0)" fill="#4a4a4a"/>
<rect id="Rectangle_33" data-name="Rectangle 33" width="5.336" height="5.336" rx="1" transform="translate(52.565 0)" fill="#4a4a4a"/>
<rect id="Rectangle_34" data-name="Rectangle 34" width="5.336" height="5.336" rx="1" transform="translate(58.889 0)" fill="#4a4a4a"/>
<rect id="Rectangle_35" data-name="Rectangle 35" width="5.336" height="5.336" rx="1" transform="translate(65.213 0)" fill="#4a4a4a"/>
<rect id="Rectangle_36" data-name="Rectangle 36" width="5.336" height="5.336" rx="1" transform="translate(71.537 0)" fill="#4a4a4a"/>
<rect id="Rectangle_37" data-name="Rectangle 37" width="5.336" height="5.336" rx="1" transform="translate(77.86 0)" fill="#4a4a4a"/>
<rect id="Rectangle_38" data-name="Rectangle 38" width="5.336" height="5.336" rx="1" transform="translate(84.183 0)" fill="#4a4a4a"/>
<rect id="Rectangle_39" data-name="Rectangle 39" width="5.336" height="5.336" rx="1" transform="translate(8.299 0)" fill="#4a4a4a"/>
<rect id="Rectangle_40" data-name="Rectangle 40" width="5.336" height="5.336" rx="1" transform="translate(14.623 0)" fill="#4a4a4a"/>
<rect id="Rectangle_41" data-name="Rectangle 41" width="5.336" height="5.336" rx="1" transform="translate(20.947 0)" fill="#4a4a4a"/>
<rect id="Rectangle_42" data-name="Rectangle 42" width="5.336" height="5.336" rx="1" transform="translate(27.271 0)" fill="#4a4a4a"/>
<rect id="Rectangle_43" data-name="Rectangle 43" width="5.336" height="5.336" rx="1" transform="translate(33.594 0)" fill="#4a4a4a"/>
<rect id="Rectangle_44" data-name="Rectangle 44" width="5.336" height="5.336" rx="1" transform="translate(39.918 0)" fill="#4a4a4a"/>
<rect id="Rectangle_45" data-name="Rectangle 45" width="5.336" height="5.336" rx="1" transform="translate(46.242 0)" fill="#4a4a4a"/>
<rect id="Rectangle_46" data-name="Rectangle 46" width="5.336" height="5.336" rx="1" transform="translate(52.565 0)" fill="#4a4a4a"/>
<rect id="Rectangle_47" data-name="Rectangle 47" width="5.336" height="5.336" rx="1" transform="translate(58.889 0)" fill="#4a4a4a"/>
<rect id="Rectangle_48" data-name="Rectangle 48" width="5.336" height="5.336" rx="1" transform="translate(65.213 0)" fill="#4a4a4a"/>
<rect id="Rectangle_49" data-name="Rectangle 49" width="5.336" height="5.336" rx="1" transform="translate(71.537 0)" fill="#4a4a4a"/>
<rect id="Rectangle_50" data-name="Rectangle 50" width="5.336" height="5.336" rx="1" transform="translate(77.86 0)" fill="#4a4a4a"/>
<rect id="Rectangle_51" data-name="Rectangle 51" width="5.336" height="5.336" rx="1" transform="translate(84.183 0)" fill="#4a4a4a"/>
</g>
<g id="Group_6" data-name="Group 6" transform="translate(1.531 16.584)">
<path id="Path_54" data-name="Path 54" d="M1.093,0h7.3A1.093,1.093,0,0,1,9.485,1.093v3.15A1.093,1.093,0,0,1,8.392,5.336h-7.3A1.093,1.093,0,0,1,0,4.243V1.094A1.093,1.093,0,0,1,1.093,0Z" transform="translate(0 0)" fill="#4a4a4a" fill-rule="evenodd"/>
<g id="Group_5" data-name="Group 5" transform="translate(10.671 0)">
<rect id="Rectangle_52" data-name="Rectangle 52" width="5.336" height="5.336" rx="1" fill="#4a4a4a"/>
<rect id="Rectangle_53" data-name="Rectangle 53" width="5.336" height="5.336" rx="1" transform="translate(6.324 0)" fill="#4a4a4a"/>
<rect id="Rectangle_54" data-name="Rectangle 54" width="5.336" height="5.336" rx="1" transform="translate(12.647 0)" fill="#4a4a4a"/>
<rect id="Rectangle_55" data-name="Rectangle 55" width="5.336" height="5.336" rx="1" transform="translate(18.971 0)" fill="#4a4a4a"/>
<rect id="Rectangle_56" data-name="Rectangle 56" width="5.336" height="5.336" rx="1" transform="translate(25.295 0)" fill="#4a4a4a"/>
<rect id="Rectangle_57" data-name="Rectangle 57" width="5.336" height="5.336" rx="1" transform="translate(31.619 0)" fill="#4a4a4a"/>
<rect id="Rectangle_58" data-name="Rectangle 58" width="5.336" height="5.336" rx="1" transform="translate(37.942 0)" fill="#4a4a4a"/>
<rect id="Rectangle_59" data-name="Rectangle 59" width="5.336" height="5.336" rx="1" transform="translate(44.265 0)" fill="#4a4a4a"/>
<rect id="Rectangle_60" data-name="Rectangle 60" width="5.336" height="5.336" rx="1" transform="translate(50.589 0)" fill="#4a4a4a"/>
<rect id="Rectangle_61" data-name="Rectangle 61" width="5.336" height="5.336" rx="1" transform="translate(56.912 0)" fill="#4a4a4a"/>
<rect id="Rectangle_62" data-name="Rectangle 62" width="5.336" height="5.336" rx="1" transform="translate(63.236 0)" fill="#4a4a4a"/>
</g>
<path id="Path_55" data-name="Path 55" d="M1.094,0H8A1.093,1.093,0,0,1,9.091,1.093v3.15A1.093,1.093,0,0,1,8,5.336H1.093A1.093,1.093,0,0,1,0,4.243V1.094A1.093,1.093,0,0,1,1.093,0Z" transform="translate(80.428 0)" fill="#4a4a4a" fill-rule="evenodd"/>
</g>
<g id="Group_7" data-name="Group 7" transform="translate(1.531 29.627)">
<rect id="Rectangle_63" data-name="Rectangle 63" width="5.336" height="5.336" rx="1" transform="translate(0 0)" fill="#4a4a4a"/>
<rect id="Rectangle_64" data-name="Rectangle 64" width="5.336" height="5.336" rx="1" transform="translate(6.324 0)" fill="#4a4a4a"/>
<rect id="Rectangle_65" data-name="Rectangle 65" width="5.336" height="5.336" rx="1" transform="translate(12.647 0)" fill="#4a4a4a"/>
<rect id="Rectangle_66" data-name="Rectangle 66" width="5.336" height="5.336" rx="1" transform="translate(18.971 0)" fill="#4a4a4a"/>
<path id="Path_56" data-name="Path 56" d="M1.093,0H31.515a1.093,1.093,0,0,1,1.093,1.093V4.244a1.093,1.093,0,0,1-1.093,1.093H1.093A1.093,1.093,0,0,1,0,4.244V1.093A1.093,1.093,0,0,1,1.093,0ZM34.687,0h3.942a1.093,1.093,0,0,1,1.093,1.093V4.244a1.093,1.093,0,0,1-1.093,1.093H34.687a1.093,1.093,0,0,1-1.093-1.093V1.093A1.093,1.093,0,0,1,34.687,0Z" transform="translate(25.294 0)" fill="#4a4a4a" fill-rule="evenodd"/>
<rect id="Rectangle_67" data-name="Rectangle 67" width="5.336" height="5.336" rx="1" transform="translate(66.003 0)" fill="#4a4a4a"/>
<rect id="Rectangle_68" data-name="Rectangle 68" width="5.336" height="5.336" rx="1" transform="translate(72.327 0)" fill="#4a4a4a"/>
<rect id="Rectangle_69" data-name="Rectangle 69" width="5.336" height="5.336" rx="1" transform="translate(84.183 0)" fill="#4a4a4a"/>
<path id="Path_57" data-name="Path 57" d="M5.336,0V1.18A1.093,1.093,0,0,1,4.243,2.273H1.093A1.093,1.093,0,0,1,0,1.18V0Z" transform="translate(83.59 2.273) rotate(180)" fill="#4a4a4a"/>
<path id="Path_58" data-name="Path 58" d="M5.336,0V1.18A1.093,1.093,0,0,1,4.243,2.273H1.093A1.093,1.093,0,0,1,0,1.18V0Z" transform="translate(78.255 3.063)" fill="#4a4a4a"/>
</g>
<rect id="Rectangle_70" data-name="Rectangle 70" width="88.927" height="2.371" rx="1.085" transform="translate(1.925 1.17)" fill="#4a4a4a"/>
<rect id="Rectangle_71" data-name="Rectangle 71" width="4.986" height="1.581" rx="0.723" transform="translate(4.1 1.566)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_72" data-name="Rectangle 72" width="4.986" height="1.581" rx="0.723" transform="translate(10.923 1.566)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_73" data-name="Rectangle 73" width="4.986" height="1.581" rx="0.723" transform="translate(16.173 1.566)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_74" data-name="Rectangle 74" width="4.986" height="1.581" rx="0.723" transform="translate(21.421 1.566)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_75" data-name="Rectangle 75" width="4.986" height="1.581" rx="0.723" transform="translate(26.671 1.566)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_76" data-name="Rectangle 76" width="4.986" height="1.581" rx="0.723" transform="translate(33.232 1.566)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_77" data-name="Rectangle 77" width="4.986" height="1.581" rx="0.723" transform="translate(38.48 1.566)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_78" data-name="Rectangle 78" width="4.986" height="1.581" rx="0.723" transform="translate(43.73 1.566)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_79" data-name="Rectangle 79" width="4.986" height="1.581" rx="0.723" transform="translate(48.978 1.566)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_80" data-name="Rectangle 80" width="4.986" height="1.581" rx="0.723" transform="translate(55.54 1.566)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_81" data-name="Rectangle 81" width="4.986" height="1.581" rx="0.723" transform="translate(60.788 1.566)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_82" data-name="Rectangle 82" width="4.986" height="1.581" rx="0.723" transform="translate(66.038 1.566)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_83" data-name="Rectangle 83" width="4.986" height="1.581" rx="0.723" transform="translate(72.599 1.566)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_84" data-name="Rectangle 84" width="4.986" height="1.581" rx="0.723" transform="translate(77.847 1.566)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_85" data-name="Rectangle 85" width="4.986" height="1.581" rx="0.723" transform="translate(83.097 1.566)" fill="#d8d8d8" opacity="0.136"/>
</g>
<path id="Path_59" data-name="Path 59" d="M146.71,159.855a5.439,5.439,0,0,0-.7.07c-.042-.164-.081-.329-.127-.493a5.457,5.457,0,1,0-5.4-9.372q-.181-.185-.366-.367a5.454,5.454,0,1,0-9.384-5.4c-.162-.046-.325-.084-.486-.126a5.467,5.467,0,1,0-10.788,0c-.162.042-.325.08-.486.126a5.457,5.457,0,1,0-9.384,5.4,21.843,21.843,0,1,0,36.421,21.02,5.452,5.452,0,1,0,.7-10.858" transform="translate(6.275 -6.025)" fill="#44d860" fill-rule="evenodd"/>
<path id="Path_60" data-name="Path 60" d="M83,124.855h43.71V103H83Z" transform="translate(4.42 -9.271)" fill="#3ecc5f" fill-rule="evenodd"/>
<path id="Path_61" data-name="Path 61" d="M134.855,116.765a2.732,2.732,0,1,0,0-5.464,2.811,2.811,0,0,0-.349.035c-.022-.082-.04-.164-.063-.246a2.733,2.733,0,0,0-1.052-5.253,2.7,2.7,0,0,0-1.648.566q-.09-.093-.184-.184a2.7,2.7,0,0,0,.553-1.633,2.732,2.732,0,0,0-5.245-1.07,10.928,10.928,0,1,0,0,21.031,2.732,2.732,0,0,0,5.245-1.07,2.7,2.7,0,0,0-.553-1.633q.093-.09.184-.184a2.7,2.7,0,0,0,1.648.566,2.732,2.732,0,0,0,1.052-5.253c.023-.081.042-.164.063-.246a2.811,2.811,0,0,0,.349.035" transform="translate(7.202 -9.377)" fill="#44d860" fill-rule="evenodd"/>
<path id="Path_62" data-name="Path 62" d="M143.232,42.33a2.967,2.967,0,0,1-.535-.055,2.754,2.754,0,0,1-.514-.153,2.838,2.838,0,0,1-.471-.251,4.139,4.139,0,0,1-.415-.339,3.2,3.2,0,0,1-.338-.415A2.7,2.7,0,0,1,140.5,39.6a2.968,2.968,0,0,1,.055-.535,3.152,3.152,0,0,1,.152-.514,2.874,2.874,0,0,1,.252-.47,2.633,2.633,0,0,1,.753-.754,2.837,2.837,0,0,1,.471-.251,2.753,2.753,0,0,1,.514-.153,2.527,2.527,0,0,1,1.071,0,2.654,2.654,0,0,1,.983.4,4.139,4.139,0,0,1,.415.339,4.019,4.019,0,0,1,.339.415,2.786,2.786,0,0,1,.251.47,2.864,2.864,0,0,1,.208,1.049,2.77,2.77,0,0,1-.8,1.934,4.139,4.139,0,0,1-.415.339,2.722,2.722,0,0,1-1.519.459m21.855-1.366a2.789,2.789,0,0,1-1.935-.8,4.162,4.162,0,0,1-.338-.415,2.7,2.7,0,0,1-.459-1.519,2.789,2.789,0,0,1,.8-1.934,4.139,4.139,0,0,1,.415-.339,2.838,2.838,0,0,1,.471-.251,2.752,2.752,0,0,1,.514-.153,2.527,2.527,0,0,1,1.071,0,2.654,2.654,0,0,1,.983.4,4.139,4.139,0,0,1,.415.339,2.79,2.79,0,0,1,.8,1.934,3.069,3.069,0,0,1-.055.535,2.779,2.779,0,0,1-.153.514,3.885,3.885,0,0,1-.251.47,4.02,4.02,0,0,1-.339.415,4.138,4.138,0,0,1-.415.339,2.722,2.722,0,0,1-1.519.459" transform="translate(9.753 -15.532)" fill-rule="evenodd"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 31 KiB

@@ -1,170 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1041.277" height="554.141" viewBox="0 0 1041.277 554.141">
<title>Powered by React</title>
<g id="Group_24" data-name="Group 24" transform="translate(-440 -263)">
<g id="Group_23" data-name="Group 23" transform="translate(439.989 262.965)">
<path id="Path_299" data-name="Path 299" d="M1040.82,611.12q-1.74,3.75-3.47,7.4-2.7,5.67-5.33,11.12c-.78,1.61-1.56,3.19-2.32,4.77-8.6,17.57-16.63,33.11-23.45,45.89A73.21,73.21,0,0,1,942.44,719l-151.65,1.65h-1.6l-13,.14-11.12.12-34.1.37h-1.38l-17.36.19h-.53l-107,1.16-95.51,1-11.11.12-69,.75H429l-44.75.48h-.48l-141.5,1.53-42.33.46a87.991,87.991,0,0,1-10.79-.54h0c-1.22-.14-2.44-.3-3.65-.49a87.38,87.38,0,0,1-51.29-27.54C116,678.37,102.75,655,93.85,629.64q-1.93-5.49-3.6-11.12C59.44,514.37,97,380,164.6,290.08q4.25-5.64,8.64-11l.07-.08c20.79-25.52,44.1-46.84,68.93-62,44-26.91,92.75-34.49,140.7-11.9,40.57,19.12,78.45,28.11,115.17,30.55,3.71.24,7.42.42,11.11.53,84.23,2.65,163.17-27.7,255.87-47.29,3.69-.78,7.39-1.55,11.12-2.28,66.13-13.16,139.49-20.1,226.73-5.51a189.089,189.089,0,0,1,26.76,6.4q5.77,1.86,11.12,4c41.64,16.94,64.35,48.24,74,87.46q1.37,5.46,2.37,11.11C1134.3,384.41,1084.19,518.23,1040.82,611.12Z" transform="translate(-79.34 -172.91)" fill="#f2f2f2"/>
<path id="Path_300" data-name="Path 300" d="M576.36,618.52a95.21,95.21,0,0,1-1.87,11.12h93.7V618.52Zm-78.25,62.81,11.11-.09V653.77c-3.81-.17-7.52-.34-11.11-.52ZM265.19,618.52v11.12h198.5V618.52ZM1114.87,279h-74V191.51q-5.35-2.17-11.12-4V279H776.21V186.58c-3.73.73-7.43,1.5-11.12,2.28V279H509.22V236.15c-3.69-.11-7.4-.29-11.11-.53V279H242.24V217c-24.83,15.16-48.14,36.48-68.93,62h-.07v.08q-4.4,5.4-8.64,11h8.64V618.52h-83q1.66,5.63,3.6,11.12h79.39v93.62a87,87,0,0,0,12.2,2.79c1.21.19,2.43.35,3.65.49h0a87.991,87.991,0,0,0,10.79.54l42.33-.46v-97H498.11v94.21l11.11-.12V629.64H765.09V721l11.12-.12V629.64H1029.7v4.77c.76-1.58,1.54-3.16,2.32-4.77q2.63-5.45,5.33-11.12,1.73-3.64,3.47-7.4v-321h76.42Q1116.23,284.43,1114.87,279ZM242.24,618.52V290.08H498.11V618.52Zm267,0V290.08H765.09V618.52Zm520.48,0H776.21V290.08H1029.7Z" transform="translate(-79.34 -172.91)" opacity="0.1"/>
<path id="Path_301" data-name="Path 301" d="M863.09,533.65v13l-151.92,1.4-1.62.03-57.74.53-1.38.02-17.55.15h-.52l-106.98.99L349.77,551.4h-.15l-44.65.42-.48.01-198.4,1.82v-15l46.65-28,93.6-.78,2-.01.66-.01,2-.03,44.94-.37,2.01-.01.64-.01,2-.01L315,509.3l.38-.01,35.55-.3h.29l277.4-2.34,6.79-.05h.68l5.18-.05,37.65-.31,2-.03,1.85-.02h.96l11.71-.09,2.32-.03,3.11-.02,9.75-.09,15.47-.13,2-.02,3.48-.02h.65l74.71-.64Z" fill="#65617d"/>
<path id="Path_302" data-name="Path 302" d="M863.09,533.65v13l-151.92,1.4-1.62.03-57.74.53-1.38.02-17.55.15h-.52l-106.98.99L349.77,551.4h-.15l-44.65.42-.48.01-198.4,1.82v-15l46.65-28,93.6-.78,2-.01.66-.01,2-.03,44.94-.37,2.01-.01.64-.01,2-.01L315,509.3l.38-.01,35.55-.3h.29l277.4-2.34,6.79-.05h.68l5.18-.05,37.65-.31,2-.03,1.85-.02h.96l11.71-.09,2.32-.03,3.11-.02,9.75-.09,15.47-.13,2-.02,3.48-.02h.65l74.71-.64Z" opacity="0.2"/>
<path id="Path_303" data-name="Path 303" d="M375.44,656.57v24.49a6.13,6.13,0,0,1-3.5,5.54,6,6,0,0,1-2.5.6l-34.9.74a6,6,0,0,1-2.7-.57,6.12,6.12,0,0,1-3.57-5.57V656.57Z" transform="translate(-79.34 -172.91)" fill="#3f3d56"/>
<path id="Path_304" data-name="Path 304" d="M375.44,656.57v24.49a6.13,6.13,0,0,1-3.5,5.54,6,6,0,0,1-2.5.6l-34.9.74a6,6,0,0,1-2.7-.57,6.12,6.12,0,0,1-3.57-5.57V656.57Z" transform="translate(-79.34 -172.91)" opacity="0.1"/>
<path id="Path_305" data-name="Path 305" d="M377.44,656.57v24.49a6.13,6.13,0,0,1-3.5,5.54,6,6,0,0,1-2.5.6l-34.9.74a6,6,0,0,1-2.7-.57,6.12,6.12,0,0,1-3.57-5.57V656.57Z" transform="translate(-79.34 -172.91)" fill="#3f3d56"/>
<rect id="Rectangle_137" data-name="Rectangle 137" width="47.17" height="31.5" transform="translate(680.92 483.65)" fill="#3f3d56"/>
<rect id="Rectangle_138" data-name="Rectangle 138" width="47.17" height="31.5" transform="translate(680.92 483.65)" opacity="0.1"/>
<rect id="Rectangle_139" data-name="Rectangle 139" width="47.17" height="31.5" transform="translate(678.92 483.65)" fill="#3f3d56"/>
<path id="Path_306" data-name="Path 306" d="M298.09,483.65v4.97l-47.17,1.26v-6.23Z" opacity="0.1"/>
<path id="Path_307" data-name="Path 307" d="M460.69,485.27v168.2a4,4,0,0,1-3.85,3.95l-191.65,5.1h-.05a4,4,0,0,1-3.95-3.95V485.27a4,4,0,0,1,3.95-3.95h191.6a4,4,0,0,1,3.95,3.95Z" transform="translate(-79.34 -172.91)" fill="#65617d"/>
<path id="Path_308" data-name="Path 308" d="M265.19,481.32v181.2h-.05a4,4,0,0,1-3.95-3.95V485.27a4,4,0,0,1,3.95-3.95Z" transform="translate(-79.34 -172.91)" opacity="0.1"/>
<path id="Path_309" data-name="Path 309" d="M194.59,319.15h177.5V467.4l-177.5,4Z" fill="#39374d"/>
<path id="Path_310" data-name="Path 310" d="M726.09,483.65v6.41l-47.17-1.26v-5.15Z" opacity="0.1"/>
<path id="Path_311" data-name="Path 311" d="M867.69,485.27v173.3a4,4,0,0,1-4,3.95h0L672,657.42a4,4,0,0,1-3.85-3.95V485.27a4,4,0,0,1,3.95-3.95H863.7a4,4,0,0,1,3.99,3.95Z" transform="translate(-79.34 -172.91)" fill="#65617d"/>
<path id="Path_312" data-name="Path 312" d="M867.69,485.27v173.3a4,4,0,0,1-4,3.95h0V481.32h0a4,4,0,0,1,4,3.95Z" transform="translate(-79.34 -172.91)" opacity="0.1"/>
<path id="Path_313" data-name="Path 313" d="M775.59,319.15H598.09V467.4l177.5,4Z" fill="#39374d"/>
<path id="Path_314" data-name="Path 314" d="M663.19,485.27v168.2a4,4,0,0,1-3.85,3.95l-191.65,5.1h0a4,4,0,0,1-4-3.95V485.27a4,4,0,0,1,3.95-3.95h191.6A4,4,0,0,1,663.19,485.27Z" transform="translate(-79.34 -172.91)" fill="#65617d"/>
<path id="Path_315" data-name="Path 315" d="M397.09,319.15h177.5V467.4l-177.5,4Z" fill="#4267b2"/>
<path id="Path_316" data-name="Path 316" d="M863.09,533.65v13l-151.92,1.4-1.62.03-57.74.53-1.38.02-17.55.15h-.52l-106.98.99L349.77,551.4h-.15l-44.65.42-.48.01-198.4,1.82v-15l202.51-1.33h.48l40.99-.28h.19l283.08-1.87h.29l.17-.01h.47l4.79-.03h1.46l74.49-.5,4.4-.02.98-.01Z" opacity="0.1"/>
<circle id="Ellipse_111" data-name="Ellipse 111" cx="51.33" cy="51.33" r="51.33" transform="translate(435.93 246.82)" fill="#fbbebe"/>
<path id="Path_317" data-name="Path 317" d="M617.94,550.07s-99.5,12-90,0c3.44-4.34,4.39-17.2,4.2-31.85-.06-4.45-.22-9.06-.45-13.65-1.1-22-3.75-43.5-3.75-43.5s87-41,77-8.5c-4,13.13-2.69,31.57.35,48.88.89,5.05,1.92,10,3,14.7a344.66,344.66,0,0,0,9.65,33.92Z" transform="translate(-79.34 -172.91)" fill="#fbbebe"/>
<path id="Path_318" data-name="Path 318" d="M585.47,546c11.51-2.13,23.7-6,34.53-1.54,2.85,1.17,5.47,2.88,8.39,3.86s6.12,1.22,9.16,1.91c10.68,2.42,19.34,10.55,24.9,20s8.44,20.14,11.26,30.72l6.9,25.83c6,22.45,12,45.09,13.39,68.3a2437.506,2437.506,0,0,1-250.84,1.43c5.44-10.34,11-21.31,10.54-33s-7.19-23.22-4.76-34.74c1.55-7.34,6.57-13.39,9.64-20.22,8.75-19.52,1.94-45.79,17.32-60.65,6.92-6.68,17-9.21,26.63-8.89,12.28.41,24.85,4.24,37,6.11C555.09,547.48,569.79,548.88,585.47,546Z" transform="translate(-79.34 -172.91)" fill="#ff6584"/>
<path id="Path_319" data-name="Path 319" d="M716.37,657.17l-.1,1.43v.1l-.17,2.3-1.33,18.51-1.61,22.3-.46,6.28-1,13.44v.17l-107,1-175.59,1.9v.84h-.14v-1.12l.45-14.36.86-28.06.74-23.79.07-2.37a10.53,10.53,0,0,1,11.42-10.17c4.72.4,10.85.89,18.18,1.41l3,.22c42.33,2.94,120.56,6.74,199.5,2,1.66-.09,3.33-.19,5-.31,12.24-.77,24.47-1.76,36.58-3a10.53,10.53,0,0,1,11.6,11.23Z" transform="translate(-79.34 -172.91)" opacity="0.1"/>
<path id="Path_320" data-name="Path 320" d="M429.08,725.44v-.84l175.62-1.91,107-1h.3v-.17l1-13.44.43-6,1.64-22.61,1.29-17.9v-.44a10.617,10.617,0,0,0-.11-2.47.3.3,0,0,0,0-.1,10.391,10.391,0,0,0-2-4.64,10.54,10.54,0,0,0-9.42-4c-12.11,1.24-24.34,2.23-36.58,3-1.67.12-3.34.22-5,.31-78.94,4.69-157.17.89-199.5-2l-3-.22c-7.33-.52-13.46-1-18.18-1.41a10.54,10.54,0,0,0-11.24,8.53,11,11,0,0,0-.18,1.64l-.68,22.16L429.54,710l-.44,14.36v1.12Z" transform="translate(-79.34 -172.91)" fill="#3f3d56"/>
<path id="Path_321" data-name="Path 321" d="M716.67,664.18l-1.23,15.33-1.83,22.85-.46,5.72-1,12.81-.06.64v.17h0l-.15,1.48.11-1.48h-.29l-107,1-175.65,1.9v-.28l.49-14.36,1-28.06.64-18.65A6.36,6.36,0,0,1,434.3,658a6.25,6.25,0,0,1,3.78-.9c2.1.17,4.68.37,7.69.59,4.89.36,10.92.78,17.94,1.22,13,.82,29.31,1.7,48,2.42,52,2,122.2,2.67,188.88-3.17,3-.26,6.1-.55,9.13-.84a6.26,6.26,0,0,1,3.48.66,5.159,5.159,0,0,1,.86.54,6.14,6.14,0,0,1,2,2.46,3.564,3.564,0,0,1,.25.61A6.279,6.279,0,0,1,716.67,664.18Z" transform="translate(-79.34 -172.91)" opacity="0.1"/>
<path id="Path_322" data-name="Path 322" d="M377.44,677.87v3.19a6.13,6.13,0,0,1-3.5,5.54l-40.1.77a6.12,6.12,0,0,1-3.57-5.57v-3Z" transform="translate(-79.34 -172.91)" opacity="0.1"/>
<path id="Path_323" data-name="Path 323" d="M298.59,515.57l-52.25,1V507.9l52.25-1Z" fill="#3f3d56"/>
<path id="Path_324" data-name="Path 324" d="M298.59,515.57l-52.25,1V507.9l52.25-1Z" opacity="0.1"/>
<path id="Path_325" data-name="Path 325" d="M300.59,515.57l-52.25,1V507.9l52.25-1Z" fill="#3f3d56"/>
<path id="Path_326" data-name="Path 326" d="M758.56,679.87v3.19a6.13,6.13,0,0,0,3.5,5.54l40.1.77a6.12,6.12,0,0,0,3.57-5.57v-3Z" transform="translate(-79.34 -172.91)" opacity="0.1"/>
<path id="Path_327" data-name="Path 327" d="M678.72,517.57l52.25,1V509.9l-52.25-1Z" opacity="0.1"/>
<path id="Path_328" data-name="Path 328" d="M676.72,517.57l52.25,1V509.9l-52.25-1Z" fill="#3f3d56"/>
<path id="Path_329" data-name="Path 329" d="M534.13,486.79c.08,7-3.16,13.6-5.91,20.07a163.491,163.491,0,0,0-12.66,74.71c.73,11,2.58,22,.73,32.9s-8.43,21.77-19,24.9c17.53,10.45,41.26,9.35,57.76-2.66,8.79-6.4,15.34-15.33,21.75-24.11a97.86,97.86,0,0,1-13.31,44.75A103.43,103.43,0,0,0,637,616.53c4.31-5.81,8.06-12.19,9.72-19.23,3.09-13-1.22-26.51-4.51-39.5a266.055,266.055,0,0,1-6.17-33c-.43-3.56-.78-7.22.1-10.7,1-4.07,3.67-7.51,5.64-11.22,5.6-10.54,5.73-23.3,2.86-34.88s-8.49-22.26-14.06-32.81c-4.46-8.46-9.3-17.31-17.46-22.28-5.1-3.1-11-4.39-16.88-5.64l-25.37-5.43c-5.55-1.19-11.26-2.38-16.87-1.51-9.47,1.48-16.14,8.32-22,15.34-4.59,5.46-15.81,15.71-16.6,22.86-.72,6.59,5.1,17.63,6.09,24.58,1.3,9,2.22,6,7.3,11.52C532,478.05,534.07,482,534.13,486.79Z" transform="translate(-79.34 -172.91)" fill="#3f3d56"/>
</g>
<g id="docusaurus_keytar" transform="translate(670.271 615.768)">
<path id="Path_40" data-name="Path 40" d="M99,52h43.635V69.662H99Z" transform="translate(-49.132 -33.936)" fill="#fff" fill-rule="evenodd"/>
<path id="Path_41" data-name="Path 41" d="M13.389,158.195A10.377,10.377,0,0,1,4.4,153a10.377,10.377,0,0,0,8.988,15.584H23.779V158.195Z" transform="translate(-3 -82.47)" fill="#3ecc5f" fill-rule="evenodd"/>
<path id="Path_42" data-name="Path 42" d="M66.967,38.083l36.373-2.273V30.615A10.389,10.389,0,0,0,92.95,20.226H46.2l-1.3-2.249a1.5,1.5,0,0,0-2.6,0L41,20.226l-1.3-2.249a1.5,1.5,0,0,0-2.6,0l-1.3,2.249-1.3-2.249a1.5,1.5,0,0,0-2.6,0l-1.3,2.249-.034,0-2.152-2.151a1.5,1.5,0,0,0-2.508.672L25.21,21.4l-2.7-.723a1.5,1.5,0,0,0-1.836,1.837l.722,2.7-2.65.71a1.5,1.5,0,0,0-.673,2.509l2.152,2.152c0,.011,0,.022,0,.033l-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3-2.249,1.3a1.5,1.5,0,0,0,0,2.6L20.226,41l-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3-2.249,1.3a1.5,1.5,0,0,0,0,2.6l2.249,1.3A10.389,10.389,0,0,0,30.615,103.34H92.95A10.389,10.389,0,0,0,103.34,92.95V51.393L66.967,49.12a5.53,5.53,0,0,1,0-11.038" transform="translate(-9.836 -17.226)" fill="#3ecc5f" fill-rule="evenodd"/>
<path id="Path_43" data-name="Path 43" d="M143,163.779h15.584V143H143Z" transform="translate(-70.275 -77.665)" fill="#3ecc5f" fill-rule="evenodd"/>
<path id="Path_44" data-name="Path 44" d="M173.779,148.389a2.582,2.582,0,0,0-.332.033c-.02-.078-.038-.156-.06-.234a2.594,2.594,0,1,0-2.567-4.455q-.086-.088-.174-.175a2.593,2.593,0,1,0-4.461-2.569c-.077-.022-.154-.04-.231-.06a2.6,2.6,0,1,0-5.128,0c-.077.02-.154.038-.231.06a2.594,2.594,0,1,0-4.461,2.569,10.384,10.384,0,1,0,17.314,9.992,2.592,2.592,0,1,0,.332-5.161" transform="translate(-75.08 -75.262)" fill="#44d860" fill-rule="evenodd"/>
<path id="Path_45" data-name="Path 45" d="M153,113.389h15.584V103H153Z" transform="translate(-75.08 -58.444)" fill="#3ecc5f" fill-rule="evenodd"/>
<path id="Path_46" data-name="Path 46" d="M183.389,108.944a1.3,1.3,0,1,0,0-2.6,1.336,1.336,0,0,0-.166.017c-.01-.039-.019-.078-.03-.117a1.3,1.3,0,0,0-.5-2.5,1.285,1.285,0,0,0-.783.269q-.043-.044-.087-.087a1.285,1.285,0,0,0,.263-.776,1.3,1.3,0,0,0-2.493-.509,5.195,5.195,0,1,0,0,10,1.3,1.3,0,0,0,2.493-.509,1.285,1.285,0,0,0-.263-.776q.044-.043.087-.087a1.285,1.285,0,0,0,.783.269,1.3,1.3,0,0,0,.5-2.5c.011-.038.02-.078.03-.117a1.337,1.337,0,0,0,.166.017" transform="translate(-84.691 -57.894)" fill="#44d860" fill-rule="evenodd"/>
<path id="Path_47" data-name="Path 47" d="M52.188,48.292a1.3,1.3,0,0,1-1.3-1.3,3.9,3.9,0,0,0-7.792,0,1.3,1.3,0,1,1-2.6,0,6.493,6.493,0,0,1,12.987,0,1.3,1.3,0,0,1-1.3,1.3" transform="translate(-21.02 -28.41)" fill-rule="evenodd"/>
<path id="Path_48" data-name="Path 48" d="M103,139.752h31.168a10.389,10.389,0,0,0,10.389-10.389V93H113.389A10.389,10.389,0,0,0,103,103.389Z" transform="translate(-51.054 -53.638)" fill="#ffff50" fill-rule="evenodd"/>
<path id="Path_49" data-name="Path 49" d="M141.1,94.017H115.106a.519.519,0,1,1,0-1.039H141.1a.519.519,0,0,1,0,1.039m0,10.389H115.106a.519.519,0,1,1,0-1.039H141.1a.519.519,0,0,1,0,1.039m0,10.389H115.106a.519.519,0,1,1,0-1.039H141.1a.519.519,0,0,1,0,1.039m0-25.877H115.106a.519.519,0,1,1,0-1.039H141.1a.519.519,0,0,1,0,1.039m0,10.293H115.106a.519.519,0,1,1,0-1.039H141.1a.519.519,0,0,1,0,1.039m0,10.389H115.106a.519.519,0,1,1,0-1.039H141.1a.519.519,0,0,1,0,1.039m7.782-47.993c-.006,0-.011,0-.018,0-1.605.055-2.365,1.66-3.035,3.077-.7,1.48-1.24,2.443-2.126,2.414-.981-.035-1.542-1.144-2.137-2.317-.683-1.347-1.462-2.876-3.1-2.819-1.582.054-2.344,1.451-3.017,2.684-.715,1.313-1.2,2.112-2.141,2.075-1-.036-1.533-.938-2.149-1.981-.686-1.162-1.479-2.467-3.084-2.423-1.555.053-2.319,1.239-2.994,2.286-.713,1.106-1.213,1.781-2.164,1.741-1.025-.036-1.554-.784-2.167-1.65-.688-.973-1.463-2.074-3.062-2.021a3.815,3.815,0,0,0-2.959,1.879c-.64.812-1.14,1.456-2.2,1.415a.52.52,0,0,0-.037,1.039,3.588,3.588,0,0,0,3.05-1.811c.611-.777,1.139-1.448,2.178-1.483,1-.043,1.47.579,2.179,1.582.674.953,1.438,2.033,2.977,2.089,1.612.054,2.387-1.151,3.074-2.217.614-.953,1.144-1.775,2.156-1.81.931-.035,1.438.7,2.153,1.912.674,1.141,1.437,2.434,3.006,2.491,1.623.056,2.407-1.361,3.09-2.616.592-1.085,1.15-2.109,2.14-2.143.931-.022,1.417.829,2.135,2.249.671,1.326,1.432,2.828,3.026,2.886l.088,0c1.592,0,2.347-1.6,3.015-3.01.592-1.252,1.152-2.431,2.113-2.479Z" transform="translate(-55.378 -38.552)" fill-rule="evenodd"/>
<path id="Path_50" data-name="Path 50" d="M83,163.779h20.779V143H83Z" transform="translate(-41.443 -77.665)" fill="#3ecc5f" fill-rule="evenodd"/>
<g id="Group_8" data-name="Group 8" transform="matrix(0.966, -0.259, 0.259, 0.966, 51.971, 43.3)">
<rect id="Rectangle_3" data-name="Rectangle 3" width="43.906" height="17.333" rx="2" transform="translate(0 0)" fill="#d8d8d8"/>
<g id="Group_2" data-name="Group 2" transform="translate(0.728 10.948)">
<rect id="Rectangle_4" data-name="Rectangle 4" width="2.537" height="2.537" rx="1" transform="translate(7.985 0)" fill="#4a4a4a"/>
<rect id="Rectangle_5" data-name="Rectangle 5" width="2.537" height="2.537" rx="1" transform="translate(10.991 0)" fill="#4a4a4a"/>
<rect id="Rectangle_6" data-name="Rectangle 6" width="2.537" height="2.537" rx="1" transform="translate(13.997 0)" fill="#4a4a4a"/>
<rect id="Rectangle_7" data-name="Rectangle 7" width="2.537" height="2.537" rx="1" transform="translate(17.003 0)" fill="#4a4a4a"/>
<rect id="Rectangle_8" data-name="Rectangle 8" width="2.537" height="2.537" rx="1" transform="translate(20.009 0)" fill="#4a4a4a"/>
<rect id="Rectangle_9" data-name="Rectangle 9" width="2.537" height="2.537" rx="1" transform="translate(23.015 0)" fill="#4a4a4a"/>
<rect id="Rectangle_10" data-name="Rectangle 10" width="2.537" height="2.537" rx="1" transform="translate(26.021 0)" fill="#4a4a4a"/>
<rect id="Rectangle_11" data-name="Rectangle 11" width="2.537" height="2.537" rx="1" transform="translate(29.028 0)" fill="#4a4a4a"/>
<rect id="Rectangle_12" data-name="Rectangle 12" width="2.537" height="2.537" rx="1" transform="translate(32.034 0)" fill="#4a4a4a"/>
<path id="Path_51" data-name="Path 51" d="M.519,0H6.9A.519.519,0,0,1,7.421.52v1.5a.519.519,0,0,1-.519.519H.519A.519.519,0,0,1,0,2.017V.519A.519.519,0,0,1,.519,0ZM35.653,0h6.383a.519.519,0,0,1,.519.519v1.5a.519.519,0,0,1-.519.519H35.652a.519.519,0,0,1-.519-.519V.519A.519.519,0,0,1,35.652,0Z" transform="translate(0 0)" fill="#4a4a4a" fill-rule="evenodd"/>
</g>
<g id="Group_3" data-name="Group 3" transform="translate(0.728 4.878)">
<path id="Path_52" data-name="Path 52" d="M.519,0H2.956a.519.519,0,0,1,.519.519v1.5a.519.519,0,0,1-.519.519H.519A.519.519,0,0,1,0,2.017V.519A.519.519,0,0,1,.519,0Z" transform="translate(0 0)" fill="#4a4a4a" fill-rule="evenodd"/>
<rect id="Rectangle_13" data-name="Rectangle 13" width="2.537" height="2.537" rx="1" transform="translate(3.945 0)" fill="#4a4a4a"/>
<rect id="Rectangle_14" data-name="Rectangle 14" width="2.537" height="2.537" rx="1" transform="translate(6.951 0)" fill="#4a4a4a"/>
<rect id="Rectangle_15" data-name="Rectangle 15" width="2.537" height="2.537" rx="1" transform="translate(9.958 0)" fill="#4a4a4a"/>
<rect id="Rectangle_16" data-name="Rectangle 16" width="2.537" height="2.537" rx="1" transform="translate(12.964 0)" fill="#4a4a4a"/>
<rect id="Rectangle_17" data-name="Rectangle 17" width="2.537" height="2.537" rx="1" transform="translate(15.97 0)" fill="#4a4a4a"/>
<rect id="Rectangle_18" data-name="Rectangle 18" width="2.537" height="2.537" rx="1" transform="translate(18.976 0)" fill="#4a4a4a"/>
<rect id="Rectangle_19" data-name="Rectangle 19" width="2.537" height="2.537" rx="1" transform="translate(21.982 0)" fill="#4a4a4a"/>
<rect id="Rectangle_20" data-name="Rectangle 20" width="2.537" height="2.537" rx="1" transform="translate(24.988 0)" fill="#4a4a4a"/>
<rect id="Rectangle_21" data-name="Rectangle 21" width="2.537" height="2.537" rx="1" transform="translate(27.994 0)" fill="#4a4a4a"/>
<rect id="Rectangle_22" data-name="Rectangle 22" width="2.537" height="2.537" rx="1" transform="translate(31 0)" fill="#4a4a4a"/>
<rect id="Rectangle_23" data-name="Rectangle 23" width="2.537" height="2.537" rx="1" transform="translate(34.006 0)" fill="#4a4a4a"/>
<rect id="Rectangle_24" data-name="Rectangle 24" width="2.537" height="2.537" rx="1" transform="translate(37.012 0)" fill="#4a4a4a"/>
<rect id="Rectangle_25" data-name="Rectangle 25" width="2.537" height="2.537" rx="1" transform="translate(40.018 0)" fill="#4a4a4a"/>
</g>
<g id="Group_4" data-name="Group 4" transform="translate(43.283 4.538) rotate(180)">
<path id="Path_53" data-name="Path 53" d="M.519,0H2.956a.519.519,0,0,1,.519.519v1.5a.519.519,0,0,1-.519.519H.519A.519.519,0,0,1,0,2.017V.519A.519.519,0,0,1,.519,0Z" transform="translate(0 0)" fill="#4a4a4a" fill-rule="evenodd"/>
<rect id="Rectangle_26" data-name="Rectangle 26" width="2.537" height="2.537" rx="1" transform="translate(3.945 0)" fill="#4a4a4a"/>
<rect id="Rectangle_27" data-name="Rectangle 27" width="2.537" height="2.537" rx="1" transform="translate(6.951 0)" fill="#4a4a4a"/>
<rect id="Rectangle_28" data-name="Rectangle 28" width="2.537" height="2.537" rx="1" transform="translate(9.958 0)" fill="#4a4a4a"/>
<rect id="Rectangle_29" data-name="Rectangle 29" width="2.537" height="2.537" rx="1" transform="translate(12.964 0)" fill="#4a4a4a"/>
<rect id="Rectangle_30" data-name="Rectangle 30" width="2.537" height="2.537" rx="1" transform="translate(15.97 0)" fill="#4a4a4a"/>
<rect id="Rectangle_31" data-name="Rectangle 31" width="2.537" height="2.537" rx="1" transform="translate(18.976 0)" fill="#4a4a4a"/>
<rect id="Rectangle_32" data-name="Rectangle 32" width="2.537" height="2.537" rx="1" transform="translate(21.982 0)" fill="#4a4a4a"/>
<rect id="Rectangle_33" data-name="Rectangle 33" width="2.537" height="2.537" rx="1" transform="translate(24.988 0)" fill="#4a4a4a"/>
<rect id="Rectangle_34" data-name="Rectangle 34" width="2.537" height="2.537" rx="1" transform="translate(27.994 0)" fill="#4a4a4a"/>
<rect id="Rectangle_35" data-name="Rectangle 35" width="2.537" height="2.537" rx="1" transform="translate(31.001 0)" fill="#4a4a4a"/>
<rect id="Rectangle_36" data-name="Rectangle 36" width="2.537" height="2.537" rx="1" transform="translate(34.007 0)" fill="#4a4a4a"/>
<rect id="Rectangle_37" data-name="Rectangle 37" width="2.537" height="2.537" rx="1" transform="translate(37.013 0)" fill="#4a4a4a"/>
<rect id="Rectangle_38" data-name="Rectangle 38" width="2.537" height="2.537" rx="1" transform="translate(40.018 0)" fill="#4a4a4a"/>
<rect id="Rectangle_39" data-name="Rectangle 39" width="2.537" height="2.537" rx="1" transform="translate(3.945 0)" fill="#4a4a4a"/>
<rect id="Rectangle_40" data-name="Rectangle 40" width="2.537" height="2.537" rx="1" transform="translate(6.951 0)" fill="#4a4a4a"/>
<rect id="Rectangle_41" data-name="Rectangle 41" width="2.537" height="2.537" rx="1" transform="translate(9.958 0)" fill="#4a4a4a"/>
<rect id="Rectangle_42" data-name="Rectangle 42" width="2.537" height="2.537" rx="1" transform="translate(12.964 0)" fill="#4a4a4a"/>
<rect id="Rectangle_43" data-name="Rectangle 43" width="2.537" height="2.537" rx="1" transform="translate(15.97 0)" fill="#4a4a4a"/>
<rect id="Rectangle_44" data-name="Rectangle 44" width="2.537" height="2.537" rx="1" transform="translate(18.976 0)" fill="#4a4a4a"/>
<rect id="Rectangle_45" data-name="Rectangle 45" width="2.537" height="2.537" rx="1" transform="translate(21.982 0)" fill="#4a4a4a"/>
<rect id="Rectangle_46" data-name="Rectangle 46" width="2.537" height="2.537" rx="1" transform="translate(24.988 0)" fill="#4a4a4a"/>
<rect id="Rectangle_47" data-name="Rectangle 47" width="2.537" height="2.537" rx="1" transform="translate(27.994 0)" fill="#4a4a4a"/>
<rect id="Rectangle_48" data-name="Rectangle 48" width="2.537" height="2.537" rx="1" transform="translate(31.001 0)" fill="#4a4a4a"/>
<rect id="Rectangle_49" data-name="Rectangle 49" width="2.537" height="2.537" rx="1" transform="translate(34.007 0)" fill="#4a4a4a"/>
<rect id="Rectangle_50" data-name="Rectangle 50" width="2.537" height="2.537" rx="1" transform="translate(37.013 0)" fill="#4a4a4a"/>
<rect id="Rectangle_51" data-name="Rectangle 51" width="2.537" height="2.537" rx="1" transform="translate(40.018 0)" fill="#4a4a4a"/>
</g>
<g id="Group_6" data-name="Group 6" transform="translate(0.728 7.883)">
<path id="Path_54" data-name="Path 54" d="M.519,0h3.47a.519.519,0,0,1,.519.519v1.5a.519.519,0,0,1-.519.519H.519A.519.519,0,0,1,0,2.017V.52A.519.519,0,0,1,.519,0Z" transform="translate(0 0)" fill="#4a4a4a" fill-rule="evenodd"/>
<g id="Group_5" data-name="Group 5" transform="translate(5.073 0)">
<rect id="Rectangle_52" data-name="Rectangle 52" width="2.537" height="2.537" rx="1" transform="translate(0 0)" fill="#4a4a4a"/>
<rect id="Rectangle_53" data-name="Rectangle 53" width="2.537" height="2.537" rx="1" transform="translate(3.006 0)" fill="#4a4a4a"/>
<rect id="Rectangle_54" data-name="Rectangle 54" width="2.537" height="2.537" rx="1" transform="translate(6.012 0)" fill="#4a4a4a"/>
<rect id="Rectangle_55" data-name="Rectangle 55" width="2.537" height="2.537" rx="1" transform="translate(9.018 0)" fill="#4a4a4a"/>
<rect id="Rectangle_56" data-name="Rectangle 56" width="2.537" height="2.537" rx="1" transform="translate(12.025 0)" fill="#4a4a4a"/>
<rect id="Rectangle_57" data-name="Rectangle 57" width="2.537" height="2.537" rx="1" transform="translate(15.031 0)" fill="#4a4a4a"/>
<rect id="Rectangle_58" data-name="Rectangle 58" width="2.537" height="2.537" rx="1" transform="translate(18.037 0)" fill="#4a4a4a"/>
<rect id="Rectangle_59" data-name="Rectangle 59" width="2.537" height="2.537" rx="1" transform="translate(21.042 0)" fill="#4a4a4a"/>
<rect id="Rectangle_60" data-name="Rectangle 60" width="2.537" height="2.537" rx="1" transform="translate(24.049 0)" fill="#4a4a4a"/>
<rect id="Rectangle_61" data-name="Rectangle 61" width="2.537" height="2.537" rx="1" transform="translate(27.055 0)" fill="#4a4a4a"/>
<rect id="Rectangle_62" data-name="Rectangle 62" width="2.537" height="2.537" rx="1" transform="translate(30.061 0)" fill="#4a4a4a"/>
</g>
<path id="Path_55" data-name="Path 55" d="M.52,0H3.8a.519.519,0,0,1,.519.519v1.5a.519.519,0,0,1-.519.519H.519A.519.519,0,0,1,0,2.017V.52A.519.519,0,0,1,.519,0Z" transform="translate(38.234 0)" fill="#4a4a4a" fill-rule="evenodd"/>
</g>
<g id="Group_7" data-name="Group 7" transform="translate(0.728 14.084)">
<rect id="Rectangle_63" data-name="Rectangle 63" width="2.537" height="2.537" rx="1" transform="translate(0 0)" fill="#4a4a4a"/>
<rect id="Rectangle_64" data-name="Rectangle 64" width="2.537" height="2.537" rx="1" transform="translate(3.006 0)" fill="#4a4a4a"/>
<rect id="Rectangle_65" data-name="Rectangle 65" width="2.537" height="2.537" rx="1" transform="translate(6.012 0)" fill="#4a4a4a"/>
<rect id="Rectangle_66" data-name="Rectangle 66" width="2.537" height="2.537" rx="1" transform="translate(9.018 0)" fill="#4a4a4a"/>
<path id="Path_56" data-name="Path 56" d="M.519,0H14.981A.519.519,0,0,1,15.5.519v1.5a.519.519,0,0,1-.519.519H.519A.519.519,0,0,1,0,2.018V.519A.519.519,0,0,1,.519,0Zm15.97,0h1.874a.519.519,0,0,1,.519.519v1.5a.519.519,0,0,1-.519.519H16.489a.519.519,0,0,1-.519-.519V.519A.519.519,0,0,1,16.489,0Z" transform="translate(12.024 0)" fill="#4a4a4a" fill-rule="evenodd"/>
<rect id="Rectangle_67" data-name="Rectangle 67" width="2.537" height="2.537" rx="1" transform="translate(31.376 0)" fill="#4a4a4a"/>
<rect id="Rectangle_68" data-name="Rectangle 68" width="2.537" height="2.537" rx="1" transform="translate(34.382 0)" fill="#4a4a4a"/>
<rect id="Rectangle_69" data-name="Rectangle 69" width="2.537" height="2.537" rx="1" transform="translate(40.018 0)" fill="#4a4a4a"/>
<path id="Path_57" data-name="Path 57" d="M2.537,0V.561a.519.519,0,0,1-.519.519H.519A.519.519,0,0,1,0,.561V0Z" transform="translate(39.736 1.08) rotate(180)" fill="#4a4a4a"/>
<path id="Path_58" data-name="Path 58" d="M2.537,0V.561a.519.519,0,0,1-.519.519H.519A.519.519,0,0,1,0,.561V0Z" transform="translate(37.2 1.456)" fill="#4a4a4a"/>
</g>
<rect id="Rectangle_70" data-name="Rectangle 70" width="42.273" height="1.127" rx="0.564" transform="translate(0.915 0.556)" fill="#4a4a4a"/>
<rect id="Rectangle_71" data-name="Rectangle 71" width="2.37" height="0.752" rx="0.376" transform="translate(1.949 0.744)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_72" data-name="Rectangle 72" width="2.37" height="0.752" rx="0.376" transform="translate(5.193 0.744)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_73" data-name="Rectangle 73" width="2.37" height="0.752" rx="0.376" transform="translate(7.688 0.744)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_74" data-name="Rectangle 74" width="2.37" height="0.752" rx="0.376" transform="translate(10.183 0.744)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_75" data-name="Rectangle 75" width="2.37" height="0.752" rx="0.376" transform="translate(12.679 0.744)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_76" data-name="Rectangle 76" width="2.37" height="0.752" rx="0.376" transform="translate(15.797 0.744)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_77" data-name="Rectangle 77" width="2.37" height="0.752" rx="0.376" transform="translate(18.292 0.744)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_78" data-name="Rectangle 78" width="2.37" height="0.752" rx="0.376" transform="translate(20.788 0.744)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_79" data-name="Rectangle 79" width="2.37" height="0.752" rx="0.376" transform="translate(23.283 0.744)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_80" data-name="Rectangle 80" width="2.37" height="0.752" rx="0.376" transform="translate(26.402 0.744)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_81" data-name="Rectangle 81" width="2.37" height="0.752" rx="0.376" transform="translate(28.897 0.744)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_82" data-name="Rectangle 82" width="2.37" height="0.752" rx="0.376" transform="translate(31.393 0.744)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_83" data-name="Rectangle 83" width="2.37" height="0.752" rx="0.376" transform="translate(34.512 0.744)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_84" data-name="Rectangle 84" width="2.37" height="0.752" rx="0.376" transform="translate(37.007 0.744)" fill="#d8d8d8" opacity="0.136"/>
<rect id="Rectangle_85" data-name="Rectangle 85" width="2.37" height="0.752" rx="0.376" transform="translate(39.502 0.744)" fill="#d8d8d8" opacity="0.136"/>
</g>
<path id="Path_59" data-name="Path 59" d="M123.779,148.389a2.583,2.583,0,0,0-.332.033c-.02-.078-.038-.156-.06-.234a2.594,2.594,0,1,0-2.567-4.455q-.086-.088-.174-.175a2.593,2.593,0,1,0-4.461-2.569c-.077-.022-.154-.04-.231-.06a2.6,2.6,0,1,0-5.128,0c-.077.02-.154.038-.231.06a2.594,2.594,0,1,0-4.461,2.569,10.384,10.384,0,1,0,17.314,9.992,2.592,2.592,0,1,0,.332-5.161" transform="translate(-51.054 -75.262)" fill="#44d860" fill-rule="evenodd"/>
<path id="Path_60" data-name="Path 60" d="M83,113.389h20.779V103H83Z" transform="translate(-41.443 -58.444)" fill="#3ecc5f" fill-rule="evenodd"/>
<path id="Path_61" data-name="Path 61" d="M123.389,108.944a1.3,1.3,0,1,0,0-2.6,1.338,1.338,0,0,0-.166.017c-.01-.039-.019-.078-.03-.117a1.3,1.3,0,0,0-.5-2.5,1.285,1.285,0,0,0-.783.269q-.043-.044-.087-.087a1.285,1.285,0,0,0,.263-.776,1.3,1.3,0,0,0-2.493-.509,5.195,5.195,0,1,0,0,10,1.3,1.3,0,0,0,2.493-.509,1.285,1.285,0,0,0-.263-.776q.044-.043.087-.087a1.285,1.285,0,0,0,.783.269,1.3,1.3,0,0,0,.5-2.5c.011-.038.02-.078.03-.117a1.335,1.335,0,0,0,.166.017" transform="translate(-55.859 -57.894)" fill="#44d860" fill-rule="evenodd"/>
<path id="Path_62" data-name="Path 62" d="M141.8,38.745a1.41,1.41,0,0,1-.255-.026,1.309,1.309,0,0,1-.244-.073,1.349,1.349,0,0,1-.224-.119,1.967,1.967,0,0,1-.2-.161,1.52,1.52,0,0,1-.161-.2,1.282,1.282,0,0,1-.218-.722,1.41,1.41,0,0,1,.026-.255,1.5,1.5,0,0,1,.072-.244,1.364,1.364,0,0,1,.12-.223,1.252,1.252,0,0,1,.358-.358,1.349,1.349,0,0,1,.224-.119,1.309,1.309,0,0,1,.244-.073,1.2,1.2,0,0,1,.509,0,1.262,1.262,0,0,1,.468.192,1.968,1.968,0,0,1,.2.161,1.908,1.908,0,0,1,.161.2,1.322,1.322,0,0,1,.12.223,1.361,1.361,0,0,1,.1.5,1.317,1.317,0,0,1-.379.919,1.968,1.968,0,0,1-.2.161,1.346,1.346,0,0,1-.223.119,1.332,1.332,0,0,1-.5.1m10.389-.649a1.326,1.326,0,0,1-.92-.379,1.979,1.979,0,0,1-.161-.2,1.282,1.282,0,0,1-.218-.722,1.326,1.326,0,0,1,.379-.919,1.967,1.967,0,0,1,.2-.161,1.351,1.351,0,0,1,.224-.119,1.308,1.308,0,0,1,.244-.073,1.2,1.2,0,0,1,.509,0,1.262,1.262,0,0,1,.468.192,1.967,1.967,0,0,1,.2.161,1.326,1.326,0,0,1,.379.919,1.461,1.461,0,0,1-.026.255,1.323,1.323,0,0,1-.073.244,1.847,1.847,0,0,1-.119.223,1.911,1.911,0,0,1-.161.2,1.967,1.967,0,0,1-.2.161,1.294,1.294,0,0,1-.722.218" transform="translate(-69.074 -26.006)" fill-rule="evenodd"/>
</g>
<g id="React-icon" transform="translate(906.3 541.56)">
<path id="Path_330" data-name="Path 330" d="M263.668,117.179c0-5.827-7.3-11.35-18.487-14.775,2.582-11.4,1.434-20.477-3.622-23.382a7.861,7.861,0,0,0-4.016-1v4a4.152,4.152,0,0,1,2.044.466c2.439,1.4,3.5,6.724,2.672,13.574-.2,1.685-.52,3.461-.914,5.272a86.9,86.9,0,0,0-11.386-1.954,87.469,87.469,0,0,0-7.459-8.965c5.845-5.433,11.332-8.41,15.062-8.41V78h0c-4.931,0-11.386,3.514-17.913,9.611-6.527-6.061-12.982-9.539-17.913-9.539v4c3.712,0,9.216,2.959,15.062,8.356a84.687,84.687,0,0,0-7.405,8.947,83.732,83.732,0,0,0-11.4,1.972c-.412-1.793-.717-3.532-.932-5.2-.843-6.85.2-12.175,2.618-13.592a3.991,3.991,0,0,1,2.062-.466v-4h0a8,8,0,0,0-4.052,1c-5.039,2.9-6.168,11.96-3.568,23.328-11.153,3.443-18.415,8.947-18.415,14.757,0,5.828,7.3,11.35,18.487,14.775-2.582,11.4-1.434,20.477,3.622,23.382a7.882,7.882,0,0,0,4.034,1c4.931,0,11.386-3.514,17.913-9.611,6.527,6.061,12.982,9.539,17.913,9.539a8,8,0,0,0,4.052-1c5.039-2.9,6.168-11.96,3.568-23.328C256.406,128.511,263.668,122.988,263.668,117.179Zm-23.346-11.96c-.663,2.313-1.488,4.7-2.421,7.083-.735-1.434-1.506-2.869-2.349-4.3-.825-1.434-1.7-2.833-2.582-4.2C235.517,104.179,237.974,104.645,240.323,105.219Zm-8.212,19.1c-1.4,2.421-2.833,4.716-4.321,6.85-2.672.233-5.379.359-8.1.359-2.708,0-5.415-.126-8.069-.341q-2.232-3.2-4.339-6.814-2.044-3.523-3.73-7.136c1.112-2.4,2.367-4.805,3.712-7.154,1.4-2.421,2.833-4.716,4.321-6.85,2.672-.233,5.379-.359,8.1-.359,2.708,0,5.415.126,8.069.341q2.232,3.2,4.339,6.814,2.044,3.523,3.73,7.136C234.692,119.564,233.455,121.966,232.11,124.315Zm5.792-2.331c.968,2.4,1.793,4.805,2.474,7.136-2.349.574-4.823,1.058-7.387,1.434.879-1.381,1.757-2.8,2.582-4.25C236.4,124.871,237.167,123.419,237.9,121.984ZM219.72,141.116a73.921,73.921,0,0,1-4.985-5.738c1.614.072,3.263.126,4.931.126,1.685,0,3.353-.036,4.985-.126A69.993,69.993,0,0,1,219.72,141.116ZM206.38,130.555c-2.546-.377-5-.843-7.352-1.417.663-2.313,1.488-4.7,2.421-7.083.735,1.434,1.506,2.869,2.349,4.3S205.5,129.192,206.38,130.555ZM219.63,93.241a73.924,73.924,0,0,1,4.985,5.738c-1.614-.072-3.263-.126-4.931-.126-1.686,0-3.353.036-4.985.126A69.993,69.993,0,0,1,219.63,93.241ZM206.362,103.8c-.879,1.381-1.757,2.8-2.582,4.25-.825,1.434-1.6,2.869-2.331,4.3-.968-2.4-1.793-4.805-2.474-7.136C201.323,104.663,203.8,104.179,206.362,103.8Zm-16.227,22.449c-6.348-2.708-10.454-6.258-10.454-9.073s4.106-6.383,10.454-9.073c1.542-.663,3.228-1.255,4.967-1.811a86.122,86.122,0,0,0,4.034,10.92,84.9,84.9,0,0,0-3.981,10.866C193.38,127.525,191.694,126.915,190.134,126.252Zm9.647,25.623c-2.439-1.4-3.5-6.724-2.672-13.574.2-1.686.52-3.461.914-5.272a86.9,86.9,0,0,0,11.386,1.954,87.465,87.465,0,0,0,7.459,8.965c-5.845,5.433-11.332,8.41-15.062,8.41A4.279,4.279,0,0,1,199.781,151.875Zm42.532-13.663c.843,6.85-.2,12.175-2.618,13.592a3.99,3.99,0,0,1-2.062.466c-3.712,0-9.216-2.959-15.062-8.356a84.689,84.689,0,0,0,7.405-8.947,83.731,83.731,0,0,0,11.4-1.972A50.194,50.194,0,0,1,242.313,138.212Zm6.9-11.96c-1.542.663-3.228,1.255-4.967,1.811a86.12,86.12,0,0,0-4.034-10.92,84.9,84.9,0,0,0,3.981-10.866c1.775.556,3.461,1.165,5.039,1.829,6.348,2.708,10.454,6.258,10.454,9.073C259.67,119.994,255.564,123.562,249.216,126.252Z" fill="#61dafb"/>
<path id="Path_331" data-name="Path 331" d="M320.8,78.4Z" transform="translate(-119.082 -0.328)" fill="#61dafb"/>
<circle id="Ellipse_112" data-name="Ellipse 112" cx="8.194" cy="8.194" r="8.194" transform="translate(211.472 108.984)" fill="#61dafb"/>
<path id="Path_332" data-name="Path 332" d="M520.5,78.1Z" transform="translate(-282.975 -0.082)" fill="#61dafb"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 35 KiB

@@ -1,40 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1129" height="663" viewBox="0 0 1129 663">
<title>Focus on What Matters</title>
<circle cx="321" cy="321" r="321" fill="#f2f2f2" />
<ellipse cx="559" cy="635.49998" rx="514" ry="27.50002" fill="#3f3d56" />
<ellipse cx="558" cy="627" rx="460" ry="22" opacity="0.2" />
<rect x="131" y="152.5" width="840" height="50" fill="#3f3d56" />
<path d="M166.5,727.3299A21.67009,21.67009,0,0,0,188.1701,749H984.8299A21.67009,21.67009,0,0,0,1006.5,727.3299V296h-840Z" transform="translate(-35.5 -118.5)" fill="#3f3d56" />
<path d="M984.8299,236H188.1701A21.67009,21.67009,0,0,0,166.5,257.6701V296h840V257.6701A21.67009,21.67009,0,0,0,984.8299,236Z" transform="translate(-35.5 -118.5)" fill="#3f3d56" />
<path d="M984.8299,236H188.1701A21.67009,21.67009,0,0,0,166.5,257.6701V296h840V257.6701A21.67009,21.67009,0,0,0,984.8299,236Z" transform="translate(-35.5 -118.5)" opacity="0.2" />
<circle cx="181" cy="147.5" r="13" fill="#3f3d56" />
<circle cx="217" cy="147.5" r="13" fill="#3f3d56" />
<circle cx="253" cy="147.5" r="13" fill="#3f3d56" />
<rect x="168" y="213.5" width="337" height="386" rx="5.33505" fill="#606060" />
<rect x="603" y="272.5" width="284" height="22" rx="5.47638" fill="#2e8555" />
<rect x="537" y="352.5" width="416" height="15" rx="5.47638" fill="#2e8555" />
<rect x="537" y="396.5" width="416" height="15" rx="5.47638" fill="#2e8555" />
<rect x="537" y="440.5" width="416" height="15" rx="5.47638" fill="#2e8555" />
<rect x="537" y="484.5" width="416" height="15" rx="5.47638" fill="#2e8555" />
<rect x="865" y="552.5" width="88" height="26" rx="7.02756" fill="#3ecc5f" />
<path d="M1088.60287,624.61594a30.11371,30.11371,0,0,0,3.98291-15.266c0-13.79652-8.54358-24.98081-19.08256-24.98081s-19.08256,11.18429-19.08256,24.98081a30.11411,30.11411,0,0,0,3.98291,15.266,31.248,31.248,0,0,0,0,30.53213,31.248,31.248,0,0,0,0,30.53208,31.248,31.248,0,0,0,0,30.53208,30.11408,30.11408,0,0,0-3.98291,15.266c0,13.79652,8.54353,24.98081,19.08256,24.98081s19.08256-11.18429,19.08256-24.98081a30.11368,30.11368,0,0,0-3.98291-15.266,31.248,31.248,0,0,0,0-30.53208,31.248,31.248,0,0,0,0-30.53208,31.248,31.248,0,0,0,0-30.53213Z" transform="translate(-35.5 -118.5)" fill="#3f3d56" />
<ellipse cx="1038.00321" cy="460.31783" rx="19.08256" ry="24.9808" fill="#3f3d56" />
<ellipse cx="1038.00321" cy="429.78574" rx="19.08256" ry="24.9808" fill="#3f3d56" />
<path d="M1144.93871,339.34489a91.61081,91.61081,0,0,0,7.10658-10.46092l-50.141-8.23491,54.22885.4033a91.566,91.566,0,0,0,1.74556-72.42605l-72.75449,37.74139,67.09658-49.32086a91.41255,91.41255,0,1,0-150.971,102.29805,91.45842,91.45842,0,0,0-10.42451,16.66946l65.0866,33.81447-69.40046-23.292a91.46011,91.46011,0,0,0,14.73837,85.83669,91.40575,91.40575,0,1,0,143.68892,0,91.41808,91.41808,0,0,0,0-113.02862Z" transform="translate(-35.5 -118.5)" fill="#3ecc5f" fill-rule="evenodd" />
<path d="M981.6885,395.8592a91.01343,91.01343,0,0,0,19.56129,56.51431,91.40575,91.40575,0,1,0,143.68892,0C1157.18982,436.82067,981.6885,385.60008,981.6885,395.8592Z" transform="translate(-35.5 -118.5)" opacity="0.1" />
<path d="M365.62,461.43628H477.094v45.12043H365.62Z" transform="translate(-35.5 -118.5)" fill="#fff" fill-rule="evenodd" />
<path d="M264.76252,608.74122a26.50931,26.50931,0,0,1-22.96231-13.27072,26.50976,26.50976,0,0,0,22.96231,39.81215H291.304V608.74122Z" transform="translate(-35.5 -118.5)" fill="#3ecc5f" fill-rule="evenodd" />
<path d="M384.17242,468.57061l92.92155-5.80726V449.49263a26.54091,26.54091,0,0,0-26.54143-26.54143H331.1161l-3.31768-5.74622a3.83043,3.83043,0,0,0-6.63536,0l-3.31768,5.74622-3.31767-5.74622a3.83043,3.83043,0,0,0-6.63536,0l-3.31768,5.74622L301.257,417.205a3.83043,3.83043,0,0,0-6.63536,0L291.304,422.9512c-.02919,0-.05573.004-.08625.004l-5.49674-5.49541a3.8293,3.8293,0,0,0-6.4071,1.71723l-1.81676,6.77338L270.607,424.1031a3.82993,3.82993,0,0,0-4.6912,4.69253l1.84463,6.89148-6.77072,1.81411a3.8315,3.8315,0,0,0-1.71988,6.40975l5.49673,5.49673c0,.02787-.004.05574-.004.08493l-5.74622,3.31768a3.83043,3.83043,0,0,0,0,6.63536l5.74621,3.31768L259.0163,466.081a3.83043,3.83043,0,0,0,0,6.63536l5.74622,3.31768-5.74622,3.31767a3.83043,3.83043,0,0,0,0,6.63536l5.74622,3.31768-5.74622,3.31768a3.83043,3.83043,0,0,0,0,6.63536l5.74622,3.31768-5.74622,3.31767a3.83043,3.83043,0,0,0,0,6.63536l5.74622,3.31768-5.74622,3.31768a3.83043,3.83043,0,0,0,0,6.63536l5.74622,3.31768-5.74622,3.31768a3.83042,3.83042,0,0,0,0,6.63535l5.74622,3.31768-5.74622,3.31768a3.83043,3.83043,0,0,0,0,6.63536l5.74622,3.31768L259.0163,558.976a3.83042,3.83042,0,0,0,0,6.63535l5.74622,3.31768-5.74622,3.31768a3.83043,3.83043,0,0,0,0,6.63536l5.74622,3.31768-5.74622,3.31768a3.83042,3.83042,0,0,0,0,6.63535l5.74622,3.31768-5.74622,3.31768a3.83043,3.83043,0,0,0,0,6.63536l5.74622,3.31768A26.54091,26.54091,0,0,0,291.304,635.28265H450.55254A26.5409,26.5409,0,0,0,477.094,608.74122V502.5755l-92.92155-5.80727a14.12639,14.12639,0,0,1,0-28.19762" transform="translate(-35.5 -118.5)" fill="#3ecc5f" fill-rule="evenodd" />
<path d="M424.01111,635.28265h39.81214V582.19979H424.01111Z" transform="translate(-35.5 -118.5)" fill="#3ecc5f" fill-rule="evenodd" />
<path d="M490.36468,602.10586a6.60242,6.60242,0,0,0-.848.08493c-.05042-.19906-.09821-.39945-.15393-.59852A6.62668,6.62668,0,1,0,482.80568,590.21q-.2203-.22491-.44457-.44589a6.62391,6.62391,0,1,0-11.39689-6.56369c-.1964-.05575-.39414-.10218-.59056-.15262a6.63957,6.63957,0,1,0-13.10086,0c-.1964.05042-.39414.09687-.59056.15262a6.62767,6.62767,0,1,0-11.39688,6.56369,26.52754,26.52754,0,1,0,44.23127,25.52756,6.6211,6.6211,0,1,0,.848-13.18579" transform="translate(-35.5 -118.5)" fill="#44d860" fill-rule="evenodd" />
<path d="M437.28182,555.65836H477.094V529.11693H437.28182Z" transform="translate(-35.5 -118.5)" fill="#3ecc5f" fill-rule="evenodd" />
<path d="M490.36468,545.70532a3.31768,3.31768,0,0,0,0-6.63536,3.41133,3.41133,0,0,0-.42333.04247c-.02655-.09953-.04911-.19907-.077-.29859a3.319,3.319,0,0,0-1.278-6.37923,3.28174,3.28174,0,0,0-2.00122.68742q-.10947-.11346-.22294-.22295a3.282,3.282,0,0,0,.67149-1.98265,3.31768,3.31768,0,0,0-6.37-1.2992,13.27078,13.27078,0,1,0,0,25.54082,3.31768,3.31768,0,0,0,6.37-1.2992,3.282,3.282,0,0,0-.67149-1.98265q.11347-.10947.22294-.22294a3.28174,3.28174,0,0,0,2.00122.68742,3.31768,3.31768,0,0,0,1.278-6.37923c.02786-.0982.05042-.19907.077-.29859a3.41325,3.41325,0,0,0,.42333.04246" transform="translate(-35.5 -118.5)" fill="#44d860" fill-rule="evenodd" />
<path d="M317.84538,466.081a3.31768,3.31768,0,0,1-3.31767-3.31768,9.953,9.953,0,1,0-19.90608,0,3.31768,3.31768,0,1,1-6.63535,0,16.58839,16.58839,0,1,1,33.17678,0,3.31768,3.31768,0,0,1-3.31768,3.31768" transform="translate(-35.5 -118.5)" fill-rule="evenodd" />
<path d="M370.92825,635.28265h79.62429A26.5409,26.5409,0,0,0,477.094,608.74122v-92.895H397.46968a26.54091,26.54091,0,0,0-26.54143,26.54143Z" transform="translate(-35.5 -118.5)" fill="#ffff50" fill-rule="evenodd" />
<path d="M457.21444,556.98543H390.80778a1.32707,1.32707,0,0,1,0-2.65414h66.40666a1.32707,1.32707,0,0,1,0,2.65414m0,26.54143H390.80778a1.32707,1.32707,0,1,1,0-2.65414h66.40666a1.32707,1.32707,0,0,1,0,2.65414m0,26.54143H390.80778a1.32707,1.32707,0,1,1,0-2.65414h66.40666a1.32707,1.32707,0,0,1,0,2.65414m0-66.10674H390.80778a1.32707,1.32707,0,0,1,0-2.65414h66.40666a1.32707,1.32707,0,0,1,0,2.65414m0,26.29459H390.80778a1.32707,1.32707,0,0,1,0-2.65414h66.40666a1.32707,1.32707,0,0,1,0,2.65414m0,26.54143H390.80778a1.32707,1.32707,0,0,1,0-2.65414h66.40666a1.32707,1.32707,0,0,1,0,2.65414M477.094,474.19076c-.01592,0-.0292-.008-.04512-.00663-4.10064.13934-6.04083,4.24132-7.75274,7.86024-1.78623,3.78215-3.16771,6.24122-5.43171,6.16691-2.50685-.09024-3.94007-2.92222-5.45825-5.91874-1.74377-3.44243-3.73438-7.34667-7.91333-7.20069-4.04227.138-5.98907,3.70784-7.70631,6.857-1.82738,3.35484-3.07084,5.39455-5.46887,5.30033-2.55727-.09289-3.91619-2.39536-5.48877-5.06013-1.75306-2.96733-3.77951-6.30359-7.8775-6.18946-3.97326.13669-5.92537,3.16507-7.64791,5.83912-1.82207,2.82666-3.09872,4.5492-5.52725,4.447-2.61832-.09289-3.9706-2.00388-5.53522-4.21611-1.757-2.4856-3.737-5.299-7.82308-5.16231-3.88567.13271-5.83779,2.61434-7.559,4.80135-1.635,2.07555-2.9116,3.71846-5.61218,3.615a1.32793,1.32793,0,1,0-.09555,2.65414c4.00377.134,6.03154-2.38873,7.79257-4.6275,1.562-1.9853,2.91027-3.69855,5.56441-3.78879,2.55594-.10882,3.75429,1.47968,5.56707,4.04093,1.7212,2.43385,3.67465,5.19416,7.60545,5.33616,4.11789.138,6.09921-2.93946,7.8536-5.66261,1.56861-2.43385,2.92221-4.53461,5.50734-4.62352,2.37944-.08892,3.67466,1.79154,5.50072,4.885,1.72121,2.91557,3.67069,6.21865,7.67977,6.36463,4.14709.14332,6.14965-3.47693,7.89475-6.68181,1.51155-2.77092,2.93814-5.38791,5.46621-5.4755,2.37944-.05573,3.62025,2.11668,5.45558,5.74622,1.71459,3.388,3.65875,7.22591,7.73019,7.37321l.22429.004c4.06614,0,5.99571-4.08074,7.70364-7.68905,1.51154-3.19825,2.94211-6.21069,5.3972-6.33411Z" transform="translate(-35.5 -118.5)" fill-rule="evenodd" />
<path d="M344.38682,635.28265h53.08286V582.19979H344.38682Z" transform="translate(-35.5 -118.5)" fill="#3ecc5f" fill-rule="evenodd" />
<path d="M424.01111,602.10586a6.60242,6.60242,0,0,0-.848.08493c-.05042-.19906-.09821-.39945-.15394-.59852A6.62667,6.62667,0,1,0,416.45211,590.21q-.2203-.22491-.44458-.44589a6.62391,6.62391,0,1,0-11.39689-6.56369c-.1964-.05575-.39413-.10218-.59054-.15262a6.63957,6.63957,0,1,0-13.10084,0c-.19641.05042-.39414.09687-.59055.15262a6.62767,6.62767,0,1,0-11.39689,6.56369,26.52755,26.52755,0,1,0,44.2313,25.52756,6.6211,6.6211,0,1,0,.848-13.18579" transform="translate(-35.5 -118.5)" fill="#44d860" fill-rule="evenodd" />
<path d="M344.38682,555.65836h53.08286V529.11693H344.38682Z" transform="translate(-35.5 -118.5)" fill="#3ecc5f" fill-rule="evenodd" />
<path d="M410.74039,545.70532a3.31768,3.31768,0,1,0,0-6.63536,3.41133,3.41133,0,0,0-.42333.04247c-.02655-.09953-.04911-.19907-.077-.29859a3.319,3.319,0,0,0-1.278-6.37923,3.28174,3.28174,0,0,0-2.00122.68742q-.10947-.11346-.22294-.22295a3.282,3.282,0,0,0,.67149-1.98265,3.31768,3.31768,0,0,0-6.37-1.2992,13.27078,13.27078,0,1,0,0,25.54082,3.31768,3.31768,0,0,0,6.37-1.2992,3.282,3.282,0,0,0-.67149-1.98265q.11347-.10947.22294-.22294a3.28174,3.28174,0,0,0,2.00122.68742,3.31768,3.31768,0,0,0,1.278-6.37923c.02786-.0982.05042-.19907.077-.29859a3.41325,3.41325,0,0,0,.42333.04246" transform="translate(-35.5 -118.5)" fill="#44d860" fill-rule="evenodd" />
<path d="M424.01111,447.8338a3.60349,3.60349,0,0,1-.65028-.06636,3.34415,3.34415,0,0,1-.62372-.18579,3.44679,3.44679,0,0,1-.572-.30522,5.02708,5.02708,0,0,1-.50429-.4114,3.88726,3.88726,0,0,1-.41007-.50428,3.27532,3.27532,0,0,1-.55737-1.84463,3.60248,3.60248,0,0,1,.06636-.65027,3.82638,3.82638,0,0,1,.18447-.62373,3.48858,3.48858,0,0,1,.30656-.57064,3.197,3.197,0,0,1,.91436-.91568,3.44685,3.44685,0,0,1,.572-.30523,3.344,3.344,0,0,1,.62372-.18578,3.06907,3.06907,0,0,1,1.30053,0,3.22332,3.22332,0,0,1,1.19436.491,5.02835,5.02835,0,0,1,.50429.41139,4.8801,4.8801,0,0,1,.41139.50429,3.38246,3.38246,0,0,1,.30522.57064,3.47806,3.47806,0,0,1,.25215,1.274A3.36394,3.36394,0,0,1,426.36,446.865a5.02708,5.02708,0,0,1-.50429.4114,3.3057,3.3057,0,0,1-1.84463.55737m26.54143-1.65884a3.38754,3.38754,0,0,1-2.35024-.96877,5.04185,5.04185,0,0,1-.41007-.50428,3.27532,3.27532,0,0,1-.55737-1.84463,3.38659,3.38659,0,0,1,.96744-2.34892,5.02559,5.02559,0,0,1,.50429-.41139,3.44685,3.44685,0,0,1,.572-.30523,3.3432,3.3432,0,0,1,.62373-.18579,3.06952,3.06952,0,0,1,1.30052,0,3.22356,3.22356,0,0,1,1.19436.491,5.02559,5.02559,0,0,1,.50429.41139,3.38792,3.38792,0,0,1,.96876,2.34892,3.72635,3.72635,0,0,1-.06636.65026,3.37387,3.37387,0,0,1-.18579.62373,4.71469,4.71469,0,0,1-.30522.57064,4.8801,4.8801,0,0,1-.41139.50429,5.02559,5.02559,0,0,1-.50429.41139,3.30547,3.30547,0,0,1-1.84463.55737" transform="translate(-35.5 -118.5)" fill-rule="evenodd" />
</svg>

Before

Width:  |  Height:  |  Size: 12 KiB

+7
View File
@@ -0,0 +1,7 @@
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'scope-case': [0],
'subject-case': [0],
},
};
+1 -1
View File
@@ -154,7 +154,7 @@ const config = {
// testLocationInResults: false, // testLocationInResults: false,
// The glob patterns Jest uses to detect test files // The glob patterns Jest uses to detect test files
testMatch: ['**/__tests__/**/*.[jt]s?(x)', '**/?(*.)+(spec|test).[tj]s?(x)'], testMatch: ['**/tests/**/*.[jt]s?(x)', '**/?(*.)+(spec|test).[tj]s?(x)'],
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
// testPathIgnorePatterns: [ // testPathIgnorePatterns: [
+30 -32
View File
@@ -7,7 +7,7 @@
], ],
"scripts": { "scripts": {
"start": "yarn workspace playground start", "start": "yarn workspace playground start",
"start:story": "yarn workspace storybook storybook", "start:docs": "yarn workspace storybook storybook",
"start:site": "yarn workspace website start", "start:site": "yarn workspace website start",
"deploy:site": "yarn workspace website deploy", "deploy:site": "yarn workspace website deploy",
"build": "lerna run build", "build": "lerna run build",
@@ -21,53 +21,51 @@
"ver": "lerna version --no-private --conventional-commits", "ver": "lerna version --no-private --conventional-commits",
"release": "yarn eslint && yarn build && yarn run ver && lerna publish from-git", "release": "yarn eslint && yarn build && yarn run ver && lerna publish from-git",
"release:beta": "yarn eslint && yarn build && yarn run ver && lerna publish from-package --dist-tag beta", "release:beta": "yarn eslint && yarn build && yarn run ver && lerna publish from-package --dist-tag beta",
"up": "yarn upgrade-interactive --latest" "up": "yarn upgrade-interactive --latest",
"prepare": "husky install"
}, },
"devDependencies": { "devDependencies": {
"@babel/preset-env": "^7.19.3", "@babel/preset-env": "^7.23.2",
"@babel/preset-react": "^7.18.6", "@babel/preset-react": "^7.22.15",
"@babel/preset-typescript": "^7.18.6", "@babel/preset-typescript": "^7.23.2",
"@commitlint/cli": "^17.5.0", "@commitlint/cli": "^17.5.0",
"@commitlint/config-conventional": "^17.3.0", "@commitlint/config-conventional": "^17.3.0",
"@types/jest": "^29.2.4", "@types/jest": "^29.5.7",
"@types/lodash-es": "^4.17.8", "@types/lodash-es": "^4.17.10",
"@types/lodash.get": "^4.4.7", "@types/lodash.get": "^4.4.8",
"@types/lodash.isequal": "^4.5.6", "@types/lodash.isequal": "^4.5.7",
"@types/lodash.set": "^4.3.7", "@types/lodash.set": "^4.3.8",
"@types/react": "^18.2.7", "@types/react": "^18.2.34",
"@types/react-dom": "^18.2.4", "@types/react-dom": "^18.2.14",
"@types/styled-components": "^5.1.25", "@types/styled-components": "^5.1.29",
"@typescript-eslint/eslint-plugin": "^6.4.1", "@typescript-eslint/eslint-plugin": "^6.9.1",
"@typescript-eslint/parser": "^6.4.1", "@typescript-eslint/parser": "^6.9.1",
"conventional-changelog-cli": "^2.2.2", "conventional-changelog-cli": "^2.2.2",
"copyfiles": "^2.4.1", "copyfiles": "^2.4.1",
"eslint": "^8.40.0", "eslint": "^8.52.0",
"eslint-config-ali": "^14.0.2", "eslint-config-ali": "^14.0.2",
"eslint-config-prettier": "^9.0.0", "eslint-config-prettier": "^9.0.0",
"eslint-import-resolver-typescript": "^3.6.0", "eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-import": "^2.28.1", "eslint-plugin-import": "^2.29.0",
"eslint-plugin-react": "^7.33.2", "eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.0",
"http-server": "^14.1.0", "http-server": "^14.1.0",
"husky": "^8.0.1", "husky": "^8.0.3",
"jest": "^29.1.1", "jest": "^29.7.0",
"jest-environment-jsdom": "^29.1.1", "jest-environment-jsdom": "^29.7.0",
"lerna": "^6.6.1", "lerna": "^6.6.1",
"less": "^4.1.2", "lint-staged": "^14.0.1",
"less-loader": "^7.3.0",
"lint-staged": "^13.1.0",
"mini-css-extract-plugin": "^1.6.2",
"prettier": "^2.8.7", "prettier": "^2.8.7",
"progress-bar-webpack-plugin": "^2.1.0",
"react": "^17.0.0", "react": "^17.0.0",
"react-dom": "^17.0.0", "react-dom": "^17.0.0",
"styled-components": "^5.3.6", "styled-components": "^5.3.6",
"typedoc": "^0.25.0", "typedoc": "^0.25.3",
"typescript": "^5.0.4", "typescript": "^5.2.2"
"webpack": "^4.46.0",
"webpack-cli": "^4.9.2"
}, },
"resolutions": { "lint-staged": {
"@yarnpkg/parsers": "3.0.0-rc.48.1" "*.{js,jsx,ts,tsx}": [
"eslint --fix",
"prettier --write"
]
} }
} }
+46
View File
@@ -3,6 +3,52 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.2.7](https://github.com/netease/tango/compare/@music163/tango-context@0.2.6...@music163/tango-context@0.2.7) (2023-11-23)
**Note:** Version bump only for package @music163/tango-context
## [0.2.6](https://github.com/netease/tango/compare/@music163/tango-context@0.2.5...@music163/tango-context@0.2.6) (2023-11-20)
**Note:** Version bump only for package @music163/tango-context
## [0.2.5](https://github.com/netease/tango/compare/@music163/tango-context@0.2.4...@music163/tango-context@0.2.5) (2023-11-16)
**Note:** Version bump only for package @music163/tango-context
## [0.2.4](https://github.com/netease/tango/compare/@music163/tango-context@0.2.3...@music163/tango-context@0.2.4) (2023-11-02)
### Bug Fixes
- optional context config ([70fc6f8](https://github.com/netease/tango/commit/70fc6f8030202f5d340281c0424752044ed0fb83))
## [0.2.3](https://github.com/netease/tango/compare/@music163/tango-context@0.2.2...@music163/tango-context@0.2.3) (2023-10-23)
**Note:** Version bump only for package @music163/tango-context
## [0.2.2](https://github.com/netease/tango/compare/@music163/tango-context@0.2.1...@music163/tango-context@0.2.2) (2023-10-12)
### Bug Fixes
- refactor expSetter ([#41](https://github.com/netease/tango/issues/41)) ([d63517e](https://github.com/netease/tango/commit/d63517ecb936e4227e70c33e610664316625f4f4))
## [0.2.1](https://github.com/netease/tango/compare/@music163/tango-context@0.2.0...@music163/tango-context@0.2.1) (2023-09-22)
**Note:** Version bump only for package @music163/tango-context
# [0.2.0](https://github.com/netease/tango/compare/@music163/tango-context@0.1.4...@music163/tango-context@0.2.0) (2023-09-21)
### Features
- support multiple service modules & refactor workspace ([a7df29c](https://github.com/netease/tango/commit/a7df29c3debc56b187792d3e203b470e9d368ea5))
## [0.1.4](https://github.com/netease/tango/compare/@music163/tango-context@0.1.3...@music163/tango-context@0.1.4) (2023-09-18)
**Note:** Version bump only for package @music163/tango-context
## [0.1.3](https://github.com/netease/tango/compare/@music163/tango-context@0.1.2...@music163/tango-context@0.1.3) (2023-09-13)
**Note:** Version bump only for package @music163/tango-context
## 0.1.2 (2023-09-06) ## 0.1.2 (2023-09-06)
**Note:** Version bump only for package @music163/tango-context **Note:** Version bump only for package @music163/tango-context
+4 -4
View File
@@ -1,6 +1,6 @@
{ {
"name": "@music163/tango-context", "name": "@music163/tango-context",
"version": "0.1.2", "version": "0.2.7",
"description": "react context for tango-apps", "description": "react context for tango-apps",
"keywords": [ "keywords": [
"react", "react",
@@ -28,11 +28,11 @@
"prepublishOnly": "yarn build" "prepublishOnly": "yarn build"
}, },
"peerDependencies": { "peerDependencies": {
"react": ">= 16.8.0" "react": ">= 16.8"
}, },
"dependencies": { "dependencies": {
"@music163/tango-core": "^0.1.2", "@music163/tango-core": "^0.2.6",
"@music163/tango-helpers": "^0.1.2", "@music163/tango-helpers": "^0.1.8",
"mobx-react-lite": "4.0.2" "mobx-react-lite": "4.0.2"
}, },
"publishConfig": { "publishConfig": {
+46 -39
View File
@@ -1,11 +1,25 @@
import type { Engine } from '@music163/tango-core'; import type { Engine } from '@music163/tango-core';
import { createContext } from '@music163/tango-helpers'; import { createContext } from '@music163/tango-helpers';
interface CustomVariableData {
key: string;
title: string;
children?: CustomVariableData[];
[key: string]: any;
}
export interface ITangoEngineContext { export interface ITangoEngineContext {
/** /**
* 低代码引擎 * 低代码引擎
*/ */
engine: Engine; engine: Engine;
/**
* 自定义配置数据
*/
config?: {
customActionVariables?: CustomVariableData[];
customExpressionVariables?: CustomVariableData[];
};
} }
const [TangoEngineProvider, useTangoEngine] = createContext<ITangoEngineContext>({ const [TangoEngineProvider, useTangoEngine] = createContext<ITangoEngineContext>({
@@ -22,38 +36,15 @@ export const useDesigner = () => {
return useTangoEngine()?.engine.designer; return useTangoEngine()?.engine.designer;
}; };
const builtinHelpers = [
{
title: 'setStoreValue',
key: '() => tango.setStoreValue("variableName", "variableValue")',
type: 'function',
},
{
title: 'getStoreValue',
key: '() => tango.getStoreValue("variableName")',
type: 'function',
},
{ title: 'openModal', key: '() => tango.openModal("")', type: 'function' },
{ title: 'closeModal', key: '() => tango.closeModal("")', type: 'function' },
{ title: 'navigateTo', key: '() => tango.navigateTo("/")', type: 'function' },
{ title: 'showToast', key: '() => tango.showToast("hello")', type: 'function' },
{ title: 'formatDate', key: '() => tango.formatDate("2022-12-12")', type: 'function' },
{ title: 'formatNumber', key: '() => tango.formatDate(9999)', type: 'function' },
{
title: 'copyToClipboard',
key: '() => tango.copyToClipboard("hello")',
type: 'function',
},
];
export const useWorkspaceData = () => { export const useWorkspaceData = () => {
const ctx = useTangoEngine();
const workspace = useWorkspace(); const workspace = useWorkspace();
const modelVariables: any[] = []; // 绑定变量列表 const modelVariables: any[] = []; // 绑定变量列表
const storeActionVariables: any[] = []; // 模型中的所有 actions const storeActionVariables: any[] = []; // 模型中的所有 actions
const storeVariables: any[] = []; // 模型中的所有变量 const storeVariables: any[] = []; // 模型中的所有变量
const serviceVariables: any[] = []; // 服务中的所有变量 const serviceVariables: any[] = []; // 服务中的所有变量
workspace.listStoreModules?.().forEach((file) => { Object.values(workspace.storeModules).forEach((file) => {
const prefix = `stores.${file.name}`; const prefix = `stores.${file.name}`;
const states = file.states.map((item) => ({ const states = file.states.map((item) => ({
title: item.name, title: item.name,
@@ -91,11 +82,17 @@ export const useWorkspaceData = () => {
}); });
}); });
Object.keys(workspace.serviceModule?.serviceFunctions || {}).forEach((key) => { Object.values(workspace.serviceModules).forEach((file) => {
const prefix = file.name !== 'index' ? `services.${file.name}` : 'services';
serviceVariables.push({ serviceVariables.push({
title: key, title: file.name,
key: `services.${key}`, key: prefix,
type: 'function', selectable: false,
children: Object.keys(file.serviceFunctions || {}).map((key) => ({
title: key,
key: [prefix, key].join('.'),
type: 'function',
})),
}); });
}); });
@@ -105,19 +102,29 @@ export const useWorkspaceData = () => {
value: item.path, value: item.path,
})); }));
let actionVariables: CustomVariableData[] = [
buildVariableOptions('数据模型', '$stores', storeActionVariables),
buildVariableOptions('服务函数', '$services', serviceVariables),
];
if (ctx.config?.customActionVariables) {
actionVariables = actionVariables.concat(ctx.config?.customActionVariables);
}
let expressionVariables: CustomVariableData[] = [
buildVariableOptions('数据模型', '$stores', storeVariables),
buildVariableOptions('服务函数', '$services', serviceVariables),
];
if (ctx.config?.customExpressionVariables) {
expressionVariables = expressionVariables.concat(ctx.config?.customExpressionVariables);
}
return { return {
modelVariables: [buildVariableOptions('数据模型', 'stores', modelVariables)], modelVariables: [buildVariableOptions('数据模型', 'stores', modelVariables)],
actionVariables: [ actionVariables,
buildVariableOptions('数据模型', 'stores', storeActionVariables),
buildVariableOptions('服务函数', 'services', serviceVariables),
buildVariableOptions('工具函数', 'helpers', builtinHelpers),
],
storeVariables, storeVariables,
expressionVariables: [ serviceVariables,
buildVariableOptions('数据模型', 'stores', storeVariables), expressionVariables,
buildVariableOptions('服务函数', 'services', serviceVariables),
buildVariableOptions('工具函数', 'helpers', builtinHelpers),
],
routeOptions, routeOptions,
}; };
}; };
+66
View File
@@ -3,6 +3,72 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.2.6](https://github.com/netease/tango/compare/@music163/tango-core@0.2.5...@music163/tango-core@0.2.6) (2023-11-23)
### Bug Fixes
- prototype to import declaration ([#67](https://github.com/netease/tango/issues/67)) ([8a1234e](https://github.com/netease/tango/commit/8a1234e565489c92f041356b7a06339eaeee48df))
- refactor update import specifiers ([#68](https://github.com/netease/tango/issues/68)) ([558f2cd](https://github.com/netease/tango/commit/558f2cd0a692c6bbc866d08250d25e2619af183f))
## [0.2.5](https://github.com/netease/tango/compare/@music163/tango-core@0.2.4...@music163/tango-core@0.2.5) (2023-11-20)
### Bug Fixes
- refactor parse expression ([#61](https://github.com/netease/tango/issues/61)) ([dbbd1dd](https://github.com/netease/tango/commit/dbbd1dddc75c532b7c9710ab0941c8680100f093))
## [0.2.4](https://github.com/netease/tango/compare/@music163/tango-core@0.2.3...@music163/tango-core@0.2.4) (2023-11-16)
### Bug Fixes
- allow override existing file via addFile ([6cf673e](https://github.com/netease/tango/commit/6cf673e7f6f330613e1d6331256a59900e8cbc68))
- export isJSXElementById from traverse ([#54](https://github.com/netease/tango/issues/54)) ([208e275](https://github.com/netease/tango/commit/208e275bb1cd98ed21d816c26179f9ebc11df223))
- parse variables from view ([089a482](https://github.com/netease/tango/commit/089a482f750e9d9a7743a6641d6e39989347d318))
- update types ([653386d](https://github.com/netease/tango/commit/653386dcc0b064b41915548129952eeabe53019f))
## [0.2.3](https://github.com/netease/tango/compare/@music163/tango-core@0.2.2...@music163/tango-core@0.2.3) (2023-11-02)
### Bug Fixes
- add onFilesChange to workspace ([#50](https://github.com/netease/tango/issues/50)) ([d775a50](https://github.com/netease/tango/commit/d775a5003ab1a14f801d3b38f7187ea15ef7d74d))
- **core:** create store without entry file ([ae806ae](https://github.com/netease/tango/commit/ae806aee3583ef5f957836e15d715035c4556867))
- **core:** logging drop action ([963bfe1](https://github.com/netease/tango/commit/963bfe1ab80fa4114637963534b2891a27538120))
- designer model add defaultActiveSidebarPanel ([#51](https://github.com/netease/tango/issues/51)) ([9dd11f7](https://github.com/netease/tango/commit/9dd11f7a6d5807c80a63c84c1ad0df5bc1ce9558))
- refactor workspace onFilesChange ([f53ce94](https://github.com/netease/tango/commit/f53ce94c6ed777d45ba6137188bfbc4566e03942))
- update types ([46d5f59](https://github.com/netease/tango/commit/46d5f59b4cd4fa71ed247f5e470e66c8c9f6d4a0))
## [0.2.2](https://github.com/netease/tango/compare/@music163/tango-core@0.2.1...@music163/tango-core@0.2.2) (2023-10-23)
### Bug Fixes
- add listImportSources to viewModule ([6bbbe63](https://github.com/netease/tango/commit/6bbbe634ad52acaf06a0d776aeff450993f7b69a))
- enhance expSetter ([#43](https://github.com/netease/tango/issues/43)) ([5ebbb42](https://github.com/netease/tango/commit/5ebbb428fb3fb786d330ab01959028443338d315))
- update interface ([0b3e632](https://github.com/netease/tango/commit/0b3e632745e30d578268458329d5702dbd729010))
## [0.2.1](https://github.com/netease/tango/compare/@music163/tango-core@0.2.0...@music163/tango-core@0.2.1) (2023-09-22)
### Bug Fixes
- add listServiceFunctions ([09a1e21](https://github.com/netease/tango/commit/09a1e2135a1f51b0a5f6c0a507ba42d2e6355c24))
# [0.2.0](https://github.com/netease/tango/compare/@music163/tango-core@0.1.4...@music163/tango-core@0.2.0) (2023-09-21)
### Features
- support multiple service modules & refactor workspace ([a7df29c](https://github.com/netease/tango/commit/a7df29c3debc56b187792d3e203b470e9d368ea5))
## [0.1.4](https://github.com/netease/tango/compare/@music163/tango-core@0.1.3...@music163/tango-core@0.1.4) (2023-09-18)
**Note:** Version bump only for package @music163/tango-core
## [0.1.3](https://github.com/netease/tango/compare/@music163/tango-core@0.1.2...@music163/tango-core@0.1.3) (2023-09-13)
### Bug Fixes
- **core:** optimize find jsxElement parent node way ([#22](https://github.com/netease/tango/issues/22)) ([19d0a45](https://github.com/netease/tango/commit/19d0a4523bb31ea9286714737a0f4f1883e1c801))
- refactor setting formItem ([b786de2](https://github.com/netease/tango/commit/b786de2f1a0e4e9141eb09fce696e45df633b232))
- refactor types ([15542d9](https://github.com/netease/tango/commit/15542d9eb2f8959597b81cae457091ee71710c83))
- update sidebar ([4d809e9](https://github.com/netease/tango/commit/4d809e9afd0d6d525850708722736847b510638e))
## 0.1.2 (2023-09-06) ## 0.1.2 (2023-09-06)
### Bug Fixes ### Bug Fixes
+11 -8
View File
@@ -1,6 +1,6 @@
{ {
"name": "@music163/tango-core", "name": "@music163/tango-core",
"version": "0.1.2", "version": "0.2.6",
"description": "tango core", "description": "tango core",
"author": "wwsun <ww.sun@outlook.com>", "author": "wwsun <ww.sun@outlook.com>",
"homepage": "", "homepage": "",
@@ -24,16 +24,19 @@
"prepublishOnly": "yarn build" "prepublishOnly": "yarn build"
}, },
"dependencies": { "dependencies": {
"@babel/generator": "^7.22.15", "@babel/generator": "^7.23.0",
"@babel/parser": "^7.22.15", "@babel/parser": "^7.23.0",
"@babel/traverse": "^7.22.15", "@babel/traverse": "^7.23.2",
"@babel/types": "^7.22.15", "@babel/types": "^7.23.0",
"@music163/tango-helpers": "^0.1.2", "@music163/tango-helpers": "^0.1.8",
"@types/babel__generator": "^7.6.4", "@types/babel__generator": "^7.6.6",
"@types/babel__traverse": "^7.18.3", "@types/babel__traverse": "^7.20.3",
"mobx": "6.9.0", "mobx": "6.9.0",
"path-browserify": "^1.0.1" "path-browserify": "^1.0.1"
}, },
"peerDependencies": {
"mobx": "6.9.0"
},
"publishConfig": { "publishConfig": {
"access": "public", "access": "public",
"registry": "https://registry.npmjs.org/" "registry": "https://registry.npmjs.org/"
+6
View File
@@ -10,6 +10,10 @@ interface ICreateEngineOptions {
* 默认的模拟器模式 * 默认的模拟器模式
*/ */
defaultSimulatorMode?: SimulatorNameType; defaultSimulatorMode?: SimulatorNameType;
/**
* 默认激活的侧边栏
*/
defaultActiveSidebarPanel?: string;
} }
/** /**
@@ -20,12 +24,14 @@ interface ICreateEngineOptions {
export function createEngine({ export function createEngine({
workspace, workspace,
defaultSimulatorMode = 'desktop', defaultSimulatorMode = 'desktop',
defaultActiveSidebarPanel = '',
}: ICreateEngineOptions) { }: ICreateEngineOptions) {
const engine = new Engine({ const engine = new Engine({
workspace, workspace,
designer: new Designer({ designer: new Designer({
workspace, workspace,
simulator: defaultSimulatorMode, simulator: defaultSimulatorMode,
activeSidebarPanel: defaultActiveSidebarPanel,
}), }),
}); });
+27
View File
@@ -1,3 +1,5 @@
import { isValidExpressionCode } from './ast';
const defineServiceHandlerNames = ['defineServices', 'createServices']; const defineServiceHandlerNames = ['defineServices', 'createServices'];
const sfHandlerPattern = new RegExp(`^(${defineServiceHandlerNames.join('|')})$`); const sfHandlerPattern = new RegExp(`^(${defineServiceHandlerNames.join('|')})$`);
@@ -20,3 +22,28 @@ const defineStoreHandlerName = 'defineStore';
export function isDefineStore(name: string) { export function isDefineStore(name: string) {
return defineStoreHandlerName === name; return defineStoreHandlerName === name;
} }
/**
* 是否是 tango 的变量引用
* @example tango.stores.app.name
* @example tango.stores?.app?.name
*
* @param name
* @returns
*/
export function isTangoVariable(name: string) {
return /^tango\??\.(stores|services)\??\./.test(name) && name.split('.').length > 2;
}
const templatePattern = /^{(.+)}$/s;
/**
* 判断给定字符串是否被表达式容器`{expCode}`包裹
* @param code
*/
export function isWrappedByExpressionContainer(code: string, isStrict = true) {
if (isStrict && isValidExpressionCode(code)) {
return false;
}
return templatePattern.test(code);
}
+8 -8
View File
@@ -6,10 +6,10 @@ import * as t from '@babel/types';
import { import {
logger, logger,
isValidObjectString, isValidObjectString,
isVariableString,
getVariableContent, getVariableContent,
isPlainObject, isPlainObject,
} from '@music163/tango-helpers'; } from '@music163/tango-helpers';
import { isWrappedByExpressionContainer } from '../assert';
// @see https://babeljs.io/docs/en/babel-parser#pluginss // @see https://babeljs.io/docs/en/babel-parser#pluginss
const babelParserConfig: ParserOptions = { const babelParserConfig: ParserOptions = {
@@ -22,7 +22,6 @@ const babelParserConfig: ParserOptions = {
'classProperties', 'classProperties',
'asyncGenerators', 'asyncGenerators',
'functionBind', 'functionBind',
'functionSent',
'dynamicImport', 'dynamicImport',
'optionalChaining', 'optionalChaining',
], ],
@@ -44,6 +43,7 @@ export function isValidCode(code: string) {
/** /**
* 检测代码是否是合法的表达式代码 * 检测代码是否是合法的表达式代码
* 表达式是一组代码的集合,它返回一个值;每一个合法的表达式都能计算成某个值
* @param code * @param code
* @returns * @returns
*/ */
@@ -91,8 +91,8 @@ export function code2expression(code: string) {
try { try {
expNode = t.cloneNode(parseExpression(code, babelParserConfig), false, true); expNode = t.cloneNode(parseExpression(code, babelParserConfig), false, true);
} catch (err) { } catch (err) {
console.error('invalid code', err); logger.error('invalid code', err);
expNode = t.identifier('undefined'); // expNode = t.identifier('undefined');
} }
return expNode; return expNode;
} }
@@ -103,7 +103,7 @@ export function code2expression(code: string) {
* @returns File * @returns File
*/ */
export function expressionCode2ast(code: string) { export function expressionCode2ast(code: string) {
if (isVariableString(code)) { if (isWrappedByExpressionContainer(code)) {
code = getVariableContent(code); code = getVariableContent(code);
} }
const node = code2expression(code); const node = code2expression(code);
@@ -130,7 +130,7 @@ export function value2node(
ret = t.numericLiteral(value); ret = t.numericLiteral(value);
break; break;
case 'string': case 'string':
if (isVariableString(value)) { if (isWrappedByExpressionContainer(value)) {
// 再检查是否是表达式容器,例如 {this.foo}, {1} // 再检查是否是表达式容器,例如 {this.foo}, {1}
const innerString = getVariableContent(value); const innerString = getVariableContent(value);
ret = code2expression(innerString); ret = code2expression(innerString);
@@ -201,7 +201,7 @@ export function value2jsxAttributeValueNode(value: any) {
if (isValidObjectString(value)) { if (isValidObjectString(value)) {
// 先检查是否是对象字符串 // 先检查是否是对象字符串
ret = t.jsxExpressionContainer(code2expression(value)); ret = t.jsxExpressionContainer(code2expression(value));
} else if (isVariableString(value)) { } else if (isWrappedByExpressionContainer(value)) {
// 再检查是否是表达式容器,例如 {this.foo}, {1} // 再检查是否是表达式容器,例如 {this.foo}, {1}
const innerString = getVariableContent(value); const innerString = getVariableContent(value);
ret = t.jsxExpressionContainer(code2expression(innerString)); ret = t.jsxExpressionContainer(code2expression(innerString));
@@ -221,7 +221,7 @@ export function value2jsxChildrenValueNode(value: any) {
let ret: t.JSXElement | t.JSXFragment | t.JSXExpressionContainer | t.JSXSpreadChild | t.JSXText; let ret: t.JSXElement | t.JSXFragment | t.JSXExpressionContainer | t.JSXSpreadChild | t.JSXText;
switch (typeof value) { switch (typeof value) {
case 'string': case 'string':
if (isVariableString(value)) { if (isWrappedByExpressionContainer(value)) {
const innerString = getVariableContent(value); const innerString = getVariableContent(value);
ret = t.jsxExpressionContainer(code2expression(innerString)); ret = t.jsxExpressionContainer(code2expression(innerString));
} else { } else {
+221 -96
View File
@@ -11,16 +11,9 @@ import {
StringOrNumber, StringOrNumber,
Dict, Dict,
parseDndId, parseDndId,
upperCamelCase upperCamelCase,
} from '@music163/tango-helpers'; } from '@music163/tango-helpers';
import { import { keyNode2value, jsxAttributeValueNode2value, node2value, node2code } from './generate';
keyNode2value,
jsxAttributeValueNode2value,
node2value,
ast2code,
expression2code,
node2code,
} from './generate';
import { import {
value2jsxAttributeValueNode, value2jsxAttributeValueNode,
value2jsxChildrenValueNode, value2jsxChildrenValueNode,
@@ -30,15 +23,15 @@ import {
object2node, object2node,
} from './parse'; } from './parse';
import { isValidComponentName } from '../string'; import { isValidComponentName } from '../string';
import { isDefineService, isDefineStore } from '../assert'; import { isDefineService, isDefineStore, isTangoVariable } from '../assert';
import type { import type {
RouteDataType, IRouteData,
StorePropertyType, IStorePropertyData,
ClassPropertyNodeType, ITangoViewNodeData,
TangoViewNodeDataType, IImportDeclarationPayload,
ImportDeclarationPayloadType, IServiceFunctionPayload,
ServiceFunctionPayloadType,
InsertChildPositionType, InsertChildPositionType,
IImportSpecifierData,
} from '../../types'; } from '../../types';
import { IdGenerator } from '../id-generator'; import { IdGenerator } from '../id-generator';
@@ -201,7 +194,7 @@ export function queryXFormItemFields(ast: t.File | t.JSXElement) {
* @param node * @param node
* @param id * @param id
*/ */
function isJSXElementById(node: t.JSXElement, jsxElementNodeId: string) { export function isJSXElementById(node: t.JSXElement, jsxElementNodeId: string) {
let isTargetJSXElement = false; let isTargetJSXElement = false;
visitJSXElementAttributes(node, (name, value) => { visitJSXElementAttributes(node, (name, value) => {
if (name === SLOT.dnd && value === jsxElementNodeId) { if (name === SLOT.dnd && value === jsxElementNodeId) {
@@ -455,11 +448,12 @@ function getLastExportDeclarationIndex(node: t.Program) {
/** /**
* 生成新的 ImportDeclaration * 生成新的 ImportDeclaration
* @deprecated 使用 makeImportDeclaration2 代替
* @param importedModule * @param importedModule
* @param importedSourcePath * @param importedSourcePath
* @returns * @returns
*/ */
export function makeImportDeclaration(importedModule: ImportDeclarationPayloadType) { export function makeImportDeclarationLegacy(importedModule: IImportDeclarationPayload) {
const specifierNodes = const specifierNodes =
importedModule.specifiers?.map((localName) => importedModule.specifiers?.map((localName) =>
t.importSpecifier(t.identifier(localName), t.identifier(localName)), t.importSpecifier(t.identifier(localName), t.identifier(localName)),
@@ -474,7 +468,37 @@ export function makeImportDeclaration(importedModule: ImportDeclarationPayloadTy
return t.importDeclaration(newSpecifierNodes, t.stringLiteral(importedModule.sourcePath)); return t.importDeclaration(newSpecifierNodes, t.stringLiteral(importedModule.sourcePath));
} }
function getImportDeclarationData(node: t.ImportDeclaration): ImportDeclarationPayloadType { function specifierData2node(data: IImportSpecifierData) {
switch (data.type) {
case 'ImportDefaultSpecifier':
return t.importDefaultSpecifier(t.identifier(data.localName));
case 'ImportSpecifier':
return t.importSpecifier(
t.identifier(data.importedName || data.localName),
t.identifier(data.localName),
);
case 'ImportNamespaceSpecifier':
return t.importNamespaceSpecifier(t.identifier(data.localName));
default:
return;
}
}
function specifierDataList2nodes(specifiers: IImportSpecifierData[]) {
return specifiers.map((item) => specifierData2node(item)).filter((item) => !!item);
}
export function makeImportDeclaration(source: string, specifiers: IImportSpecifierData[]) {
const specifierNodes = specifierDataList2nodes(specifiers);
return t.importDeclaration(specifierNodes, t.stringLiteral(source));
}
/**
* @deprecated 使用 parseImportDeclaration 代替
* @param node
* @returns
*/
function getImportDeclarationData(node: t.ImportDeclaration): IImportDeclarationPayload {
const sourcePath = node2value(node.source); const sourcePath = node2value(node.source);
let defaultSpecifier; let defaultSpecifier;
const specifiers: string[] = []; const specifiers: string[] = [];
@@ -494,16 +518,33 @@ function getImportDeclarationData(node: t.ImportDeclaration): ImportDeclarationP
/** /**
* 添加新的导入语句 * 添加新的导入语句
* @deprecated 使用 addImportDeclaration2 代替
* @param ast * @param ast
* @param importedModule * @param importedModule
* @param importedSourcePath * @param importedSourcePath
* @returns * @returns
*/ */
export function addImportDeclaration(ast: t.File, importedModule: ImportDeclarationPayloadType) { export function addImportDeclarationLegacy(ast: t.File, importedModule: IImportDeclarationPayload) {
traverse(ast, { traverse(ast, {
Program(path) { Program(path) {
const lastIndex = getLastImportDeclarationIndex(path.node); const lastIndex = getLastImportDeclarationIndex(path.node);
const newImportDeclaration = makeImportDeclaration(importedModule); const newImportDeclaration = makeImportDeclarationLegacy(importedModule);
path.node.body.splice(lastIndex, 0, newImportDeclaration);
path.stop();
},
});
return ast;
}
export function addImportDeclaration(
ast: t.File,
source: string,
specifiers: IImportSpecifierData[],
) {
traverse(ast, {
Program(path) {
const lastIndex = getLastImportDeclarationIndex(path.node);
const newImportDeclaration = makeImportDeclaration(source, specifiers);
path.node.body.splice(lastIndex, 0, newImportDeclaration); path.node.body.splice(lastIndex, 0, newImportDeclaration);
path.stop(); path.stop();
}, },
@@ -513,17 +554,21 @@ export function addImportDeclaration(ast: t.File, importedModule: ImportDeclarat
/** /**
* 更新已有的导入语句 * 更新已有的导入语句
* @deprecated 使用 updateImportDeclaration2 代替
* @param ast * @param ast
* @param importedModule * @param importedModule
* @param importedSourcePath * @param importedSourcePath
* @returns * @returns
*/ */
export function updateImportDeclaration(ast: t.File, importedModule: ImportDeclarationPayloadType) { export function updateImportDeclarationLegacy(
ast: t.File,
importedModule: IImportDeclarationPayload,
) {
traverse(ast, { traverse(ast, {
ImportDeclaration(path) { ImportDeclaration(path) {
const currentSourcePath = node2value(path.node.source); const currentSourcePath = node2value(path.node.source);
if (currentSourcePath === importedModule.sourcePath) { if (currentSourcePath === importedModule.sourcePath) {
const newImportDeclaration = makeImportDeclaration(importedModule); const newImportDeclaration = makeImportDeclarationLegacy(importedModule);
path.replaceWith(newImportDeclaration); path.replaceWith(newImportDeclaration);
path.stop(); // 只修改匹配到的第一条 path.stop(); // 只修改匹配到的第一条
} }
@@ -532,6 +577,50 @@ export function updateImportDeclaration(ast: t.File, importedModule: ImportDecla
return ast; return ast;
} }
export function updateImportDeclaration(
ast: t.File,
source: string,
specifiers: IImportSpecifierData[],
) {
traverse(ast, {
ImportDeclaration(path) {
const currentSourcePath = node2value(path.node.source);
if (currentSourcePath === source) {
const newImportDeclaration = makeImportDeclaration(source, specifiers);
path.replaceWith(newImportDeclaration);
path.stop(); // 只修改匹配到的第一条
}
},
});
return ast;
}
/**
* 再已有的导入语句中添加新的导入符号
* @param ast
* @param source
* @param newSpecifiers
* @returns
*/
export function insertImportSpecifiers(
ast: t.File,
source: string,
newSpecifiers: IImportSpecifierData[],
) {
traverse(ast, {
ImportDeclaration(path) {
const currentSourcePath = node2value(path.node.source);
if (currentSourcePath === source) {
const nodes = specifierDataList2nodes(newSpecifiers);
path.node.specifiers.push(...nodes);
// 只在匹配到的第一个导入声明语句添加即可,不再重复执行
path.stop();
}
},
});
return ast;
}
/** /**
* 新增类属性 * 新增类属性
* @param ast * @param ast
@@ -656,7 +745,7 @@ export function removeRouteFromRouteFile(ast: t.File, routePath: string, importP
} }
export function traverseRouteFile(ast: t.File) { export function traverseRouteFile(ast: t.File) {
const routes: RouteDataType[] = []; const routes: IRouteData[] = [];
const importMap: Dict<string> = {}; const importMap: Dict<string> = {};
traverse(ast, { traverse(ast, {
@@ -819,8 +908,8 @@ export function updateStoreState(ast: t.File, stateName: string, code: string) {
export function traverseStoreFile(ast: t.File) { export function traverseStoreFile(ast: t.File) {
let namespace: string; let namespace: string;
const actions: StorePropertyType[] = []; const actions: IStorePropertyData[] = [];
const states: StorePropertyType[] = []; const states: IStorePropertyData[] = [];
traverse(ast, { traverse(ast, {
CallExpression(path) { CallExpression(path) {
@@ -909,7 +998,7 @@ export function deleteServiceConfigFromServiceFile(ast: t.File, serviceFunctionN
* @param payload * @param payload
* @returns * @returns
*/ */
export function serviceConfig2Node(payload: ServiceFunctionPayloadType) { export function serviceConfig2Node(payload: IServiceFunctionPayload) {
return object2node(payload, (value, key) => { return object2node(payload, (value, key) => {
if (key === 'formatter' && value) { if (key === 'formatter' && value) {
return code2expression(value); return code2expression(value);
@@ -920,7 +1009,7 @@ export function serviceConfig2Node(payload: ServiceFunctionPayloadType) {
export function updateServiceConfigToServiceFile( export function updateServiceConfigToServiceFile(
ast: t.File, ast: t.File,
config: { [key: string]: ServiceFunctionPayloadType }, config: { [key: string]: IServiceFunctionPayload },
) { ) {
traverse(ast, { traverse(ast, {
CallExpression(path) { CallExpression(path) {
@@ -959,42 +1048,22 @@ export function updateServiceConfigToServiceFile(
return ast; return ast;
} }
function visitObjectProperties( export function updateBaseConfigToServiceFile(ast: t.File, config: object) {
node: t.ObjectExpression,
visitor: (propName: string, node: t.ObjectProperty) => void,
) {
for (const propNode of node.properties) {
if (t.isObjectProperty(propNode)) {
const propName = keyNode2value(propNode.key);
visitor?.(propName as string, propNode);
}
}
}
export function updateBaseConfigToServiceFile(ast: t.File, configName: string, configValue: any) {
traverse(ast, { traverse(ast, {
CallExpression(path) { CallExpression(path) {
const calleeName = keyNode2value(path.node.callee) as string; const calleeName = keyNode2value(path.node.callee) as string;
if (isDefineService(calleeName)) { if (isDefineService(calleeName)) {
switch (path.node.arguments.length) { switch (path.node.arguments.length) {
case 1: case 1:
path.node.arguments.push(value2node({ [configName]: configValue })); // 没有 baseConfig,直接创建新的参数即可
path.node.arguments.push(value2node(config));
break; break;
case 2: { case 2: {
// 已存在 baseConfig,需要进行参数的合并
const baseConfigNode = path.node.arguments[1]; const baseConfigNode = path.node.arguments[1];
if (t.isObjectExpression(baseConfigNode)) { if (t.isObjectExpression(baseConfigNode)) {
let found = false; const baseConfig = node2value(baseConfigNode);
visitObjectProperties(baseConfigNode, (propName, propNode) => { path.node.arguments[1] = value2node({ ...baseConfig, ...config });
if (propName === configName) {
found = true;
propNode.value = value2node(configValue);
}
});
if (!found) {
baseConfigNode.properties.push(
t.objectProperty(t.identifier(configName), value2node(configValue)),
);
}
} }
break; break;
} }
@@ -1013,7 +1082,12 @@ export function traverseServiceFile(ast: t.File) {
const baseConfig: any = { const baseConfig: any = {
encryptFetch: false, encryptFetch: false,
}; };
const imports: Record<string, IImportSpecifierData[]> = {};
traverse(ast, { traverse(ast, {
ImportDeclaration(path) {
const { source, specifiers } = parseImportDeclaration(path.node);
imports[source] = specifiers;
},
CallExpression(path) { CallExpression(path) {
const calleeName = keyNode2value(path.node.callee) as string; const calleeName = keyNode2value(path.node.callee) as string;
if (isDefineService(calleeName)) { if (isDefineService(calleeName)) {
@@ -1022,6 +1096,7 @@ export function traverseServiceFile(ast: t.File) {
if (path.node.arguments[1]) { if (path.node.arguments[1]) {
const config = node2value(path.node.arguments[1]); const config = node2value(path.node.arguments[1]);
baseConfig.encryptFetch = !!config.encryptFetch; baseConfig.encryptFetch = !!config.encryptFetch;
baseConfig.namespace = config.namespace || 'index';
} }
} }
path.stop(); path.stop();
@@ -1029,6 +1104,7 @@ export function traverseServiceFile(ast: t.File) {
}, },
}); });
return { return {
imports,
services, services,
baseConfig, baseConfig,
}; };
@@ -1133,23 +1209,52 @@ export function cloneJSXElementWithoutTrackingData(node: t.JSXElement) {
} }
export function traverseViewFile(ast: t.File, idGenerator: IdGenerator) { export function traverseViewFile(ast: t.File, idGenerator: IdGenerator) {
const importedModules: Dict<ImportDeclarationPayloadType | ImportDeclarationPayloadType[]> = {}; const imports: Record<string, IImportSpecifierData[]> = {};
const classProperties: Dict<ClassPropertyNodeType> = {}; const importedModules: Dict<IImportDeclarationPayload | IImportDeclarationPayload[]> = {};
let stateProperties: string[] = []; const nodes: Array<ITangoViewNodeData<t.JSXElement>> = [];
let stateCode = '';
const nodes: Array<TangoViewNodeDataType<t.JSXElement>> = [];
const cloneAst = t.cloneNode(ast, true, true); const cloneAst = t.cloneNode(ast, true, true);
const cleanAst = removeTrackingDataFromViewAst(cloneAst); const cleanAst = removeTrackingDataFromViewAst(cloneAst);
const variables: string[] = []; // 使用的 tango 变量 const variables: string[] = []; // 使用的 tango 变量
traverse(ast, { traverse(ast, {
ImportDeclaration(path) { ImportDeclaration(path) {
const declarationData = getImportDeclarationData(path.node); const { source, specifiers } = parseImportDeclaration(path.node);
const exist = importedModules[declarationData.sourcePath]; if (imports[source]) {
if (!exist) { // 存在重复的导入语句,合并导入符号列表
importedModules[declarationData.sourcePath] = declarationData; imports[source] = imports[source].concat(specifiers);
} else { } else {
importedModules[declarationData.sourcePath] = Array.isArray(exist) imports[source] = specifiers;
}
// FIXME: 下面的逻辑兼容旧的逻辑,后续需要移除掉
const declarationData = specifiers.reduce(
(prev, cur) => {
switch (cur.type) {
case 'ImportDefaultSpecifier':
prev.defaultSpecifier = cur.localName;
break;
case 'ImportSpecifier':
prev.specifiers.push(cur.localName);
break;
case 'ImportNamespaceSpecifier':
prev.specifiers.push(cur.localName);
break;
default:
break;
}
return prev;
},
{
defaultSpecifier: undefined,
specifiers: [],
sourcePath: source,
},
);
const exist = importedModules[source];
if (!exist) {
importedModules[source] = declarationData;
} else {
importedModules[source] = Array.isArray(exist)
? exist.concat([declarationData]) ? exist.concat([declarationData])
: [exist, declarationData]; : [exist, declarationData];
} }
@@ -1157,36 +1262,15 @@ export function traverseViewFile(ast: t.File, idGenerator: IdGenerator) {
MemberExpression(path) { MemberExpression(path) {
const variable = node2value(path.node, false); const variable = node2value(path.node, false);
if ( if (isTangoVariable(variable) && !variables.includes(variable)) {
/^tango\.(stores|services)\./.test(variable) &&
variable.split('.').length === 3 &&
!variables.includes(variable)
) {
variables.push(variable); variables.push(variable);
} }
}, },
ClassProperty(path) { OptionalMemberExpression(path) {
const propName = keyNode2value(path.node.key) as string; const variable = node2value(path.node, false);
if (!propName) { if (isTangoVariable(variable) && !variables.includes(variable)) {
return; variables.push(variable);
}
const reference = `this.${propName}`;
if (['defaultProps', 'propTypes', 'constructor'].includes(propName)) {
return;
}
if (propName === 'state') {
stateProperties = Object.keys(node2value(path.node.value));
stateCode = ast2code(path.node);
} else {
// TODO: 这里需要完善下,并不是所有的都是 arrowFunction
classProperties[reference] = {
reference,
propertyName: propName,
propertyBody: expression2code(path.node.value),
};
} }
}, },
@@ -1216,9 +1300,10 @@ export function traverseViewFile(ast: t.File, idGenerator: IdGenerator) {
// parentId 用于追溯上下游关系 // parentId 用于追溯上下游关系
let parentId; let parentId;
const parentNode = path.parentPath.node; const parentNode = path.findParent((p) => p.isJSXElement());
if (t.isJSXElement(parentNode)) {
const parentAttributes = getJSXElementAttributes(parentNode); if (t.isJSXElement(parentNode?.node)) {
const parentAttributes = getJSXElementAttributes(parentNode.node);
parentId = parentAttributes[SLOT.dnd]; parentId = parentAttributes[SLOT.dnd];
} }
@@ -1235,10 +1320,50 @@ export function traverseViewFile(ast: t.File, idGenerator: IdGenerator) {
ast, ast,
cleanAst, cleanAst,
nodes, nodes,
stateCode, imports,
classProperties,
stateProperties,
importedModules, importedModules,
variables, variables,
}; };
} }
/**
* 解析导入语句
*/
function parseImportDeclaration(node: t.ImportDeclaration) {
const source = node2value(node.source) as string;
const specifiers: IImportSpecifierData[] = [];
node.specifiers.forEach((specifierNode) => {
const data: IImportSpecifierData = {
localName: keyNode2value(specifierNode.local) as string,
type: specifierNode.type,
};
if (specifierNode.type === 'ImportSpecifier') {
data.importedName = keyNode2value(specifierNode.imported) as string;
}
specifiers.push(data);
});
return {
source,
specifiers,
};
}
/**
* 基本的文件解析过程
* @param ast
*/
export function traverseFile(ast: t.File) {
const imports: Record<string, IImportSpecifierData[]> = {};
traverse(ast, {
ImportDeclaration(path) {
const { source, specifiers } = parseImportDeclaration(path.node);
imports[source] = specifiers;
},
});
return {
imports,
};
}
+63
View File
@@ -0,0 +1,63 @@
import { getVariableContent } from '@music163/tango-helpers';
import { value2node, expression2code, isValidExpressionCode } from './ast';
import { isWrappedByExpressionContainer } from './assert';
/**
* 将 js value 转换为代码字符串
*/
export function value2code(value: any) {
const node = value2node(value);
const code = expression2code(node);
return code;
}
/**
* 是否是字符串代码
* @param code
* @returns
*/
function isStringCode(code: string) {
return /^".*"$/.test(code?.trim());
}
/**
* js value 转为表达式代码
* @example 1 => 1
* @example hello => "hello"
* @example { foo: bar } => {{ foo: bar }}
* @example [1,2,3] => {[1,2,3]}
*
* @param val js value
* @returns 表达式代码
*/
export function value2expressionCode(val: any) {
if (!val) return '';
let ret;
switch (typeof val) {
case 'string': {
if (isValidExpressionCode(val)) {
ret = val;
} else if (isWrappedByExpressionContainer(val, false)) {
ret = getVariableContent(val);
} else if (isStringCode(val)) {
ret = val;
} else {
ret = `"${val}"`;
}
break;
}
case 'number':
ret = String(val);
break;
case 'object':
ret = value2code(val);
break;
default:
ret = '';
break;
}
return ret;
}
+1
View File
@@ -1,5 +1,6 @@
export * from './ast'; export * from './ast';
export * from './assert'; export * from './assert';
export * from './code-helpers';
export * from './string'; export * from './string';
export * from './object'; export * from './object';
export * from './prototype'; export * from './prototype';
+5 -4
View File
@@ -1,4 +1,5 @@
import { ImportDeclarationPayloadType } from '../types'; import { Dict } from '@music163/tango-helpers';
import { IImportDeclarationPayload, IImportSpecifierSourceData } from '../types';
/** /**
* 导入列表解析为导入声明对象 * 导入列表解析为导入声明对象
@@ -8,19 +9,19 @@ import { ImportDeclarationPayloadType } from '../types';
*/ */
export function namesToImportDeclarations( export function namesToImportDeclarations(
names: string[], names: string[],
nameMap: Record<string, { package: string; isDefault?: boolean }>, nameMap: Dict<IImportSpecifierSourceData>,
) { ) {
const map = {}; const map = {};
names.forEach((name) => { names.forEach((name) => {
const mod = nameMap[name]; const mod = nameMap[name];
if (mod) { if (mod) {
updateMod(map, mod.package, name, mod.isDefault, !map[mod.package]); updateMod(map, mod.source, name, mod.isDefault, !map[mod.source]);
} }
}); });
return Object.keys(map).map((sourcePath) => ({ return Object.keys(map).map((sourcePath) => ({
sourcePath, sourcePath,
...map[sourcePath], ...map[sourcePath],
})) as ImportDeclarationPayloadType[]; })) as IImportDeclarationPayload[];
} }
function updateMod( function updateMod(
+42 -4
View File
@@ -3,21 +3,56 @@ import {
ComponentPropType, ComponentPropType,
ComponentPrototypeType, ComponentPrototypeType,
isNil, isNil,
isVariableString,
logger, logger,
uuid, uuid,
} from '@music163/tango-helpers'; } from '@music163/tango-helpers';
import { getRelativePath, isFilepath } from './string'; import { getRelativePath, isFilepath } from './string';
import type { ImportDeclarationPayloadType } from '../types'; import type { IImportDeclarationPayload, IImportSpecifierData } from '../types';
import { code2expression } from './ast'; import { code2expression } from './ast';
import { isWrappedByExpressionContainer } from './assert';
export function prototype2importDeclarationData(
prototype: ComponentPrototypeType,
relativeFilepath?: string,
): { source: string; specifiers: IImportSpecifierData[] } {
let source = prototype.package;
if (relativeFilepath && isFilepath(source)) {
source = getRelativePath(relativeFilepath, source);
}
if (source.endsWith('.js')) {
source = source.slice(0, -3);
}
const specifiers: IImportSpecifierData[] = [];
if (prototype.exportType === 'defaultExport') {
specifiers.push({
localName: prototype.name,
type: 'ImportDefaultSpecifier',
});
} else {
[prototype.name, ...(prototype.relatedImports || [])].forEach((item) => {
specifiers.push({
localName: item,
type: 'ImportSpecifier',
});
});
}
return {
source,
specifiers,
};
}
/** /**
* 根据组件的 prototype 生成 ImportDeclarationPayload * 根据组件的 prototype 生成 ImportDeclarationPayload
* @deprecated
*/ */
export function getImportDeclarationPayloadByPrototype( export function getImportDeclarationPayloadByPrototype(
prototype: ComponentPrototypeType, prototype: ComponentPrototypeType,
relativeFilepath?: string, relativeFilepath?: string,
): ImportDeclarationPayloadType { ): IImportDeclarationPayload {
let defaultSpecifier; let defaultSpecifier;
let specifiers; let specifiers;
@@ -35,6 +70,9 @@ export function getImportDeclarationPayloadByPrototype(
if (relativeFilepath && isFilepath(sourcePath)) { if (relativeFilepath && isFilepath(sourcePath)) {
sourcePath = getRelativePath(relativeFilepath, sourcePath); sourcePath = getRelativePath(relativeFilepath, sourcePath);
} }
if (sourcePath.endsWith('.js')) {
sourcePath = sourcePath.slice(0, -3);
}
return { return {
defaultSpecifier, defaultSpecifier,
@@ -82,7 +120,7 @@ function getPropKeyValuePair(item: ComponentPropType, generateValue: (...args: a
break; break;
} }
case 'string': { case 'string': {
if (!isVariableString(value)) { if (!isWrappedByExpressionContainer(value)) {
// 不是变量字符串 // 不是变量字符串
value = `"${value}"`; value = `"${value}"`;
} else { } else {
+1 -12
View File
@@ -1,6 +1,5 @@
import path from 'path'; import path from 'path';
import { camelCase, upperCamelCase } from '@music163/tango-helpers'; import { camelCase, upperCamelCase } from '@music163/tango-helpers';
import { value2node, expression2code } from './ast';
import { FileType } from './../types'; import { FileType } from './../types';
/** /**
@@ -34,8 +33,7 @@ export function inferFileType(filename: string): FileType {
return FileType.JsonViewModule; return FileType.JsonViewModule;
} }
// FIXME: 暂时写死,后续要支持多 services 文件解析 if (/\/services\/.+\.js$/.test(filename)) {
if (/\/services\/index.js$/.test(filename)) {
return FileType.ServiceModule; return FileType.ServiceModule;
} }
@@ -141,15 +139,6 @@ export function formatCode(code: string, parser = 'babel') {
return code; return code;
} }
/**
* 将 js value 转换为代码字符串
*/
export function value2code(value: any) {
const node = value2node(value);
const code = expression2code(node);
return code;
}
/** /**
* 是否匹配路由 * 是否匹配路由
* @example isPathnameMatchRoute('/user/123', '/users/:id') -> true * @example isPathnameMatchRoute('/user/123', '/users/:id') -> true
+5 -1
View File
@@ -1,4 +1,8 @@
export * from './models'; export * from './models';
export * from './factory'; export * from './factory';
export * from './types';
export * from './helpers'; export * from './helpers';
export * from './types';
export { default as generator } from '@babel/generator';
export { default as traverse } from '@babel/types';
export * from '@babel/parser';
+36 -18
View File
@@ -3,22 +3,26 @@ import { IWorkspace } from './interfaces';
export type SimulatorNameType = 'desktop' | 'phone'; export type SimulatorNameType = 'desktop' | 'phone';
type SimulatorType = { export type DesignerViewType = 'design' | 'code';
interface ISimulatorType {
name: SimulatorNameType; name: SimulatorNameType;
width: number; width: number;
height: number; height: number;
}; }
type ViewportBoundingType = { interface IViewportBounding {
width: number; width: number;
height: number; height: number;
}; }
export type DesignerViewType = 'design' | 'code'; interface IDesignerOptions {
workspace: IWorkspace;
simulator?: SimulatorNameType | ISimulatorType;
activeSidebarPanel?: string;
}
type DesignerOptionsType = { workspace: IWorkspace; simulator?: SimulatorNameType | SimulatorType }; const ISimulatorTypes: Record<string, ISimulatorType> = {
const simulatorTypes: Record<string, SimulatorType> = {
desktop: { desktop: {
name: 'desktop', name: 'desktop',
width: 1366, width: 1366,
@@ -35,12 +39,12 @@ export class Designer {
/** /**
* 当前的沙箱模拟器类型 * 当前的沙箱模拟器类型
*/ */
_simulator: SimulatorType = simulatorTypes.desktop; _simulator: ISimulatorType = ISimulatorTypes.desktop;
/** /**
* 当前的视图尺寸 * 当前的视图尺寸
*/ */
_viewport: ViewportBoundingType = { _viewport: IViewportBounding = {
width: 1366, width: 1366,
height: 800, height: 800,
}; };
@@ -70,9 +74,14 @@ export class Designer {
*/ */
_isPreview = false; _isPreview = false;
/**
* 默认展开的侧边栏
*/
defaultActiveSidebarPanel?: string;
private readonly workspace: IWorkspace; private readonly workspace: IWorkspace;
get simulator(): SimulatorType { get simulator(): ISimulatorType {
return toJS(this._simulator); return toJS(this._simulator);
} }
@@ -100,11 +109,21 @@ export class Designer {
return this._showRightPanel; return this._showRightPanel;
} }
constructor(options: DesignerOptionsType) { constructor(options: IDesignerOptions) {
this.workspace = options.workspace; this.workspace = options.workspace;
if (options.simulator) {
this.setSimulator(options.simulator); const { simulator, activeSidebarPanel: defaultActiveSidebarPanel } = options;
// 默认设计器模式
if (simulator) {
this.setSimulator(simulator);
} }
// 默认展开的侧边栏
if (defaultActiveSidebarPanel) {
this.setActiveSidebarPanel(defaultActiveSidebarPanel);
}
makeObservable(this, { makeObservable(this, {
_simulator: observable, _simulator: observable,
_viewport: observable, _viewport: observable,
@@ -131,15 +150,15 @@ export class Designer {
}); });
} }
setSimulator(value: SimulatorType | SimulatorNameType) { setSimulator(value: ISimulatorType | SimulatorNameType) {
if (typeof value === 'string') { if (typeof value === 'string') {
this._simulator = simulatorTypes[value]; this._simulator = ISimulatorTypes[value];
} else { } else {
this._simulator = value; this._simulator = value;
} }
} }
setViewport(value: ViewportBoundingType) { setViewport(value: IViewportBounding) {
this._viewport = value; this._viewport = value;
} }
@@ -154,7 +173,6 @@ export class Designer {
this._activeSidebarPanel = ''; this._activeSidebarPanel = '';
} }
} }
closeSidebarPanel() { closeSidebarPanel() {
this._activeSidebarPanel = ''; this._activeSidebarPanel = '';
} }
+5 -5
View File
@@ -1,8 +1,8 @@
import { action, computed, makeObservable, observable, toJS } from 'mobx'; import { action, computed, makeObservable, observable, toJS } from 'mobx';
import { getValue, isNil, logger, setValue } from '@music163/tango-helpers'; import { getValue, isNil, logger, setValue } from '@music163/tango-helpers';
import type { FileType, ModulePropsType } from '../types'; import type { FileType, IFileConfig } from '../types';
import { formatCode } from '../helpers';
import { IWorkspace } from './interfaces'; import { IWorkspace } from './interfaces';
import { formatCode } from '../helpers';
/** /**
* 普通文件,不进行 AST 解析 * 普通文件,不进行 AST 解析
@@ -35,7 +35,7 @@ export class TangoFile {
return this._cleanCode; return this._cleanCode;
} }
constructor(workspace: IWorkspace, props: ModulePropsType, isSyncCode = true) { constructor(workspace: IWorkspace, props: IFileConfig, isSyncCode = true) {
this.workspace = workspace; this.workspace = workspace;
this.filename = props.filename; this.filename = props.filename;
this.type = props.type; this.type = props.type;
@@ -60,7 +60,7 @@ export class TangoFile {
} }
export class TangoLessFile extends TangoFile { export class TangoLessFile extends TangoFile {
constructor(workspace: IWorkspace, props: ModulePropsType) { constructor(workspace: IWorkspace, props: IFileConfig) {
super(workspace, props, false); super(workspace, props, false);
this.update(props.code); this.update(props.code);
makeObservable(this, { makeObservable(this, {
@@ -87,7 +87,7 @@ export class TangoJsonFile extends TangoFile {
return toJS(this._object); return toJS(this._object);
} }
constructor(workspace: IWorkspace, props: ModulePropsType) { constructor(workspace: IWorkspace, props: IFileConfig) {
super(workspace, props, false); super(workspace, props, false);
this.update(props.code); this.update(props.code);
makeObservable(this, { makeObservable(this, {
+4
View File
@@ -7,5 +7,9 @@ export * from './drag-source';
export * from './history'; export * from './history';
export * from './file'; export * from './file';
export * from './module'; export * from './module';
export * from './route-module';
export * from './service-module';
export * from './store-module';
export * from './view-module';
export * from './node'; export * from './node';
export * from './interfaces'; export * from './interfaces';
+110 -81
View File
@@ -3,17 +3,19 @@ import { TangoHistory } from './history';
import { SelectSource } from './select-source'; import { SelectSource } from './select-source';
import { DragSource } from './drag-source'; import { DragSource } from './drag-source';
import { import {
FileItemType, IFileConfig,
FileType, FileType,
ImportDeclarationPayloadType,
InsertChildPositionType, InsertChildPositionType,
PackageConfigType, ITangoConfigPackages,
PageConfigType, IPageConfigData,
ProjectDataType, IServiceFunctionPayload,
ServiceFunctionPayloadType, IImportSpecifierSourceData,
} from '../types/types'; IImportSpecifierData,
} from '../types';
import { TangoFile, TangoJsonFile } from './file'; import { TangoFile, TangoJsonFile } from './file';
import { TangoRouteModule, TangoServiceModule, TangoStoreModule } from './module'; import { TangoRouteModule } from './route-module';
import { TangoStoreModule } from './store-module';
import { TangoServiceModule } from './service-module';
export interface IViewFile { export interface IViewFile {
readonly workspace: IWorkspace; readonly workspace: IWorkspace;
@@ -23,10 +25,22 @@ export interface IViewFile {
/** /**
* 通过导入组件名查找组件来自的包 * 通过导入组件名查找组件来自的包
*/ */
importMap?: Dict<{ package: string; isDefault?: boolean }>; importMap?: Dict<IImportSpecifierSourceData>;
listImportSources?: () => string[];
listModals?: () => Array<{ label: string; value: string }>;
listForms?: () => Record<string, string[]>;
update: (code?: string, isFormatCode?: boolean, refreshWorkspace?: boolean) => void; update: (code?: string, isFormatCode?: boolean, refreshWorkspace?: boolean) => void;
/**
* 添加新的导入符号
* @param source 导入来源
* @param newSpecifiers 新导入符号列表
* @returns this
*/
addImportSpecifiers: (source: string, newSpecifiers: IImportSpecifierData[]) => IViewFile;
getNode: (targetNodeId: string) => IViewNode; getNode: (targetNodeId: string) => IViewNode;
removeNode: (targetNodeId: string) => IViewFile; removeNode: (targetNodeId: string) => IViewFile;
@@ -35,20 +49,28 @@ export interface IViewFile {
targetNodeId: string, targetNodeId: string,
newNode: any, newNode: any,
position?: InsertChildPositionType, position?: InsertChildPositionType,
sourcePrototype?: string | ComponentPrototypeType,
) => IViewFile; ) => IViewFile;
insertAfter: (targetNodeId: string, newNode: any, sourcePrototype?: string | ComponentPrototypeType) => IViewFile; insertAfter: (targetNodeId: string, newNode: any) => IViewFile;
insertBefore: (targetNodeId: string, newNode: any, sourcePrototype?: string | ComponentPrototypeType) => IViewFile; insertBefore: (targetNodeId: string, newNode: any) => IViewFile;
replaceNode: (targetNodeId: string, newNode: any, sourcePrototype?: string | ComponentPrototypeType) => IViewFile; replaceNode: (targetNodeId: string, newNode: any) => IViewFile;
replaceViewChildren: (rawNodes: any[], importDeclarations?: ImportDeclarationPayloadType[]) => IViewFile; replaceViewChildren: (rawNodes: any[]) => IViewFile;
updateNodeAttribute: (nodeId: string, attrName: string, attrValue?: any, relatedImports?: string[]) => IViewFile; updateNodeAttribute: (
nodeId: string,
attrName: string,
attrValue?: any,
relatedImports?: string[],
) => IViewFile;
updateNodeAttributes: (nodeId: string, config: Record<string, any>, relatedImports?: string[]) => IViewFile; updateNodeAttributes: (
nodeId: string,
config: Record<string, any>,
relatedImports?: string[],
) => IViewFile;
get code(): string; get code(): string;
get nodes(): Map<string, IViewNode>; get nodes(): Map<string, IViewNode>;
@@ -115,10 +137,11 @@ export interface IWorkspace {
tangoConfigJson: TangoJsonFile; tangoConfigJson: TangoJsonFile;
routeModule?: TangoRouteModule; routeModule?: TangoRouteModule;
serviceModule?: TangoServiceModule; storeModules?: Record<string, TangoStoreModule>;
serviceModules?: Record<string, TangoServiceModule>;
refresh: (names: string[]) => void;
ready: () => void; ready: () => void;
refresh: (names: string[]) => void;
setActiveRoute: (path: string) => void; setActiveRoute: (path: string) => void;
setActiveFile: (filename: string) => void; setActiveFile: (filename: string) => void;
@@ -126,10 +149,46 @@ export interface IWorkspace {
setComponentPrototypes: (prototypes: Record<string, ComponentPrototypeType>) => void; setComponentPrototypes: (prototypes: Record<string, ComponentPrototypeType>) => void;
getPrototype: (name: string | ComponentPrototypeType) => ComponentPrototypeType; getPrototype: (name: string | ComponentPrototypeType) => ComponentPrototypeType;
/** onFilesChange?: (filenames: string[]) => void;
* 获取项目数据
*/ // ----------------- 文件操作 -----------------
getProjectData?: () => ProjectDataType; addFiles: (files: IFileConfig[]) => void;
addFile: (filename: string, code: string, fileType?: FileType) => void;
addServiceFile: (serviceName: string, code: string) => void;
addStoreFile: (storeName: string, code: string) => void;
addViewFile: (viewName: string, code: string) => void;
removeFile: (filename: string) => void;
renameFile: (oldFilename: string, newFilename: string) => void;
renameFolder: (oldFoldername: string, newFoldername: string) => void;
updateFile: (filename: string, code: string, shouldFormatCode?: boolean) => void;
listFiles: () => Record<string, string>;
getFile: (filename: string) => TangoFile;
// ----------------- 节点操作 -----------------
removeSelectedNode: () => void;
cloneSelectedNode: () => void;
copySelectedNode: () => void;
pasteSelectedNode: () => void;
insertToSelectedNode: (childNameOrPrototype: string | ComponentPrototypeType) => void;
dropNode: () => void;
insertToNode: (
targetNodeId: string,
sourceNameOrPrototype: string | ComponentPrototypeType,
) => void;
replaceNode: (
targetNodeId: string,
sourceNameOrPrototype: string | ComponentPrototypeType,
) => void;
updateSelectedNodeAttributes: (
attributes: Record<string, any>,
relatedImports?: string[],
) => void;
/** /**
* 查询节点 * 查询节点
@@ -138,74 +197,44 @@ export interface IWorkspace {
* @returns 返回节点对象 * @returns 返回节点对象
*/ */
getNode: (id: string, module?: string) => IViewNode; getNode: (id: string, module?: string) => IViewNode;
listModals?: () => Array<{ label: string; value: string }>;
listForms?: () => Record<string, string[]>;
addFile: (filename: string, code: string, fileType?: FileType) => void; // ----------------- 服务函数文件操作 -----------------
addFiles: (files: FileItemType[]) => void;
updateFile: (filename: string, code: string, shouldFormatCode?: boolean) => void;
removeFile: (filename: string) => void;
renameFile: (oldFilename: string, newFilename: string) => void;
getFile: (filename: string) => TangoFile;
listFiles: () => Record<string, string>;
addViewPage: (name: string, code: string) => void; getServiceFunction?: (serviceKey: string) => object;
listServiceFunctions?: () => Record<string, object>;
removeSelectedNode: () => void; removeServiceFunction?: (serviceName: string, modName?: string) => void;
addServiceFunction?: (
cloneSelectedNode: () => void; payload: IServiceFunctionPayload | IServiceFunctionPayload[],
modName?: string,
copySelectedNode: () => void; ) => void;
updateServiceFunction?: (payload: IServiceFunctionPayload, modName?: string) => void;
pasteSelectedNode: () => void; updateServiceBaseConfig?: (IServiceFunctionPayload: object, modName?: string) => void;
insertToSelectedNode: (childNameOrPrototype: string | ComponentPrototypeType) => void;
dropNode: () => void;
insertToNode: (targetNodeId: string, sourceNameOrPrototype: string | ComponentPrototypeType) => void;
replaceNode: (targetNodeId: string, sourceNameOrPrototype: string | ComponentPrototypeType) => void;
updateSelectedNodeAttributes: (attributes: Record<string, any>, relatedImports?: string[]) => void;
addBlock?: (files: object, name: string) => void;
generateBlockFilesBySelectedNode?: () => Record<string, string>;
removeServiceFunction?: (serviceName: string) => void;
addServiceFunction?: (payload: ServiceFunctionPayloadType | ServiceFunctionPayloadType[]) => void;
updateServiceFunction?: (payload: any) => void;
updateServiceBaseConfig?: (name: string, value: any) => void;
listStoreModules?: () => TangoStoreModule[];
addStoreModule?: (storeName: string, code: string) => void;
// ----------------- 状态管理文件操作 -----------------
addStoreState?: (storeName: string, stateName: string, initValue: string) => void;
removeStoreState?: (storeName: string, stateName: string) => void;
removeStoreModule?: (storeName: string) => void; removeStoreModule?: (storeName: string) => void;
addStoreState?: (storeName: string, stateName: string, initValue: string) => void; // ----------------- 视图文件操作 -----------------
removeStoreState?: (storeName: string, stateName: string) => void;
updateModuleCodeByVariablePath?: (variablePath: string, code: string) => void;
removeViewModule: (routePath: string) => void; removeViewModule: (routePath: string) => void;
copyViewPage: (sourceRoutePath: string, targetPageData: IPageConfigData) => void;
copyViewPage: (sourceRoutePath: string, targetPageData: PageConfigType) => void; // ----------------- 路由文件操作 -----------------
updateRoute: (sourceRoutePath: string, targetPageData: PageConfigType) => void; updateRoute: (sourceRoutePath: string, targetPageData: IPageConfigData) => void;
// ----------------- 依赖包操作 -----------------
addDependency?: (data: any) => void;
listDependencies?: () => any; listDependencies?: () => any;
getDependency?: (pkgName: string) => object;
updateDependency?: ( updateDependency?: (
name: string, name: string,
version: string, version: string,
options?: { options?: {
package?: PackageConfigType; package?: ITangoConfigPackages;
[x: string]: any; [x: string]: any;
}, },
) => void; ) => void;
@@ -216,22 +245,22 @@ export interface IWorkspace {
name: string, name: string,
version: string, version: string,
options?: { options?: {
package?: PackageConfigType; package?: ITangoConfigPackages;
[x: string]: any; [x: string]: any;
}, },
) => void; ) => void;
removeBizComp?: (name: string) => void; removeBizComp?: (name: string) => void;
// ----------------- 其他操作 -----------------
updateModuleCodeByVariablePath?: (variablePath: string, code: string) => void;
// ----------------- getter -----------------
get activeViewModule(): IViewFile; get activeViewModule(): IViewFile;
// TODO: -> getStoreModules
get storeModules(): TangoStoreModule[];
// TODO: -> getPages
get pages(): any[]; get pages(): any[];
// TODO: getBizComps
get bizComps(): string[]; get bizComps(): string[];
// TODO: getBaseComps
get baseComps(): string[]; get baseComps(): string[];
// TODO: getBlocks
get blocks(): any[]; get blocks(): any[];
} }
+29 -672
View File
@@ -1,63 +1,32 @@
import * as t from '@babel/types'; import * as t from '@babel/types';
import { action, computed, makeObservable, observable, toJS } from 'mobx'; import { action, computed, makeObservable, observable } from 'mobx';
import { clone, ComponentPrototypeType, Dict, isNil, logger } from '@music163/tango-helpers'; import { isNil } from '@music163/tango-helpers';
import { import {
code2ast, code2ast,
ast2code, ast2code,
traverseRouteFile, formatCode,
traverseViewFile, traverseFile,
traverseServiceFile,
traverseStoreFile,
traverseStoreEntryFile,
addStoreToEntryFile,
updateServiceConfigToServiceFile,
removeJSXElement,
insertSiblingAfterJSXElement,
getModuleNameByFilename,
addRouteToRouteFile,
removeRouteFromRouteFile,
appendChildToJSXElement,
addImportDeclaration, addImportDeclaration,
updateImportDeclaration, updateImportDeclaration,
deleteServiceConfigFromServiceFile,
replaceJSXElement,
formatCode,
getImportDeclarationPayloadByPrototype,
addStoreState,
removeUnusedImportSpecifiers,
insertSiblingBeforeJSXElement,
updateStoreState,
removeStoreState,
removeStoreToEntryFile,
replaceRootJSXElementChildren,
updateRouteToRouteFile,
updateBaseConfigToServiceFile,
IdGenerator,
updateJSXAttributes,
} from '../helpers'; } from '../helpers';
import { TangoNode } from './node';
import { TangoFile } from './file'; import { TangoFile } from './file';
import { import { IFileConfig, IImportSpecifierData, ImportDeclarationDataType } from '../types';
RouteDataType, import { IWorkspace } from './interfaces';
ModulePropsType,
ClassPropertyNodeType,
ServiceFunctionPayloadType,
TangoViewNodeDataType,
StorePropertyType,
ImportDeclarationPayloadType,
InsertChildPositionType,
} from '../types';
import { IViewFile, IWorkspace } from './interfaces';
/** /**
* 模块实现规范 * JS 模块实现规范
* - ast 操纵类方法,统一返回 this,支持外层链式调用 * - ast 操纵类方法,统一返回 this,支持外层链式调用
* - observable state 统一用 _foo 格式,并提供 getter 方法 * - observable state 统一用 _foo 格式,并提供 getter 方法
*/ */
class TangoModule extends TangoFile { export class TangoModule extends TangoFile {
ast: t.File; ast: t.File;
constructor(workspace: IWorkspace, props: ModulePropsType, isSyncCode = true) { /**
* 导入语句
*/
imports: ImportDeclarationDataType;
constructor(workspace: IWorkspace, props: IFileConfig, isSyncCode = true) {
super(workspace, props, isSyncCode); super(workspace, props, isSyncCode);
} }
@@ -82,6 +51,16 @@ class TangoModule extends TangoFile {
} }
} }
addImportDeclaration(source: string, specifiers: IImportSpecifierData[]) {
this.ast = addImportDeclaration(this.ast, source, specifiers);
return this;
}
updateImportDeclaration(source: string, specifiers: IImportSpecifierData[]) {
this.ast = updateImportDeclaration(this.ast, source, specifiers);
return this;
}
/** /**
* 基于最新的 ast 进行源码同步 * 基于最新的 ast 进行源码同步
*/ */
@@ -112,14 +91,17 @@ class TangoModule extends TangoFile {
this.ast = code2ast(code); this.ast = code2ast(code);
} }
_analysisAst() {} _analysisAst() {
const { imports } = traverseFile(this.ast);
this.imports = imports;
}
} }
/** /**
* 普通 JS 文件 * 普通 JS 文件
*/ */
export class TangoJsModule extends TangoModule { export class TangoJsModule extends TangoModule {
constructor(workspace: IWorkspace, props: ModulePropsType) { constructor(workspace: IWorkspace, props: IFileConfig) {
super(workspace, props, false); super(workspace, props, false);
this.update(props.code, false, false); this.update(props.code, false, false);
@@ -132,628 +114,3 @@ export class TangoJsModule extends TangoModule {
}); });
} }
} }
/**
* 入口配置模块
*/
export class TangoStoreEntryModule extends TangoModule {
_stores: string[] = [];
get stores() {
return toJS(this._stores);
}
constructor(workspace: IWorkspace, props: ModulePropsType) {
super(workspace, props, false);
this.update(props.code, true, false);
makeObservable(this, {
_stores: observable,
_code: observable,
_cleanCode: observable,
stores: computed,
code: computed,
cleanCode: computed,
update: action,
});
}
_analysisAst() {
this._stores = traverseStoreEntryFile(this.ast);
}
/**
* 新建模型
* @param name
*/
addStore(name: string) {
this.ast = addStoreToEntryFile(this.ast, name);
return this;
}
/**
* 删除模型
* @param name
*/
removeStore(name: string) {
this.ast = removeStoreToEntryFile(this.ast, name);
return this;
}
}
/**
* 路由配置模块
*/
export class TangoRouteModule extends TangoModule {
_routes: RouteDataType[];
get routes() {
return toJS(this._routes);
}
constructor(workspace: IWorkspace, props: ModulePropsType) {
super(workspace, props, false);
this.update(props.code, true, false);
makeObservable(this, {
_routes: observable,
_code: observable,
_cleanCode: observable,
routes: computed,
code: computed,
cleanCode: computed,
update: action,
});
}
/**
* 根据路由地址获取 route 对象
*/
getRouteByRoutePath(route: string) {
let record;
for (const item of this.routes) {
if (item.path === route) {
record = item;
break;
}
}
return record;
}
/**
* 添加一条新路由
* @param name
*/
addRoute(routePath: string, importFilePath: string) {
this.ast = addRouteToRouteFile(this.ast, routePath, importFilePath);
return this;
}
/**
* 更新页面路由
* @param oldRoutePath
* @param newRoutePath
* @returns
*/
updateRoute(oldRoutePath: string, newRoutePath: string) {
this.ast = updateRouteToRouteFile(this.ast, oldRoutePath, newRoutePath);
return this;
}
/**
* 删除一条路由
* @param route 路由地址
*/
removeRoute(route: string) {
if (route === '/') {
console.warn('index route should not be removed!');
return;
}
const record = this.getRouteByRoutePath(route);
this.ast = removeRouteFromRouteFile(this.ast, route, record.importPath);
return this;
}
_analysisAst() {
this._routes = traverseRouteFile(this.ast);
}
}
/**
* 视图模块
*/
export class TangoViewModule extends TangoModule implements IViewFile {
// 解析为树结构的 jsxNodes 数组
_nodesTree: TangoViewNodeDataType[];
/**
* 通过导入组件名查找组件来自的包
*/
importMap: Dict<{ package: string; isDefault?: boolean }>;
/**
* 视图中依赖的 tango 变量,仅 stores 和 services
*/
variables: string[];
/**
* 节点列表
*/
private _nodes: Map<string, TangoNode>;
/**
* 导入的模块
*/
private _importedModules: Dict<ImportDeclarationPayloadType | ImportDeclarationPayloadType[]>;
/**
* 类属性
* @deprecated
*/
private _classProperties: Dict<ClassPropertyNodeType>;
/**
* 状态属性
*/
private _stateProperties: string[];
/**
* 状态代码
*/
private _stateCode: string;
/**
* ID 生成器
*/
private _idGenerator: IdGenerator;
get classProperties() {
return this._classProperties;
}
get stateProperties() {
return this._stateProperties;
}
get nodes() {
return this._nodes;
}
get nodesTree() {
return toJS(this._nodesTree);
}
get stateCode() {
return this._stateCode;
}
get tree() {
return this.ast;
}
constructor(workspace: IWorkspace, props: ModulePropsType) {
super(workspace, props, false);
this._nodes = new Map();
this._idGenerator = new IdGenerator({ prefix: props.filename });
this.update(props.code, true, false);
makeObservable(this, {
_nodesTree: observable,
_code: observable,
_cleanCode: observable,
code: computed,
cleanCode: computed,
update: action,
});
}
_syncByAst() {
// 空方法,逻辑合并到 this._analysisAst
}
_analysisAst() {
const {
ast: newAst,
cleanAst,
nodes,
stateCode,
stateProperties,
classProperties,
importedModules,
variables,
} = traverseViewFile(this.ast, this._idGenerator);
this.ast = newAst;
this._code = ast2code(newAst);
this._cleanCode = ast2code(cleanAst);
this._stateCode = stateCode;
this._stateProperties = stateProperties;
this._classProperties = classProperties;
this._importedModules = importedModules;
this.importMap = this.buildImportMap(importedModules);
this.variables = variables;
this._nodes.clear();
nodes.forEach((cur) => {
const node = new TangoNode({
...cur,
file: this,
});
this._nodes.set(cur.id, node);
});
this._nodesTree = nodeListToTreeData(nodes);
}
/**
* 基于组件的 prototype 信息更新导入信息
* @param prototype
* @param shouldUpdateCode
*/
updateImportSpecifiersByPrototype(sourcePrototype: string | ComponentPrototypeType) {
const prototype = this.workspace.getPrototype(sourcePrototype);
if (prototype) {
const importDeclaration = getImportDeclarationPayloadByPrototype(prototype, this.filename);
this.updateImportSpecifiers(importDeclaration);
}
return this;
}
/**
* 更新导入的变量(新版)
*/
updateImportSpecifiers(importDeclaration: ImportDeclarationPayloadType) {
const mods = this._importedModules[importDeclaration.sourcePath];
let ast;
// 如果模块已存在,需要去重
if (mods) {
const targetMod = Array.isArray(mods) ? mods[0] : mods;
const specifiers = Array.isArray(mods)
? mods.reduce((prev, cur) => prev.concat(cur.specifiers || []), [])
: mods.specifiers;
// 去掉已存在的导入声明
const newSpecifiers = importDeclaration.specifiers.filter(
(name) => !specifiers.includes(name),
);
ast = updateImportDeclaration(this.ast, {
...importDeclaration,
specifiers: newSpecifiers.concat(targetMod.specifiers),
});
} else {
ast = addImportDeclaration(this.ast, importDeclaration);
}
this.ast = ast;
return this;
}
/**
* 清除无效的导入声明
*/
removeUnusedImportSpecifiers() {
this.ast = removeUnusedImportSpecifiers(this.ast);
return this;
}
getNode(nodeId: string) {
return this._nodes.get(nodeId);
}
/**
* 删除节点
* @param nodeId
*/
removeNode(nodeId: string) {
this.ast = removeJSXElement(this.ast, nodeId);
return this;
}
/**
* 更新节点的属性
* @deprecated 使用 updateNodeAttributes 代替
*/
updateNodeAttribute(
nodeId: string,
attrName: string,
attrValue?: any,
relatedImports?: string[],
) {
return this.updateNodeAttributes(nodeId, { [attrName]: attrValue }, relatedImports);
}
updateNodeAttributes(nodeId: string, config: Record<string, any>, relatedImports?: string[]) {
if (relatedImports && relatedImports.length) {
// 导入依赖的组件
relatedImports.forEach((name: string) => {
const proto = this.workspace.getPrototype(name);
this.updateImportSpecifiersByPrototype(proto);
});
}
this.ast = updateJSXAttributes(this.ast, nodeId, config);
return this;
}
/**
* 插入子节点的最后面
* @param targetNodeId
* @param newNode
* @param sourceName
* @returns
*/
insertChild(
targetNodeId: string,
newNode: t.JSXElement,
position: InsertChildPositionType = 'last',
sourceName: string | ComponentPrototypeType,
) {
this.ast = appendChildToJSXElement(this.ast, targetNodeId, newNode, position);
if (sourceName) {
this.updateImportSpecifiersByPrototype(sourceName);
}
return this;
}
insertAfter(
targetNodeId: string,
newNode: t.JSXElement,
sourceName?: string | ComponentPrototypeType,
) {
this.ast = insertSiblingAfterJSXElement(this.ast, targetNodeId, newNode);
if (sourceName) {
this.updateImportSpecifiersByPrototype(sourceName);
}
return this;
}
insertBefore(
targetNodeId: string,
newNode: t.JSXElement,
sourceName?: string | ComponentPrototypeType,
) {
this.ast = insertSiblingBeforeJSXElement(this.ast, targetNodeId, newNode);
if (sourceName) {
this.updateImportSpecifiersByPrototype(sourceName);
}
return this;
}
/**
* 替换目标节点为新节点
* @param targetNodeId
* @param newNode
* @param sourcePrototype
*/
replaceNode(
targetNodeId: string,
newNode: t.JSXElement,
sourceName?: string | ComponentPrototypeType,
) {
this.ast = replaceJSXElement(this.ast, targetNodeId, newNode);
if (sourceName) {
this.updateImportSpecifiersByPrototype(sourceName);
}
return this;
}
/**
* 替换 jsx 跟结点的子元素
*/
replaceViewChildren(
childrenNodes: t.JSXElement[],
importDeclarations?: ImportDeclarationPayloadType[],
) {
if (childrenNodes.length) {
this.ast = replaceRootJSXElementChildren(this.ast, childrenNodes);
}
if (importDeclarations?.length) {
importDeclarations.forEach((item) => {
this.updateImportSpecifiers(item);
});
}
return this;
}
private buildImportMap(
importedModules: Dict<ImportDeclarationPayloadType | ImportDeclarationPayloadType[]>,
) {
const map = {};
Object.keys(importedModules).forEach((modName) => {
const mod = importedModules[modName];
(Array.isArray(mod) ? mod : [mod]).forEach((item) => {
if (item.defaultSpecifier) {
map[item.defaultSpecifier] = {
package: modName,
isDefault: true,
};
}
if (item.specifiers.length) {
item.specifiers.forEach((spe) => {
map[spe] = { package: modName };
});
}
});
});
return map;
}
}
/**
* 将节点列表转换为 tree data 嵌套数组
* @param list
*/
export function nodeListToTreeData(list: TangoViewNodeDataType[]) {
const map: Record<string, TangoViewNodeDataType> = {};
list.forEach((item) => {
// 如果不存在,则初始化
if (!map[item.id]) {
map[item.id] = {
...item,
children: [],
};
}
// 是否找到父节点,找到则塞进去
if (item.parentId && map[item.parentId]) {
map[item.parentId].children.push(map[item.id]);
}
});
// 保留根节点
const ret = Object.values(map).filter((item) => !item.parentId);
return ret;
}
/**
* 数据服务模块
*/
export class TangoServiceModule extends TangoModule {
/**
* 模块名
*/
name: string;
_serviceFunctions: Dict;
_baseConfig: Dict;
get serviceFunctions() {
return toJS(this._serviceFunctions);
}
get baseConfig() {
return toJS(this._baseConfig);
}
constructor(workspace: IWorkspace, props: ModulePropsType) {
super(workspace, props, false);
this.name = getModuleNameByFilename(props.filename);
this.update(props.code, true, false);
makeObservable(this, {
_serviceFunctions: observable,
_baseConfig: observable,
_code: observable,
_cleanCode: observable,
serviceFunctions: computed,
baseConfig: computed,
cleanCode: computed,
code: computed,
update: action,
});
}
_analysisAst() {
const { services, baseConfig } = traverseServiceFile(this.ast);
this._serviceFunctions = services;
this._baseConfig = baseConfig;
}
addServiceFunction(payload: ServiceFunctionPayloadType) {
const { name, ...rest } = payload;
this.ast = updateServiceConfigToServiceFile(this.ast, { [name]: clone(rest, false) });
return this;
}
addServiceFunctions(payloads: ServiceFunctionPayloadType[]) {
const config = payloads.reduce((acc, cur) => {
const { name, ...rest } = cur;
acc[name] = clone(rest, false);
return acc;
}, {});
this.ast = updateServiceConfigToServiceFile(this.ast, config);
return this;
}
updateServiceFunction(payload: ServiceFunctionPayloadType) {
const { name, ...rest } = payload;
this.ast = updateServiceConfigToServiceFile(this.ast, { [name]: clone(rest, false) });
return this;
}
deleteServiceFunction(serviceFunctionName: string) {
try {
this.ast = deleteServiceConfigFromServiceFile(this.ast, serviceFunctionName);
} catch (e) {
logger.error(e);
}
return this;
}
/**
* 更新服务的基础配置
*/
updateBaseConfig(optionName: string, optionValue: any) {
this.ast = updateBaseConfigToServiceFile(this.ast, optionName, optionValue);
return this;
}
}
/**
* 状态模型模块
*/
export class TangoStoreModule extends TangoModule {
/**
* 模块名
*/
name: string;
namespace: string;
states: StorePropertyType[];
actions: StorePropertyType[];
constructor(workspace: IWorkspace, props: ModulePropsType) {
super(workspace, props, false);
this.name = getModuleNameByFilename(props.filename);
this.update(props.code, true, false);
makeObservable(this, {
states: observable,
actions: observable,
_code: observable,
_cleanCode: observable,
cleanCode: computed,
code: computed,
update: action,
});
}
/**
* 添加状态属性
* @param stateName
* @param initValue
*/
addState(stateName: string, initValue: string) {
this.ast = addStoreState(this.ast, stateName, initValue);
return this;
}
/**
* 移除状态
*/
removeState(stateName: string) {
this.ast = removeStoreState(this.ast, stateName);
return this;
}
/**
* 更新状态代码
* @param stateName 状态名
* @param code 代码
*/
updateState(stateName: string, code: string) {
this.ast = updateStoreState(this.ast, stateName, code);
return this;
}
_analysisAst() {
const { namespace, states, actions } = traverseStoreFile(this.ast);
this.namespace = namespace || this.name;
this.states = states;
this.actions = actions;
}
}
+3 -3
View File
@@ -1,10 +1,10 @@
import { JSXElement, SourceLocation } from '@babel/types'; import { JSXElement, SourceLocation } from '@babel/types';
import { cloneJSXElementWithoutTrackingData, getJSXElementAttributes } from '../helpers'; import { cloneJSXElementWithoutTrackingData, getJSXElementAttributes } from '../helpers';
import { TangoViewNodeDataType } from '../types'; import { ITangoViewNodeData } from '../types';
import { TangoViewModule } from './module'; import { TangoViewModule } from './view-module';
import { IViewNode } from './interfaces'; import { IViewNode } from './interfaces';
type TangoNodeConstructorPropsType = TangoViewNodeDataType & { type TangoNodeConstructorPropsType = ITangoViewNodeData & {
file: TangoViewModule; file: TangoViewModule;
}; };
+88
View File
@@ -0,0 +1,88 @@
import { action, computed, makeObservable, observable, toJS } from 'mobx';
import {
traverseRouteFile,
addRouteToRouteFile,
removeRouteFromRouteFile,
updateRouteToRouteFile,
} from '../helpers';
import { IRouteData, IFileConfig } from '../types';
import { IWorkspace } from './interfaces';
import { TangoModule } from './module';
/**
* 路由配置模块
*/
export class TangoRouteModule extends TangoModule {
_routes: IRouteData[];
get routes() {
return toJS(this._routes);
}
constructor(workspace: IWorkspace, props: IFileConfig) {
super(workspace, props, false);
this.update(props.code, true, false);
makeObservable(this, {
_routes: observable,
_code: observable,
_cleanCode: observable,
routes: computed,
code: computed,
cleanCode: computed,
update: action,
});
}
/**
* 根据路由地址获取 route 对象
*/
getRouteByRoutePath(route: string) {
let record;
for (const item of this.routes) {
if (item.path === route) {
record = item;
break;
}
}
return record;
}
/**
* 添加一条新路由
* @param name
*/
addRoute(routePath: string, importFilePath: string) {
this.ast = addRouteToRouteFile(this.ast, routePath, importFilePath);
return this;
}
/**
* 更新页面路由
* @param oldRoutePath
* @param newRoutePath
* @returns
*/
updateRoute(oldRoutePath: string, newRoutePath: string) {
this.ast = updateRouteToRouteFile(this.ast, oldRoutePath, newRoutePath);
return this;
}
/**
* 删除一条路由
* @param route 路由地址
*/
removeRoute(route: string) {
if (route === '/') {
console.warn('index route should not be removed!');
return;
}
const record = this.getRouteByRoutePath(route);
this.ast = removeRouteFromRouteFile(this.ast, route, record.importPath);
return this;
}
_analysisAst() {
this._routes = traverseRouteFile(this.ast);
}
}
+108
View File
@@ -0,0 +1,108 @@
import { action, computed, makeObservable, observable, toJS } from 'mobx';
import { clone, Dict, logger } from '@music163/tango-helpers';
import {
traverseServiceFile,
updateServiceConfigToServiceFile,
getModuleNameByFilename,
deleteServiceConfigFromServiceFile,
updateBaseConfigToServiceFile,
} from '../helpers';
import { IFileConfig, IServiceFunctionPayload } from '../types';
import { IWorkspace } from './interfaces';
import { TangoModule } from './module';
/**
* 数据服务模块
*/
export class TangoServiceModule extends TangoModule {
/**
* 服务函数的模块名,默认为 index
*/
name: string;
/**
* 原始注册的服务函数
* @example { get: {}, list: {} }
*/
_serviceFunctions: Dict;
/**
* 原始的基础配置
*/
_baseConfig: Dict;
get serviceFunctions() {
return toJS(this._serviceFunctions);
}
get baseConfig() {
return toJS(this._baseConfig);
}
constructor(workspace: IWorkspace, props: IFileConfig) {
super(workspace, props, false);
this.name = getModuleNameByFilename(props.filename);
this.update(props.code, true, false);
makeObservable(this, {
_serviceFunctions: observable,
_baseConfig: observable,
_code: observable,
_cleanCode: observable,
serviceFunctions: computed,
baseConfig: computed,
cleanCode: computed,
code: computed,
update: action,
});
}
_analysisAst() {
const { imports, services, baseConfig } = traverseServiceFile(this.ast);
this.imports = imports;
this._serviceFunctions = services;
this._baseConfig = baseConfig;
if (baseConfig.namespace) {
this.name = baseConfig.namespace;
}
}
addServiceFunction(payload: IServiceFunctionPayload) {
const { name, ...rest } = payload;
this.ast = updateServiceConfigToServiceFile(this.ast, { [name]: clone(rest, false) });
return this;
}
addServiceFunctions(payloads: IServiceFunctionPayload[]) {
const config = payloads.reduce((acc, cur) => {
const { name, ...rest } = cur;
acc[name] = clone(rest, false);
return acc;
}, {});
this.ast = updateServiceConfigToServiceFile(this.ast, config);
return this;
}
updateServiceFunction(payload: IServiceFunctionPayload) {
const { name, ...rest } = payload;
this.ast = updateServiceConfigToServiceFile(this.ast, { [name]: clone(rest, false) });
return this;
}
deleteServiceFunction(serviceFunctionName: string) {
try {
this.ast = deleteServiceConfigFromServiceFile(this.ast, serviceFunctionName);
} catch (e) {
logger.error(e);
}
return this;
}
/**
* 更新服务的基础配置
*/
updateBaseConfig(data: object) {
this.ast = updateBaseConfigToServiceFile(this.ast, data);
return this;
}
}
+129
View File
@@ -0,0 +1,129 @@
import { action, computed, makeObservable, observable, toJS } from 'mobx';
import {
traverseStoreFile,
traverseStoreEntryFile,
addStoreToEntryFile,
getModuleNameByFilename,
addStoreState,
updateStoreState,
removeStoreState,
removeStoreToEntryFile,
} from '../helpers';
import { IFileConfig, IStorePropertyData } from '../types';
import { IWorkspace } from './interfaces';
import { TangoModule } from './module';
/**
* 入口配置模块
*/
export class TangoStoreEntryModule extends TangoModule {
_stores: string[] = [];
get stores() {
return toJS(this._stores);
}
constructor(workspace: IWorkspace, props: IFileConfig) {
super(workspace, props, false);
this.update(props.code, true, false);
makeObservable(this, {
_stores: observable,
_code: observable,
_cleanCode: observable,
stores: computed,
code: computed,
cleanCode: computed,
update: action,
});
}
_analysisAst() {
this._stores = traverseStoreEntryFile(this.ast);
}
/**
* 新建模型
* @param name
*/
addStore(name: string) {
this.ast = addStoreToEntryFile(this.ast, name);
return this;
}
/**
* 删除模型
* @param name
*/
removeStore(name: string) {
this.ast = removeStoreToEntryFile(this.ast, name);
return this;
}
}
/**
* 状态模型模块
*/
export class TangoStoreModule extends TangoModule {
/**
* 模块名
*/
name: string;
namespace: string;
states: IStorePropertyData[];
actions: IStorePropertyData[];
constructor(workspace: IWorkspace, props: IFileConfig) {
super(workspace, props, false);
this.name = getModuleNameByFilename(props.filename);
this.update(props.code, true, false);
makeObservable(this, {
states: observable,
actions: observable,
_code: observable,
_cleanCode: observable,
cleanCode: computed,
code: computed,
update: action,
});
}
/**
* 添加状态属性
* @param stateName
* @param initValue
*/
addState(stateName: string, initValue: string) {
this.ast = addStoreState(this.ast, stateName, initValue);
return this;
}
/**
* 移除状态
*/
removeState(stateName: string) {
this.ast = removeStoreState(this.ast, stateName);
return this;
}
/**
* 更新状态代码
* @param stateName 状态名
* @param code 代码
*/
updateState(stateName: string, code: string) {
this.ast = updateStoreState(this.ast, stateName, code);
return this;
}
_analysisAst() {
const { namespace, states, actions } = traverseStoreFile(this.ast);
this.namespace = namespace || this.name;
this.states = states;
this.actions = actions;
}
}
+376
View File
@@ -0,0 +1,376 @@
import * as t from '@babel/types';
import { action, computed, makeObservable, observable, toJS } from 'mobx';
import { Dict } from '@music163/tango-helpers';
import {
ast2code,
traverseViewFile,
removeJSXElement,
insertSiblingAfterJSXElement,
appendChildToJSXElement,
addImportDeclaration,
replaceJSXElement,
removeUnusedImportSpecifiers,
insertSiblingBeforeJSXElement,
replaceRootJSXElementChildren,
IdGenerator,
updateJSXAttributes,
queryXFormItemFields,
prototype2importDeclarationData,
insertImportSpecifiers,
addImportDeclarationLegacy,
updateImportDeclarationLegacy,
} from '../helpers';
import { TangoNode } from './node';
import {
IFileConfig,
ITangoViewNodeData,
IImportDeclarationPayload,
InsertChildPositionType,
IImportSpecifierSourceData,
ImportDeclarationDataType,
IImportSpecifierData,
} from '../types';
import { IViewFile, IWorkspace } from './interfaces';
import { TangoModule } from './module';
/**
* 导入信息转为 变量名->来源 的 map 结构
* @param importedModules
* @returns
*/
function buildImportMap(importedModules: ImportDeclarationDataType) {
const map: Dict<IImportSpecifierSourceData> = {};
Object.keys(importedModules).forEach((source) => {
const specifiers = importedModules[source];
specifiers?.forEach((specifier) => {
map[specifier.localName] = {
source,
isDefault: specifier.type === 'ImportDefaultSpecifier',
};
});
});
return map;
}
/**
* 将节点列表转换为 tree data 嵌套数组
* @param list
*/
function nodeListToTreeData(list: ITangoViewNodeData[]) {
const map: Record<string, ITangoViewNodeData> = {};
list.forEach((item) => {
// 如果不存在,则初始化
if (!map[item.id]) {
map[item.id] = {
...item,
children: [],
};
}
// 是否找到父节点,找到则塞进去
if (item.parentId && map[item.parentId]) {
map[item.parentId].children.push(map[item.id]);
}
});
// 保留根节点
const ret = Object.values(map).filter((item) => !item.parentId);
return ret;
}
/**
* 视图模块
*/
export class TangoViewModule extends TangoModule implements IViewFile {
// 解析为树结构的 jsxNodes 数组
_nodesTree: ITangoViewNodeData[];
/**
* 通过导入组件名查找组件来自的包
*/
importMap: Dict<IImportSpecifierSourceData>;
/**
* 视图中依赖的 tango 变量,仅 stores 和 services
*/
variables: string[];
/**
* 节点列表
*/
private _nodes: Map<string, TangoNode>;
/**
* 导入的模块
* @deprecated
*/
private _importedModules: Dict<IImportDeclarationPayload | IImportDeclarationPayload[]>;
/**
* ID 生成器
*/
private _idGenerator: IdGenerator;
get nodes() {
return this._nodes;
}
get nodesTree() {
return toJS(this._nodesTree);
}
get tree() {
return this.ast;
}
constructor(workspace: IWorkspace, props: IFileConfig) {
super(workspace, props, false);
this._nodes = new Map();
this._idGenerator = new IdGenerator({ prefix: props.filename });
this.update(props.code, true, false);
makeObservable(this, {
_nodesTree: observable,
_code: observable,
_cleanCode: observable,
code: computed,
cleanCode: computed,
update: action,
});
}
_syncByAst() {
// 空方法,逻辑合并到 this._analysisAst
}
_analysisAst() {
const {
ast: newAst,
cleanAst,
nodes,
imports,
importedModules,
variables,
} = traverseViewFile(this.ast, this._idGenerator);
this.ast = newAst;
this._code = ast2code(newAst);
this._cleanCode = ast2code(cleanAst);
this._importedModules = importedModules;
this.imports = imports;
this.importMap = buildImportMap(imports);
this.variables = variables;
this._nodes.clear();
nodes.forEach((cur) => {
const node = new TangoNode({
...cur,
file: this,
});
this._nodes.set(cur.id, node);
});
this._nodesTree = nodeListToTreeData(nodes);
}
/**
* 依赖列表
*/
listImportSources() {
return Object.keys(this.imports);
}
/**
* 弹窗列表
*/
listModals() {
const modals: Array<{ label: string; value: string }> = [];
const activeViewNodes = this.nodes || new Map();
Array.from(activeViewNodes.values()).forEach((node) => {
if (['Modal', 'Drawer'].includes(node.component) && node.props.id) {
modals.push({
label: `${node.component}(${node.props.id})`,
value: node.props.id,
});
}
});
return modals;
}
/**
* 表单列表
*/
listForms() {
const forms: Record<string, string[]> = {};
const activeViewNodes = this.nodes;
Array.from(activeViewNodes.values()).forEach((node) => {
if (
['XAction', 'XColumnAction', 'XForm', 'XStepForm', 'XSearchForm', 'XFormList'].includes(
node.component,
)
) {
forms[node.id] = queryXFormItemFields(node.rawNode);
}
});
return forms;
}
/**
* 添加导入符号
* @param source
* @param newSpecifiers
* @returns
*/
addImportSpecifiers(source: string, newSpecifiers: IImportSpecifierData[]) {
const existSpecifiers = this.imports[source];
if (existSpecifiers) {
const insertedSpecifiers = newSpecifiers.filter((item) => {
return !existSpecifiers.find((existItem) => existItem.localName === item.localName);
});
this.ast = insertImportSpecifiers(this.ast, source, insertedSpecifiers);
} else {
// 不存在导入来源,直接添加新的导入语句
this.ast = addImportDeclaration(this.ast, source, newSpecifiers);
}
return this;
}
/**
* 更新导入的变量
* @deprecated 使用 updateImportDeclaration 代替
*/
updateImportSpecifiersLegacy(importDeclaration: IImportDeclarationPayload) {
const mods = this._importedModules[importDeclaration.sourcePath];
let ast;
// 如果模块已存在,需要去重
if (mods) {
const targetMod = Array.isArray(mods) ? mods[0] : mods;
const specifiers = Array.isArray(mods)
? mods.reduce((prev, cur) => prev.concat(cur.specifiers || []), [])
: mods.specifiers;
// 去掉已存在的导入声明
const newSpecifiers = importDeclaration.specifiers.filter(
(name) => !specifiers.includes(name),
);
ast = updateImportDeclarationLegacy(this.ast, {
...importDeclaration,
specifiers: newSpecifiers.concat(targetMod.specifiers),
});
} else {
ast = addImportDeclarationLegacy(this.ast, importDeclaration);
}
this.ast = ast;
return this;
}
/**
* 清除无效的导入声明
*/
removeUnusedImportSpecifiers() {
this.ast = removeUnusedImportSpecifiers(this.ast);
return this;
}
getNode(nodeId: string) {
return this._nodes.get(nodeId);
}
/**
* 删除节点
* @param nodeId
*/
removeNode(nodeId: string) {
this.ast = removeJSXElement(this.ast, nodeId);
return this;
}
/**
* 更新节点的属性
* @deprecated 使用 updateNodeAttributes 代替
*/
updateNodeAttribute(
nodeId: string,
attrName: string,
attrValue?: any,
relatedImports?: string[],
) {
return this.updateNodeAttributes(nodeId, { [attrName]: attrValue }, relatedImports);
}
updateNodeAttributes(nodeId: string, config: Record<string, any>, relatedImports?: string[]) {
if (relatedImports && relatedImports.length) {
// 导入依赖的组件
relatedImports.forEach((name: string) => {
const proto = this.workspace.getPrototype(name);
const { source, specifiers } = prototype2importDeclarationData(proto, this.filename);
this.addImportSpecifiers(source, specifiers);
});
}
this.ast = updateJSXAttributes(this.ast, nodeId, config);
return this;
}
/**
* 插入子节点的最后面
* @param targetNodeId
* @param newNode
* @param position
* @returns
*/
insertChild(
targetNodeId: string,
newNode: t.JSXElement,
position: InsertChildPositionType = 'last',
) {
this.ast = appendChildToJSXElement(this.ast, targetNodeId, newNode, position);
return this;
}
insertAfter(targetNodeId: string, newNode: t.JSXElement) {
this.ast = insertSiblingAfterJSXElement(this.ast, targetNodeId, newNode);
return this;
}
insertBefore(targetNodeId: string, newNode: t.JSXElement) {
this.ast = insertSiblingBeforeJSXElement(this.ast, targetNodeId, newNode);
return this;
}
/**
* 替换目标节点为新节点
* @param targetNodeId
* @param newNode
*/
replaceNode(targetNodeId: string, newNode: t.JSXElement) {
this.ast = replaceJSXElement(this.ast, targetNodeId, newNode);
return this;
}
/**
* 替换 jsx 跟结点的子元素
* @deprecated 不推荐使用
*/
replaceViewChildren(
childrenNodes: t.JSXElement[],
importDeclarations?: IImportDeclarationPayload[],
) {
if (childrenNodes.length) {
this.ast = replaceRootJSXElementChildren(this.ast, childrenNodes);
}
if (importDeclarations?.length) {
importDeclarations.forEach((item) => {
this.updateImportSpecifiersLegacy(item);
});
}
return this;
}
}
+302 -276
View File
@@ -16,34 +16,30 @@ import {
getJSXElementChildrenNames, getJSXElementChildrenNames,
namesToImportDeclarations, namesToImportDeclarations,
getBlockNameByFilename, getBlockNameByFilename,
queryXFormItemFields,
getPrivilegeCode, getPrivilegeCode,
prototype2importDeclarationData,
} from '../helpers'; } from '../helpers';
import { DropMethod } from './drop-target'; import { DropMethod } from './drop-target';
import { HistoryMessage, TangoHistory } from './history'; import { HistoryMessage, TangoHistory } from './history';
import { TangoNode } from './node'; import { TangoNode } from './node';
import { import { TangoJsModule } from './module';
TangoStoreEntryModule,
TangoRouteModule,
TangoServiceModule,
TangoStoreModule,
TangoViewModule,
TangoJsModule,
} from './module';
import { TangoFile, TangoJsonFile, TangoLessFile } from './file'; import { TangoFile, TangoJsonFile, TangoLessFile } from './file';
import {
FileItemType,
FileType,
PackageConfigType,
PageConfigType,
ProjectDataType,
ServiceFunctionPayloadType,
} from '../types';
import { IWorkspace } from './interfaces'; import { IWorkspace } from './interfaces';
import {
IFileConfig,
FileType,
ITangoConfigPackages,
IPageConfigData,
IServiceFunctionPayload,
} from '../types';
import { SelectSource } from './select-source'; import { SelectSource } from './select-source';
import { DragSource } from './drag-source'; import { DragSource } from './drag-source';
import { TangoRouteModule } from './route-module';
import { TangoStoreEntryModule, TangoStoreModule } from './store-module';
import { TangoServiceModule } from './service-module';
import { TangoViewModule } from './view-module';
export type WorkspaceOptionsType = { export interface IWorkspaceOptions {
/** /**
* 入口文件 * 入口文件
*/ */
@@ -51,7 +47,7 @@ export type WorkspaceOptionsType = {
/** /**
* 初始化文件列表 * 初始化文件列表
*/ */
files?: FileItemType[]; files?: IFileConfig[];
/** /**
* 默认的激活的路由 * 默认的激活的路由
*/ */
@@ -60,7 +56,11 @@ export type WorkspaceOptionsType = {
* 组件描述列表 * 组件描述列表
*/ */
prototypes?: Record<string, ComponentPrototypeType>; prototypes?: Record<string, ComponentPrototypeType>;
}; /**
* 工作区文件变更事件
*/
onFilesChange?: (filenames: string[]) => void;
}
/** /**
* 工作区 * 工作区
@@ -96,14 +96,10 @@ export class Workspace extends EventTarget implements IWorkspace {
/** /**
* 本地区块 { [blockName]: filePath } * 本地区块 { [blockName]: filePath }
* FIXME: 修正类型 Record<string, TangoBlockModule>
*/ */
localBlocks: Record<string, string>; localBlocks: Record<string, string>;
/**
* service 模块
*/
serviceModule: TangoServiceModule;
/** /**
* 路由配置模块 * 路由配置模块
*/ */
@@ -114,6 +110,10 @@ export class Workspace extends EventTarget implements IWorkspace {
*/ */
storeEntryModule: TangoStoreEntryModule; storeEntryModule: TangoStoreEntryModule;
storeModules: Record<string, TangoStoreModule> = {};
serviceModules: Record<string, TangoServiceModule> = {};
/** /**
* package.json 文件 * package.json 文件
*/ */
@@ -130,6 +130,11 @@ export class Workspace extends EventTarget implements IWorkspace {
*/ */
appJson: TangoJsonFile; appJson: TangoJsonFile;
/**
* 代码变更回调
*/
onFilesChange?: (filenames: string[]) => void;
/** /**
* 绑定事件 * 绑定事件
* TODO: 是否需要自己来管理 listeners,并及时进行 gc * TODO: 是否需要自己来管理 listeners,并及时进行 gc
@@ -161,26 +166,13 @@ export class Workspace extends EventTarget implements IWorkspace {
return this.files.get(this.activeViewFile) as TangoViewModule; return this.files.get(this.activeViewFile) as TangoViewModule;
} }
/**
* store 模块列表
*/
get storeModules() {
const mods: TangoStoreModule[] = [];
this.files.forEach((file) => {
if (file instanceof TangoStoreModule) {
mods.push(file);
}
});
return mods;
}
/** /**
* 获取页面列表 * 获取页面列表
*/ */
get pages() { get pages() {
const appJsonPages = this.appJson?.getValue('pages'); const appJsonPages = this.appJson?.getValue('pages');
const pagesMap = array2object(appJsonPages || [], (item) => item.path); const pagesMap = array2object(appJsonPages || [], (item) => item.path);
const ret: PageConfigType[] = []; const ret: IPageConfigData[] = [];
this.routeModule?.routes.forEach((item) => { this.routeModule?.routes.forEach((item) => {
const data = pagesMap[item.path]; const data = pagesMap[item.path];
if (item.path !== '*') { if (item.path !== '*') {
@@ -225,7 +217,7 @@ export class Workspace extends EventTarget implements IWorkspace {
return Object.keys(this.localBlocks); return Object.keys(this.localBlocks);
} }
constructor(options?: WorkspaceOptionsType) { constructor(options?: IWorkspaceOptions) {
super(); super();
this.history = new TangoHistory(this); this.history = new TangoHistory(this);
this.selectSource = new SelectSource(this); this.selectSource = new SelectSource(this);
@@ -238,6 +230,7 @@ export class Workspace extends EventTarget implements IWorkspace {
this.activeViewFile = ''; this.activeViewFile = '';
this.files = new Map(); this.files = new Map();
this.isReady = false; this.isReady = false;
this.onFilesChange = options?.onFilesChange;
if (options?.files) { if (options?.files) {
this.addFiles(options.files); this.addFiles(options.files);
@@ -270,36 +263,6 @@ export class Workspace extends EventTarget implements IWorkspace {
return name as ComponentPrototypeType; return name as ComponentPrototypeType;
} }
getProjectData() {
const pages = this.pages.map((item) => {
const filename = this.getFilenameByRoutePath(item.path);
return {
title: item.name,
path: item.path,
filename,
includes: {
variables: (this.getFile(filename) as TangoViewModule).variables,
},
};
});
const stores = this.storeModules.reduce((acc, item) => {
acc[item.name] = { filename: item.filename };
return acc;
}, {});
const services = {
index: {
filename: this.serviceModule?.filename,
functions: Object.keys(this.serviceModule.serviceFunctions),
},
};
const data: ProjectDataType = {
pages,
stores,
services,
};
return data;
}
/** /**
* 设置当前路由 * 设置当前路由
* @param routePath 路由路径 * @param routePath 路由路径
@@ -350,10 +313,10 @@ export class Workspace extends EventTarget implements IWorkspace {
} }
/** /**
* 添加一组文件到工作区 * 添加一组文件到工作区,如果文件同名,后面的文件会覆盖前面的文件
* @param files * @param files
*/ */
addFiles(files: FileItemType[] = []) { addFiles(files: IFileConfig[] = []) {
files.forEach((file) => { files.forEach((file) => {
this.addFile(file.filename, file.code, file.type); this.addFile(file.filename, file.code, file.type);
}); });
@@ -366,80 +329,99 @@ export class Workspace extends EventTarget implements IWorkspace {
* @param fileType 模块类型 * @param fileType 模块类型
*/ */
addFile(filename: string, code: string, fileType?: FileType) { addFile(filename: string, code: string, fileType?: FileType) {
if (!this.files.has(filename)) { const moduleType = fileType || inferFileType(filename);
const moduleType = fileType || inferFileType(filename); const props = {
const props = { filename,
filename, code,
code, type: moduleType,
type: moduleType, };
};
let module; let module;
switch (moduleType) { switch (moduleType) {
case FileType.StoreEntryModule: case FileType.StoreEntryModule:
module = new TangoStoreEntryModule(this, props); module = new TangoStoreEntryModule(this, props);
this.storeEntryModule = module; this.storeEntryModule = module;
break; break;
case FileType.RouteModule: { case FileType.RouteModule: {
module = new TangoRouteModule(this, props); module = new TangoRouteModule(this, props);
this.routeModule = module; this.routeModule = module;
// check if activeRoute exists // check if activeRoute exists
const route = module.routes.find((item) => item.path === this.activeRoute); const route = module.routes.find((item) => item.path === this.activeRoute);
if (!route) { if (!route) {
this.setActiveRoute(module.routes[0]?.path); this.setActiveRoute(module.routes[0]?.path);
}
break;
} }
case FileType.JsxViewModule: break;
module = new TangoViewModule(this, props);
break;
case FileType.ServiceModule:
module = new TangoServiceModule(this, props);
this.serviceModule = module; // 仅支持单个 service 文件
break;
case FileType.StoreModule:
module = new TangoStoreModule(this, props);
break;
case FileType.BlockEntryModule: {
const blockName = getBlockNameByFilename(props.filename);
const prototype: ComponentPrototypeType = {
name: blockName,
exportType: 'defaultExport',
package: props.filename,
type: 'block',
};
this.localBlocks[blockName] = props.filename;
this.componentPrototypes.set(blockName, prototype);
module = new TangoViewModule(this, props);
break;
}
case FileType.Module:
module = new TangoJsModule(this, props);
break;
case FileType.Less:
module = new TangoLessFile(this, props);
break;
case FileType.PackageJson:
module = new TangoJsonFile(this, props);
this.packageJson = module;
break;
case FileType.TangoConfigJson:
module = new TangoJsonFile(this, props);
this.tangoConfigJson = module;
break;
case FileType.AppJson:
module = new TangoJsonFile(this, props);
this.appJson = module;
break;
case FileType.Json:
module = new TangoJsonFile(this, props);
break;
default:
module = new TangoFile(this, props);
} }
case FileType.JsxViewModule:
this.files.set(filename, module); module = new TangoViewModule(this, props);
break;
case FileType.ServiceModule:
module = new TangoServiceModule(this, props);
this.serviceModules[module.name] = module;
break;
case FileType.StoreModule:
module = new TangoStoreModule(this, props);
this.storeModules[module.name] = module;
break;
case FileType.BlockEntryModule: {
const blockName = getBlockNameByFilename(props.filename);
const prototype: ComponentPrototypeType = {
name: blockName,
exportType: 'defaultExport',
package: props.filename,
type: 'block',
};
this.localBlocks[blockName] = props.filename;
this.componentPrototypes.set(blockName, prototype);
module = new TangoViewModule(this, props);
break;
}
case FileType.Module:
module = new TangoJsModule(this, props);
break;
case FileType.Less:
module = new TangoLessFile(this, props);
break;
case FileType.PackageJson:
module = new TangoJsonFile(this, props);
this.packageJson = module;
break;
case FileType.TangoConfigJson:
module = new TangoJsonFile(this, props);
this.tangoConfigJson = module;
break;
case FileType.AppJson:
module = new TangoJsonFile(this, props);
this.appJson = module;
break;
case FileType.Json:
module = new TangoJsonFile(this, props);
break;
default:
module = new TangoFile(this, props);
} }
this.files.set(filename, module);
}
addServiceFile(serviceName: string, code: string) {
const filename = `/src/services/${serviceName}.js`;
this.addFile(filename, code, FileType.ServiceModule);
const indexServiceModule = this.serviceModules.index;
indexServiceModule?.addImportDeclaration(`./${serviceName}`, []).update();
}
addStoreFile(storeName: string, code: string) {
const filename = `/src/stores/${storeName}.js`;
this.addFile(filename, code);
if (!this.storeEntryModule) {
this.addFile('/src/stores/index.js', '');
}
this.storeEntryModule.addStore(storeName).update();
}
addViewFile(viewName: string, code: string) {
// TODO: implement it
} }
updateFile(filename: string, code: string, shouldFormatCode = false) { updateFile(filename: string, code: string, shouldFormatCode = false) {
@@ -483,8 +465,24 @@ export class Workspace extends EventTarget implements IWorkspace {
*/ */
renameFile(oldFilename: string, newFilename: string) { renameFile(oldFilename: string, newFilename: string) {
const file = this.files.get(oldFilename); const file = this.files.get(oldFilename);
this.removeFile(oldFilename); if (file) {
this.addFile(newFilename, file.code); this.removeFile(oldFilename);
this.addFile(newFilename, file.code);
}
}
/**
* 重命名文件夹
* @param oldFoldername 旧文件夹名
* @param newFoldername 新文件夹名
*/
renameFolder(oldFoldername: string, newFoldername: string) {
Array.from(this.files.keys()).forEach((key) => {
if (key.startsWith(`${oldFoldername}/`)) {
const newKey = key.replace(oldFoldername, newFoldername);
this.renameFile(key, newKey);
}
});
} }
/** /**
@@ -508,24 +506,6 @@ export class Workspace extends EventTarget implements IWorkspace {
return ret; return ret;
} }
listStoreModules() {
return Array.from(this.files.values()).filter(
(file) => file instanceof TangoStoreModule,
) as TangoStoreModule[];
}
getStoreModuleByName(storeName: string) {
let mod;
const files = this.files.values();
for (const file of files) {
if (file instanceof TangoStoreModule && file.name === storeName) {
mod = file;
break;
}
}
return mod;
}
/** /**
* 删除视图模块 * 删除视图模块
* @param route 路由名称 * @param route 路由名称
@@ -540,7 +520,7 @@ export class Workspace extends EventTarget implements IWorkspace {
// remove appJson page // remove appJson page
this.appJson this.appJson
?.setValue('pages', (pages) => { ?.setValue('pages', (pages) => {
return (pages as PageConfigType[]).filter((page) => page.path !== routePath); return (pages as IPageConfigData[]).filter((page) => page.path !== routePath);
}) })
.update(); .update();
this.removeFile(filename); this.removeFile(filename);
@@ -548,11 +528,12 @@ export class Workspace extends EventTarget implements IWorkspace {
/** /**
* 添加新的视图文件 * 添加新的视图文件
* @deprecated * @deprecated 使用 addViewFile 代替
* FIXME: 重构这个逻辑
* @param route 视图名 * @param route 视图名
* @param code 视图代码 * @param code 视图代码
*/ */
addViewPage(routeConfig: string | PageConfigType, code: string) { addViewPage(routeConfig: string | IPageConfigData, code: string) {
const config = const config =
typeof routeConfig === 'string' typeof routeConfig === 'string'
? { ? {
@@ -569,11 +550,11 @@ export class Workspace extends EventTarget implements IWorkspace {
/** /**
* 添加新的路由 * 添加新的路由
*/ */
addRoute(routeData: PageConfigType, importFilePath: string) { addRoute(routeData: IPageConfigData, importFilePath: string) {
this.routeModule?.addRoute(routeData.path, importFilePath).update(); this.routeModule?.addRoute(routeData.path, importFilePath).update();
this.appJson this.appJson
?.setValue('pages', (pages) => { ?.setValue('pages', (pages) => {
(pages as PageConfigType[]).push({ (pages as IPageConfigData[]).push({
name: routeData.name || routeData.path, name: routeData.name || routeData.path,
path: routeData.path, path: routeData.path,
privilegeCode: getPrivilegeCode(this.packageJson?.getValue('name'), routeData.path), privilegeCode: getPrivilegeCode(this.packageJson?.getValue('name'), routeData.path),
@@ -588,13 +569,13 @@ export class Workspace extends EventTarget implements IWorkspace {
* @param sourceRoutePath * @param sourceRoutePath
* @param targetPageData * @param targetPageData
*/ */
updateRoute(sourceRoutePath: string, targetPageData: PageConfigType) { updateRoute(sourceRoutePath: string, targetPageData: IPageConfigData) {
if (sourceRoutePath !== targetPageData.path) { if (sourceRoutePath !== targetPageData.path) {
this.routeModule?.updateRoute(sourceRoutePath, targetPageData.path).update(); this.routeModule?.updateRoute(sourceRoutePath, targetPageData.path).update();
} }
this.appJson this.appJson
?.setValue('pages', (pages) => { ?.setValue('pages', (pages) => {
for (const page of pages as PageConfigType[]) { for (const page of pages as IPageConfigData[]) {
if (page.path === sourceRoutePath) { if (page.path === sourceRoutePath) {
page.path = targetPageData.path; page.path = targetPageData.path;
page.name = targetPageData.name; page.name = targetPageData.name;
@@ -611,7 +592,7 @@ export class Workspace extends EventTarget implements IWorkspace {
* @param sourceRoute * @param sourceRoute
* @param targetRouteConfig * @param targetRouteConfig
*/ */
copyViewPage(sourceRoutePath: string, targetPageData: PageConfigType) { copyViewPage(sourceRoutePath: string, targetPageData: IPageConfigData) {
const sourceFilePath = this.getRealViewFilePath(this.getFilenameByRoutePath(sourceRoutePath)); const sourceFilePath = this.getRealViewFilePath(this.getFilenameByRoutePath(sourceRoutePath));
const targetFilePath = getFilepath(targetPageData.path, '/src/pages'); const targetFilePath = getFilepath(targetPageData.path, '/src/pages');
this.copyFiles(sourceFilePath, targetFilePath); this.copyFiles(sourceFilePath, targetFilePath);
@@ -625,45 +606,6 @@ export class Workspace extends EventTarget implements IWorkspace {
} }
} }
/**
* TODO: 仅获取当前视图即可
* 获取当前视图的弹窗列表
* @returns
*/
listModals() {
const modals: Array<{ label: string; value: string }> = [];
const activeViewNodes = this.activeViewModule?.nodes || new Map();
Array.from(activeViewNodes.values()).forEach((node) => {
if (['Modal', 'Drawer'].includes(node.component) && node.props.id) {
modals.push({
label: `${node.component}(${node.props.id})`,
value: node.props.id,
});
}
});
return modals;
}
/**
* TODO: 获取当前视图的表单列表
*/
listForms() {
const forms: Record<string, string[]> = {};
const activeViewNodes = this.activeViewModule?.nodes;
Array.from(activeViewNodes.values()).forEach((node) => {
if (
['XAction', 'XColumnAction', 'XForm', 'XStepForm', 'XSearchForm', 'XFormList'].includes(
node.component,
)
) {
forms[node.id] = queryXFormItemFields(node.rawNode);
}
});
return forms;
}
/** /**
* 应用代码初始化完成 * 应用代码初始化完成
*/ */
@@ -680,43 +622,12 @@ export class Workspace extends EventTarget implements IWorkspace {
} }
} }
/**
* 添加区块文件
* @param files 文件
* @param name 区块文件名
*/
addBlock(files: Object, name: string) {
const blockFiles = {};
Object.keys(files).forEach((path) => {
if (!path.includes('/src')) return;
blockFiles[path.replace('/src/', '')] = files[path];
});
this.addFiles(
Object.keys(blockFiles).map((filepath) => ({
filename: `/src/blocks/${name}/${filepath}`,
code: blockFiles[filepath],
})),
);
const { dependencies } = JSON.parse(files['/package.json']);
Object.keys(dependencies).forEach((pkg) => {
this.packageJson?.setValue('dependencies', (deps) => {
if (!deps[pkg]) {
deps[pkg] = dependencies[pkg];
}
return deps;
});
});
}
/** /**
* 添加新的模型文件 * 添加新的模型文件
* @param name 模型名 * @deprecated 使用 addStoreFile 代替
* @param code 模型代码
*/ */
addStoreModule(name: string, code: string) { addStoreModule(name: string, code: string) {
const filename = getFilepath(name, '/src/stores', '.js'); this.addStoreFile(name, code);
this.addFile(filename, code);
this.storeEntryModule?.addStore(name).update();
} }
/** /**
@@ -736,7 +647,7 @@ export class Workspace extends EventTarget implements IWorkspace {
* @param initValue * @param initValue
*/ */
addStoreState(storeName: string, stateName: string, initValue: string) { addStoreState(storeName: string, stateName: string, initValue: string) {
this.getStoreModuleByName(storeName).addState(stateName, initValue).update(); this.storeModules[storeName]?.addState(stateName, initValue).update();
} }
/** /**
@@ -745,7 +656,7 @@ export class Workspace extends EventTarget implements IWorkspace {
* @param stateName * @param stateName
*/ */
removeStoreState(storeName: string, stateName: string) { removeStoreState(storeName: string, stateName: string) {
this.getStoreModuleByName(storeName).removeState(stateName).update(); this.storeModules[storeName]?.removeState(stateName).update();
} }
/** /**
@@ -756,26 +667,65 @@ export class Workspace extends EventTarget implements IWorkspace {
*/ */
updateModuleCodeByVariablePath(variablePath: string, code: string) { updateModuleCodeByVariablePath(variablePath: string, code: string) {
if (/^stores\.\w+\.\w+$/.test(variablePath)) { if (/^stores\.\w+\.\w+$/.test(variablePath)) {
const [type, storeName, stateName] = variablePath.split('.'); const [, storeName, stateName] = variablePath.split('.');
this.getStoreModuleByName(storeName).updateState(stateName, code).update(); this.storeModules[storeName]?.updateState(stateName, code).update();
} }
} }
/**
* 获取服务函数的详情
* TODO: 不要 services 前缀
* @param serviceKey `services.list` 或 `services.sub.list`
* @returns
*/
getServiceFunction(serviceKey: string) {
const { name, moduleName } = this.parseServiceKey(serviceKey);
if (!name) {
return;
}
return {
name,
moduleName,
config: this.serviceModules[moduleName]?.serviceFunctions[name],
};
}
/**
* 获取服务函数的列表
*/
listServiceFunctions() {
const ret: Record<string, object> = {};
Object.values(this.serviceModules).forEach((module) => {
Object.keys(module.serviceFunctions).forEach((name) => {
const serviceKey =
module.baseConfig.namespace !== 'index'
? name
: [module.baseConfig.namespace, name].join('.');
ret[serviceKey] = module.serviceFunctions[name];
});
});
return ret;
}
/** /**
* 更新服务函数 * 更新服务函数
*/ */
updateServiceFunction(payload: any) { updateServiceFunction(payload: IServiceFunctionPayload, moduleName = 'index') {
this.serviceModule.updateServiceFunction(payload).update(); this.serviceModules[moduleName].updateServiceFunction(payload).update();
} }
/** /**
* 新增服务函数,支持批量添加 * 新增服务函数,支持批量添加
*/ */
addServiceFunction(payload: ServiceFunctionPayloadType | ServiceFunctionPayloadType[]) { addServiceFunction(
payload: IServiceFunctionPayload | IServiceFunctionPayload[],
moduleName = 'index',
) {
if (Array.isArray(payload)) { if (Array.isArray(payload)) {
this.serviceModule.addServiceFunctions(payload).update(); this.serviceModules[moduleName]?.addServiceFunctions(payload).update();
} else { } else {
this.serviceModule.addServiceFunction(payload).update(); this.serviceModules[moduleName]?.addServiceFunction(payload).update();
} }
} }
@@ -783,25 +733,40 @@ export class Workspace extends EventTarget implements IWorkspace {
* 删除服务函数 * 删除服务函数
* @param name * @param name
*/ */
removeServiceFunction(name: string) { removeServiceFunction(name: string, moduleName = 'index') {
this.serviceModule.deleteServiceFunction(name).update(); this.serviceModules[moduleName]?.deleteServiceFunction(name).update();
} }
/** /**
* 更新服务的基础配置 * 更新服务的基础配置
*/ */
updateServiceBaseConfig(name: string, value: any) { updateServiceBaseConfig(config: object, moduleName = 'index') {
this.serviceModule.updateBaseConfig(name, value).update(); this.serviceModules[moduleName]?.updateBaseConfig(config).update();
}
addDependency(data: any) {
// TODO: implement it to replace addBizComp & addServiceComp
} }
/** /**
* 获取 package.json 中的依赖列表 * 获取 package.json 中的依赖列表
* @returns * @returns
* TODO: fix this logic to merge dependencies from package.json and tango.config.json
*/ */
listDependencies() { listDependencies() {
return this.packageJson?.getValue('dependencies'); return this.packageJson?.getValue('dependencies');
} }
getDependency(pkgName: string) {
const packages = this.tangoConfigJson?.getValue('packages');
const dependencies = this.packageJson?.getValue('dependencies'); // 兼容老版本
const detail = {
version: dependencies?.[pkgName],
...(packages?.[pkgName] || {}),
};
return detail;
}
/** /**
* 更新依赖,没有就添加 * 更新依赖,没有就添加
* @param name * @param name
@@ -811,7 +776,7 @@ export class Workspace extends EventTarget implements IWorkspace {
name: string, name: string,
version: string, version: string,
options?: { options?: {
package?: PackageConfigType; package?: ITangoConfigPackages;
[x: string]: any; [x: string]: any;
}, },
) { ) {
@@ -906,7 +871,7 @@ export class Workspace extends EventTarget implements IWorkspace {
name: string, name: string,
version: string, version: string,
options?: { options?: {
package?: PackageConfigType; package?: ITangoConfigPackages;
[x: string]: any; [x: string]: any;
}, },
) { ) {
@@ -986,7 +951,7 @@ export class Workspace extends EventTarget implements IWorkspace {
); );
importDeclarations.forEach((importDeclaration) => { importDeclarations.forEach((importDeclaration) => {
this.activeViewModule.updateImportSpecifiers(importDeclaration); this.activeViewModule.updateImportSpecifiersLegacy(importDeclaration);
}); });
this.copyTempNodes.forEach((node) => { this.copyTempNodes.forEach((node) => {
@@ -1037,7 +1002,11 @@ export class Workspace extends EventTarget implements IWorkspace {
const sourcePrototype = this.getPrototype(sourceName); const sourcePrototype = this.getPrototype(sourceName);
const newNode = prototype2jsxElement(sourcePrototype); const newNode = prototype2jsxElement(sourcePrototype);
const file = this.getNode(targetNodeId).file; const file = this.getNode(targetNodeId).file;
file.insertChild(targetNodeId, newNode, 'last', sourcePrototype).update(); const { source, specifiers } = prototype2importDeclarationData(sourcePrototype, file.filename);
file
.insertChild(targetNodeId, newNode, 'last')
.addImportSpecifiers(source, specifiers)
.update();
this.history.push({ this.history.push({
message: HistoryMessage.InsertNode, message: HistoryMessage.InsertNode,
data: { data: {
@@ -1057,7 +1026,8 @@ export class Workspace extends EventTarget implements IWorkspace {
const sourcePrototype = this.getPrototype(sourceName); const sourcePrototype = this.getPrototype(sourceName);
const newNode = prototype2jsxElement(sourcePrototype); const newNode = prototype2jsxElement(sourcePrototype);
const file = this.getNode(targetNodeId).file; const file = this.getNode(targetNodeId).file;
file.replaceNode(targetNodeId, newNode, sourcePrototype).update(); const { source, specifiers } = prototype2importDeclarationData(sourcePrototype, file.filename);
file.replaceNode(targetNodeId, newNode).addImportSpecifiers(source, specifiers).update();
this.history.push({ this.history.push({
message: HistoryMessage.ReplaceNode, message: HistoryMessage.ReplaceNode,
data: { data: {
@@ -1075,7 +1045,14 @@ export class Workspace extends EventTarget implements IWorkspace {
if (insertedPrototype) { if (insertedPrototype) {
const newNode = prototype2jsxElement(insertedPrototype); const newNode = prototype2jsxElement(insertedPrototype);
const file = this.selectSource.file; const file = this.selectSource.file;
file.insertChild(this.selectSource.first.id, newNode, 'last', insertedPrototype).update(); const { source, specifiers } = prototype2importDeclarationData(
insertedPrototype,
file.filename,
);
file
.insertChild(this.selectSource.first.id, newNode, 'last')
.addImportSpecifiers(source, specifiers)
.update();
this.history.push({ this.history.push({
message: HistoryMessage.InsertNode, message: HistoryMessage.InsertNode,
data: { data: {
@@ -1131,30 +1108,40 @@ export class Workspace extends EventTarget implements IWorkspace {
const targetFile = dropTarget.node?.file; const targetFile = dropTarget.node?.file;
const sourceFile = dragSource.node?.file; const sourceFile = dragSource.node?.file;
// dragSourcePrototype to importDeclarations
const { source, specifiers } = prototype2importDeclarationData(
dragSourcePrototype,
targetFile.filename,
);
let isValidOperation = true; let isValidOperation = true;
switch (dropTarget.method) { switch (dropTarget.method) {
// 直接往目标节点的 children 里添加一个节点 // 直接往目标节点的 children 里添加一个节点
case DropMethod.InsertChild: { case DropMethod.InsertChild: {
targetFile.insertChild(dropTarget.id, newNode, 'last', dragSourcePrototype); targetFile
.insertChild(dropTarget.id, newNode, 'last')
.addImportSpecifiers(source, specifiers);
break; break;
} }
case DropMethod.InsertFirstChild: { case DropMethod.InsertFirstChild: {
targetFile.insertChild(dropTarget.id, newNode, 'first', dragSourcePrototype); targetFile
.insertChild(dropTarget.id, newNode, 'first')
.addImportSpecifiers(source, specifiers);
break; break;
} }
// 往目标节点的后边插入一个节点 // 往目标节点的后边插入一个节点
case DropMethod.InsertAfter: { case DropMethod.InsertAfter: {
targetFile.insertAfter(dropTarget.id, newNode, dragSourcePrototype); targetFile.insertAfter(dropTarget.id, newNode).addImportSpecifiers(source, specifiers);
break; break;
} }
// 往目标节点的前方插入一个节点 // 往目标节点的前方插入一个节点
case DropMethod.InsertBefore: { case DropMethod.InsertBefore: {
targetFile.insertBefore(dropTarget.id, newNode, dragSourcePrototype); targetFile.insertBefore(dropTarget.id, newNode).addImportSpecifiers(source, specifiers);
break; break;
} }
// 替换目标节点 // 替换目标节点
case DropMethod.ReplaceNode: { case DropMethod.ReplaceNode: {
targetFile.replaceNode(dropTarget.id, newNode, dragSourcePrototype); targetFile.replaceNode(dropTarget.id, newNode).addImportSpecifiers(source, specifiers);
break; break;
} }
default: default:
@@ -1171,13 +1158,6 @@ export class Workspace extends EventTarget implements IWorkspace {
} }
this.selectSource.clear(); this.selectSource.clear();
this.history.push({
message: HistoryMessage.DropNode,
data: {
[targetFile.filename]: targetFile.code,
},
});
} }
targetFile.update(); targetFile.update();
@@ -1186,6 +1166,15 @@ export class Workspace extends EventTarget implements IWorkspace {
} }
dragSource.clear(); dragSource.clear();
if (isValidOperation) {
this.history.push({
message: HistoryMessage.DropNode,
data: {
[targetFile.filename]: targetFile.code,
},
});
}
} }
/** /**
@@ -1201,6 +1190,7 @@ export class Workspace extends EventTarget implements IWorkspace {
}, },
}), }),
); );
this.onFilesChange?.(filenames);
} }
/** /**
@@ -1262,11 +1252,11 @@ export class Workspace extends EventTarget implements IWorkspace {
if (this.files.has(sourceFilePath)) { if (this.files.has(sourceFilePath)) {
// 来源是文件 // 来源是文件
const file = this.files.get(sourceFilePath); const file = this.files.get(sourceFilePath);
this.addFile(targetFilePath + '.js', file.cleanCode, file.type); this.addFile(`${targetFilePath}.js`, file.cleanCode, file.type);
} else if (this.files.has(sourceFilePath + '/index.js')) { } else if (this.files.has(`${sourceFilePath}/index.js`)) {
// 来源是目录 // 来源是目录
Array.from(this.files.keys()).forEach((key) => { Array.from(this.files.keys()).forEach((key) => {
if (key.startsWith(sourceFilePath + '/')) { if (key.startsWith(`${sourceFilePath}/`)) {
const sourceFile = this.getFile(key); const sourceFile = this.getFile(key);
this.addFile( this.addFile(
targetFilePath + key.slice(sourceFilePath.length), targetFilePath + key.slice(sourceFilePath.length),
@@ -1279,4 +1269,40 @@ export class Workspace extends EventTarget implements IWorkspace {
logger.error('copyFiles failed, source: %s, target: %s', sourceFilePath, targetFilePath); logger.error('copyFiles failed, source: %s, target: %s', sourceFilePath, targetFilePath);
} }
} }
/**
* 解析 serviceKey
* @param serviceKey
* @returns
*
* @example services.list => { moduleName: 'index', name: 'list' }
* @example services.sub.list => { moduleName: 'sub', name: 'list' }
* @example foo => undefined
*/
private parseServiceKey(serviceKey: string) {
const parts = serviceKey.split('.');
if (parts[0] !== 'services') {
return {};
}
let moduleName = 'index';
let name = '';
switch (parts.length) {
case 2: {
name = parts[1];
break;
}
case 3: {
moduleName = parts[1];
name = parts[2];
break;
}
default:
break;
}
return {
moduleName,
name,
};
}
} }
@@ -27,7 +27,7 @@ export enum FileType {
Scss = 'scss', Scss = 'scss',
} }
export type FileItemType = { export interface IFileConfig {
/** /**
* *
*/ */
@@ -40,14 +40,12 @@ export type FileItemType = {
* *
*/ */
type?: FileType; type?: FileType;
}; }
export type ModulePropsType = FileItemType;
/** /**
* *
*/ */
export type TangoViewNodeDataType<T = JSXElement> = { export interface ITangoViewNodeData<T = JSXElement> {
/** /**
* ID * ID
*/ */
@@ -71,39 +69,66 @@ export type TangoViewNodeDataType<T = JSXElement> = {
/** /**
* *
*/ */
children?: Array<TangoViewNodeDataType<T>>; children?: Array<ITangoViewNodeData<T>>;
}; }
/**
*
*/
export interface IImportSpecifierSourceData {
source: string;
isDefault?: boolean;
}
/**
*
*/
export interface IImportSpecifierData {
/**
*
*/
importedName?: string;
/**
*
*/
localName: string;
/**
*
*/
type: 'ImportDefaultSpecifier' | 'ImportSpecifier' | 'ImportNamespaceSpecifier';
}
/**
*
* FIXME: 需要考虑到有两行重复的语句的情况
*/
export type ImportDeclarationDataType = Record<string, IImportSpecifierData[]>;
/** /**
* *
* @deprecated 使 IImportSpecifierData
*/ */
export type ImportDeclarationPayloadType = { export interface IImportDeclarationPayload {
defaultSpecifier?: string; defaultSpecifier?: string;
specifiers?: string[]; specifiers?: string[];
sourcePath: string; sourcePath: string;
}; }
export type ClassPropertyNodeType = {
reference: string;
propertyName: string;
propertyBody: string;
};
/** /**
* *
*/ */
export type ServiceFunctionPayloadType = { export interface IServiceFunctionPayload {
[key: string]: any; [key: string]: any;
/** /**
* *
*/ */
name: string; name: string;
}; }
/** /**
* Store * Store
*/ */
export type StorePropertyType = { export interface IStorePropertyData {
/** /**
* *
*/ */
@@ -130,50 +155,12 @@ export type StorePropertyType = {
| 'type' | 'type'
| 'variable' | 'variable'
| 'object'; | 'object';
};
/*
*
*/
export enum ESFOperationType {
ADD = 'add',
UPDATE = 'update',
DELETE = 'delete',
}
/**
*
*/
export enum ESFDataSourceType {
OvermindX = 'ox',
Custom = 'custom',
}
/**
* HTTP Type
* get post
*/
export enum ESFHTTPMethodType {
GET = 'GET',
// PUT = 'PUT',
POST = 'POST',
// PATCH = 'PATCH',
// DELETE = 'DELETE',
}
/**
* requestType
* headers: Content-Type
*/
export enum ESFHTTRequestType {
'application/json' = 'json',
'application/x-www-form-urlencoded' = 'x-www-form-urlencoded',
} }
/** /**
* *
*/ */
export type RouteDataType = { export interface IRouteData {
/** /**
* *
*/ */
@@ -187,12 +174,12 @@ export type RouteDataType = {
*/ */
importPath?: string; importPath?: string;
[key: string]: any; [key: string]: any;
}; }
/** /**
* *
*/ */
export type PageConfigType = { export interface IPageConfigData {
/** /**
* *
*/ */
@@ -209,12 +196,12 @@ export type PageConfigType = {
* PMS * PMS
*/ */
privilegeCode?: string; privilegeCode?: string;
}; }
/** /**
* *
*/ */
export type ProjectDataType = { export interface IProjectData {
/** /**
* *
*/ */
@@ -241,14 +228,14 @@ export type ProjectDataType = {
functions: string[]; functions: string[];
}; };
}; };
}; }
export type InsertChildPositionType = 'first' | 'last'; export type InsertChildPositionType = 'first' | 'last';
/** /**
* tango.config.json packages * tango.config.json packages
*/ */
export type PackageConfigType = { export interface ITangoConfigPackages {
/** /**
* *
*/ */
@@ -273,4 +260,4 @@ export type PackageConfigType = {
* umd * umd
*/ */
designerResources?: string[]; designerResources?: string[];
}; }
-1
View File
@@ -1 +0,0 @@
export * from './types';
+25
View File
@@ -0,0 +1,25 @@
import { isTangoVariable, isWrappedByExpressionContainer } from '../src/helpers';
describe('assert', () => {
it('isTangoVariable', () => {
expect(isTangoVariable('tango.stores.app.name')).toBeTruthy();
expect(isTangoVariable('tango.stores?.app?.name')).toBeTruthy();
expect(isTangoVariable('tango.stores.app?.name')).toBeTruthy();
// expect(isTangoVariable('tango.copyToClipboard')).toBeTruthy();
});
it('isWrappedByExpressionContainer', () => {
expect(isWrappedByExpressionContainer('{this.foo}')).toBeTruthy();
expect(isWrappedByExpressionContainer('{!false}')).toBeTruthy();
expect(isWrappedByExpressionContainer('{[]}')).toBeTruthy();
expect(isWrappedByExpressionContainer('{{ foo: "bar" }}')).toBeTruthy();
expect(isWrappedByExpressionContainer('{[{ foo: "bar" }]}')).toBeTruthy();
expect(isWrappedByExpressionContainer('{123}')).toBeTruthy();
expect(isWrappedByExpressionContainer('{"hello"}')).toBeTruthy();
expect(isWrappedByExpressionContainer('{ foo: "bar" }')).toBeFalsy();
expect(isWrappedByExpressionContainer('{ type: tango.stores?.homePage?.tabKey }')).toBeFalsy();
expect(isWrappedByExpressionContainer('{ type: tango.stores.homePage.tabKey }')).toBeFalsy();
expect(isWrappedByExpressionContainer('{ foo: "bar" }')).toBeFalsy();
expect(isWrappedByExpressionContainer('{ color: tango.stores.app.color }')).toBeFalsy();
});
});
+31 -4
View File
@@ -3,15 +3,16 @@ import {
serviceConfig2Node, serviceConfig2Node,
isValidCode, isValidCode,
isValidExpressionCode, isValidExpressionCode,
code2expression,
} from '../src/helpers'; } from '../src/helpers';
describe('helpers', () => { describe('ast helpers', () => {
test('isValidCode', () => { it('isValidCode', () => {
expect(isValidCode('() => { hello world }')).toBeFalsy(); expect(isValidCode('() => { hello world }')).toBeFalsy();
expect(isValidCode('function() {}')).toBeFalsy(); expect(isValidCode('function() {}')).toBeFalsy();
}); });
test('isValidExpression', () => { it('isValidExpression', () => {
expect(isValidExpressionCode('() => { }')).toBeTruthy(); expect(isValidExpressionCode('() => { }')).toBeTruthy();
expect(isValidExpressionCode('1')).toBeTruthy(); expect(isValidExpressionCode('1')).toBeTruthy();
expect(isValidExpressionCode('"hello"')).toBeTruthy(); expect(isValidExpressionCode('"hello"')).toBeTruthy();
@@ -20,9 +21,17 @@ describe('helpers', () => {
expect(isValidExpressionCode('[1,2,3]')).toBeTruthy(); expect(isValidExpressionCode('[1,2,3]')).toBeTruthy();
expect(isValidExpressionCode('<div>hello</div>')).toBeTruthy(); expect(isValidExpressionCode('<div>hello</div>')).toBeTruthy();
expect(isValidExpressionCode('<div>hello</div>')).toBeTruthy(); expect(isValidExpressionCode('<div>hello</div>')).toBeTruthy();
expect(isValidExpressionCode('{1}')).toBeFalsy();
expect(isValidExpressionCode('{"1"}')).toBeFalsy();
expect(isValidExpressionCode('{ 1+1 }')).toBeFalsy();
expect(isValidExpressionCode('{<div>aaa</div>}')).toBeFalsy();
expect(isValidExpressionCode('{() => {}}')).toBeFalsy();
expect(isValidExpressionCode('{[1,2,3]}')).toBeFalsy();
expect(isValidExpressionCode('{tango.stores.app.title}')).toBeFalsy();
}); });
test('object2node', () => { it('object2node', () => {
const node = object2node({ const node = object2node({
url: '/api/backend/clientversion/appmarket/list', url: '/api/backend/clientversion/appmarket/list',
method: 'POST', method: 'POST',
@@ -40,4 +49,22 @@ describe('helpers', () => {
} as any); } as any);
expect(node.type).toEqual('ObjectExpression'); expect(node.type).toEqual('ObjectExpression');
}); });
it('code2expression', () => {
expect(code2expression('')).toBeUndefined();
expect(code2expression('{tango.stores.app}')).toBeUndefined();
expect(code2expression('{ type: window.bar }').type).toEqual('ObjectExpression');
expect(code2expression('() => {};').type).toEqual('ArrowFunctionExpression');
expect(code2expression('<Button>hello</Button>').type).toBe('JSXElement');
expect(code2expression('<BreadcrumbItem children="节点名称" />').type).toBe('JSXElement');
const arrayCode = `
[
{ label: 'foo', value: 'foo' },
{ label: 'bar', value: 'bar' },
]
`;
expect(code2expression(arrayCode).type).toEqual('ArrayExpression');
});
}); });
-2
View File
@@ -27,8 +27,6 @@ describe('engine', () => {
}), }),
}); });
expect(engine.workspace.activeViewModule).toBeUndefined(); expect(engine.workspace.activeViewModule).toBeUndefined();
expect(engine.workspace.serviceModule).toBeUndefined();
expect(engine.workspace.routeModule).toBeUndefined(); expect(engine.workspace.routeModule).toBeUndefined();
expect(engine.workspace?.storeModules.length).toEqual(0);
}); });
}); });
+3 -43
View File
@@ -25,40 +25,6 @@ describe('helpers', () => {
expect(code2ast('function App() {}').type).toEqual('File'); expect(code2ast('function App() {}').type).toEqual('File');
}); });
it('code2expression: null', () => {
expect(code2expression('')).toBeUndefined();
});
it('code2expression: object', () => {
const code = `
{
foo: 'bar',
}
`;
const node = code2expression(code);
expect(node.type).toEqual('ObjectExpression');
});
it('code2expression: arrow function', () => {
expect(code2expression('() => {};').type).toEqual('ArrowFunctionExpression');
});
it('code2expression: list', () => {
const code = `
[
{ label: 'foo', value: 'foo' },
{ label: 'bar', value: 'bar' },
]
`;
const node = code2expression(code);
expect(node.type).toEqual('ArrayExpression');
});
it('code2expression: jsxElement', () => {
const node = code2expression('<Button>hello</Button>');
expect(node.type).toEqual('JSXElement');
});
it('parse jsxElement attributes', () => { it('parse jsxElement attributes', () => {
const node = code2expression( const node = code2expression(
"<XColumn dataIndex='col' enumMap={{ 1: '已解决', 2: '未解决' }} />", "<XColumn dataIndex='col' enumMap={{ 1: '已解决', 2: '未解决' }} />",
@@ -73,11 +39,6 @@ describe('helpers', () => {
}); });
}); });
it('code2expression: closed jsxElement', () => {
const node = code2expression('<BreadcrumbItem children="节点名称" />');
expect(node.type).toEqual('JSXElement');
});
it('value2node: number', () => { it('value2node: number', () => {
expect(value2node(1).type).toEqual('NumericLiteral'); expect(value2node(1).type).toEqual('NumericLiteral');
}); });
@@ -126,7 +87,6 @@ describe('helpers', () => {
}); });
describe('string helpers', () => { describe('string helpers', () => {
it('value2code: empty array', () => { it('value2code: empty array', () => {
expect(value2code([])).toEqual('[]'); expect(value2code([])).toEqual('[]');
}); });
@@ -151,9 +111,9 @@ describe('string helpers', () => {
it('namesToImportDeclarations', () => { it('namesToImportDeclarations', () => {
expect( expect(
namesToImportDeclarations(['Button', 'Box', 'React'], { namesToImportDeclarations(['Button', 'Box', 'React'], {
Button: { package: '@music163/antd' }, Button: { source: '@music163/antd' },
Box: { package: '@music163/antd' }, Box: { source: '@music163/antd' },
React: { package: 'react', isDefault: true }, React: { source: 'react', isDefault: true },
}), }),
).toEqual([ ).toEqual([
{ sourcePath: '@music163/antd', specifiers: ['Button', 'Box'] }, { sourcePath: '@music163/antd', specifiers: ['Button', 'Box'] },
+97
View File
@@ -3,6 +3,103 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.5.6](https://github.com/netease/tango/compare/@music163/tango-designer@0.5.5...@music163/tango-designer@0.5.6) (2023-11-23)
**Note:** Version bump only for package @music163/tango-designer
## [0.5.5](https://github.com/netease/tango/compare/@music163/tango-designer@0.5.4...@music163/tango-designer@0.5.5) (2023-11-20)
### Bug Fixes
- pass down sandbox props ([d6ffb50](https://github.com/netease/tango/commit/d6ffb50e85573cb17ee52a797b84de5e6cedb044))
## [0.5.4](https://github.com/netease/tango/compare/@music163/tango-designer@0.5.3...@music163/tango-designer@0.5.4) (2023-11-20)
### Bug Fixes
- refactor parse expression ([#61](https://github.com/netease/tango/issues/61)) ([dbbd1dd](https://github.com/netease/tango/commit/dbbd1dddc75c532b7c9710ab0941c8680100f093))
## [0.5.3](https://github.com/netease/tango/compare/@music163/tango-designer@0.5.2...@music163/tango-designer@0.5.3) (2023-11-16)
### Bug Fixes
- allow override existing file via addFile ([6cf673e](https://github.com/netease/tango/commit/6cf673e7f6f330613e1d6331256a59900e8cbc68))
## [0.5.2](https://github.com/netease/tango/compare/@music163/tango-designer@0.5.1...@music163/tango-designer@0.5.2) (2023-11-02)
### Bug Fixes
- limit outline tree node icon ([c5840f2](https://github.com/netease/tango/commit/c5840f29b724966930e239fe00ee13b2fb882503))
- outline-panel tree expand bug & add props ([#52](https://github.com/netease/tango/issues/52)) ([b29e235](https://github.com/netease/tango/commit/b29e23599befbd60845053544648b10976a65824))
- resizable-box support horizontal and vertical dragging ([#48](https://github.com/netease/tango/issues/48)) ([fd2fa94](https://github.com/netease/tango/commit/fd2fa943b05e9f1fa57574e703307725c5aed5d8))
- update style of components panel ([11f166a](https://github.com/netease/tango/commit/11f166a2a67313200922a514a8219f34cfafd8ab))
## [0.5.1](https://github.com/netease/tango/compare/@music163/tango-designer@0.5.0...@music163/tango-designer@0.5.1) (2023-10-23)
### Bug Fixes
- add url validator ([8fefa63](https://github.com/netease/tango/commit/8fefa6344fc884355c32c4e62a5f1199ef31d65c))
- custom store template ([3f073ee](https://github.com/netease/tango/commit/3f073eeb436b7a929c9cad7af3daa15ba20e6be5))
- enhance expSetter ([#43](https://github.com/netease/tango/issues/43)) ([5ebbb42](https://github.com/netease/tango/commit/5ebbb428fb3fb786d330ab01959028443338d315))
# [0.5.0](https://github.com/netease/tango/compare/@music163/tango-designer@0.4.1...@music163/tango-designer@0.5.0) (2023-10-12)
### Bug Fixes
- refactor expSetter ([#41](https://github.com/netease/tango/issues/41)) ([d63517e](https://github.com/netease/tango/commit/d63517ecb936e4227e70c33e610664316625f4f4))
### Features
- add umd resources config when modifying dependency ([#36](https://github.com/netease/tango/issues/36)) ([e1bab66](https://github.com/netease/tango/commit/e1bab66819cf9a0bd7b87b72b2962498dfd51b2c))
## [0.4.1](https://github.com/netease/tango/compare/@music163/tango-designer@0.4.0...@music163/tango-designer@0.4.1) (2023-10-09)
### Bug Fixes
- always switch to design mode after preview is on ([85898de](https://github.com/netease/tango/commit/85898dee19b6031016679e33a4ce9edbd1089713))
- update export entry ([e429610](https://github.com/netease/tango/commit/e4296106066ad290ac838135fe6a57bc50ad2680))
# [0.4.0](https://github.com/netease/tango/compare/@music163/tango-designer@0.3.0...@music163/tango-designer@0.4.0) (2023-09-22)
### Bug Fixes
- remove useless api ([226d71c](https://github.com/netease/tango/commit/226d71c704f41a756ee777d6de4627a7eb7b4259))
### Features
- add global widget map ([1c72ee6](https://github.com/netease/tango/commit/1c72ee6ac287c1b6fb5eb3806a68b877f3749405))
# [0.3.0](https://github.com/netease/tango/compare/@music163/tango-designer@0.2.1...@music163/tango-designer@0.3.0) (2023-09-21)
### Features
- support multiple service modules & refactor workspace ([a7df29c](https://github.com/netease/tango/commit/a7df29c3debc56b187792d3e203b470e9d368ea5))
## [0.2.1](https://github.com/netease/tango/compare/@music163/tango-designer@0.2.0...@music163/tango-designer@0.2.1) (2023-09-18)
**Note:** Version bump only for package @music163/tango-designer
# [0.2.0](https://github.com/netease/tango/compare/@music163/tango-designer@0.1.3...@music163/tango-designer@0.2.0) (2023-09-13)
### Bug Fixes
- nodeTree defaultExpandAll rerender bug ([#24](https://github.com/netease/tango/issues/24)) ([12de10b](https://github.com/netease/tango/commit/12de10b0bc89a27d80b1b3bbb4d4b5aed4b73b35))
- refactor setting formItem ([b786de2](https://github.com/netease/tango/commit/b786de2f1a0e4e9141eb09fce696e45df633b232))
- refactor types ([15542d9](https://github.com/netease/tango/commit/15542d9eb2f8959597b81cae457091ee71710c83))
- remove some useless setters ([7e9ab12](https://github.com/netease/tango/commit/7e9ab12503ed33c1e6acb8a1fa5fd89fc82d35fd))
- update exports ([443ae58](https://github.com/netease/tango/commit/443ae589f67255f3815b587fd8f928c2b8742e9a))
- update sidebar ([4d809e9](https://github.com/netease/tango/commit/4d809e9afd0d6d525850708722736847b510638e))
### Features
- refactor setting-form ([8d396ff](https://github.com/netease/tango/commit/8d396ff13459beeb57b6b3c48f7e8fe1765041ae))
## [0.1.3](https://github.com/netease/tango/compare/@music163/tango-designer@0.1.2...@music163/tango-designer@0.1.3) (2023-09-07)
### Bug Fixes
- add WorkspaceView ([5ecf444](https://github.com/netease/tango/commit/5ecf444fc3563a1bf8e533136e93c1a1355d39dc))
## 0.1.2 (2023-09-06) ## 0.1.2 (2023-09-06)
### Bug Fixes ### Bug Fixes
+17 -11
View File
@@ -1,6 +1,6 @@
{ {
"name": "@music163/tango-designer", "name": "@music163/tango-designer",
"version": "0.1.2", "version": "0.5.6",
"description": "lowcode designer", "description": "lowcode designer",
"keywords": [ "keywords": [
"react" "react"
@@ -27,29 +27,35 @@
"prepublishOnly": "yarn build" "prepublishOnly": "yarn build"
}, },
"peerDependencies": { "peerDependencies": {
"react": ">= 16.8.0", "react": ">= 16.8",
"styled-components": "5" "styled-components": ">= 4"
}, },
"dependencies": { "dependencies": {
"@ant-design/icons": "^4.8.0", "@ant-design/icons": "^4.8.0",
"@music163/request": "^0.1.0", "@music163/request": "^0.1.2",
"@music163/tango-context": "^0.1.2", "@music163/tango-context": "^0.2.7",
"@music163/tango-core": "^0.1.2", "@music163/tango-core": "^0.2.6",
"@music163/tango-helpers": "^0.1.2", "@music163/tango-helpers": "^0.1.8",
"@music163/tango-sandbox": "^0.1.2", "@music163/tango-sandbox": "^0.1.11",
"@music163/tango-setting-form": "^0.1.2", "@music163/tango-setting-form": "^0.2.10",
"@music163/tango-ui": "^0.1.2", "@music163/tango-ui": "^0.1.11",
"antd": "^4.24.2", "antd": "^4.24.2",
"cash-dom": "^8.1.2", "cash-dom": "^8.1.2",
"classnames": "^2.3.2", "classnames": "^2.3.2",
"color": "^4.2.3",
"coral-system": "^1.0.5", "coral-system": "^1.0.5",
"cssjson": "^2.1.3",
"date-fns": "^2.29.2", "date-fns": "^2.29.2",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"moment": "^2.29.4",
"react-color": "^2.19.3",
"react-resizable": "^3.0.5", "react-resizable": "^3.0.5",
"semver": "^7.3.8" "semver": "^7.3.8"
}, },
"devDependencies": { "devDependencies": {
"@types/react-resizable": "^3.0.4" "@types/color": "^3.0.5",
"@types/react-color": "^3.0.9",
"@types/react-resizable": "^3.0.6"
}, },
"publishConfig": { "publishConfig": {
"access": "public", "access": "public",
@@ -1,9 +1,7 @@
import React, { useState, useRef, useEffect, memo } from 'react'; import React, { useState, useRef, useEffect, memo } from 'react';
import { Input } from 'antd'; import { Input } from 'antd';
import { css } from 'styled-components'; import { css, Box } from 'coral-system';
import { Box } from 'coral-system';
import { DeleteOutlined, PlusOutlined } from '@ant-design/icons'; import { DeleteOutlined, PlusOutlined } from '@ant-design/icons';
import { formatValue, compareValue } from './util';
const rowCss = css` const rowCss = css`
display: flex; display: flex;
@@ -99,25 +97,25 @@ const OneInputKV = (props: any) => {
!newData.length && setValidate(false); !newData.length && setValidate(false);
}; };
const handleInput = (name: string, index: number, value: string) => { const handleInput = (name: string, index: number, val: string) => {
const newData = [...data]; const newData = [...data];
if (!newData[index]) return; if (!newData[index]) return;
// 更新值 // 更新值
newData[index][name] = value; newData[index][name] = val;
_updateData(newData); _updateData(newData);
}; };
const _updateData = (values: Array<Record<string, string>>) => { const _updateData = (values: Array<Record<string, string>>) => {
const omit = ({ index, ...rest }: any) => rest; const omit = ({ index, ...rest }: any) => rest;
let value = formatValue({ data: values, format }); let nextValue = formatValue({ data: values, format });
Array.isArray(value) && (value = value.map((item) => omit(item))); Array.isArray(nextValue) && (nextValue = nextValue.map((item) => omit(item)));
setData(values); setData(values);
onChange?.(value); onChange?.(nextValue);
}; };
// render // render
@@ -166,3 +164,57 @@ const OneInputKV = (props: any) => {
}; };
export const InputKV = memo(OneInputKV, (prev, next) => prev.value === next.value); export const InputKV = memo(OneInputKV, (prev, next) => prev.value === next.value);
interface IFormatValueConfig {
format: 'simple' | 'original';
data?: Record<string, string> | Array<Record<string, string>>;
}
const formatData = (data?: Array<Record<string, string>>) => {
const result: Record<string, string> = {};
// 遍历数据
data?.forEach((item) => {
item.key && !(item.key in result) && (result[item.key] = item.value);
});
return result;
};
// 格式化value
const formatValue = ({ format, data }: IFormatValueConfig) => {
if (format === 'original') {
if (!data) return [{ index: 0 }];
const list = Array.isArray(data)
? data
: Object.keys(data).map((key) => ({ key, value: data[key] }));
return list.map((item, idx) => ({ ...item, index: idx - list.length - 1 }));
} else {
if (!data) return {};
return Array.isArray(data) ? formatData(data) : data;
}
};
const compareValue = (origin: Array<any>, data: Array<any>) => {
const resolve = (list: Array<any>) => {
let keyStr = '';
let valueStr = '';
list.forEach((item) => {
const { key, value } = item;
keyStr += key || '$';
valueStr += value || '$';
});
return { keyStr, valueStr };
};
const oldRes = resolve(origin);
const newRes = resolve(data);
return oldRes.keyStr === newRes.keyStr && oldRes.valueStr === newRes.valueStr;
};
@@ -11,10 +11,15 @@ import {
Empty, Empty,
Popconfirm, Popconfirm,
Tooltip, Tooltip,
Alert,
} from 'antd'; } from 'antd';
import { Box, Text, css, Link } from 'coral-system'; import { Box, Text, css } from 'coral-system';
import { PlusOutlined, FunctionOutlined, DeleteOutlined } from '@ant-design/icons'; import {
PlusOutlined,
FunctionOutlined,
DeleteOutlined,
CopyOutlined,
EyeOutlined,
} from '@ant-design/icons';
import { import {
filterTreeData, filterTreeData,
isFunction, isFunction,
@@ -33,7 +38,7 @@ import {
} from '@music163/tango-ui'; } from '@music163/tango-ui';
import { isValidExpressionCode } from '@music163/tango-core'; import { isValidExpressionCode } from '@music163/tango-core';
export type VariableTreeNodeType = { export interface IVariableTreeNode {
/** /**
* *
*/ */
@@ -65,9 +70,9 @@ export type VariableTreeNodeType = {
/** /**
* *
*/ */
children?: VariableTreeNodeType[]; children?: IVariableTreeNode[];
[key: string]: any; [key: string]: any;
}; }
type VariableModelType = 'preview' | 'define' | 'add'; type VariableModelType = 'preview' | 'define' | 'add';
@@ -91,15 +96,11 @@ export interface EditableVariableTreeProps {
/** /**
* *
*/ */
getPreviewValue?: (node: VariableTreeNodeType) => unknown; getPreviewValue?: (node: IVariableTreeNode) => unknown;
/** /**
* *
*/ */
onSelect?: (data: VariableTreeNodeType) => void; onSelect?: (data: IVariableTreeNode) => void;
/**
* 使
*/
onUse?: (data: VariableTreeNodeType) => void;
/** /**
* *
*/ */
@@ -124,10 +125,6 @@ export interface EditableVariableTreeProps {
* *
*/ */
searchAddonAfter?: React.ReactNode; searchAddonAfter?: React.ReactNode;
/**
*
*/
showPreviewAlert?: boolean;
} }
const previewOptions = [ const previewOptions = [
@@ -145,14 +142,12 @@ export function EditableVariableTree({
onDeleteStore = noop, onDeleteStore = noop,
getPreviewValue = noop, getPreviewValue = noop,
onSelect = noop, onSelect = noop,
onUse = noop,
onSave = noop, onSave = noop,
modes = ['add', 'preview', 'define'], modes = ['add', 'preview', 'define'],
defaultMode = 'preview', defaultMode = 'preview',
showPreviewAlert = true,
}: EditableVariableTreeProps) { }: EditableVariableTreeProps) {
const [keyword, setKeyword] = useState(''); const [keyword, setKeyword] = useState('');
const [node, setNode] = useState<VariableTreeNodeType>(); const [node, setNode] = useState<IVariableTreeNode>();
const [mode, setMode] = useState<EditableVariableTreeProps['defaultMode']>(defaultMode); const [mode, setMode] = useState<EditableVariableTreeProps['defaultMode']>(defaultMode);
const hasPanelSwitch = modes.includes('define') && modes.includes('preview'); const hasPanelSwitch = modes.includes('define') && modes.includes('preview');
@@ -184,17 +179,22 @@ export function EditableVariableTree({
</Box> </Box>
<VariableTree <VariableTree
dataSource={treeData} dataSource={treeData}
onSelect={(node) => { showViewIcon
setNode(node); onSelect={(item) => {
setNode(item);
mode === 'add' && setMode(defaultMode); mode === 'add' && setMode(defaultMode);
onSelect(node); onSelect(item);
}} }}
onAdd={(node) => { onView={(item) => {
setNode(node); setNode(item);
mode === 'add' && setMode(defaultMode);
}}
onAdd={(item) => {
setNode(item);
setMode('add'); setMode('add');
}} }}
onRemove={(node) => { onRemove={(item) => {
const [type, storeName] = node.key.split('.'); const [type, storeName] = item.key.split('.');
setNode(null); setNode(null);
onDeleteStore(storeName); onDeleteStore(storeName);
}} }}
@@ -225,40 +225,22 @@ export function EditableVariableTree({
borderRadius="m" borderRadius="m"
position="sticky" position="sticky"
top="0" top="0"
bodyProps={{ px: 'l' }} bodyProps={{ px: 'm' }}
> >
{mode === 'preview' && ( {mode === 'preview' && (
<> <ValuePreview
<Box mb="m" display={showPreviewAlert ? 'block' : 'none'}> value={getPreviewValue(node)}
<Alert onSelect={(valuePath) => {
message={ return ['tango', node.key.replaceAll('.', '?.'), valuePath].join('.');
<Space> }}
<Text> />
<Text as="strong"></Text>tango.{node.key}
</Text>
<Link onClick={() => onUse(node)}>使</Link>
<CopyClipboard text={`tango.${node.key.replaceAll('.', '?.')}`}>
{(copied) => <Link>{copied ? '已复制' : '复制变量路径'}</Link>}
</CopyClipboard>
</Space>
}
type="info"
/>
</Box>
<ValuePreview
value={getPreviewValue(node)}
onSelect={(valuePath) => {
return ['tango', node.key.replaceAll('.', '?.'), valuePath].join('.');
}}
/>
</>
)} )}
{mode === 'define' && ( {mode === 'define' && (
<NodeDefineForm <NodeDefineForm
node={node} node={node}
onSave={onSave} onSave={onSave}
onDelete={(node) => { onDelete={(item) => {
const [type, storeName, stateName] = node.key.split('.'); const [type, storeName, stateName] = item.key.split('.');
onDeleteVariable(storeName, stateName); onDeleteVariable(storeName, stateName);
setNode(null); setNode(null);
}} }}
@@ -301,14 +283,14 @@ export function EditableVariableTreeModal({
onSelect = noop, onSelect = noop,
...rest ...rest
}: EditableVariableTreeModalProps) { }: EditableVariableTreeModalProps) {
const [node, setNode] = useState<VariableTreeNodeType>(); const [node, setNode] = useState<IVariableTreeNode>();
const [visible, { on, off }] = useBoolean(false); const [visible, { on, off }] = useBoolean(false);
return ( return (
<Box> <Box>
{React.cloneElement(trigger, { onClick: on })} {React.cloneElement(trigger, { onClick: on })}
<Modal <Modal
title={title} title={title}
visible={visible} open={visible}
onCancel={off} onCancel={off}
okButtonProps={{ okButtonProps={{
disabled: !node, disabled: !node,
@@ -345,24 +327,38 @@ const varTreeStyle = css`
.ant-tree-indent-unit { .ant-tree-indent-unit {
width: 12px; width: 12px;
} }
.anticon-function {
margin-left: 4px;
color: var(--tango-colors-text2);
}
`; `;
interface VariableTreeProps { interface VariableTreeProps {
dataSource?: VariableTreeNodeType[]; dataSource?: IVariableTreeNode[];
onSelect?: (data: VariableTreeNodeType) => void; onSelect?: (data: IVariableTreeNode) => void;
onAdd?: (data: VariableTreeNodeType) => void; onAdd?: (data: IVariableTreeNode) => void;
onRemove?: (data: VariableTreeNodeType) => void; onRemove?: (data: IVariableTreeNode) => void;
onCopy?: (data: IVariableTreeNode) => void;
onView?: (data: IVariableTreeNode) => void;
showDeleteIcon?: boolean;
showViewIcon?: boolean;
} }
function VariableTree({ export function VariableTree({
dataSource = [], dataSource = [],
onSelect = noop, onSelect = noop,
onAdd = noop, onAdd = noop,
onRemove = noop, onRemove = noop,
onCopy = noop,
onView = noop,
showDeleteIcon = false,
showViewIcon = false,
}: VariableTreeProps) { }: VariableTreeProps) {
return ( return (
<Box className="VariableTree" css={varTreeStyle}> <Box className="VariableTree" css={varTreeStyle}>
<Tree.DirectoryTree <Tree
blockNode
showIcon={false} showIcon={false}
defaultExpandAll defaultExpandAll
treeData={dataSource} treeData={dataSource}
@@ -370,25 +366,18 @@ function VariableTree({
onSelect(detail.node); onSelect(detail.node);
}} }}
titleRender={(node) => { titleRender={(node) => {
if (node.showAddChildIcon || node.showRemoveIcon) { const isLeaf = !node.children;
if (isLeaf) {
const isDeletable = node.showDeleteIcon ?? showDeleteIcon;
return ( return (
<Box display="flex" justifyContent="space-between" alignItems="center" pr="l"> <Box display="flex" justifyContent="space-between" alignItems="center">
<Text>{node.title}</Text> <Text flex="1" truncated>
<Box> {node.title}
{node.showAddChildIcon && ( {node.type === 'function' && <FunctionOutlined />}
<Tooltip title={`${node.title} 中添加变量`}> </Text>
<Button <Box flex="0 0 72px" textAlign="right">
type="text" {isDeletable && (
size="small"
icon={<PlusOutlined />}
onClick={(e) => {
e.stopPropagation();
onAdd(node);
}}
/>
</Tooltip>
)}
{node.showRemoveIcon && (
<Popconfirm <Popconfirm
title="确认删除吗?该操作会导致引用此模型的代码报错,请谨慎操作!" title="确认删除吗?该操作会导致引用此模型的代码报错,请谨慎操作!"
onConfirm={() => onRemove(node)} onConfirm={() => onRemove(node)}
@@ -403,14 +392,61 @@ function VariableTree({
/> />
</Popconfirm> </Popconfirm>
)} )}
<CopyClipboard text={`tango.${node.key.replaceAll('.', '?.')}`}>
{({ copied, onClick }) => {
const label = copied ? '已复制' : '复制变量路径';
return (
<Tooltip title={label}>
<Button
type="text"
size="small"
icon={<CopyOutlined />}
onClick={(e) => {
e.stopPropagation();
onClick();
onCopy(node);
}}
/>
</Tooltip>
);
}}
</CopyClipboard>
{showViewIcon && (
<Tooltip title="查看变量详情">
<Button
type="text"
size="small"
icon={<EyeOutlined />}
onClick={(e) => {
e.stopPropagation();
onView(node);
}}
/>
</Tooltip>
)}
</Box> </Box>
</Box> </Box>
); );
} }
return ( return (
<Box> <Box display="flex" alignItems="center" justifyContent="space-between">
<Text mr="m">{node.title}</Text> <Text mr="m">{node.title}</Text>
{node.type === 'function' && <FunctionOutlined />} <Box>
{node.showAddChildIcon && (
<Tooltip title={`${node.title} 中添加变量`}>
<Button
type="text"
size="small"
icon={<PlusOutlined />}
onClick={(e) => {
e.stopPropagation();
onAdd(node);
}}
/>
</Tooltip>
)}
</Box>
</Box> </Box>
); );
}} }}
@@ -450,9 +486,9 @@ export function ValuePreview({ value, onSelect }: NodePreviewProps) {
} }
interface NodeDefineProps { interface NodeDefineProps {
node: VariableTreeNodeType; node: IVariableTreeNode;
onSave?: (code: string, node: VariableTreeNodeType) => void; onSave?: (code: string, node: IVariableTreeNode) => void;
onDelete?: (node: VariableTreeNodeType) => void; onDelete?: (node: IVariableTreeNode) => void;
} }
/** /**
@@ -470,7 +506,7 @@ function NodeDefineForm({ node, onSave = noop, onDelete = noop }: NodeDefineProp
<InputCode <InputCode
shape="inset" shape="inset"
value={node.raw} value={node.raw}
onChange={(value) => setValue(value)} onChange={(nextValue) => setValue(nextValue)}
editable={editable} editable={editable}
onBlur={() => { onBlur={() => {
if (!isValidExpressionCode(value)) { if (!isValidExpressionCode(value)) {
@@ -527,7 +563,7 @@ function NodeDefineForm({ node, onSave = noop, onDelete = noop }: NodeDefineProp
} }
interface AddNodeFormProps { interface AddNodeFormProps {
parentNode: VariableTreeNodeType; parentNode: IVariableTreeNode;
onCancel?: React.MouseEventHandler<HTMLButtonElement>; onCancel?: React.MouseEventHandler<HTMLButtonElement>;
onSubmit?: (storeName: string, data: { name: string; initialValue: string }) => void; onSubmit?: (storeName: string, data: { name: string; initialValue: string }) => void;
} }
+3 -6
View File
@@ -1,10 +1,7 @@
import { createContext, TangoRemoteServicesType } from '@music163/tango-helpers'; import { createContext } from '@music163/tango-helpers';
import type { DndQuery } from './dnd'; import type { DndQuery } from './dnd';
/** export interface IDesignerContext<S = any> {
* TODO: 是不是直接合并到 Context 中
*/
export interface IDesignerContext {
/** /**
* 沙箱查询实例 * 沙箱查询实例
*/ */
@@ -12,7 +9,7 @@ export interface IDesignerContext {
/** /**
* 远程服务 * 远程服务
*/ */
remoteServices?: TangoRemoteServicesType; remoteServices?: Record<string, S>;
} }
const [DesignerProvider, useDesigner] = createContext<IDesignerContext>({ const [DesignerProvider, useDesigner] = createContext<IDesignerContext>({
+7 -6
View File
@@ -1,11 +1,11 @@
import React, { useEffect } from 'react'; import React, { useEffect, useMemo } from 'react';
import { SystemProvider } from 'coral-system'; import { SystemProvider, extendTheme } from 'coral-system';
import { ConfigProvider } from 'antd'; import { ConfigProvider } from 'antd';
import zhCN from 'antd/lib/locale/zh_CN';
import { TangoEngineProvider, ITangoEngineContext } from '@music163/tango-context'; import { TangoEngineProvider, ITangoEngineContext } from '@music163/tango-context';
import { createFromIconfontCN } from '@ant-design/icons';
import { DesignerProvider, IDesignerContext } from './context'; import { DesignerProvider, IDesignerContext } from './context';
import { theme as baseTheme } from './theme'; import { theme as baseTheme } from './theme';
import { createFromIconfontCN } from '@ant-design/icons'; import zhCN from 'antd/lib/locale/zh_CN';
export interface DesignerProps extends IDesignerContext, ITangoEngineContext { export interface DesignerProps extends IDesignerContext, ITangoEngineContext {
/** /**
@@ -27,13 +27,14 @@ export interface DesignerProps extends IDesignerContext, ITangoEngineContext {
export function Designer(props: DesignerProps) { export function Designer(props: DesignerProps) {
const { const {
engine, engine,
config,
theme: themeProp, theme: themeProp,
sandboxQuery, sandboxQuery,
remoteServices = {}, remoteServices = {},
iconfontScriptUrl = '//at.alicdn.com/t/c/font_2891794_lzc7rtwuzf.js', iconfontScriptUrl = '//at.alicdn.com/t/c/font_2891794_lzc7rtwuzf.js',
children, children,
} = props; } = props;
const theme = themeProp ?? baseTheme; const theme = useMemo(() => extendTheme(themeProp, baseTheme), [themeProp]);
useEffect(() => { useEffect(() => {
createFromIconfontCN({ createFromIconfontCN({
scriptUrl: iconfontScriptUrl, scriptUrl: iconfontScriptUrl,
@@ -42,7 +43,7 @@ export function Designer(props: DesignerProps) {
return ( return (
<SystemProvider theme={theme} prefix="--tango"> <SystemProvider theme={theme} prefix="--tango">
<ConfigProvider locale={zhCN}> <ConfigProvider locale={zhCN}>
<TangoEngineProvider value={{ engine }}> <TangoEngineProvider value={{ engine, config }}>
<DesignerProvider value={{ sandboxQuery, remoteServices }}>{children}</DesignerProvider> <DesignerProvider value={{ sandboxQuery, remoteServices }}>{children}</DesignerProvider>
</TangoEngineProvider> </TangoEngineProvider>
</ConfigProvider> </ConfigProvider>
+5 -9
View File
@@ -69,14 +69,6 @@ export const CodeEditor = observer(
[workspace, autoRemoveUnusedImports, activeFile, files], [workspace, autoRemoveUnusedImports, activeFile, files],
); );
const handleRenameFile = useCallback(
(oldFilename: string, newFilename: string) => {
workspace.renameFile(oldFilename, newFilename);
workspace.setActiveFile(newFilename);
},
[workspace],
);
const handleFileChange = useCallback( const handleFileChange = useCallback(
(type: string, info: any) => { (type: string, info: any) => {
switch (type) { switch (type) {
@@ -88,7 +80,12 @@ export const CodeEditor = observer(
workspace.removeFile(info.path); workspace.removeFile(info.path);
break; break;
case 'renameFile': case 'renameFile':
workspace.renameFile(info.path, info.newpath);
workspace.setActiveFile(info.newpath);
break;
case 'renameFolder': case 'renameFolder':
workspace.renameFolder(info.path, info.newpath);
break;
case 'addFolder': case 'addFolder':
default: default:
break; break;
@@ -114,7 +111,6 @@ export const CodeEditor = observer(
}} }}
ideConfig={ideConfig} ideConfig={ideConfig}
onFileSave={fileSave} onFileSave={fileSave}
onRenameFile={handleRenameFile}
onPathChange={handlePathChange} onPathChange={handlePathChange}
onFileChange={handleFileChange} onFileChange={handleFileChange}
defaultPath={activeFile} defaultPath={activeFile}
+7 -5
View File
@@ -3,14 +3,16 @@ export * from './designer';
export * from './designer-panel'; export * from './designer-panel';
export * from './workspace-panel'; export * from './workspace-panel';
export * from './setting-panel'; export * from './setting-panel';
export * from './view-panel'; export * from './workspace-view';
export * from './simulator';
export * from './dnd';
export * from './editor'; export * from './editor';
export * from './sandbox'; export * from './sandbox';
export * from './sidebar'; export * from './sidebar';
export * from './toolbar'; export * from './toolbar';
export * from './dnd'; export * from './selection-menu';
export * from './widgets';
export { INTERNAL_SELECTION_TOOLS } from './simulator/selection';
export { useWorkspace, useDesigner, observer } from '@music163/tango-context';
export { register as registerSetter } from '@music163/tango-setting-form'; export { register as registerSetter } from '@music163/tango-setting-form';
export type { FormItemComponentProps, IFormItemCreateOptions } from '@music163/tango-setting-form';
+36 -31
View File
@@ -1,7 +1,7 @@
import React, { useImperativeHandle, ForwardedRef, useRef, useState, useEffect } from 'react'; import React, { useImperativeHandle, ForwardedRef, useRef, useState, useEffect } from 'react';
import { Box } from 'coral-system'; import { Box } from 'coral-system';
import { CodeSandbox, CodeSandboxProps } from '@music163/tango-sandbox'; import { CodeSandbox, CodeSandboxProps } from '@music163/tango-sandbox';
import { getValue, logger, pick, setValue } from '@music163/tango-helpers'; import { getValue, isFunction, logger, pick, setValue } from '@music163/tango-helpers';
import { observer, useWorkspace, useDesigner } from '@music163/tango-context'; import { observer, useWorkspace, useDesigner } from '@music163/tango-context';
import { Simulator, Viewport } from '../simulator'; import { Simulator, Viewport } from '../simulator';
import { useSandboxQuery } from '../context'; import { useSandboxQuery } from '../context';
@@ -9,33 +9,38 @@ import { DndQuery, useDnd } from '../dnd';
import { Navigator } from './navigator'; import { Navigator } from './navigator';
import { SelectionToolsProps } from '../simulator/selection'; import { SelectionToolsProps } from '../simulator/selection';
type SandboxEventHandlerConfig = { interface ISandboxEventHandlerConfig {
sandboxQuery?: DndQuery; sandboxQuery?: DndQuery;
sandboxType?: 'design' | 'preview'; sandboxType?: 'design' | 'preview';
isActive: boolean; isActive: boolean;
[x: string]: any; [x: string]: any;
}; }
export type SandboxProps = Omit<CodeSandboxProps, 'files' | 'eventHandlers' | 'onMessage'> & { export type SandboxProps = Omit<CodeSandboxProps, 'files' | 'eventHandlers' | 'onMessage'> & {
isPreview?: boolean; isPreview?: boolean;
selectionTools?: SelectionToolsProps['actions']; selectionTools?: SelectionToolsProps['actions'];
/**
* tangoConfigJson 处理器
*/
configFormatter?: IMergeTangoConfigJsonConfig['formatter'];
sandboxType?: 'design' | 'preview'; sandboxType?: 'design' | 'preview';
mode?: 'single' | 'combined'; mode?: 'single' | 'combined';
injectScript?: string; injectScript?: string;
onViewChange?: (data: any, config?: SandboxEventHandlerConfig) => void; onViewChange?: (data: any, config?: ISandboxEventHandlerConfig) => void;
onMessage?: (data: any, config?: SandboxEventHandlerConfig) => void; onMessage?: (data: any, config?: ISandboxEventHandlerConfig) => void;
onLoad?: (config?: SandboxEventHandlerConfig) => void; onLoad?: (config?: ISandboxEventHandlerConfig) => void;
}; };
export type CombinedSandboxRef = { export interface CombinedSandboxRef {
designSandbox: CodeSandbox; designSandbox: CodeSandbox;
previewSandbox: CodeSandbox; previewSandbox: CodeSandbox;
}; }
const LANDING_PAGE_PATH = '/__background_landing_page__'; const LANDING_PAGE_PATH = '/__background_landing_page__';
function useSandbox({ function useSandbox({
isPreview: isPreviewProp, isPreview: isPreviewProp,
configFormatter,
onViewChange, onViewChange,
onMessage: onMessageProp, onMessage: onMessageProp,
onLoad: onLoadProp, onLoad: onLoadProp,
@@ -69,12 +74,12 @@ function useSandbox({
let files = Array.from(workspace.files.keys()).reduce((prev, filename) => { let files = Array.from(workspace.files.keys()).reduce((prev, filename) => {
let code = workspace.getFile(filename).code; let code = workspace.getFile(filename).code;
if (filename === '/tango.config.json') { if (filename === '/tango.config.json') {
code = mergeTangoConfigJson(code, isPreview, { injectScript }); code = mergeTangoConfigJson(code, { isPreview, injectScript, formatter: configFormatter });
} }
prev[filename] = { code }; prev[filename] = { code };
return prev; return prev;
}, {}); }, {});
files = normalizeFiles(files, workspace.entry); files = fixSandboxFiles(files, workspace.entry);
const onMessage = (data: any) => onMessageProp && onMessageProp(data, getSandboxConfig()); const onMessage = (data: any) => onMessageProp && onMessageProp(data, getSandboxConfig());
const onLoad = () => onLoadProp && onLoadProp(getSandboxConfig()); const onLoad = () => onLoadProp && onLoadProp(getSandboxConfig());
@@ -114,6 +119,7 @@ const PreviewSandbox = observer(
startRoute, startRoute,
injectScript, injectScript,
sandboxType: 'preview', sandboxType: 'preview',
...rest,
}); });
return ( return (
@@ -161,6 +167,7 @@ const DesignSandbox = observer(
startRoute, startRoute,
injectScript, injectScript,
sandboxType: 'design', sandboxType: 'design',
...rest,
}); });
return ( return (
@@ -187,7 +194,7 @@ export const CombinedSandbox = observer(
const activeSandbox = useRef<string>(); const activeSandbox = useRef<string>();
const [startRoute, setStartRoute] = useState(workspace.activeRoute); const [startRoute, setStartRoute] = useState(workspace.activeRoute);
const onViewChange = (data: any, config: SandboxEventHandlerConfig) => { const onViewChange = (data: any, config: ISandboxEventHandlerConfig) => {
if (config.isActive) { if (config.isActive) {
const curPath = data?.pathname + data?.search; const curPath = data?.pathname + data?.search;
const isSandboxChanged = config.sandboxType !== activeSandbox.current; const isSandboxChanged = config.sandboxType !== activeSandbox.current;
@@ -294,7 +301,7 @@ export const Sandbox = observer(
: combinedSandboxRef.current?.designSandbox; : combinedSandboxRef.current?.designSandbox;
} }
const onViewChange = (data: any, { isActive }: SandboxEventHandlerConfig) => { const onViewChange = (data: any, { isActive }: ISandboxEventHandlerConfig) => {
if (isActive) { if (isActive) {
navigatorRef.current.changeRelativeUrl(data?.pathname + data?.search); navigatorRef.current.changeRelativeUrl(data?.pathname + data?.search);
} }
@@ -378,7 +385,7 @@ export const Sandbox = observer(
); );
// 兼容 tango.config.json,转成 sandbox.config.json // 兼容 tango.config.json,转成 sandbox.config.json
function normalizeFiles(files: object, entry = '/src/index.js') { function fixSandboxFiles(files: Record<string, { code: string }>, entry = '/src/index.js') {
if (files['/tango.config.json']) { if (files['/tango.config.json']) {
const tangConfigJsonStr = files['/tango.config.json'].code; const tangConfigJsonStr = files['/tango.config.json'].code;
const tangConfigJson = JSON.parse(tangConfigJsonStr); const tangConfigJson = JSON.parse(tangConfigJsonStr);
@@ -386,6 +393,7 @@ function normalizeFiles(files: object, entry = '/src/index.js') {
code: JSON.stringify(tangConfigJson.sandbox, null, 2), code: JSON.stringify(tangConfigJson.sandbox, null, 2),
}; };
} }
if (!files['/index.html']) { if (!files['/index.html']) {
files['/index.html'] = { files['/index.html'] = {
code: ` code: `
@@ -405,28 +413,30 @@ function normalizeFiles(files: object, entry = '/src/index.js') {
`, `,
}; };
} }
return files; return files;
} }
function mergeTangoConfigJson(code: string, isPreview: boolean, config?: { [x: string]: any }) { interface IMergeTangoConfigJsonConfig {
isPreview?: boolean;
injectScript?: string;
formatter?: (json: object) => object;
}
function mergeTangoConfigJson(
code: string,
{ isPreview, injectScript, formatter }: IMergeTangoConfigJsonConfig = {},
) {
let json; let json;
try { try {
json = JSON.parse(code); json = JSON.parse(code);
} catch (err) { } catch (err) {
logger.error(err); logger.error('Json parse failed!', err);
return code; return code;
} }
const ox = getValue(json, 'dataSource.ox');
const userJs = getValue(json, 'sandbox.evaluateJavaScript') || ''; const userJs = getValue(json, 'sandbox.evaluateJavaScript') || '';
let mergedUserJs = userJs; let mergedUserJs = userJs;
const { injectScript } = config || {};
if (ox) {
// TIP: 自动拼装 __tango_ox__ 注入到沙箱中
mergedUserJs = `window.__tango_ox__=${JSON.stringify(ox)};${mergedUserJs}`;
}
if (injectScript) { if (injectScript) {
mergedUserJs = `${mergedUserJs};${injectScript}`; mergedUserJs = `${mergedUserJs};${injectScript}`;
} }
@@ -435,15 +445,6 @@ function mergeTangoConfigJson(code: string, isPreview: boolean, config?: { [x: s
setValue(json, 'sandbox.evaluateJavaScript', mergedUserJs); setValue(json, 'sandbox.evaluateJavaScript', mergedUserJs);
} }
const i18n = getValue(json, 'i18n');
if (i18n) {
// TIP: 合并 i18n 配置到沙箱配置中
setValue(json, 'sandbox.i18n', {
id: i18n.appId,
preModule: i18n.preModule,
});
}
// 合并 packages 内的信息至 sandbox // 合并 packages 内的信息至 sandbox
const packages = getValue(json, 'packages'); const packages = getValue(json, 'packages');
const externals = getValue(json, 'sandbox.externals') || {}; const externals = getValue(json, 'sandbox.externals') || {};
@@ -490,6 +491,10 @@ function mergeTangoConfigJson(code: string, isPreview: boolean, config?: { [x: s
setValue(json, 'sandbox.externals', externals); setValue(json, 'sandbox.externals', externals);
setValue(json, 'sandbox.externalResources', [...new Set(externalResources)]); setValue(json, 'sandbox.externalResources', [...new Set(externalResources)]);
if (isFunction(formatter)) {
json = formatter?.(json);
}
return JSON.stringify(json); return JSON.stringify(json);
} }
@@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { Select } from 'antd'; import { Select } from 'antd';
import { FormItemComponentProps } from '../form-item'; import { FormItemComponentProps } from '@music163/tango-setting-form';
import { ListSetter, NewOptionFormFieldType } from './list-setter'; import { ListSetter, NewOptionFormFieldType } from './list-setter';
const defaultNewOptionValue = { const defaultNewOptionValue = {
@@ -28,7 +28,11 @@ const optionFormFields: NewOptionFormFieldType[] = [
{ label: 'key', name: 'key', required: true }, { label: 'key', name: 'key', required: true },
{ label: '文本', name: 'label', required: true }, { label: '文本', name: 'label', required: true },
{ label: '外观', name: 'shape', component: <Select placeholder="请选择" options={shapes} /> }, { label: '外观', name: 'shape', component: <Select placeholder="请选择" options={shapes} /> },
{ label: '按钮类型', name: 'buttonType', component: <Select placeholder="请选择" options={buttonTypes} /> }, {
label: '按钮类型',
name: 'buttonType',
component: <Select placeholder="请选择" options={buttonTypes} />,
},
{ label: '类型', name: 'actionType', component: <Select placeholder="请选择" options={types} /> }, { label: '类型', name: 'actionType', component: <Select placeholder="请选择" options={types} /> },
{ label: '确认气泡内容', name: 'confirmContent' }, { label: '确认气泡内容', name: 'confirmContent' },
]; ];
@@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { Switch } from 'antd'; import { Switch } from 'antd';
import { FormItemComponentProps } from '../form-item'; import { FormItemComponentProps } from '@music163/tango-setting-form/src/form-item';
export function BoolSetter({ value, onChange, ...props }: FormItemComponentProps<boolean>) { export function BoolSetter({ value, onChange, ...props }: FormItemComponentProps<boolean>) {
return <Switch checked={value} onChange={(val) => onChange?.(val)} {...props} />; return <Switch checked={value} onChange={(val) => onChange?.(val)} {...props} />;
@@ -2,21 +2,13 @@ import React from 'react';
import { Radio, RadioProps, Tooltip } from 'antd'; import { Radio, RadioProps, Tooltip } from 'antd';
import { IconFont } from '@music163/tango-ui'; import { IconFont } from '@music163/tango-ui';
import type { OptionType } from '@music163/tango-helpers'; import type { OptionType } from '@music163/tango-helpers';
import { FormItemComponentProps } from '../form-item'; import { FormItemComponentProps } from '@music163/tango-setting-form';
interface ChoiceSetterProps { interface ChoiceSetterProps {
mode?: 'text' | 'icon'; mode?: 'text' | 'icon';
options?: OptionType[]; options?: OptionType[];
} }
function RadioButton({ tip, ...rest }: RadioProps & { tip?: string }) {
let ret = <Radio.Button {...rest} />;
if (tip) {
ret = <Tooltip title={tip}>{ret}</Tooltip>;
}
return ret;
}
export function ChoiceSetter({ export function ChoiceSetter({
options, options,
mode = 'text', mode = 'text',
@@ -45,3 +37,11 @@ export function ChoiceSetter({
</Radio.Group> </Radio.Group>
); );
} }
function RadioButton({ tip, ...rest }: RadioProps & { tip?: string }) {
let ret = <Radio.Button {...rest} />;
if (tip) {
ret = <Tooltip title={tip}>{ret}</Tooltip>;
}
return ret;
}
@@ -2,7 +2,7 @@ import React from 'react';
import { Switch } from 'antd'; import { Switch } from 'antd';
import { Box } from 'coral-system'; import { Box } from 'coral-system';
import { FieldStringOutlined } from '@ant-design/icons'; import { FieldStringOutlined } from '@ant-design/icons';
import { FormItemComponentProps } from '../form-item'; import { FormItemComponentProps } from '@music163/tango-setting-form';
import { ListSetter, NewOptionFormFieldType } from './list-setter'; import { ListSetter, NewOptionFormFieldType } from './list-setter';
const defaultNewOptionValue = { const defaultNewOptionValue = {
@@ -37,7 +37,7 @@ const renderSetterItem = (item: any) => {
export function ColumnSetter(props: FormItemComponentProps<any[]>) { export function ColumnSetter(props: FormItemComponentProps<any[]>) {
return ( return (
<ListSetter <ListSetter
getListItemKey={(item) => item.key || item.dataIndex} getListItemKey={(item: any) => item.key || item.dataIndex}
addBtnText="添加新列" addBtnText="添加新列"
listItemFormFields={optionFormFields} listItemFormFields={optionFormFields}
newItemDefaultValues={defaultNewOptionValue} newItemDefaultValues={defaultNewOptionValue}
@@ -3,8 +3,8 @@ import { Box } from 'coral-system';
// @ts-ignore // @ts-ignore
import { toJSON } from 'cssjson'; import { toJSON } from 'cssjson';
import { InputNumber, Space } from 'antd'; import { InputNumber, Space } from 'antd';
import { FormItemComponentProps } from '@music163/tango-setting-form';
import { SliderSetter } from './number-setter'; import { SliderSetter } from './number-setter';
import { FormItemComponentProps } from '../form-item';
import { import {
BgSetter, BgSetter,
BorderSetter, BorderSetter,
@@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import moment from 'moment'; import moment from 'moment';
import { DatePicker, TimePicker } from 'antd'; import { DatePicker, TimePicker } from 'antd';
import { FormItemComponentProps } from '../form-item'; import { FormItemComponentProps } from '@music163/tango-setting-form';
function toMoment(value: any, format: string) { function toMoment(value: any, format: string) {
let ret: moment.Moment; let ret: moment.Moment;
@@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { FormItemComponentProps } from '../form-item'; import { FormItemComponentProps } from '@music163/tango-setting-form';
import { InputKV } from '../components'; import { InputKV } from '../components';
export function EnumSetter(props: FormItemComponentProps<string>) { export function EnumSetter(props: FormItemComponentProps<string>) {
@@ -1,11 +1,10 @@
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import { css } from 'styled-components'; import { css, Box } from 'coral-system';
import { Box } from 'coral-system';
import { AutoComplete } from 'antd'; import { AutoComplete } from 'antd';
import { ActionSelect } from '@music163/tango-ui'; import { ActionSelect } from '@music163/tango-ui';
import { ExpressionModal, getInputValue } from './expression-setter'; import { FormItemComponentProps } from '@music163/tango-setting-form';
import { FormItemComponentProps } from '../form-item'; import { useWorkspace, useWorkspaceData } from '@music163/tango-context';
import { useFormVariable } from '../context'; import { ExpressionModal } from './expression-setter';
enum EventAction { enum EventAction {
NoAction = 'noAction', NoAction = 'noAction',
@@ -42,7 +41,9 @@ export function EventSetter(props: EventSetterProps) {
const [temp, setTemp] = useState(''); // 二级暂存值 const [temp, setTemp] = useState(''); // 二级暂存值
const [expModalVisible, setExpModalVisible] = useState(false); // 弹窗是否显示 const [expModalVisible, setExpModalVisible] = useState(false); // 弹窗是否显示
const { actionVariables, routeOptions, modalOptions } = useFormVariable(); const { actionVariables, routeOptions } = useWorkspaceData();
const workspace = useWorkspace();
const modalOptions = workspace.activeViewModule.listModals() || [];
const handleChange = useCallback<FormItemComponentProps['onChange']>( const handleChange = useCallback<FormItemComponentProps['onChange']>(
(nextValue: any, ...args) => { (nextValue: any, ...args) => {
@@ -70,19 +71,19 @@ export function EventSetter(props: EventSetterProps) {
}; };
const actionText = getActionText(type, temp, value); const actionText = getActionText(type, temp, value);
const inputValue = getInputValue(value); const inputValue = value;
return ( return (
<Box css={wrapperStyle}> <Box css={wrapperStyle}>
<ActionSelect options={options} onSelect={onAction} text={actionText} /> <ActionSelect options={options} onSelect={onAction} text={actionText} />
<ExpressionModal <ExpressionModal
title={modalTitle} title={modalTitle}
defaultValue={inputValue} value={inputValue}
visible={expModalVisible} visible={expModalVisible}
onCancel={() => setExpModalVisible(false)} onCancel={() => setExpModalVisible(false)}
onOk={(value) => { onOk={(nextValue) => {
setExpModalVisible(false); setExpModalVisible(false);
handleChange(value); handleChange(`{${nextValue}}`);
}} }}
dataSource={actionVariables} dataSource={actionVariables}
/> />
@@ -140,7 +141,7 @@ function getActionText(type: EventAction, temp: string, value: any) {
if (handlerMap[type]) { if (handlerMap[type]) {
text = getExpressionValue(type, temp); text = getExpressionValue(type, temp);
} else if (value) { } else if (value) {
text = getInputValue(value); text = value;
} }
text = text || '请选择'; text = text || '请选择';
return text; return text;
@@ -0,0 +1,26 @@
import React from 'react';
import { SingleMonacoEditor } from '@music163/tango-ui';
import { Box } from 'coral-system';
import { FormItemComponentProps } from '@music163/tango-setting-form/src/form-item';
export function ExpressionSetter({ value, onChange }: FormItemComponentProps) {
return (
<Box height="120px" border="solid" borderColor="line.normal" borderRadius="s">
<SingleMonacoEditor
defaultValue={value}
onBlur={(newValue) => {
if (newValue !== value) {
onChange(`{${newValue}}`);
}
}}
language="javascript"
options={{
lineNumbers: 'off',
minimap: {
enabled: false,
},
}}
/>
</Box>
);
}
@@ -0,0 +1,271 @@
import React, { useState, useEffect, useCallback } from 'react';
import { Box, Text, css } from 'coral-system';
import { Modal } from 'antd';
import {
isValidExpressionCode,
isWrappedByExpressionContainer,
value2expressionCode,
} from '@music163/tango-core';
import { getVariableContent, noop, useBoolean, getValue } from '@music163/tango-helpers';
import { CloseCircleFilled, ExpandAltOutlined } from '@ant-design/icons';
import { IconButton, Panel, InputCode } from '@music163/tango-ui';
import { FormItemComponentProps } from '@music163/tango-setting-form';
import { useWorkspace, useWorkspaceData } from '@music163/tango-context';
import { EditableVariableTree, IVariableTreeNode } from '../components';
import { useSandboxQuery } from '../context';
export const expressionValueValidate = (value: string) => {
if (isWrappedByExpressionContainer(value)) {
const exp = getVariableContent(value);
if (!isValidExpressionCode(exp)) {
return '表达式存在语法错误!';
}
}
};
export const jsonValueValidate = (value: string) => {
if (isWrappedByExpressionContainer(value)) {
const jsonStr = getVariableContent(value);
try {
JSON.parse(jsonStr);
} catch (e) {
return '不是合法的 JSON 语法!';
}
}
};
const suffixStyle = css`
.anticon-close-circle {
color: rgba(0, 0, 0, 0.25);
&:hover {
color: rgba(0, 0, 0, 0.45);
}
}
`;
export interface ExpressionSetterProps extends FormItemComponentProps<string> {
modalTitle?: string;
modalTip?: string;
autoCompleteOptions?: string[];
allowClear?: boolean;
}
export function ExpressionSetter(props: ExpressionSetterProps) {
const {
onChange,
modalTitle,
modalTip,
autoCompleteOptions,
placeholder = '输入JS表达式代码',
value: valueProp,
status,
allowClear = true,
} = props;
const [visible, { on, off }] = useBoolean();
const [inputValue, setInputValue] = useState(() => {
return value2expressionCode(valueProp);
});
const sandbox = useSandboxQuery();
const evaluateContext = sandbox.window;
// when receive new value, sync state
useEffect(() => {
setInputValue(value2expressionCode(valueProp));
}, [valueProp]);
const change = useCallback(
(code: string) => {
if (code === valueProp) {
return;
}
let ret;
if (!code) {
// do nothing
} else if (isWrappedByExpressionContainer(code)) {
ret = code;
} else {
ret = `{${code}}`;
}
if (ret === valueProp) {
return;
}
onChange(ret);
},
[valueProp, onChange],
);
return (
<Box className="ExpressionSetter">
<InputCode
placeholder={placeholder}
suffix={
<Box css={suffixStyle}>
{allowClear && (
<CloseCircleFilled
title="清空"
onClick={() => {
change('');
}}
/>
)}
<IconButton
tooltip="打开表达式变量选择面板"
icon={<ExpandAltOutlined />}
onClick={on}
/>
</Box>
}
value={inputValue}
onChange={setInputValue}
onBlur={() => {
change(inputValue);
}}
autoCompleteContext={evaluateContext}
autoCompleteOptions={autoCompleteOptions}
status={status}
maxHeight="200px"
/>
<ExpressionModal
title={modalTitle}
subTitle={modalTip}
placeholder={placeholder}
autoCompleteOptions={autoCompleteOptions}
visible={visible}
value={inputValue}
onCancel={() => off()}
onOk={(value) => {
onChange(value);
off();
}}
/>
</Box>
);
}
export interface ExpressionModalProps {
title?: string;
subTitle?: string;
placeholder?: string;
visible?: boolean;
defaultValue?: string;
value?: string;
onCancel?: () => void;
onOk?: (value: string) => void;
dataSource?: IVariableTreeNode[];
autoCompleteOptions?: string[];
}
export function ExpressionModal({
title,
subTitle,
placeholder,
visible,
onCancel = noop,
onOk = noop,
defaultValue,
value,
dataSource,
autoCompleteOptions,
}: ExpressionModalProps) {
const [exp, setExp] = useState(value ?? defaultValue);
const [error, setError] = useState('');
const workspace = useWorkspace();
const onAction = useCallback(
(action: string, args: unknown[]) => {
workspace[action]?.(...args);
},
[workspace],
);
const sandbox = useSandboxQuery();
const { expressionVariables } = useWorkspaceData();
const evaluateContext = sandbox.window;
const handleExpInputChange = (val: string) => {
setExp(val?.trim());
};
useEffect(() => {
setExp(value);
}, [value]);
return (
<Modal
closable={false}
destroyOnClose
width="60%"
open={visible}
onCancel={onCancel}
onOk={() => {
onOk(exp);
}}
bodyStyle={{
padding: 0,
}}
>
<Panel title={`${title} 设置为引用变量或自定义表达式`} subTitle={subTitle} shape="solid">
<InputCode
shape="inset"
minHeight="56px"
maxHeight="200px"
value={exp}
placeholder={placeholder}
onChange={handleExpInputChange}
onBlur={() => {
setError(expressionValueValidate(exp));
}}
autoCompleteContext={evaluateContext}
autoCompleteOptions={autoCompleteOptions}
/>
{error ? <Text color="red"></Text> : null}
</Panel>
<Panel title="从变量列表中选中" shape="solid" borderTop="0">
<EditableVariableTree
height={380}
dataSource={dataSource || expressionVariables}
onSelect={(node) => {
if (!node.key) {
return;
}
if (node.key.split('.').length < 2) {
return;
}
let str;
if (/^(stores|services)\./.test(node.key)) {
str = `{tango.${node.key.replaceAll('.', '?.')}}`;
} else {
str = `{${node.key}}`;
}
setExp(str);
}}
onAddVariable={(storeName, data) => {
onAction('addStoreState', [storeName, data.name, data.initialValue]);
}}
onDeleteVariable={(storeName, stateName) => {
onAction('removeStoreState', [storeName, stateName]);
}}
onDeleteStore={(storeName) => {
onAction('removeStoreModule', [storeName]);
}}
onSave={(code, node) => {
onAction('updateModuleCodeByVariablePath', [node.key, code]);
}}
getPreviewValue={(node) => {
if (!node || !node.key) {
return;
}
if (node.type === 'function') {
return node.raw;
}
return getValue(evaluateContext['tango'], node.key);
}}
/>
</Panel>
</Modal>
);
}
@@ -1,18 +1,19 @@
import { register, IFormItemCreateOptions } from '@music163/tango-setting-form';
import { ActionListSetter } from './action-list-setter'; import { ActionListSetter } from './action-list-setter';
import { EnumSetter } from './enum-setter';
import { TextAreaSetter, TextSetter } from './text-setter';
import { PickerSetter } from './picker-setter';
import { ChoiceSetter } from './choice-setter';
import { FormItemCreateOptionsType } from '../form-item';
import { ListSetter } from './list-setter';
import { IconSetter } from './icon-setter';
import { ColumnSetter } from './column-setter'; import { ColumnSetter } from './column-setter';
import { NumberSetter, SliderSetter } from './number-setter'; import { CssSetter } from './css-setter';
import { ExpressionSetter, expressionValueValidate, jsonValueValidate } from './expression-setter'; import { DateRangeSetter, DateSetter, TimeRangeSetter } from './date-setter';
import { OptionSetter } from './option-setter'; import { EnumSetter } from './enum-setter';
import { DateSetter, DateRangeSetter, TimeSetter, TimeRangeSetter } from './date-setter';
import { JsxSetter } from './jsx-setter';
import { EventSetter } from './event-setter'; import { EventSetter } from './event-setter';
import { ExpressionSetter } from './expression-setter';
import { JSONSetter } from './json-setter';
import { JsxSetter } from './jsx-setter';
import { ListSetter } from './list-setter';
import { ModelSetter } from './model-setter';
import { OptionSetter } from './option-setter';
import { PickerSetter } from './picker-setter';
import { RenderSetter, TableCellSetter, TableExpandableSetter } from './render-props-setter';
import { RouterSetter } from './router-setter';
import { import {
SpacingSetter, SpacingSetter,
ColorSetter, ColorSetter,
@@ -24,131 +25,119 @@ import {
FlexAlignItemsSetter, FlexAlignItemsSetter,
FlexDirectionSetter, FlexDirectionSetter,
} from './style-setter'; } from './style-setter';
import { CssSetter } from './css-setter';
import { ListenerSetter } from './listener-setter';
import { RuleSetter } from './rule-setter';
import { ModelSetter } from './model-setter';
import {
RenderSetter,
TableCellSetter,
TableExpandableSetter,
XToolbarSetter,
} from './render-props-setter';
import { ImageSetter } from './image-setter';
import { RouterSetter } from './router-setter';
import { BoolSetter } from './bool-setter'; import { BoolSetter } from './bool-setter';
import { ChoiceSetter } from './choice-setter';
import { NumberSetter, SliderSetter } from './number-setter';
import { TextAreaSetter, TextSetter } from './text-setter';
export const INTERNAL_SETTERS: FormItemCreateOptionsType[] = [ const setters: IFormItemCreateOptions[] = [
{
name: 'actionListSetter',
component: ActionListSetter,
},
{
name: 'jsxSetter',
component: JsxSetter,
},
{
name: 'modelSetter',
component: ModelSetter,
disableVariableSetter: true,
validate(value: string) {
if (value.split('.').length < 2) {
return '请输入合法的模型变量路径,如:modelName.variableName';
}
},
},
{
name: 'eventSetter',
alias: 'actionSetter',
component: EventSetter,
disableVariableSetter: true,
},
{ {
name: 'boolSetter', name: 'boolSetter',
component: BoolSetter, component: BoolSetter,
}, },
{ {
name: 'expressionSetter', name: 'choiceSetter',
alias: 'expSetter', component: ChoiceSetter,
},
{
name: 'expSetter',
alias: ['expressionSetter'],
component: ExpressionSetter, component: ExpressionSetter,
disableVariableSetter: true,
validate: expressionValueValidate,
}, },
{ {
name: 'jsonSetter', name: 'numberSetter',
component: ExpressionSetter, component: NumberSetter,
disableVariableSetter: true,
validate: jsonValueValidate,
},
{
name: 'dateSetter',
component: DateSetter,
},
{
name: 'enumSetter',
component: EnumSetter,
},
{
name: 'dateRangeSetter',
component: DateRangeSetter,
},
{
name: 'timeSetter',
component: TimeSetter,
},
{
name: 'timeRangeSetter',
component: TimeRangeSetter,
}, },
{ {
name: 'textSetter', name: 'textSetter',
component: TextSetter, component: TextSetter,
}, },
{ name: 'textAreaSetter', component: TextAreaSetter }, { name: 'textAreaSetter', component: TextAreaSetter },
{
name: 'numberSetter',
component: NumberSetter,
},
{ {
name: 'sliderSetter', name: 'sliderSetter',
component: SliderSetter, component: SliderSetter,
}, },
{ {
name: 'pickerSetter', name: 'actionListSetter',
alias: 'selectSetter', component: ActionListSetter,
component: PickerSetter,
}, },
{ {
name: 'choiceSetter', name: 'columnSetter',
component: ChoiceSetter, component: ColumnSetter,
}, },
{ {
name: 'iconSetter', name: 'cssSetter',
component: IconSetter, component: CssSetter,
},
{
name: 'dateSetter',
component: DateSetter,
},
{
name: 'dateRangeSetter',
component: DateRangeSetter,
},
{
name: 'timeSetter',
component: DateRangeSetter,
},
{
name: 'timeRangeSetter',
component: TimeRangeSetter,
},
{
name: 'enumSetter',
component: EnumSetter,
},
{
name: 'eventSetter',
alias: ['actionSetter', 'functionSetter', 'callbackSetter'],
component: EventSetter,
},
{
name: 'expressionSetter',
alias: ['expSetter'],
component: ExpressionSetter,
},
{
name: 'jsonSetter',
component: JSONSetter,
},
{
name: 'jsxSetter',
component: JsxSetter,
},
{
name: 'listSetter',
component: ListSetter as any,
},
{
name: 'modelSetter',
component: ModelSetter,
}, },
{ {
name: 'optionSetter', name: 'optionSetter',
component: OptionSetter, component: OptionSetter,
}, },
// {
// name: 'jsonSetter',
// alias: 'styleSetter',
// component: JSONSetter,
// },
{ {
name: 'listSetter', name: 'pickerSetter',
component: ListSetter as any, component: PickerSetter,
}, },
// {
// name: 'stateSetter',
// component: StateSetter,
// disableVariableSetter: true,
// },
{ {
name: 'cssSetter', name: 'renderPropsSetter',
component: CssSetter, component: RenderSetter,
type: 'subForm', },
defaultCollapsed: false, {
name: 'tableCellSetter',
component: TableCellSetter,
},
{
name: 'tableExpandableSetter',
component: TableExpandableSetter,
},
{
name: 'routerSetter',
component: RouterSetter,
}, },
{ {
name: 'displaySetter', name: 'displaySetter',
@@ -177,40 +166,6 @@ export const INTERNAL_SETTERS: FormItemCreateOptionsType[] = [
}, },
{ name: 'bgSetter', component: BgSetter }, { name: 'bgSetter', component: BgSetter },
{ name: 'borderSetter', component: BorderSetter }, { name: 'borderSetter', component: BorderSetter },
{
name: 'columnSetter',
component: ColumnSetter,
},
{
name: 'listenerSetter',
component: ListenerSetter,
},
{
name: 'ruleSetter',
component: RuleSetter,
},
{
name: 'renderPropsSetter',
component: RenderSetter,
},
{
name: 'tableCellSetter',
component: TableCellSetter,
},
{
name: 'tableExpandableSetter',
component: TableExpandableSetter,
},
{
name: 'xtoolbarSetter',
component: XToolbarSetter,
},
{
name: 'imageSetter',
component: ImageSetter,
},
{
name: 'routerSetter',
component: RouterSetter,
},
]; ];
setters.forEach(register);
@@ -1,11 +1,10 @@
import React from 'react'; import React from 'react';
import { SingleMonacoEditor } from '@music163/tango-ui';
import { Box } from 'coral-system'; import { Box } from 'coral-system';
import { FormItemComponentProps } from '../form-item'; import { SingleMonacoEditor } from '@music163/tango-ui';
import { FormItemComponentProps } from '@music163/tango-setting-form';
/** /**
* 使 ExpressionSetter * JSON Setter
* @deprecated
*/ */
export function JSONSetter({ value, onChange }: FormItemComponentProps) { export function JSONSetter({ value, onChange }: FormItemComponentProps) {
return ( return (
@@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { isPlainString } from '@music163/tango-helpers'; import { isPlainString } from '@music163/tango-helpers';
import { ActionSelect } from '@music163/tango-ui'; import { ActionSelect } from '@music163/tango-ui';
import { FormItemComponentProps } from '../form-item'; import { FormItemComponentProps } from '@music163/tango-setting-form';
const options = [ const options = [
{ label: '取消自定义', value: '' }, { label: '取消自定义', value: '' },
@@ -1,10 +1,9 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { css, Box } from 'coral-system';
import { Form, Input, Button, Popover } from 'antd'; import { Form, Input, Button, Popover } from 'antd';
import { DeleteOutlined, EditOutlined, HolderOutlined } from '@ant-design/icons'; import { DeleteOutlined, EditOutlined, HolderOutlined } from '@ant-design/icons';
import { Box } from 'coral-system'; import { FormItemComponentProps } from '@music163/tango-setting-form';
import { FormItemComponentProps } from '../form-item'; import { DragBox } from '../components';
import { css } from 'styled-components';
import { DragBox } from '../components/index';
function copyWithoutUndefined(source: Record<string, any>) { function copyWithoutUndefined(source: Record<string, any>) {
const ret = {}; const ret = {};
@@ -254,7 +253,7 @@ function NewOptionForm({ fields = [], initialValues = {}, onSubmit }: NewOptionF
); );
} }
type ListSetterProps = FormItemComponentProps<any[]> & { interface ListSetterProps extends FormItemComponentProps<any[]> {
listItemFormFields: ListSetterItemProps['formFields']; listItemFormFields: ListSetterItemProps['formFields'];
newItemDefaultValues?: AddListItemButtonProps['formInitialValues']; newItemDefaultValues?: AddListItemButtonProps['formInitialValues'];
getNewItemDefaultValues?: AddListItemButtonProps['getFormInitialValues']; getNewItemDefaultValues?: AddListItemButtonProps['getFormInitialValues'];
@@ -2,10 +2,11 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { Box } from 'coral-system'; import { Box } from 'coral-system';
import { Input, Tooltip } from 'antd'; import { Input, Tooltip } from 'antd';
import { getValue, isFunction } from '@music163/tango-helpers'; import { getValue, isFunction } from '@music163/tango-helpers';
import { useFormVariable } from '../context'; import { FormItemComponentProps } from '@music163/tango-setting-form';
import { FormItemComponentProps } from '../form-item';
import { EditableVariableTreeModal } from '../components';
import { MenuOutlined } from '@ant-design/icons'; import { MenuOutlined } from '@ant-design/icons';
import { useWorkspace, useWorkspaceData } from '@music163/tango-context';
import { EditableVariableTreeModal } from '../components';
import { useSandboxQuery } from '../context';
function object2treeData( function object2treeData(
val: any, val: any,
@@ -53,7 +54,15 @@ function traverseTreeData(val: any, callback: (val: any) => void) {
export function ModelSetter({ value, onChange }: FormItemComponentProps) { export function ModelSetter({ value, onChange }: FormItemComponentProps) {
const [inputValue, setInputValue] = useState(value); const [inputValue, setInputValue] = useState(value);
const { onAction, modelVariables, evaluateContext = {} } = useFormVariable(); const { modelVariables } = useWorkspaceData();
const evaluateContext = useSandboxQuery().window || {};
const workspace = useWorkspace();
const onAction = useCallback(
(action: string, args: unknown[]) => {
workspace[action]?.(...args);
},
[workspace],
);
const definedVariables = useMemo(() => { const definedVariables = useMemo(() => {
const map = new Map(); const map = new Map();
traverseTreeData(modelVariables, (node) => { traverseTreeData(modelVariables, (node) => {
@@ -114,7 +123,6 @@ export function ModelSetter({ value, onChange }: FormItemComponentProps) {
onBlur={onInputBlur} onBlur={onInputBlur}
suffix={ suffix={
<EditableVariableTreeModal <EditableVariableTreeModal
showPreviewAlert={false}
title="同步到的变量" title="同步到的变量"
trigger={ trigger={
<Tooltip title="从模型列表选择" placement="topRight"> <Tooltip title="从模型列表选择" placement="topRight">
@@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { InputNumber, InputNumberProps, Slider } from 'antd'; import { InputNumber, InputNumberProps, Slider } from 'antd';
import { FormItemComponentProps } from '../form-item'; import { FormItemComponentProps } from '@music163/tango-setting-form';
const style = { const style = {
width: '100%', width: '100%',
@@ -1,6 +1,6 @@
import { uuid } from '@music163/tango-helpers'; import { uuid } from '@music163/tango-helpers';
import React from 'react'; import React from 'react';
import { FormItemComponentProps } from '../form-item'; import { FormItemComponentProps } from '@music163/tango-setting-form';
import { ListSetter, NewOptionFormFieldType } from './list-setter'; import { ListSetter, NewOptionFormFieldType } from './list-setter';
const optionFormFields: NewOptionFormFieldType[] = [ const optionFormFields: NewOptionFormFieldType[] = [
@@ -1,6 +1,6 @@
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import { Select } from 'antd'; import { Select } from 'antd';
import { FormItemComponentProps } from '../form-item'; import { FormItemComponentProps } from '@music163/tango-setting-form';
import { ListStore, OptionType } from '@music163/tango-helpers'; import { ListStore, OptionType } from '@music163/tango-helpers';
const style = { const style = {
@@ -1,18 +1,18 @@
import React, { useCallback, useMemo } from 'react'; import React, { useCallback, useMemo } from 'react';
import { ActionSelect } from '@music163/tango-ui'; import { ActionSelect } from '@music163/tango-ui';
import { FormItemComponentProps } from '../form-item'; import { FormItemComponentProps } from '@music163/tango-setting-form';
type RenderOptionType = { interface IRenderOption {
label: string; label: string;
value: string; value: string;
render?: string; render?: string;
relatedImports?: string[]; relatedImports?: string[];
}; }
export interface RenderSetterProps { export interface RenderSetterProps {
text?: string; text?: string;
options?: RenderOptionType[]; options?: IRenderOption[];
fallbackOption?: RenderOptionType; fallbackOption?: IRenderOption;
} }
/** /**
@@ -97,16 +97,6 @@ const tableExpandableOptions: RenderSetterProps['options'] = [
{ label: '取消可展开行', value: '' }, { label: '取消可展开行', value: '' },
]; ];
const xtoolbarOptions: RenderSetterProps['options'] = [
{
label: '配置左侧区域',
value: 'Box',
render: getRender('<Box display="flex"></Box>'),
relatedImports: ['Box'],
},
{ label: '取消左侧区域', value: undefined },
];
export function TableCellSetter(props: FormItemComponentProps) { export function TableCellSetter(props: FormItemComponentProps) {
return <RenderSetter options={tableCellOptions} {...props} />; return <RenderSetter options={tableCellOptions} {...props} />;
} }
@@ -114,7 +104,3 @@ export function TableCellSetter(props: FormItemComponentProps) {
export function TableExpandableSetter(props: FormItemComponentProps) { export function TableExpandableSetter(props: FormItemComponentProps) {
return <RenderSetter options={tableExpandableOptions} text="配置表格可展开行" {...props} />; return <RenderSetter options={tableExpandableOptions} text="配置表格可展开行" {...props} />;
} }
export function XToolbarSetter(props: FormItemComponentProps) {
return <RenderSetter options={xtoolbarOptions} text="配置工具栏左侧区域" {...props} />;
}
@@ -2,8 +2,8 @@ import React, { useState } from 'react';
import { Box } from 'coral-system'; import { Box } from 'coral-system';
import { Input, Radio } from 'antd'; import { Input, Radio } from 'antd';
import { isValidUrl } from '@music163/tango-helpers'; import { isValidUrl } from '@music163/tango-helpers';
import { useFormVariable } from '../context'; import { FormItemComponentProps } from '@music163/tango-setting-form';
import { FormItemComponentProps } from '../form-item'; import { useWorkspaceData } from '@music163/tango-context';
import { PickerSetter } from './picker-setter'; import { PickerSetter } from './picker-setter';
export function RouterSetter(props: FormItemComponentProps) { export function RouterSetter(props: FormItemComponentProps) {
@@ -11,7 +11,7 @@ export function RouterSetter(props: FormItemComponentProps) {
return isValidUrl(props.value) ? 'input' : 'select'; return isValidUrl(props.value) ? 'input' : 'select';
}); });
const [input, setInput] = useState<string>(props.value); const [input, setInput] = useState<string>(props.value);
const { routeOptions } = useFormVariable(); const { routeOptions } = useWorkspaceData();
return ( return (
<Box> <Box>
<Box mb="m"> <Box mb="m">
@@ -5,9 +5,10 @@ import { SketchPicker } from 'react-color';
import Color from 'color'; import Color from 'color';
import { BgColorsOutlined, FileImageOutlined } from '@ant-design/icons'; import { BgColorsOutlined, FileImageOutlined } from '@ant-design/icons';
import { SingleMonacoEditor, IconFont } from '@music163/tango-ui'; import { SingleMonacoEditor, IconFont } from '@music163/tango-ui';
import { FormItemComponentProps } from '../form-item'; import { FormItemComponentProps } from '@music163/tango-setting-form';
import { ImageSetter } from './image-setter';
import { ChoiceSetter } from './choice-setter'; import { ChoiceSetter } from './choice-setter';
import { TextSetter } from './text-setter';
// import { ImageSetter } from './image-setter';
function getRawCssValue(value: string) { function getRawCssValue(value: string) {
if (value && value.startsWith('{css`')) { if (value && value.startsWith('{css`')) {
@@ -175,8 +176,8 @@ export function ColorSetter(props: FormItemComponentProps<string>) {
const colorPanel = ( const colorPanel = (
<SketchPicker <SketchPicker
color={color} color={color}
onChangeComplete={(color: any) => { onChangeComplete={(newColor: any) => {
const nextValue = color.rgb.a === 1 ? color.hex : rgbaObjectToString(color.rgb); const nextValue = newColor.rgb.a === 1 ? newColor.hex : rgbaObjectToString(newColor.rgb);
if (typeof onChange === 'function') { if (typeof onChange === 'function') {
onChange(nextValue); onChange(nextValue);
} }
@@ -238,7 +239,7 @@ export function BgSetter({ value, onChange }: FormItemComponentProps<string>) {
<Box mt="m"> <Box mt="m">
{mode === 'color' && <ColorSetter value={value} onChange={onChange} />} {mode === 'color' && <ColorSetter value={value} onChange={onChange} />}
{mode === 'image' && ( {mode === 'image' && (
<ImageSetter <TextSetter
value={getImageUrl(value)} value={getImageUrl(value)}
onChange={(imgUrl) => { onChange={(imgUrl) => {
onChange(imgUrl ? `url(${imgUrl})` : undefined); onChange(imgUrl ? `url(${imgUrl})` : undefined);
@@ -353,19 +354,19 @@ export function BorderSetter({ value, onChange }: FormItemComponentProps<string>
const [width, setWidth] = useState<number>(Number(getPxValue(valueParts[0])) || 0); const [width, setWidth] = useState<number>(Number(getPxValue(valueParts[0])) || 0);
const [color, setColor] = useState<string>(valueParts[2] || '#000'); const [color, setColor] = useState<string>(valueParts[2] || '#000');
const handleChange = (key: string, value: any) => { const handleChange = (key: string, val: any) => {
let ret: any[]; let ret: any[];
switch (key) { switch (key) {
case 'style': { case 'style': {
ret = [width, value, color]; ret = [width, val, color];
break; break;
} }
case 'width': { case 'width': {
ret = [`${value}px`, style, color]; ret = [`${val}px`, style, color];
break; break;
} }
case 'color': { case 'color': {
ret = [width, style, value]; ret = [width, style, val];
break; break;
} }
default: default:
@@ -1,13 +1,13 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Input, InputProps } from 'antd'; import { Input, InputProps } from 'antd';
import { FormItemComponentProps } from '../form-item'; import { FormItemComponentProps } from '@music163/tango-setting-form';
const noop = () => {}; const noop = () => {};
type TextSetterProps = { interface TextSetterProps extends Omit<InputProps, 'onChange' | 'value'> {
value?: string; value?: string;
onChange?: (value: string) => void; onChange?: (value: string) => void;
} & Omit<InputProps, 'onChange' | 'value'>; }
export function TextSetter({ export function TextSetter({
value: valueProp, value: valueProp,

Some files were not shown because too many files have changed in this diff Show More