Compare commits

..

73 Commits

Author SHA1 Message Date
Wells 73aded3672 chore(release): publish
- @music163/tango-context@1.0.0-alpha.5
 - @music163/tango-core@1.0.0-alpha.5
 - @music163/tango-designer@1.0.0-alpha.6
 - @music163/tango-helpers@1.0.0-alpha.1
 - @music163/tango-sandbox@1.0.0-alpha.5
 - @music163/tango-setting-form@1.0.0-alpha.5
 - @music163/tango-ui@1.0.0-alpha.3
2023-12-29 17:41:58 +08:00
Wells 8c07821d93 fix: refactor VariableTree & Workspace (#83)
* fix: refactor VariableTree

* fix: update

* fix: update

* fix: update

* fix: update

* fix: refactor IVariableTreeNode

* fix: update

* fix: refactor workspace

---------

Co-authored-by: wwsun <ww.sww@outlook.com>
2023-12-29 17:30:11 +08:00
wwsun 765ea07cd5 fix: check if store value is valid 2023-12-26 15:43:20 +08:00
wwsun a27c241d9c chore(release): publish
- @music163/tango-context@1.0.0-alpha.4
 - @music163/tango-core@1.0.0-alpha.4
 - @music163/tango-designer@1.0.0-alpha.5
 - @music163/tango-sandbox@1.0.0-alpha.4
 - @music163/tango-setting-form@1.0.0-alpha.4
2023-12-26 14:38:54 +08:00
wwsun 4d6b67eecd fix: list overflow in expSetter 2023-12-26 14:36:13 +08:00
wwsun 6fc889a161 chore(release): publish
- @music163/tango-context@1.0.0-alpha.3
 - @music163/tango-core@1.0.0-alpha.3
 - @music163/tango-designer@1.0.0-alpha.4
 - @music163/tango-sandbox@1.0.0-alpha.3
 - @music163/tango-setting-form@1.0.0-alpha.3
2023-12-25 11:08:29 +08:00
wwsun c07b554360 chore: up 2023-12-25 11:06:30 +08:00
wwsun 149002dddd fix: export componentEntry module 2023-12-25 11:00:04 +08:00
wwsun 45b0d856c1 chore(release): publish
- @music163/tango-context@1.0.0-alpha.2
 - @music163/tango-core@1.0.0-alpha.2
 - @music163/tango-designer@1.0.0-alpha.3
 - @music163/tango-helpers@1.0.0-alpha.0
 - @music163/tango-sandbox@1.0.0-alpha.2
 - @music163/tango-setting-form@1.0.0-alpha.2
 - @music163/tango-ui@1.0.0-alpha.2
2023-12-19 15:17:19 +08:00
Wells 6fc0498901 feat: support local components (#78)
* docs: update

* feat: parse local components

* fix: update

* refactor: module exports to exportList

* fix: update workspace

---------

Co-authored-by: wwsun <ww.sww@outlook.com>
2023-12-19 15:07:44 +08:00
wwsun f2919c1aa0 chore(release): publish
- @music163/tango-designer@1.0.0-alpha.2
 - tango-spec@1.0.2
2023-12-13 15:59:20 +08:00
wwsun 5dd5ab62a8 chore: up 2023-12-13 15:57:54 +08:00
Wells 5e7e285452 fix: update variable tree with help messages and styling & add tangoConfig schema
* fix: update variable tree with help messages and styling

* chore: add spec package

* fix: update schema

* chore: disabled umi https2

---------

Co-authored-by: wwsun <ww.sww@outlook.com>
2023-12-13 14:03:07 +08:00
wwsun b3a458a914 chore(release): publish
- @music163/tango-context@1.0.0-alpha.1
 - @music163/tango-core@1.0.0-alpha.1
 - @music163/tango-designer@1.0.0-alpha.1
 - @music163/tango-sandbox@1.0.0-alpha.1
 - @music163/tango-setting-form@1.0.0-alpha.1
 - @music163/tango-ui@1.0.0-alpha.1
2023-12-12 11:12:09 +08:00
Wells 1acfa6864b fix: render form item without setter & enhance tools (#76)
* fix: render form item without setter

* fix: update toggle-button style

* fix: update tools

* fix: remove useless code

---------

Co-authored-by: wwsun <ww.sww@outlook.com>
2023-12-12 11:10:17 +08:00
Wells baf071f130 chore: use umi4 (#75)
Co-authored-by: wwsun <ww.sww@outlook.com>
2023-12-08 15:46:51 +08:00
Wells 6b1d55a11f RC (#74)
* chore: up

* chore: update release scripts

* chore: up

* chore: update lerna config

* chore(release): publish

 - @music163/tango-context@1.0.0-alpha.0
 - @music163/tango-core@1.0.0-alpha.0
 - @music163/tango-designer@1.0.0-alpha.0
 - @music163/tango-sandbox@1.0.0-alpha.0
 - @music163/tango-setting-form@1.0.0-alpha.0
 - @music163/tango-ui@1.0.0-alpha.0

---------

Co-authored-by: wwsun <ww.sww@outlook.com>
2023-12-07 12:40:18 +08:00
Wells a8357f45b8 fix: add showSearch and showGroups to SettingForm & update internal icons (#73)
* fix: add internal setters & fix ui

* feat: add showSearch and showGroups to SettingForm

* fix: update icons

* fix: use antd form

---------

Co-authored-by: wwsun <ww.sww@outlook.com>
2023-12-07 10:31:19 +08:00
Wells 9db6358588 fix: disable search keymap of InputCode (#72)
Co-authored-by: wwsun <ww.sww@outlook.com>
2023-12-06 15:33:47 +08:00
wwsun 5bc2fb2d5a chore: up 2023-12-06 11:33:05 +08:00
wwsun 05a0f8f490 chore(release): publish
- @music163/tango-context@0.2.12
 - @music163/tango-core@0.2.11
 - @music163/tango-designer@0.5.14
 - @music163/tango-sandbox@0.1.16
 - @music163/tango-setting-form@0.2.16
2023-12-04 18:43:14 +08:00
wwsun c884712331 fix: update FileType 2023-12-04 18:38:38 +08:00
wwsun e35ff792a0 chore(release): publish
- @music163/tango-context@0.2.11
 - @music163/tango-core@0.2.10
 - @music163/tango-designer@0.5.13
 - @music163/tango-sandbox@0.1.15
 - @music163/tango-setting-form@0.2.15
2023-12-04 12:54:30 +08:00
Wells 138cbe9b20 fix: add filesFormatter to sandbox & fix update props with imports (#71)
* fix: update types

* fix: add filesFormatter to sandbox

* fix: update node attributes with imports

---------

Co-authored-by: wwsun <ww.sww@outlook.com>
2023-12-04 12:52:10 +08:00
wwsun 979aa34259 chore(release): publish
- @music163/tango-context@0.2.10
 - @music163/tango-core@0.2.9
 - @music163/tango-designer@0.5.12
 - @music163/tango-sandbox@0.1.14
 - @music163/tango-setting-form@0.2.14
2023-11-30 15:46:50 +08:00
wwsun 76a5a2c659 fix: fix render components tree 2023-11-30 15:44:42 +08:00
wwsun 53ebd89c9a chore(release): publish
- @music163/tango-context@0.2.9
 - @music163/tango-core@0.2.8
 - @music163/tango-designer@0.5.11
 - @music163/tango-sandbox@0.1.13
 - @music163/tango-setting-form@0.2.13
2023-11-29 14:50:40 +08:00
wwsun 3980810502 docs: update milestone 2023-11-29 14:47:40 +08:00
Wells accd826376 fix: workspace onFilesChange (#70)
* fix: workspace onFilesChange

* fix: remove appJson parse logic

* style: remove comments

---------

Co-authored-by: wwsun <ww.sww@outlook.com>
2023-11-29 14:43:06 +08:00
wwsun 5ca1118f88 chore(release): publish
- @music163/tango-designer@0.5.10
2023-11-28 14:55:20 +08:00
wwsun 14cd4c54a1 fix: update default theme 2023-11-28 14:51:10 +08:00
wwsun 7f46137c56 chore(release): publish
- @music163/tango-designer@0.5.9
 - @music163/tango-setting-form@0.2.12
 - @music163/tango-ui@0.1.12
2023-11-28 13:51:35 +08:00
wwsun 1db52948db fix: update type 2023-11-28 13:49:44 +08:00
wwsun 2550411c1c fix: update theme 2023-11-28 13:45:17 +08:00
wwsun 640859d36e chore(release): publish
- @music163/tango-context@0.2.8
 - @music163/tango-core@0.2.7
 - @music163/tango-designer@0.5.8
 - @music163/tango-sandbox@0.1.12
 - @music163/tango-setting-form@0.2.11
2023-11-27 14:02:30 +08:00
wwsun d91763379c fix: parse tango variables in view file 2023-11-27 13:55:53 +08:00
wwsun 588861f649 fix: event setter 2023-11-27 13:46:45 +08:00
wwsun 91b53da38e fix: list service functions 2023-11-24 09:50:14 +08:00
wwsun 9b0ada94a0 chore(release): publish
- @music163/tango-designer@0.5.7
2023-11-23 18:50:57 +08:00
Wells f604996e56 fix: update varTreeNode style (#69)
Co-authored-by: wwsun <ww.sww@outlook.com>
2023-11-23 18:46:56 +08:00
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
145 changed files with 11594 additions and 9275 deletions
+2 -1
View File
@@ -2,11 +2,12 @@
"extends": ["eslint-config-ali/typescript/react", "prettier"],
"ignorePatterns": ["**/dist/**/*", "**/lib/**/*", "**/node_modules/**/*", "scripts/**/*"],
"rules": {
"@typescript-eslint/consistent-type-definitions": "off",
"@typescript-eslint/dot-notation": "off",
"@typescript-eslint/no-unused-vars": "warn",
"import/no-cycle": "off",
"no-nested-ternary": "off",
"no-useless-return":"off",
"no-useless-return": "off",
"no-param-reassign": "off",
"prefer-destructuring": "off"
}
+2
View File
@@ -0,0 +1,2 @@
registry=https://registry.npmmirror.com/
+21 -13
View File
@@ -5,7 +5,7 @@
<h1 align="center">Tango LowCode Builder</h1>
<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)
[![NPM version](https://img.shields.io/npm/v/@music163/tango-designer.svg?style=flat-square)](http://npmjs.org/package/@music163/tango-designer)
@@ -45,18 +45,18 @@ gantt
%% (`excludes` accepts specific dates in YYYY-MM-DD format, days of the week ("sunday") or "weekends", but not the word "weekdays".)
section Builder Release
Alpha version :active, des1, 2023-08-10, 2023-08-30
Beta version : des2, 2023-09-01, 2023-09-30
1.0 RC : des3, after des2, 40d
1.0 version : des4, after des3, 21d
Alpha version :active, m1, 2023-08-10, 2023-08-30
Beta version : m2, 2023-09-01, 2023-09-30
1.0 RC : m3, 2023-10-01, 2023-12-15
1.0 version : m4, after m3, 10d
```
## 💻 Development
### Recommended Development Environment
### Environment
- Node.js >= 16.0.0
- Yarn >= 1.22.0
- Node `>= 18`
- Yarn `>= 1.22 && < 2`
### Development Quick Start
@@ -79,6 +79,7 @@ yarn start
Join NetEase Tango Community to share your ideas, suggestions, or questions and connect with other users and contributors.
- 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
@@ -89,16 +90,23 @@ Please read the [github contribution guide](https://docs.github.com/en/get-start
- Commit and push your code
- 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
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.
## 📣 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
This project is licensed under the terms of the [MIT license](./LICENSE)
+20 -12
View File
@@ -5,7 +5,7 @@
<h1 align="center">Tango 低代码设计器</h1>
<div align="center">
一个源码驱动的低代码设计器,来自网易云音乐技术团队
一个源码驱动的低代码设计器框架
[![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)
@@ -45,18 +45,18 @@ gantt
%% (`excludes` accepts specific dates in YYYY-MM-DD format, days of the week ("sunday") or "weekends", but not the word "weekdays".)
section Builder Release
Alpha version :active, des1, 2023-08-10, 2023-08-30
Beta version : des2, 2023-09-01, 2023-09-30
1.0 RC : des3, after des2, 40d
1.0 version : des4, after des3, 21d
Alpha version :active, m1, 2023-08-10, 2023-08-30
Beta version : m2, 2023-09-01, 2023-09-30
1.0 RC : m3, 2023-10-01, 2023-12-15
1.0 version : m4, after m3, 10d
```
## 💻 开发
### 推荐开发环境
- Node.js >= 16.0.0
- Yarn >= 1.22.0
- Node `>= 18`
- Yarn `>= 1.22 && < 2`
### 本地开发调试方法
@@ -79,6 +79,7 @@ yarn start
参与 NetEase Tango 的社区,以分享您的想法、建议或问题,并与其他用户和贡献者建立联系。
- 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
- 发起 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 提供了强大的基于浏览器的代码构建与执行能力。
## 📣 产品推广
![](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)
-2
View File
@@ -1,2 +0,0 @@
PORT=6006
HOST=local.netease.com
+9
View File
@@ -0,0 +1,9 @@
/node_modules
/.env.local
/.umirc.local.ts
/config/config.local.ts
/src/.umi
/src/.umi-production
/src/.umi-test
/dist
.swc
@@ -1,33 +1,17 @@
import path from 'path';
import { defineConfig } from 'umi';
const resolvePackageIndex = (relativeEntry: string) =>
path.resolve(__dirname, '../../../packages/', relativeEntry);
path.resolve(__dirname, '../../packages/', relativeEntry);
export default {
export default defineConfig({
routes: [
{
exact: true,
path: '/',
component: 'index',
name: '首页',
},
{ path: '/', component: 'index' },
{ path: '/docs', component: 'docs' },
],
devServer: {
host: 'local.netease.com',
port: 8008,
https: {
key: path.resolve(__dirname, 'local.netease.com-key.pem'),
cert: path.resolve(__dirname, 'local.netease.com.pem'),
},
headers: { 'Origin-Agent-Cluster': '?0' },
},
targets: {
chrome: 79,
firefox: false,
safari: false,
edge: false,
ios: false,
},
npmClient: 'yarn',
mfsu: false,
codeSplitting: false,
alias: {
'@music163/tango-helpers': resolvePackageIndex('helpers/src/index.ts'),
'@music163/tango-core': resolvePackageIndex('core/src/index.ts'),
@@ -44,6 +28,21 @@ export default {
moment: 'moment',
antd: 'antd',
},
headScripts: [
'https://cdn.jsdelivr.net/npm/react@18.2.0/umd/react.development.js',
'https://cdn.jsdelivr.net/npm/react-dom@18.2.0/umd/react-dom.development.js',
'https://cdn.jsdelivr.net/npm/react-is@18.2.0/umd/react-is.production.min.js',
'https://cdn.jsdelivr.net/npm/moment/min/moment-with-locales.js',
'https://cdn.jsdelivr.net/npm/styled-components@5.3.11/dist/styled-components.js',
'https://cdn.jsdelivr.net/npm/antd@4.24.13/dist/antd-with-locales.min.js',
'https://cdn.jsdelivr.net/npm/prettier@2.6.0/standalone.js',
'https://cdn.jsdelivr.net/npm/prettier@2.6.0/parser-babel.js',
],
https: {
key: path.resolve(__dirname, 'local.netease.com-key.pem'),
cert: path.resolve(__dirname, 'local.netease.com.pem'),
http2: false,
},
chainWebpack: (config: any) => {
// @see https://github.com/graphql/graphql-js/issues/1272#issuecomment-393903706
config.module
@@ -61,4 +60,5 @@ export default {
config.module.rule('js').include.add(resolvePackageIndex('ui/src'));
return config;
},
};
monorepoRedirect: {},
});
-5
View File
@@ -1,5 +0,0 @@
# `playground`
搭建平台演示示例
## 说明
+11 -12
View File
@@ -2,24 +2,23 @@
"name": "playground",
"version": "0.0.0",
"private": true,
"description": "playground of tango-apps",
"license": "MIT",
"author": "wwsun <ww.sun@outlook.com>",
"files": [
"lib"
],
"author": "Wells <ww.sun@outlook.com>",
"scripts": {
"build": "COMPRESS=none umi build",
"start": "umi dev"
"dev": "HOST=local.netease.com PORT=8001 umi dev",
"build": "umi build",
"postinstall": "umi setup",
"setup": "umi setup",
"start": "npm run dev"
},
"dependencies": {
"@ant-design/icons": "^4.8.0",
"@music163/tango-designer": "*",
"@music163/tango-helpers": "*",
"antd": "^4.24.2",
"coral-system": "^1.0.5"
"coral-system": "^1.0.5",
"umi": "^4.0.89"
},
"devDependencies": {
"umi": "^3.5.24"
"@types/react": "^18.0.33",
"@types/react-dom": "^18.0.11",
"typescript": "^5.0.3"
}
}
+10
View File
@@ -0,0 +1,10 @@
import type { IApi } from 'umi';
export default (api: IApi) => {
api.addMiddlewares(() => {
return (req, res, next) => {
res.setHeader('Origin-Agent-Cluster', '?0');
next();
};
});
};
Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

@@ -6,29 +6,30 @@ import { registerSetter } from '@music163/tango-designer';
import type { ComponentPrototypeType } from '@music163/tango-helpers';
import { FooSetter } from '../components';
export * from './mock-files';
export const bootHelperVariables = [
{
key: '$helpers',
title: '工具函数',
children: [
{
title: 'setStoreValue',
title: '设置变量值',
key: '() => tango.setStoreValue("variableName", "variableValue")',
type: 'function',
help: '设置状态值,tango.setStoreValue("variableName", "variableValue")',
},
{
title: 'getStoreValue',
title: '获取变量值',
key: '() => tango.getStoreValue("variableName")',
type: 'function',
help: '获取状态值',
},
{ 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: '打开弹窗', key: '() => tango.openModal("")', type: 'function' },
{ title: '关闭弹窗', key: '() => tango.closeModal("")', type: 'function' },
{ title: '切换路由', key: '() => tango.navigateTo("/")', type: 'function' },
{
title: 'copyToClipboard',
title: '拷贝到剪贴板',
key: '() => tango.copyToClipboard("hello")',
type: 'function',
},
@@ -17,37 +17,43 @@ const tangoConfigJson = {
version: '17.0.2',
library: 'React',
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': {
version: '17.0.2',
library: 'ReactDOM',
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': {
version: '16.13.1',
library: 'ReactIs',
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': {
version: '5.3.5',
library: 'styled',
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: {
version: '2.29.4',
library: 'moment',
type: 'dependency',
resources: ['https://unpkg.com/moment@{{version}}/moment.js'],
resources: ['https://cdn.jsdelivr.net/npm/moment@{{version}}/moment.js'],
},
'@music163/tango-boot': {
version: '0.2.1',
library: 'TangoBoot',
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'],
description: '云音乐低代码运行时框架',
},
@@ -56,27 +62,30 @@ const tangoConfigJson = {
library: 'TangoAntd',
type: 'baseDependency',
resources: [
'https://unpkg.com/@music163/antd@{{version}}/dist/index.js',
'https://unpkg.com/antd@4.24.13/dist/antd.css',
'https://cdn.jsdelivr.net/npm/@music163/antd@{{version}}/dist/index.js',
'https://cdn.jsdelivr.net/npm/antd@4.24.13/dist/antd.css',
],
description: '云音乐低代码中后台应用基础物料',
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',
'https://unpkg.com/antd@4.24.13/dist/antd.css',
'https://cdn.jsdelivr.net/npm/antd@4.24.13/dist/antd.css',
],
},
},
};
const appJson: any = {
pages: [
{
path: '/',
name: '首页',
},
],
};
const helperCode = `
export function registerComponentPrototype(proto) {
if (!proto) return;
if (!window.localTangoComponentPrototypes) {
window.localTangoComponentPrototypes = {};
}
if (proto.name) {
window.localTangoComponentPrototypes[proto.name] = proto;
}
}
`;
const routesCode = `
import Index from "./pages/list";
@@ -145,6 +154,9 @@ import {
Input,
FormilyForm,
} from "@music163/antd";
import { Space } from '@music163/antd';
import { LocalButton } from '../components';
class App extends React.Component {
render() {
return (
@@ -152,16 +164,63 @@ class App extends React.Component {
<Section title="Section Title">
</Section>
<Section>
<Space>
<LocalButton />
<Button>button</Button>
<Input />
</Space>
</Section>
</Page>
);
}
}
export default definePage(App);
`;
const componentsButtonCode = `
import React from 'react';
import { registerComponentPrototype } from '../utils';
export default function MyButton(props) {
return <button {...props}>my button</button>
}
registerComponentPrototype({
name: 'LocalButton',
title: 'Local Button',
exportType: 'namedExport',
package: '/src/components',
props: [
{ name: 'background', title: '背景色', setter: 'colorSetter' },
],
});
`;
const componentsInputCode = `
import React from 'react';
import { registerComponentPrototype } from '../utils';
export default function MyInput(props) {
return <input {...props} />;
}
registerComponentPrototype({
name: 'LocalInput',
title: 'Local Input',
exportType: 'namedExport',
package: '/src/components',
props: [
{ name: 'color', title: '文本色', setter: 'colorSetter' },
],
});
`;
const componentsEntryCode = `
export { default as LocalButton } from './button';
export { default as LocalInput } from './input';
`;
const storeApp = `
import { defineStore } from '@music163/tango-boot';
@@ -170,6 +229,35 @@ export default defineStore({
title: 'Page Title',
array: [1, 2, 3],
test: function() {
console.log('test');
console.log('test');
console.log('test');
console.log('test');
console.log('test');
console.log('test');
console.log('test');
console.log('test');
console.log('test');
console.log('test');
console.log('test');
console.log('test');
console.log('test');
console.log('test');
console.log('test');
console.log('test');
console.log('test');
console.log('test');
console.log('test');
console.log('test');
console.log('test');
console.log('test');
console.log('test');
console.log('test');
console.log('test');
console.log('test');
}
}, 'app');
`;
@@ -178,6 +266,13 @@ import { defineServices } from '@music163/tango-boot';
import './sub';
export default defineServices({
longLongLongLongLongLongLongLongGet: {
url: 'https://nei.hz.netease.com/api/apimock-v2/cc974ffbaa7a85c77f30e4ce67deb67f/api/getUserProfile',
formatter: res => res.data,
headers: {
'Content-Type': 'application/json',
}
},
get: {
url: 'https://nei.hz.netease.com/api/apimock-v2/cc974ffbaa7a85c77f30e4ce67deb67f/api/getUserProfile',
formatter: res => res.data,
@@ -233,20 +328,22 @@ p {
export const sampleFiles = [
{ filename: '/package.json', code: JSON.stringify(packageJson) },
{ filename: '/appJson.json', code: JSON.stringify(appJson) },
{ filename: '/tango.config.json', code: JSON.stringify(tangoConfigJson) },
{ filename: '/README.md', code: '# readme' },
{ filename: '/src/index.less', code: lessCode },
{ filename: '/src/style.css', code: cssCode },
{ filename: '/src/index.js', code: entryCode },
{ filename: '/src/pages/list.js', code: viewHomeCode },
{ filename: '/src/components/button.js', code: componentsButtonCode },
{ filename: '/src/components/input.js', code: componentsInputCode },
{ filename: '/src/components/index.js', code: componentsEntryCode },
{ filename: '/src/routes.js', code: routesCode },
{ filename: '/src/stores/index.js', code: storeIndexCode },
{ filename: '/src/stores/app.js', code: storeApp },
{ filename: '/src/stores/counter.js', code: storeCounter },
{ filename: '/src/services/index.js', code: serviceCode },
{ filename: '/src/services/sub.js', code: subServiceCode },
{ filename: '/src/utils/index.js', code: `export function foo() {}` },
{ filename: '/src/utils/index.js', code: helperCode },
];
export const genDefaultPage = (index: number) => ({
@@ -2,4 +2,5 @@
@import '~antd/dist/antd.less'; // 引入官方提供的 less 样式入口文件
@primary-color: #2f54eb;
@border-radius-base: 2px;
@border-radius-base: 2px;
+11
View File
@@ -0,0 +1,11 @@
import React from 'react';
import { Outlet } from 'umi';
import './index.less';
export default function Layout() {
return (
<div>
<Outlet />
</div>
);
}
+9
View File
@@ -0,0 +1,9 @@
const DocsPage = () => {
return (
<div>
<p>This is umi docs.</p>
</div>
);
};
export default DocsPage;
-21
View File
@@ -1,21 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Tango Playground</title>
</head>
<body>
<div id="root"></div>
<script src="https://unpkg.com/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://unpkg.com/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://unpkg.com/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://unpkg.com/prettier@2.6.0/standalone.js"></script>
<script src="https://unpkg.com/prettier@2.6.0/parser-babel.js"></script>
<script src="https://unpkg.com/antd@4.24.13/dist/antd-with-locales.min.js"></script>
</body>
</html>
+8 -4
View File
@@ -12,11 +12,10 @@ import {
CodeEditor,
Sandbox,
DndQuery,
themeLight,
} from '@music163/tango-designer';
import { createEngine, Workspace } from '@music163/tango-core';
import { Logo, ProjectDetail, bootHelperVariables } from './share';
import { sampleFiles } from '../mock/project';
import './index.less';
import { Logo, ProjectDetail, bootHelperVariables, sampleFiles } from '../helpers';
import {
ApiOutlined,
AppstoreAddOutlined,
@@ -51,6 +50,7 @@ export default function App() {
const [menuData, setMenuData] = useState(false);
return (
<Designer
theme={themeLight}
engine={engine}
config={{
customActionVariables: bootHelperVariables,
@@ -65,6 +65,7 @@ export default function App() {
<Box px="l">
<Toolbar>
<Toolbar.Item key="routeSwitch" placement="left" />
<Toolbar.Item key="history" placement="left" />
<Toolbar.Item key="preview" placement="left" />
<Toolbar.Item key="modeSwitch" placement="right" />
<Toolbar.Item key="togglePanel" placement="right" />
@@ -79,7 +80,6 @@ export default function App() {
}
>
<Sidebar>
<Sidebar.Item key="outline" label="结构" icon={<BuildOutlined />} />
<Sidebar.Item
key="components"
label="组件"
@@ -89,6 +89,7 @@ export default function App() {
loading: menuLoading,
}}
/>
<Sidebar.Item key="outline" label="结构" icon={<BuildOutlined />} />
<Sidebar.Item
key="variables"
label="变量"
@@ -119,6 +120,9 @@ export default function App() {
workspace.setComponentPrototypes(sandboxWindow.TangoAntd.prototypes);
}
}
if (sandboxWindow.localTangoComponentPrototypes) {
workspace.setComponentPrototypes(sandboxWindow.localTangoComponentPrototypes);
}
setMenuLoading(false);
}
}}
+1 -1
View File
@@ -1,3 +1,3 @@
{
"extends": "../../tsconfig.json"
"extends": "./src/.umi/tsconfig.json"
}
+1
View File
@@ -0,0 +1 @@
import 'umi/typings';
+2 -2
View File
@@ -16,8 +16,8 @@
"dependencies": {
"@music163/tango-setting-form": "*",
"@music163/tango-ui": "*",
"mobx": "6.9.0",
"mobx-react-lite": "4.0.2"
"mobx": "6.12.0",
"mobx-react-lite": "4.0.5"
},
"devDependencies": {
"@storybook/addon-actions": "^6.5.14",
+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>
);
}
+14 -131
View File
@@ -1,7 +1,6 @@
import React, { useState } from 'react';
import { EditableVariableTree, FormModel, SettingForm } from '@music163/tango-setting-form';
import { ComponentPrototypeType, getValue } from '@music163/tango-helpers';
import { createServices } from '@music163/request';
import React from 'react';
import { FormModel, SettingForm } from '@music163/tango-setting-form';
import { ComponentPrototypeType } from '@music163/tango-helpers';
import { Box } from 'coral-system';
import { JsonView } from '@music163/tango-ui';
import { toJS } from 'mobx';
@@ -9,51 +8,9 @@ import { observer } from 'mobx-react-lite';
import { Card } from 'antd';
export default {
title: 'SettingForm/ Setters',
title: 'SettingForm',
};
const modelVariables = [
{
title: 'stores',
key: 'stores',
selectable: false,
children: [
{
title: 'app',
key: 'stores.app',
selectable: false,
children: [
{ title: 'app.title', key: 'stores.app.title', raw: '"hello"' },
{ title: 'app.age', key: 'stores.app.age', raw: '20' },
{ title: 'app.detail', key: 'stores.app.detail', raw: '{ foo: "foo" }' },
{ title: 'app.list', key: 'stores.app.list', type: 'function', raw: '() => {}' },
],
},
{
title: 'user',
key: 'stores.user',
selectable: false,
children: [
{ title: 'name', key: 'stores.user.name' },
{ title: 'age', key: 'stores.user.age' },
],
},
],
},
{
title: 'services',
key: 'services',
selectable: false,
children: [
{
title: 'listUsers',
key: 'services.listUsers',
type: 'function',
},
],
},
];
const prototype: ComponentPrototypeType = {
name: 'Test',
exportType: 'namedExport',
@@ -257,47 +214,6 @@ const prototype: ComponentPrototypeType = {
],
};
const deerService = createServices(
{
listMy: {
url: '/my/upload/list',
},
listFav: {
url: '/my/star/list',
},
listPub: {
url: '/list',
},
},
{
baseURL: 'https://febase-openapi.fn.netease.com/deer/api/deer/pic',
withCredentials: false, // 解决跨域时必须非*问题
headers: {
'Febase-Auth': 'dskPVkIRnQ2dEn1DbyxURmUj4rl4BsCh53xFAsJnvVs=',
},
},
);
const context = {
stores: {
app: {
title: 'Sample App',
age: 20,
detail: {
foo: 'foo',
bar: 'bar',
biz: {
x: 'xxx',
},
},
newKey: 'xxx',
},
foo: {
test: 'test string',
},
},
};
/**
* 表单值预览
*/
@@ -310,9 +226,7 @@ export function Basic() {
const model = new FormModel(
{
router: 'www.163.com',
expression: {
foo: 'foo',
},
expression: `{ foo: 'foo' }`,
object: {
name: 'Alice',
},
@@ -324,19 +238,7 @@ export function Basic() {
return (
<Box display="flex">
<SettingForm
model={model}
remoteServices={{
ImageService: deerService as any,
}}
prototype={prototype}
evaluateContext={{
tango: context,
__UNSAFE_TANGO_CURRENT_PAGE_RENDER_RUNTIME__: { routeData: { params: {}, query: {} } },
}}
modelVariables={modelVariables}
expressionVariables={modelVariables}
/>
<SettingForm model={model} prototype={prototype} />
<Box position="relative">
<Card title="表单状态预览" style={{ position: 'sticky', top: 0 }}>
<FormValuePreview model={model} />
@@ -346,6 +248,14 @@ export function Basic() {
);
}
export function Lite() {
return (
<Box width={320} border="solid">
<SettingForm showSearch={false} showGroups={false} prototype={prototype} />
</Box>
);
}
export function NoExpressionSwitch() {
const model = new FormModel({});
return (
@@ -354,30 +264,3 @@ export function NoExpressionSwitch() {
</Box>
);
}
export function Binding() {
const [data, setData] = useState<any>({});
return (
<Box>
<Box as="code" bg="highlight" color="white" fontSize="24px">
selected: {data?.key}
</Box>
<EditableVariableTree
dataSource={modelVariables as any}
getPreviewValue={(node) => {
if (!node || !node.key) {
return;
}
if (node.type === 'function') {
return node.raw;
}
const keyPath = node.key.replaceAll('?', '');
return getValue(context, keyPath);
}}
onSelect={setData}
onAddVariable={console.log}
onSave={console.log}
/>
</Box>
);
}
@@ -28,6 +28,41 @@ export function Basic() {
);
}
const code = `
function foo() {
console.log("test");
console.log("test");
console.log("test");
console.log("test");
console.log("test");
console.log("test");
console.log("test");
console.log("test");
console.log("test");
console.log("test");
console.log("test");
console.log("test");
console.log("test");
console.log("test");
console.log("test");
console.log("test");
console.log("test");
console.log("test");
console.log("test");
console.log("test");
console.log("test");
console.log("test");
console.log("test");
console.log("test");
console.log("test");
console.log("test");
}
`;
export function Readonly() {
return <InputCode shape="inset" readOnly value={code} />;
}
export function Inset() {
return <InputCode shape="inset" />;
}
+57 -12
View File
@@ -1,6 +1,6 @@
import React from 'react';
import { ToggleButton, IconFont } from '@music163/tango-ui';
import { Box, Flex } from 'coral-system';
import { ToggleButton, UndoOutlined, RedoOutlined } from '@music163/tango-ui';
import { Box, Group, Flex } from 'coral-system';
export default {
title: 'UI/ToggleButton',
@@ -9,28 +9,73 @@ export default {
export function Basic() {
return (
<Box>
<Flex gap="l" bg="#efefef" p="l">
<Box display="flex" columnGap="l" p="l">
<ToggleButton>
<IconFont type="icon-undo" />
<UndoOutlined />
</ToggleButton>
<ToggleButton selected>
<IconFont type="icon-redo" />
<RedoOutlined />
</ToggleButton>
<ToggleButton disabled>
<IconFont type="icon-redo" />
<RedoOutlined />
</ToggleButton>
</Box>
<Box display="flex" columnGap="l" p="l">
<ToggleButton type="primary">
<UndoOutlined />
</ToggleButton>
<ToggleButton type="primary" selected>
<RedoOutlined />
</ToggleButton>
<ToggleButton type="primary" disabled>
<RedoOutlined />
</ToggleButton>
</Box>
</Box>
);
}
export function DarkMode() {
return (
<Box>
<Flex gap="l" bg="#222" p="l">
<ToggleButton shape="ghost">
<UndoOutlined />
</ToggleButton>
<ToggleButton shape="ghost" selected>
<RedoOutlined />
</ToggleButton>
<ToggleButton shape="ghost" disabled>
<UndoOutlined />
</ToggleButton>
</Flex>
<Flex gap="l" bg="#222" p="l">
<ToggleButton shape="ghost">
<IconFont type="icon-undo" />
<ToggleButton shape="text">
<UndoOutlined />
</ToggleButton>
<ToggleButton shape="ghost" selected>
<IconFont type="icon-redo" />
<ToggleButton shape="text" selected>
<RedoOutlined />
</ToggleButton>
<ToggleButton shape="ghost" disabled>
<IconFont type="icon-undo" />
<ToggleButton shape="text" disabled>
<RedoOutlined />
</ToggleButton>
</Flex>
</Box>
);
}
export function ButtonGroup() {
return (
<Group attached>
<ToggleButton>
<UndoOutlined />
</ToggleButton>
<ToggleButton selected>
<RedoOutlined />
</ToggleButton>
<ToggleButton disabled>
<RedoOutlined />
</ToggleButton>
</Group>
);
}
@@ -0,0 +1,72 @@
import React from 'react';
import { VariableTree } from '@music163/tango-designer';
import { Box } from 'coral-system';
export default {
title: 'Designer/VariableTree',
};
const data = [
{
title: 'Stores',
key: 'stores',
selectable: false,
children: [
{
title: 'user',
key: 'stores.user',
children: [
{
title: 'name',
key: 'stores.user.name',
raw: '"Tom"',
},
{
title: 'age',
key: 'stores.user.age',
raw: '18',
},
],
},
{
title: 'book',
key: 'stores.book',
children: [
{
title: 'name',
key: 'stores.book.name',
raw: '"JavaScript 高级程序设计"',
},
{
title: 'price',
key: 'stores.book.price',
raw: '99.99',
},
],
},
],
},
{
title: 'Services',
key: 'services',
selectable: false,
children: [
{
title: 'add',
key: 'services.add',
},
{
title: 'multiply',
key: 'services.multiply',
},
],
},
];
export function Basic() {
return (
<Box width={600} border="solid">
<VariableTree dataSource={data} />
</Box>
);
}
+14 -1
View File
@@ -137,7 +137,7 @@ const config = {
],
},
{
title: '更多',
title: '相关资源',
items: [
{
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: {
alt: 'NetEase Cloud Music',
+2 -2
View File
@@ -24,7 +24,7 @@ const timelines = [
{
icon: null,
title: '1.0 RC',
date: '2023.10.30',
date: '2023.11.30',
description: translate({
id: 'homepage.timeline.rc',
message: '核心 API 基本稳定,不再发生 BR,发布 1.0 RC 版本。',
@@ -33,7 +33,7 @@ const timelines = [
{
icon: null,
title: '1.0',
date: 'Before 2023.12.25',
date: 'Before 2023.12.29',
description: translate({
id: 'homepage.timeline.stable',
message: 'API 完全稳定,提供良好的社区支持,可用于生产环境。',
+1 -1
View File
@@ -154,7 +154,7 @@ const config = {
// testLocationInResults: false,
// 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
// testPathIgnorePatterns: [
-1
View File
@@ -1,6 +1,5 @@
{
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
"useWorkspaces": true,
"version": "independent",
"packages": ["packages/*"],
"npmClient": "yarn",
+34 -33
View File
@@ -6,8 +6,8 @@
"apps/*"
],
"scripts": {
"start": "yarn workspace playground start",
"start:story": "yarn workspace storybook storybook",
"start": "yarn workspace playground dev",
"start:docs": "yarn workspace storybook storybook",
"start:site": "yarn workspace website start",
"deploy:site": "yarn workspace website deploy",
"build": "lerna run build",
@@ -18,52 +18,53 @@
"test:watch": "jest --watch",
"eslint": "eslint packages/**/src/*.{ts,tsx} --cache",
"publish": "lerna publish",
"ver": "lerna version --no-private --conventional-commits",
"release": "yarn eslint && yarn build && yarn run ver && lerna publish from-git",
"ver": "lerna version --no-private",
"release": "yarn eslint && yarn build && lerna version --no-private --conventional-commits && lerna publish from-git",
"release:npm": "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",
"up": "yarn upgrade-interactive --latest",
"prepare": "husky install"
},
"devDependencies": {
"@babel/preset-env": "^7.19.3",
"@babel/preset-react": "^7.18.6",
"@babel/preset-typescript": "^7.18.6",
"@commitlint/cli": "^17.5.0",
"@commitlint/config-conventional": "^17.3.0",
"@types/jest": "^29.2.4",
"@types/lodash-es": "^4.17.8",
"@types/lodash.get": "^4.4.7",
"@types/lodash.isequal": "^4.5.6",
"@types/lodash.set": "^4.3.7",
"@types/react": "^18.2.7",
"@types/react-dom": "^18.2.4",
"@types/styled-components": "^5.1.25",
"@typescript-eslint/eslint-plugin": "^6.4.1",
"@typescript-eslint/parser": "^6.4.1",
"conventional-changelog-cli": "^2.2.2",
"@babel/preset-env": "^7.23.6",
"@babel/preset-react": "^7.23.3",
"@babel/preset-typescript": "^7.23.3",
"@commitlint/cli": "^18.4.3",
"@commitlint/config-conventional": "^18.4.3",
"@types/jest": "^29.5.7",
"@types/lodash-es": "^4.17.10",
"@types/lodash.get": "^4.4.8",
"@types/lodash.isequal": "^4.5.7",
"@types/lodash.set": "^4.3.8",
"@types/react": "^18.2.34",
"@types/react-dom": "^18.2.14",
"@types/styled-components": "^5.1.29",
"@typescript-eslint/eslint-plugin": "^6.13.2",
"@typescript-eslint/parser": "^6.13.2",
"conventional-changelog-cli": "^4.1.0",
"copyfiles": "^2.4.1",
"eslint": "^8.40.0",
"eslint": "^8.55.0",
"eslint-config-ali": "^14.0.2",
"eslint-config-prettier": "^9.0.0",
"eslint-import-resolver-typescript": "^3.6.0",
"eslint-plugin-import": "^2.28.1",
"eslint-config-prettier": "^9.1.0",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-import": "^2.29.0",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"http-server": "^14.1.0",
"husky": "^8.0.3",
"jest": "^29.1.1",
"jest-environment-jsdom": "^29.1.1",
"lerna": "^6.6.1",
"lint-staged": "^14.0.1",
"prettier": "^2.8.7",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"lerna": "^8.0.0",
"lint-staged": "^15.2.0",
"prettier": "^3.1.0",
"react": "^17.0.0",
"react-dom": "^17.0.0",
"styled-components": "^5.3.6",
"typedoc": "^0.25.0",
"typescript": "^5.0.4"
"typedoc": "^0.25.4",
"typescript": "^5.3.3"
},
"resolutions": {
"@yarnpkg/parsers": "3.0.0-rc.48.1"
"engines": {
"node": ">=18"
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
+56
View File
@@ -3,6 +3,62 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.0.0-alpha.5](https://github.com/netease/tango/compare/@music163/tango-context@1.0.0-alpha.4...@music163/tango-context@1.0.0-alpha.5) (2023-12-29)
### Bug Fixes
- refactor VariableTree & Workspace ([#83](https://github.com/netease/tango/issues/83)) ([8c07821](https://github.com/netease/tango/commit/8c07821d93cea4dfc43f81ca948b845176821184))
# [1.0.0-alpha.4](https://github.com/netease/tango/compare/@music163/tango-context@1.0.0-alpha.3...@music163/tango-context@1.0.0-alpha.4) (2023-12-26)
**Note:** Version bump only for package @music163/tango-context
# [1.0.0-alpha.3](https://github.com/netease/tango/compare/@music163/tango-context@1.0.0-alpha.2...@music163/tango-context@1.0.0-alpha.3) (2023-12-25)
**Note:** Version bump only for package @music163/tango-context
# [1.0.0-alpha.1](https://github.com/netease/tango/compare/@music163/tango-context@0.2.12...@music163/tango-context@1.0.0-alpha.1) (2023-12-12)
**Note:** Version bump only for package @music163/tango-context
## [0.2.12](https://github.com/netease/tango/compare/@music163/tango-context@0.2.11...@music163/tango-context@0.2.12) (2023-12-04)
**Note:** Version bump only for package @music163/tango-context
## [0.2.11](https://github.com/netease/tango/compare/@music163/tango-context@0.2.10...@music163/tango-context@0.2.11) (2023-12-04)
**Note:** Version bump only for package @music163/tango-context
## [0.2.10](https://github.com/netease/tango/compare/@music163/tango-context@0.2.9...@music163/tango-context@0.2.10) (2023-11-30)
**Note:** Version bump only for package @music163/tango-context
## [0.2.9](https://github.com/netease/tango/compare/@music163/tango-context@0.2.8...@music163/tango-context@0.2.9) (2023-11-29)
**Note:** Version bump only for package @music163/tango-context
## [0.2.8](https://github.com/netease/tango/compare/@music163/tango-context@0.2.7...@music163/tango-context@0.2.8) (2023-11-27)
**Note:** Version bump only for package @music163/tango-context
## [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
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@music163/tango-context",
"version": "0.2.3",
"version": "1.0.0-alpha.5",
"description": "react context for tango-apps",
"keywords": [
"react",
@@ -31,9 +31,9 @@
"react": ">= 16.8"
},
"dependencies": {
"@music163/tango-core": "^0.2.2",
"@music163/tango-helpers": "^0.1.4",
"mobx-react-lite": "4.0.2"
"@music163/tango-core": "^1.0.0-alpha.5",
"@music163/tango-helpers": "^1.0.0-alpha.1",
"mobx-react-lite": "4.0.5"
},
"publishConfig": {
"access": "public",
+19 -22
View File
@@ -1,12 +1,5 @@
import type { Engine } from '@music163/tango-core';
import { createContext } from '@music163/tango-helpers';
interface CustomVariableData {
key: string;
title: string;
children?: CustomVariableData[];
[key: string]: any;
}
import { IVariableTreeNode, createContext } from '@music163/tango-helpers';
export interface ITangoEngineContext {
/**
@@ -16,9 +9,9 @@ export interface ITangoEngineContext {
/**
* 自定义配置数据
*/
config: {
customActionVariables: CustomVariableData[];
customExpressionVariables: CustomVariableData[];
config?: {
customActionVariables?: IVariableTreeNode[];
customExpressionVariables?: IVariableTreeNode[];
};
}
@@ -39,23 +32,25 @@ export const useDesigner = () => {
export const useWorkspaceData = () => {
const ctx = useTangoEngine();
const workspace = useWorkspace();
const modelVariables: any[] = []; // 绑定变量列表
const storeActionVariables: any[] = []; // 模型中的所有 actions
const storeVariables: any[] = []; // 模型中的所有变量
const serviceVariables: any[] = []; // 服务中的所有变量
const modelVariables: IVariableTreeNode[] = []; // 绑定变量列表
const storeActionVariables: IVariableTreeNode[] = []; // 模型中的所有 actions
const storeVariables: IVariableTreeNode[] = []; // 模型中的所有变量
const serviceVariables: IVariableTreeNode[] = []; // 服务中的所有变量
Object.values(workspace.storeModules).forEach((file) => {
const prefix = `stores.${file.name}`;
const states = file.states.map((item) => ({
const states: IVariableTreeNode[] = file.states.map((item) => ({
title: item.name,
key: `${prefix}.${item.name}`,
raw: item.code,
showRemoveButton: true,
}));
const actions = file.actions.map((item) => ({
const actions: IVariableTreeNode[] = file.actions.map((item) => ({
title: item.name,
key: `${prefix}.${item.name}`,
type: 'function',
raw: item.code,
showRemoveButton: true,
}));
modelVariables.push({
@@ -63,6 +58,7 @@ export const useWorkspaceData = () => {
key: prefix,
selectable: false,
children: states,
showAddButton: true,
});
storeActionVariables.push({
@@ -77,8 +73,7 @@ export const useWorkspaceData = () => {
key: prefix,
selectable: false,
children: [...states, ...actions],
showAddChildIcon: true,
showRemoveIcon: true,
showAddButton: true,
});
});
@@ -88,10 +83,12 @@ export const useWorkspaceData = () => {
title: file.name,
key: prefix,
selectable: false,
showAddButton: true,
children: Object.keys(file.serviceFunctions || {}).map((key) => ({
title: key,
key: [prefix, key].join('.'),
type: 'function',
showRemoveButton: true,
})),
});
});
@@ -102,7 +99,7 @@ export const useWorkspaceData = () => {
value: item.path,
}));
let actionVariables: CustomVariableData[] = [
let actionVariables: IVariableTreeNode[] = [
buildVariableOptions('数据模型', '$stores', storeActionVariables),
buildVariableOptions('服务函数', '$services', serviceVariables),
];
@@ -111,7 +108,7 @@ export const useWorkspaceData = () => {
actionVariables = actionVariables.concat(ctx.config?.customActionVariables);
}
let expressionVariables: CustomVariableData[] = [
let expressionVariables: IVariableTreeNode[] = [
buildVariableOptions('数据模型', '$stores', storeVariables),
buildVariableOptions('服务函数', '$services', serviceVariables),
];
@@ -129,7 +126,7 @@ export const useWorkspaceData = () => {
};
};
function buildVariableOptions(title: string, key: string, children: any[]) {
function buildVariableOptions(title: string, key: string, children: IVariableTreeNode[]) {
return {
key,
title,
+87
View File
@@ -3,6 +3,93 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.0.0-alpha.5](https://github.com/netease/tango/compare/@music163/tango-core@1.0.0-alpha.4...@music163/tango-core@1.0.0-alpha.5) (2023-12-29)
### Bug Fixes
- check if store value is valid ([765ea07](https://github.com/netease/tango/commit/765ea07cd5be6528e3b326e4ecb163647774d9e5))
- refactor VariableTree & Workspace ([#83](https://github.com/netease/tango/issues/83)) ([8c07821](https://github.com/netease/tango/commit/8c07821d93cea4dfc43f81ca948b845176821184))
# [1.0.0-alpha.4](https://github.com/netease/tango/compare/@music163/tango-core@1.0.0-alpha.3...@music163/tango-core@1.0.0-alpha.4) (2023-12-26)
### Bug Fixes
- list overflow in expSetter ([4d6b67e](https://github.com/netease/tango/commit/4d6b67eecd02b31f01d1bc896c6eeb83f6b34b35))
# [1.0.0-alpha.3](https://github.com/netease/tango/compare/@music163/tango-core@1.0.0-alpha.2...@music163/tango-core@1.0.0-alpha.3) (2023-12-25)
### Bug Fixes
- export componentEntry module ([149002d](https://github.com/netease/tango/commit/149002ddddaee859333469f65c054d811bde2f7f))
# [1.0.0-alpha.1](https://github.com/netease/tango/compare/@music163/tango-core@0.2.11...@music163/tango-core@1.0.0-alpha.1) (2023-12-12)
**Note:** Version bump only for package @music163/tango-core
## [0.2.11](https://github.com/netease/tango/compare/@music163/tango-core@0.2.10...@music163/tango-core@0.2.11) (2023-12-04)
### Bug Fixes
- update FileType ([c884712](https://github.com/netease/tango/commit/c8847123312bc47ea06575f4e714dfd596893e39))
## [0.2.10](https://github.com/netease/tango/compare/@music163/tango-core@0.2.9...@music163/tango-core@0.2.10) (2023-12-04)
### Bug Fixes
- add filesFormatter to sandbox & fix update props with imports ([#71](https://github.com/netease/tango/issues/71)) ([138cbe9](https://github.com/netease/tango/commit/138cbe9b203b370aff42c1ae8086d69edacf35e9))
## [0.2.9](https://github.com/netease/tango/compare/@music163/tango-core@0.2.8...@music163/tango-core@0.2.9) (2023-11-30)
### Bug Fixes
- fix render components tree ([76a5a2c](https://github.com/netease/tango/commit/76a5a2c65920bc42b019cd1f32a3cacd0d888638))
## [0.2.8](https://github.com/netease/tango/compare/@music163/tango-core@0.2.7...@music163/tango-core@0.2.8) (2023-11-29)
### Bug Fixes
- workspace onFilesChange ([#70](https://github.com/netease/tango/issues/70)) ([accd826](https://github.com/netease/tango/commit/accd8263764c811ea8175a9cd341fc6fa6c75967))
## [0.2.7](https://github.com/netease/tango/compare/@music163/tango-core@0.2.6...@music163/tango-core@0.2.7) (2023-11-27)
### Bug Fixes
- list service functions ([91b53da](https://github.com/netease/tango/commit/91b53da38e362ae320457429037dc347adf90bd3))
- parse tango variables in view file ([d917633](https://github.com/netease/tango/commit/d91763379c4ccf9d826b717aa80a07abdaf2e3a2))
## [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
+9 -9
View File
@@ -1,6 +1,6 @@
{
"name": "@music163/tango-core",
"version": "0.2.2",
"version": "1.0.0-alpha.5",
"description": "tango core",
"author": "wwsun <ww.sun@outlook.com>",
"homepage": "",
@@ -24,14 +24,14 @@
"prepublishOnly": "yarn build"
},
"dependencies": {
"@babel/generator": "^7.22.15",
"@babel/parser": "^7.22.15",
"@babel/traverse": "^7.22.15",
"@babel/types": "^7.22.15",
"@music163/tango-helpers": "^0.1.4",
"@types/babel__generator": "^7.6.4",
"@types/babel__traverse": "^7.18.3",
"mobx": "6.9.0",
"@babel/generator": "^7.23.5",
"@babel/parser": "^7.23.5",
"@babel/traverse": "^7.23.5",
"@babel/types": "^7.23.5",
"@music163/tango-helpers": "^1.0.0-alpha.1",
"@types/babel__generator": "^7.6.7",
"@types/babel__traverse": "^7.20.4",
"mobx": "6.12.0",
"path-browserify": "^1.0.1"
},
"peerDependencies": {
+6
View File
@@ -10,6 +10,10 @@ interface ICreateEngineOptions {
* 默认的模拟器模式
*/
defaultSimulatorMode?: SimulatorNameType;
/**
* 默认激活的侧边栏
*/
defaultActiveSidebarPanel?: string;
}
/**
@@ -20,12 +24,14 @@ interface ICreateEngineOptions {
export function createEngine({
workspace,
defaultSimulatorMode = 'desktop',
defaultActiveSidebarPanel = '',
}: ICreateEngineOptions) {
const engine = new Engine({
workspace,
designer: new Designer({
workspace,
simulator: defaultSimulatorMode,
activeSidebarPanel: defaultActiveSidebarPanel,
}),
});
+27
View File
@@ -1,3 +1,5 @@
import { isValidExpressionCode } from './ast';
const defineServiceHandlerNames = ['defineServices', 'createServices'];
const sfHandlerPattern = new RegExp(`^(${defineServiceHandlerNames.join('|')})$`);
@@ -20,3 +22,28 @@ const defineStoreHandlerName = 'defineStore';
export function isDefineStore(name: string) {
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);
}
+13 -8
View File
@@ -6,10 +6,10 @@ import * as t from '@babel/types';
import {
logger,
isValidObjectString,
isVariableString,
getVariableContent,
isPlainObject,
} from '@music163/tango-helpers';
import { isWrappedByExpressionContainer } from '../assert';
// @see https://babeljs.io/docs/en/babel-parser#pluginss
const babelParserConfig: ParserOptions = {
@@ -22,7 +22,6 @@ const babelParserConfig: ParserOptions = {
'classProperties',
'asyncGenerators',
'functionBind',
'functionSent',
'dynamicImport',
'optionalChaining',
],
@@ -44,6 +43,12 @@ export function isValidCode(code: string) {
/**
* 检测代码是否是合法的表达式代码
* 表达式是一组代码的集合,它返回一个值;每一个合法的表达式都能计算成某个值
* 只要你输入这段代码,可以形成一个值,就算是表达式
* @example x = 1; // 1
* @example 1=1
* @example 'hello'
* @example { foo: 'bar' }
* @param code
* @returns
*/
@@ -91,8 +96,8 @@ export function code2expression(code: string) {
try {
expNode = t.cloneNode(parseExpression(code, babelParserConfig), false, true);
} catch (err) {
console.error('invalid code', err);
expNode = t.identifier('undefined');
logger.error('invalid code', err);
// expNode = t.identifier('undefined');
}
return expNode;
}
@@ -103,7 +108,7 @@ export function code2expression(code: string) {
* @returns File
*/
export function expressionCode2ast(code: string) {
if (isVariableString(code)) {
if (isWrappedByExpressionContainer(code)) {
code = getVariableContent(code);
}
const node = code2expression(code);
@@ -130,7 +135,7 @@ export function value2node(
ret = t.numericLiteral(value);
break;
case 'string':
if (isVariableString(value)) {
if (isWrappedByExpressionContainer(value)) {
// 再检查是否是表达式容器,例如 {this.foo}, {1}
const innerString = getVariableContent(value);
ret = code2expression(innerString);
@@ -201,7 +206,7 @@ export function value2jsxAttributeValueNode(value: any) {
if (isValidObjectString(value)) {
// 先检查是否是对象字符串
ret = t.jsxExpressionContainer(code2expression(value));
} else if (isVariableString(value)) {
} else if (isWrappedByExpressionContainer(value)) {
// 再检查是否是表达式容器,例如 {this.foo}, {1}
const innerString = getVariableContent(value);
ret = t.jsxExpressionContainer(code2expression(innerString));
@@ -221,7 +226,7 @@ export function value2jsxChildrenValueNode(value: any) {
let ret: t.JSXElement | t.JSXFragment | t.JSXExpressionContainer | t.JSXSpreadChild | t.JSXText;
switch (typeof value) {
case 'string':
if (isVariableString(value)) {
if (isWrappedByExpressionContainer(value)) {
const innerString = getVariableContent(value);
ret = t.jsxExpressionContainer(code2expression(innerString));
} else {
+231 -23
View File
@@ -22,16 +22,16 @@ import {
code2expression,
object2node,
} from './parse';
import { isValidComponentName } from '../string';
import { isDefineService, isDefineStore } from '../assert';
import { getFullPath, isValidComponentName } from '../string';
import { isDefineService, isDefineStore, isTangoVariable } from '../assert';
import type {
IRouteData,
IStorePropertyData,
IClassPropertyNodeData,
ITangoViewNodeData,
IImportDeclarationPayload,
IServiceFunctionPayload,
InsertChildPositionType,
IImportSpecifierData,
IExportSpecifierData,
} from '../../types';
import { IdGenerator } from '../id-generator';
@@ -194,7 +194,7 @@ export function queryXFormItemFields(ast: t.File | t.JSXElement) {
* @param node
* @param id
*/
function isJSXElementById(node: t.JSXElement, jsxElementNodeId: string) {
export function isJSXElementById(node: t.JSXElement, jsxElementNodeId: string) {
let isTargetJSXElement = false;
visitJSXElementAttributes(node, (name, value) => {
if (name === SLOT.dnd && value === jsxElementNodeId) {
@@ -448,11 +448,12 @@ function getLastExportDeclarationIndex(node: t.Program) {
/**
* 生成新的 ImportDeclaration
* @deprecated 使用 makeImportDeclaration2 代替
* @param importedModule
* @param importedSourcePath
* @returns
*/
export function makeImportDeclaration(importedModule: IImportDeclarationPayload) {
export function makeImportDeclarationLegacy(importedModule: IImportDeclarationPayload) {
const specifierNodes =
importedModule.specifiers?.map((localName) =>
t.importSpecifier(t.identifier(localName), t.identifier(localName)),
@@ -467,6 +468,36 @@ export function makeImportDeclaration(importedModule: IImportDeclarationPayload)
return t.importDeclaration(newSpecifierNodes, t.stringLiteral(importedModule.sourcePath));
}
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);
let defaultSpecifier;
@@ -487,16 +518,33 @@ function getImportDeclarationData(node: t.ImportDeclaration): IImportDeclaration
/**
* 添加新的导入语句
* @deprecated 使用 addImportDeclaration2 代替
* @param ast
* @param importedModule
* @param importedSourcePath
* @returns
*/
export function addImportDeclaration(ast: t.File, importedModule: IImportDeclarationPayload) {
export function addImportDeclarationLegacy(ast: t.File, importedModule: IImportDeclarationPayload) {
traverse(ast, {
Program(path) {
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.stop();
},
@@ -506,17 +554,21 @@ export function addImportDeclaration(ast: t.File, importedModule: IImportDeclara
/**
* 更新已有的导入语句
* @deprecated 使用 updateImportDeclaration2 代替
* @param ast
* @param importedModule
* @param importedSourcePath
* @returns
*/
export function updateImportDeclaration(ast: t.File, importedModule: IImportDeclarationPayload) {
export function updateImportDeclarationLegacy(
ast: t.File,
importedModule: IImportDeclarationPayload,
) {
traverse(ast, {
ImportDeclaration(path) {
const currentSourcePath = node2value(path.node.source);
if (currentSourcePath === importedModule.sourcePath) {
const newImportDeclaration = makeImportDeclaration(importedModule);
const newImportDeclaration = makeImportDeclarationLegacy(importedModule);
path.replaceWith(newImportDeclaration);
path.stop(); // 只修改匹配到的第一条
}
@@ -525,6 +577,50 @@ export function updateImportDeclaration(ast: t.File, importedModule: IImportDecl
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
@@ -592,7 +688,6 @@ export function addRouteToRouteFile(ast: t.File, routePath: string, importFilePa
const newNode = t.objectExpression([
t.objectProperty(t.identifier('path'), t.stringLiteral(routePath)),
t.objectProperty(t.identifier('component'), t.identifier(component)),
// FIXME: tango/issues#144 https://g.hz.netease.com/tango/issues/-/issues/144
t.objectProperty(t.identifier('exact'), t.booleanLiteral(true)),
]);
path.node.elements.push(newNode);
@@ -902,7 +997,7 @@ export function deleteServiceConfigFromServiceFile(ast: t.File, serviceFunctionN
* @param payload
* @returns
*/
export function serviceConfig2Node(payload: IServiceFunctionPayload) {
export function serviceConfig2Node(payload: object) {
return object2node(payload, (value, key) => {
if (key === 'formatter' && value) {
return code2expression(value);
@@ -911,10 +1006,7 @@ export function serviceConfig2Node(payload: IServiceFunctionPayload) {
});
}
export function updateServiceConfigToServiceFile(
ast: t.File,
config: { [key: string]: IServiceFunctionPayload },
) {
export function updateServiceConfigToServiceFile(ast: t.File, config: object) {
traverse(ast, {
CallExpression(path) {
const calleeName = keyNode2value(path.node.callee) as string;
@@ -986,7 +1078,12 @@ export function traverseServiceFile(ast: t.File) {
const baseConfig: any = {
encryptFetch: false,
};
const imports: Record<string, IImportSpecifierData[]> = {};
traverse(ast, {
ImportDeclaration(path) {
const { source, specifiers } = parseImportDeclaration(path.node);
imports[source] = specifiers;
},
CallExpression(path) {
const calleeName = keyNode2value(path.node.callee) as string;
if (isDefineService(calleeName)) {
@@ -1003,6 +1100,7 @@ export function traverseServiceFile(ast: t.File) {
},
});
return {
imports,
services,
baseConfig,
};
@@ -1107,6 +1205,7 @@ export function cloneJSXElementWithoutTrackingData(node: t.JSXElement) {
}
export function traverseViewFile(ast: t.File, idGenerator: IdGenerator) {
const imports: Record<string, IImportSpecifierData[]> = {};
const importedModules: Dict<IImportDeclarationPayload | IImportDeclarationPayload[]> = {};
const nodes: Array<ITangoViewNodeData<t.JSXElement>> = [];
const cloneAst = t.cloneNode(ast, true, true);
@@ -1115,12 +1214,43 @@ export function traverseViewFile(ast: t.File, idGenerator: IdGenerator) {
traverse(ast, {
ImportDeclaration(path) {
const declarationData = getImportDeclarationData(path.node);
const exist = importedModules[declarationData.sourcePath];
if (!exist) {
importedModules[declarationData.sourcePath] = declarationData;
const { source, specifiers } = parseImportDeclaration(path.node);
if (imports[source]) {
// 存在重复的导入语句,合并导入符号列表
imports[source] = imports[source].concat(specifiers);
} 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, declarationData];
}
@@ -1128,15 +1258,23 @@ export function traverseViewFile(ast: t.File, idGenerator: IdGenerator) {
MemberExpression(path) {
const variable = node2value(path.node, false);
const parentNode = path.parentPath.node;
if (
/^tango\.(stores|services)\./.test(variable) &&
variable.split('.').length === 3 &&
!t.isMemberExpression(parentNode) &&
isTangoVariable(variable) &&
!variables.includes(variable)
) {
variables.push(variable);
}
},
OptionalMemberExpression(path) {
const variable = node2value(path.node, false);
if (isTangoVariable(variable) && !variables.includes(variable)) {
variables.push(variable);
}
},
JSXElement(path) {
const attributes = getJSXElementAttributes(path.node);
@@ -1183,7 +1321,77 @@ export function traverseViewFile(ast: t.File, idGenerator: IdGenerator) {
ast,
cleanAst,
nodes,
imports,
importedModules,
variables,
};
}
export function traverseComponentsEntryFile(ast: t.File, baseDir?: string) {
const exportMap: Record<string, IExportSpecifierData> = {};
traverse(ast, {
ExportNamedDeclaration(path) {
const node = path.node;
let source = node2value(node.source);
if (baseDir) {
// fix relative source path
source = getFullPath(baseDir, source);
}
node.specifiers.forEach((specifier) => {
if (t.isExportSpecifier(specifier)) {
const name = keyNode2value(specifier.exported) as string;
if (name) {
exportMap[name] = {
source,
exportedName: name,
};
}
}
});
},
});
return { ast, exportMap };
}
/**
* 解析导入语句
*/
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,
};
}
+6 -3
View File
@@ -1,5 +1,6 @@
import { getVariableContent, isVariableString } from '@music163/tango-helpers';
import { value2node, expression2code } from './ast';
import { getVariableContent } from '@music163/tango-helpers';
import { value2node, expression2code, isValidExpressionCode } from './ast';
import { isWrappedByExpressionContainer } from './assert';
/**
* 将 js value 转换为代码字符串
@@ -36,7 +37,9 @@ export function value2expressionCode(val: any) {
switch (typeof val) {
case 'string': {
if (isVariableString(val)) {
if (isValidExpressionCode(val)) {
ret = val;
} else if (isWrappedByExpressionContainer(val, false)) {
ret = getVariableContent(val);
} else if (isStringCode(val)) {
ret = val;
+5 -2
View File
@@ -1,5 +1,5 @@
import { Dict } from '@music163/tango-helpers';
import { IImportDeclarationPayload, IImportSpecifierData } from '../types';
import { IImportDeclarationPayload, IImportSpecifierSourceData } from '../types';
/**
* 导入列表解析为导入声明对象
@@ -7,7 +7,10 @@ import { IImportDeclarationPayload, IImportSpecifierData } from '../types';
* @param nameMap
* @returns
*/
export function namesToImportDeclarations(names: string[], nameMap: Dict<IImportSpecifierData>) {
export function namesToImportDeclarations(
names: string[],
nameMap: Dict<IImportSpecifierSourceData>,
) {
const map = {};
names.forEach((name) => {
const mod = nameMap[name];
+41 -3
View File
@@ -3,16 +3,51 @@ import {
ComponentPropType,
ComponentPrototypeType,
isNil,
isVariableString,
logger,
uuid,
} from '@music163/tango-helpers';
import { getRelativePath, isFilepath } from './string';
import type { IImportDeclarationPayload } from '../types';
import type { IImportDeclarationPayload, IImportSpecifierData } from '../types';
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
* @deprecated
*/
export function getImportDeclarationPayloadByPrototype(
prototype: ComponentPrototypeType,
@@ -35,6 +70,9 @@ export function getImportDeclarationPayloadByPrototype(
if (relativeFilepath && isFilepath(sourcePath)) {
sourcePath = getRelativePath(relativeFilepath, sourcePath);
}
if (sourcePath.endsWith('.js')) {
sourcePath = sourcePath.slice(0, -3);
}
return {
defaultSpecifier,
@@ -82,7 +120,7 @@ function getPropKeyValuePair(item: ComponentPropType, generateValue: (...args: a
break;
}
case 'string': {
if (!isVariableString(value)) {
if (!isWrappedByExpressionContainer(value)) {
// 不是变量字符串
value = `"${value}"`;
} else {
+23 -13
View File
@@ -33,7 +33,10 @@ export function inferFileType(filename: string): FileType {
return FileType.JsonViewModule;
}
// FIXME: 暂时写死,后续要支持多 services 文件解析
if (/\/(blocks|components)\/index\.js/.test(filename)) {
return FileType.ComponentsEntryModule;
}
if (/\/services\/.+\.js$/.test(filename)) {
return FileType.ServiceModule;
}
@@ -50,10 +53,6 @@ export function inferFileType(filename: string): FileType {
return FileType.StoreModule;
}
if (/\/blocks\/[\w-]+\/index\.js/.test(filename)) {
return FileType.BlockEntryModule;
}
if (/\.jsx?$/.test(filename)) {
return FileType.Module;
}
@@ -171,24 +170,35 @@ export function getBlockNameByFilename(filename: string) {
}
/**
* FIXME: 有问题,需要优化下
* 基于 from 文件的地址计算 to 文件的相对引用路径
* @param from
* @param to
* 合并两个路径
* @param root
* @param filename
* @returns
*/
export function getRelativePath(from: string, to: string) {
const fromFolder = path.dirname(from);
return path.relative(fromFolder, to);
export function getFullPath(root: string, filename: string) {
return path.join(root, filename);
}
/**
* 计算 targetFile 在 sourceFile 中的相对引用路径
* @param sourceFile
* @param targetFile
* @returns
*/
export function getRelativePath(sourceFile: string, targetFile: string) {
sourceFile = path.dirname(sourceFile);
return path.relative(sourceFile, targetFile);
}
/**
* 判断给定字符串是否是文件路径
* @example ./pages/index.js -- yes
* @example ../pages/index.js -- yes
* @example ../components -- yes
* @example /src/pages/index.js -- yes
* @example @music163/tango-designer -- no
* @param str
*/
export function isFilepath(str: string) {
return /^(\.\.?\/|\/).*\.[a-z]+$/.test(str);
return /^(\.\.?\/|\/).*(\.[a-z]+)?$/.test(str);
}
+5 -1
View File
@@ -1,4 +1,8 @@
export * from './models';
export * from './factory';
export * from './helpers';
export * from './types';
export * from './types';
export { default as generator } from '@babel/generator';
export { default as traverse } from '@babel/traverse';
export * from '@babel/parser';
@@ -0,0 +1,40 @@
import path from 'path';
import { action, computed, makeObservable, observable } from 'mobx';
import { TangoModule } from './module';
import { IWorkspace } from './interfaces';
import { IExportSpecifierData, IFileConfig } from '../types';
import { traverseComponentsEntryFile } from '../helpers';
/**
* 本地组件目录的入口文件,例如 '/components/index.js' 或 `/blocks/index.js`
*/
export class TangoComponentsEntryModule extends TangoModule {
exportList: Record<string, IExportSpecifierData>;
constructor(workspace: IWorkspace, props: IFileConfig) {
super(workspace, props, false);
this.update(props.code, false, false);
makeObservable(this, {
_code: observable,
_cleanCode: observable,
exportList: observable,
code: computed,
cleanCode: computed,
update: action,
});
}
_analysisAst() {
const baseDir = path.dirname(this.filename);
const { exportMap } = traverseComponentsEntryFile(this.ast, baseDir);
this.exportList = exportMap;
Object.keys(this.exportList).forEach((key) => {
this.workspace.componentPrototypes.set(key, {
name: key,
exportType: 'namedExport',
package: baseDir,
type: 'element',
});
});
}
}
+36 -18
View File
@@ -3,22 +3,26 @@ import { IWorkspace } from './interfaces';
export type SimulatorNameType = 'desktop' | 'phone';
type SimulatorType = {
export type DesignerViewType = 'design' | 'code';
interface ISimulatorType {
name: SimulatorNameType;
width: number;
height: number;
};
}
type ViewportBoundingType = {
interface IViewportBounding {
width: 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 simulatorTypes: Record<string, SimulatorType> = {
const ISimulatorTypes: Record<string, ISimulatorType> = {
desktop: {
name: 'desktop',
width: 1366,
@@ -35,12 +39,12 @@ export class Designer {
/**
* 当前的沙箱模拟器类型
*/
_simulator: SimulatorType = simulatorTypes.desktop;
_simulator: ISimulatorType = ISimulatorTypes.desktop;
/**
* 当前的视图尺寸
*/
_viewport: ViewportBoundingType = {
_viewport: IViewportBounding = {
width: 1366,
height: 800,
};
@@ -70,9 +74,14 @@ export class Designer {
*/
_isPreview = false;
/**
* 默认展开的侧边栏
*/
defaultActiveSidebarPanel?: string;
private readonly workspace: IWorkspace;
get simulator(): SimulatorType {
get simulator(): ISimulatorType {
return toJS(this._simulator);
}
@@ -100,11 +109,21 @@ export class Designer {
return this._showRightPanel;
}
constructor(options: DesignerOptionsType) {
constructor(options: IDesignerOptions) {
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, {
_simulator: observable,
_viewport: observable,
@@ -131,15 +150,15 @@ export class Designer {
});
}
setSimulator(value: SimulatorType | SimulatorNameType) {
setSimulator(value: ISimulatorType | SimulatorNameType) {
if (typeof value === 'string') {
this._simulator = simulatorTypes[value];
this._simulator = ISimulatorTypes[value];
} else {
this._simulator = value;
}
}
setViewport(value: ViewportBoundingType) {
setViewport(value: IViewportBounding) {
this._viewport = value;
}
@@ -154,7 +173,6 @@ export class Designer {
this._activeSidebarPanel = '';
}
}
closeSidebarPanel() {
this._activeSidebarPanel = '';
}
+2
View File
@@ -56,6 +56,7 @@ export class TangoFile {
this._code = code;
this._cleanCode = code;
}
this.workspace.onFilesChange([this.filename]);
}
}
@@ -134,6 +135,7 @@ export class TangoJsonFile extends TangoFile {
logger.error(err);
}
}
this.workspace.onFilesChange([this.filename]);
}
/**
+1
View File
@@ -11,5 +11,6 @@ export * from './route-module';
export * from './service-module';
export * from './store-module';
export * from './view-module';
export * from './component-module';
export * from './node';
export * from './interfaces';
+70 -51
View File
@@ -5,11 +5,10 @@ import { DragSource } from './drag-source';
import {
IFileConfig,
FileType,
IImportDeclarationPayload,
InsertChildPositionType,
ITangoConfigPackages,
IPageConfigData,
IServiceFunctionPayload,
IImportSpecifierSourceData,
IImportSpecifierData,
} from '../types';
import { TangoFile, TangoJsonFile } from './file';
@@ -25,7 +24,7 @@ export interface IViewFile {
/**
* 通过导入组件名查找组件来自的包
*/
importMap?: Dict<IImportSpecifierData>;
importMap?: Dict<IImportSpecifierSourceData>;
listImportSources?: () => string[];
listModals?: () => Array<{ label: string; value: string }>;
@@ -33,6 +32,14 @@ export interface IViewFile {
update: (code?: string, isFormatCode?: boolean, refreshWorkspace?: boolean) => void;
/**
* 添加新的导入符号
* @param source 导入来源
* @param newSpecifiers 新导入符号列表
* @returns this
*/
addImportSpecifiers: (source: string, newSpecifiers: IImportSpecifierData[]) => IViewFile;
getNode: (targetNodeId: string) => IViewNode;
removeNode: (targetNodeId: string) => IViewFile;
@@ -41,31 +48,15 @@ export interface IViewFile {
targetNodeId: string,
newNode: any,
position?: InsertChildPositionType,
sourcePrototype?: string | ComponentPrototypeType,
) => 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?: IImportDeclarationPayload[],
) => IViewFile;
replaceViewChildren: (rawNodes: any[]) => IViewFile;
updateNodeAttribute: (
nodeId: string,
@@ -148,8 +139,8 @@ export interface IWorkspace {
storeModules?: Record<string, TangoStoreModule>;
serviceModules?: Record<string, TangoServiceModule>;
refresh: (names: string[]) => void;
ready: () => void;
refresh: (names: string[]) => void;
setActiveRoute: (path: string) => void;
setActiveFile: (filename: string) => void;
@@ -157,23 +148,32 @@ export interface IWorkspace {
setComponentPrototypes: (prototypes: Record<string, ComponentPrototypeType>) => void;
getPrototype: (name: string | ComponentPrototypeType) => ComponentPrototypeType;
/**
* 查询节点
* @param id 节点 ID
* @param module 节点所在的模块名
* @returns 返回节点对象
*/
getNode: (id: string, module?: string) => IViewNode;
getFile: (filename: string) => TangoFile;
listFiles: () => Record<string, string>;
addFile: (filename: string, code: string, fileType?: FileType) => void;
// ----------------- 文件操作 -----------------
addFiles: (files: IFileConfig[]) => void;
updateFile: (filename: string, code: string, shouldFormatCode?: boolean) => 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;
/**
* 文件变化回调
* @param filenames 文件名列表
*/
onFilesChange: (filenames: string[]) => void;
// ----------------- 节点操作 -----------------
removeSelectedNode: () => void;
cloneSelectedNode: () => void;
copySelectedNode: () => void;
@@ -193,31 +193,48 @@ export interface IWorkspace {
relatedImports?: string[],
) => void;
getServiceFunction?: (serviceKey: string) => object;
/**
* 查询节点
* @param id 节点 ID
* @param module 节点所在的模块名
* @returns 返回节点对象
*/
getNode: (id: string, module?: string) => IViewNode;
// ----------------- 服务函数文件操作 -----------------
getServiceFunction?: (serviceKey: string) => {
name: string;
moduleName: string;
config: object;
};
listServiceFunctions?: () => Record<string, object>;
removeServiceFunction?: (serviceName: string, modName?: string) => void;
addServiceFunction?: (
payload: IServiceFunctionPayload | IServiceFunctionPayload[],
modName?: string,
) => void;
updateServiceFunction?: (payload: IServiceFunctionPayload, modName?: string) => void;
updateServiceBaseConfig?: (IServiceFunctionPayload: object, modName?: string) => void;
removeServiceFunction?: (serviceKey: string) => void;
addServiceFunction?: (serviceName: string, config: object, modName?: string) => void;
addServiceFunctions?: (configs: object, modName?: string) => void;
updateServiceFunction?: (serviceName: string, payload: object, modName?: string) => void;
updateServiceBaseConfig?: (config: object, modName?: string) => void;
addStoreModule?: (storeName: string, code: string) => void;
removeStoreModule?: (storeName: string) => void;
// ----------------- 状态管理文件操作 -----------------
addStoreState?: (storeName: string, stateName: string, initValue: string) => void;
removeStoreState?: (storeName: string, stateName: string) => void;
removeStoreModule?: (storeName: string) => void;
removeStoreVariable?: (variablePath: string) => void;
updateStoreVariable?: (variablePath: string, code: string) => void;
updateModuleCodeByVariablePath?: (variablePath: string, code: string) => void;
// ----------------- 视图文件操作 -----------------
// TODO: 是否需要,和 addFile 有啥区别 ???
addViewPage: (name: string, code: string) => void;
removeViewModule: (routePath: string) => void;
copyViewPage: (sourceRoutePath: string, targetPageData: IPageConfigData) => void;
// ----------------- 路由文件操作 -----------------
updateRoute: (sourceRoutePath: string, targetPageData: IPageConfigData) => void;
// ----------------- 依赖包操作 -----------------
addDependency?: (data: any) => void;
listDependencies?: () => any;
getDependency?: (pkgName: string) => object;
updateDependency?: (
name: string,
@@ -241,9 +258,11 @@ export interface IWorkspace {
removeBizComp?: (name: string) => void;
// ----------------- getter -----------------
get activeViewModule(): IViewFile;
get pages(): any[];
get bizComps(): string[];
get baseComps(): string[];
get blocks(): any[];
get localComps(): string[];
}
+30 -3
View File
@@ -1,9 +1,16 @@
import * as t from '@babel/types';
import { action, computed, makeObservable, observable } from 'mobx';
import { isNil } from '@music163/tango-helpers';
import { code2ast, ast2code, formatCode } from '../helpers';
import {
code2ast,
ast2code,
formatCode,
traverseFile,
addImportDeclaration,
updateImportDeclaration,
} from '../helpers';
import { TangoFile } from './file';
import { IFileConfig } from '../types';
import { IFileConfig, IImportSpecifierData, ImportDeclarationDataType } from '../types';
import { IWorkspace } from './interfaces';
/**
@@ -14,6 +21,11 @@ import { IWorkspace } from './interfaces';
export class TangoModule extends TangoFile {
ast: t.File;
/**
* 导入的依赖列表
*/
importList: ImportDeclarationDataType;
constructor(workspace: IWorkspace, props: IFileConfig, isSyncCode = true) {
super(workspace, props, isSyncCode);
}
@@ -34,11 +46,23 @@ export class TangoModule extends TangoFile {
this._analysisAst();
this.workspace.onFilesChange([this.filename]);
if (refreshWorkspace) {
this.workspace.refresh([this.filename]);
}
}
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 进行源码同步
*/
@@ -69,7 +93,10 @@ export class TangoModule extends TangoFile {
this.ast = code2ast(code);
}
_analysisAst() {}
_analysisAst() {
const { imports } = traverseFile(this.ast);
this.importList = imports;
}
}
/**
+14 -15
View File
@@ -7,7 +7,7 @@ import {
deleteServiceConfigFromServiceFile,
updateBaseConfigToServiceFile,
} from '../helpers';
import { IFileConfig, IServiceFunctionPayload } from '../types';
import { IFileConfig } from '../types';
import { IWorkspace } from './interfaces';
import { TangoModule } from './module';
@@ -58,7 +58,8 @@ export class TangoServiceModule extends TangoModule {
}
_analysisAst() {
const { services, baseConfig } = traverseServiceFile(this.ast);
const { imports, services, baseConfig } = traverseServiceFile(this.ast);
this.importList = imports;
this._serviceFunctions = services;
this._baseConfig = baseConfig;
if (baseConfig.namespace) {
@@ -66,25 +67,23 @@ export class TangoServiceModule extends TangoModule {
}
}
addServiceFunction(payload: IServiceFunctionPayload) {
const { name, ...rest } = payload;
this.ast = updateServiceConfigToServiceFile(this.ast, { [name]: clone(rest, false) });
addServiceFunction(name: string, config: object) {
this.ast = updateServiceConfigToServiceFile(this.ast, { [name]: clone(config, 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);
/**
* 批量添加服务函数
* @param configs { [name: string]: object }
* @returns
*/
addServiceFunctions(configs: object) {
this.ast = updateServiceConfigToServiceFile(this.ast, configs);
return this;
}
updateServiceFunction(payload: IServiceFunctionPayload) {
const { name, ...rest } = payload;
this.ast = updateServiceConfigToServiceFile(this.ast, { [name]: clone(rest, false) });
updateServiceFunction(name: string, payload: object) {
this.ast = updateServiceConfigToServiceFile(this.ast, { [name]: clone(payload, false) });
return this;
}
+101 -101
View File
@@ -1,6 +1,6 @@
import * as t from '@babel/types';
import { action, computed, makeObservable, observable, toJS } from 'mobx';
import { ComponentPrototypeType, Dict } from '@music163/tango-helpers';
import { Dict } from '@music163/tango-helpers';
import {
ast2code,
traverseViewFile,
@@ -8,15 +8,17 @@ import {
insertSiblingAfterJSXElement,
appendChildToJSXElement,
addImportDeclaration,
updateImportDeclaration,
replaceJSXElement,
getImportDeclarationPayloadByPrototype,
removeUnusedImportSpecifiers,
insertSiblingBeforeJSXElement,
replaceRootJSXElementChildren,
IdGenerator,
updateJSXAttributes,
queryXFormItemFields,
prototype2importDeclarationData,
insertImportSpecifiers,
addImportDeclarationLegacy,
updateImportDeclarationLegacy,
} from '../helpers';
import { TangoNode } from './node';
import {
@@ -24,11 +26,59 @@ import {
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;
}
/**
* 视图模块
*/
@@ -38,7 +88,7 @@ export class TangoViewModule extends TangoModule implements IViewFile {
/**
* 通过导入组件名查找组件来自的包
*/
importMap: Dict<IImportSpecifierData>;
importMap: Dict<IImportSpecifierSourceData>;
/**
* 视图中依赖的 tango 变量,仅 stores 和 services
@@ -51,6 +101,7 @@ export class TangoViewModule extends TangoModule implements IViewFile {
private _nodes: Map<string, TangoNode>;
/**
* 导入的模块
* @deprecated
*/
private _importedModules: Dict<IImportDeclarationPayload | IImportDeclarationPayload[]>;
@@ -98,6 +149,7 @@ export class TangoViewModule extends TangoModule implements IViewFile {
ast: newAst,
cleanAst,
nodes,
imports,
importedModules,
variables,
} = traverseViewFile(this.ast, this._idGenerator);
@@ -107,7 +159,8 @@ export class TangoViewModule extends TangoModule implements IViewFile {
this._cleanCode = ast2code(cleanAst);
this._importedModules = importedModules;
this.importMap = this.buildImportMap(importedModules);
this.importList = imports;
this.importMap = buildImportMap(imports);
this.variables = variables;
this._nodes.clear();
@@ -127,7 +180,7 @@ export class TangoViewModule extends TangoModule implements IViewFile {
* 依赖列表
*/
listImportSources() {
return Object.keys(this._importedModules);
return Object.keys(this.importList);
}
/**
@@ -168,23 +221,30 @@ export class TangoViewModule extends TangoModule implements IViewFile {
}
/**
* 基于组件的 prototype 信息更新导入信息
* @param prototype
* @param shouldUpdateCode
* 添加导入符号
* @param source
* @param newSpecifiers
* @returns
*/
updateImportSpecifiersByPrototype(sourcePrototype: string | ComponentPrototypeType) {
const prototype = this.workspace.getPrototype(sourcePrototype);
if (prototype) {
const importDeclaration = getImportDeclarationPayloadByPrototype(prototype, this.filename);
this.updateImportSpecifiers(importDeclaration);
addImportSpecifiers(source: string, newSpecifiers: IImportSpecifierData[]) {
const existSpecifiers = this.importList[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 代替
*/
updateImportSpecifiers(importDeclaration: IImportDeclarationPayload) {
updateImportSpecifiersLegacy(importDeclaration: IImportDeclarationPayload) {
const mods = this._importedModules[importDeclaration.sourcePath];
let ast;
// 如果模块已存在,需要去重
@@ -199,12 +259,12 @@ export class TangoViewModule extends TangoModule implements IViewFile {
(name) => !specifiers.includes(name),
);
ast = updateImportDeclaration(this.ast, {
ast = updateImportDeclarationLegacy(this.ast, {
...importDeclaration,
specifiers: newSpecifiers.concat(targetMod.specifiers),
});
} else {
ast = addImportDeclaration(this.ast, importDeclaration);
ast = addImportDeclarationLegacy(this.ast, importDeclaration);
}
this.ast = ast;
return this;
@@ -247,9 +307,24 @@ export class TangoViewModule extends TangoModule implements IViewFile {
updateNodeAttributes(nodeId: string, config: Record<string, any>, relatedImports?: string[]) {
if (relatedImports && relatedImports.length) {
// 导入依赖的组件
relatedImports.forEach((name: string) => {
const newImportData = relatedImports.reduce((prev, name) => {
const proto = this.workspace.getPrototype(name);
this.updateImportSpecifiersByPrototype(proto);
const { source, specifiers } = prototype2importDeclarationData(proto, this.filename);
const existSpecifiers: IImportSpecifierData[] = prev[source];
if (existSpecifiers) {
// merge specifiers
specifiers.forEach((item) => {
if (!existSpecifiers.find((existItem) => existItem.localName === item.localName)) {
existSpecifiers.push(item);
}
});
} else {
prev[source] = specifiers;
}
return prev;
}, {});
Object.keys(newImportData).forEach((source) => {
this.addImportSpecifiers(source, newImportData[source]);
});
}
this.ast = updateJSXAttributes(this.ast, nodeId, config);
@@ -260,43 +335,25 @@ export class TangoViewModule extends TangoModule implements IViewFile {
* 插入子节点的最后面
* @param targetNodeId
* @param newNode
* @param sourceName
* @param position
* @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,
) {
insertAfter(targetNodeId: string, newNode: t.JSXElement) {
this.ast = insertSiblingAfterJSXElement(this.ast, targetNodeId, newNode);
if (sourceName) {
this.updateImportSpecifiersByPrototype(sourceName);
}
return this;
}
insertBefore(
targetNodeId: string,
newNode: t.JSXElement,
sourceName?: string | ComponentPrototypeType,
) {
insertBefore(targetNodeId: string, newNode: t.JSXElement) {
this.ast = insertSiblingBeforeJSXElement(this.ast, targetNodeId, newNode);
if (sourceName) {
this.updateImportSpecifiersByPrototype(sourceName);
}
return this;
}
@@ -304,22 +361,15 @@ export class TangoViewModule extends TangoModule implements IViewFile {
* 替换目标节点为新节点
* @param targetNodeId
* @param newNode
* @param sourcePrototype
*/
replaceNode(
targetNodeId: string,
newNode: t.JSXElement,
sourceName?: string | ComponentPrototypeType,
) {
replaceNode(targetNodeId: string, newNode: t.JSXElement) {
this.ast = replaceJSXElement(this.ast, targetNodeId, newNode);
if (sourceName) {
this.updateImportSpecifiersByPrototype(sourceName);
}
return this;
}
/**
* 替换 jsx 跟结点的子元素
* @deprecated 不推荐使用
*/
replaceViewChildren(
childrenNodes: t.JSXElement[],
@@ -331,60 +381,10 @@ export class TangoViewModule extends TangoModule implements IViewFile {
if (importDeclarations?.length) {
importDeclarations.forEach((item) => {
this.updateImportSpecifiers(item);
this.updateImportSpecifiersLegacy(item);
});
}
return this;
}
private buildImportMap(
importedModules: Dict<IImportDeclarationPayload | IImportDeclarationPayload[]>,
) {
const map: Dict<IImportSpecifierData> = {};
Object.keys(importedModules).forEach((modName) => {
const mod = importedModules[modName];
(Array.isArray(mod) ? mod : [mod]).forEach((item) => {
if (item.defaultSpecifier) {
map[item.defaultSpecifier] = {
source: modName,
isDefault: true,
};
}
if (item.specifiers.length) {
item.specifiers.forEach((spe) => {
map[spe] = { source: modName };
});
}
});
});
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;
}
+209 -235
View File
@@ -1,11 +1,13 @@
import { action, computed, makeObservable, observable } from 'mobx';
import { JSXElement } from '@babel/types';
import {
array2object,
ComponentPrototypeType,
hasFileExtension,
isStoreVariablePath,
isString,
logger,
parseServiceVariablePath,
parseStoreVariablePath,
uniq,
} from '@music163/tango-helpers';
import {
@@ -15,8 +17,7 @@ import {
isPathnameMatchRoute,
getJSXElementChildrenNames,
namesToImportDeclarations,
getBlockNameByFilename,
getPrivilegeCode,
prototype2importDeclarationData,
} from '../helpers';
import { DropMethod } from './drop-target';
import { HistoryMessage, TangoHistory } from './history';
@@ -24,19 +25,14 @@ import { TangoNode } from './node';
import { TangoJsModule } from './module';
import { TangoFile, TangoJsonFile, TangoLessFile } from './file';
import { IWorkspace } from './interfaces';
import {
IFileConfig,
FileType,
ITangoConfigPackages,
IPageConfigData,
IServiceFunctionPayload,
} from '../types';
import { IFileConfig, FileType, ITangoConfigPackages, IPageConfigData } from '../types';
import { SelectSource } from './select-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';
import { TangoComponentsEntryModule } from './component-module';
export interface IWorkspaceOptions {
/**
@@ -55,6 +51,10 @@ export interface IWorkspaceOptions {
* 组件描述列表
*/
prototypes?: Record<string, ComponentPrototypeType>;
/**
* 工作区文件变更事件
*/
onFilesChange?: IWorkspace['onFilesChange'];
}
/**
@@ -70,8 +70,14 @@ export class Workspace extends EventTarget implements IWorkspace {
*/
files: Map<string, TangoFile>;
/**
* 组件配置
*/
componentPrototypes: Map<string, ComponentPrototypeType>;
/**
* 入口文件
*/
entry: string;
/**
@@ -89,12 +95,6 @@ export class Workspace extends EventTarget implements IWorkspace {
*/
activeViewFile: string;
/**
* 本地区块 { [blockName]: filePath }
* FIXME: 修正类型 Record<string, TangoBlockModule>
*/
localBlocks: Record<string, string>;
/**
* 路由配置模块
*/
@@ -109,6 +109,8 @@ export class Workspace extends EventTarget implements IWorkspace {
serviceModules: Record<string, TangoServiceModule> = {};
componentsEntryModule: TangoComponentsEntryModule;
/**
* package.json 文件
*/
@@ -119,12 +121,6 @@ export class Workspace extends EventTarget implements IWorkspace {
*/
tangoConfigJson: TangoJsonFile;
/**
* appJson.json 文件
* FIXME: 是否保留 ???
*/
appJson: TangoJsonFile;
/**
* 绑定事件
* TODO: 是否需要自己来管理 listeners,并及时进行 gc
@@ -160,15 +156,12 @@ export class Workspace extends EventTarget implements IWorkspace {
* 获取页面列表
*/
get pages() {
const appJsonPages = this.appJson?.getValue('pages');
const pagesMap = array2object(appJsonPages || [], (item) => item.path);
const ret: IPageConfigData[] = [];
this.routeModule?.routes.forEach((item) => {
const data = pagesMap[item.path];
if (item.path !== '*') {
ret.push({
...data,
path: item.path,
name: item.component,
});
}
});
@@ -203,8 +196,8 @@ export class Workspace extends EventTarget implements IWorkspace {
return list;
}
get blocks() {
return Object.keys(this.localBlocks);
get localComps(): string[] {
return Object.keys(this.componentsEntryModule?.exportList || {});
}
constructor(options?: IWorkspaceOptions) {
@@ -213,7 +206,6 @@ export class Workspace extends EventTarget implements IWorkspace {
this.selectSource = new SelectSource(this);
this.dragSource = new DragSource(this);
this.componentPrototypes = new Map();
this.localBlocks = {};
this.entry = options?.entry;
this.activeRoute = options?.defaultActiveRoute || '/';
this.activeFile = options?.entry;
@@ -221,6 +213,11 @@ export class Workspace extends EventTarget implements IWorkspace {
this.files = new Map();
this.isReady = false;
if (options?.onFilesChange) {
// 使用用户提供的 onFilesChange
this.onFilesChange = options.onFilesChange;
}
if (options?.files) {
this.addFiles(options.files);
}
@@ -234,8 +231,6 @@ export class Workspace extends EventTarget implements IWorkspace {
activeRoute: observable,
activeFile: observable,
activeViewFile: observable,
localBlocks: observable,
blocks: computed,
pages: computed,
bizComps: computed,
setActiveRoute: action,
@@ -302,7 +297,7 @@ export class Workspace extends EventTarget implements IWorkspace {
}
/**
* 添加一组文件到工作区
* 添加一组文件到工作区,如果文件同名,后面的文件会覆盖前面的文件
* @param files
*/
addFiles(files: IFileConfig[] = []) {
@@ -318,81 +313,100 @@ export class Workspace extends EventTarget implements IWorkspace {
* @param fileType 模块类型
*/
addFile(filename: string, code: string, fileType?: FileType) {
if (!this.files.has(filename)) {
const moduleType = fileType || inferFileType(filename);
const props = {
filename,
code,
type: moduleType,
};
const moduleType = fileType || inferFileType(filename);
const props = {
filename,
code,
type: moduleType,
};
let module;
switch (moduleType) {
case FileType.StoreEntryModule:
module = new TangoStoreEntryModule(this, props);
this.storeEntryModule = module;
break;
case FileType.RouteModule: {
module = new TangoRouteModule(this, props);
this.routeModule = module;
// check if activeRoute exists
const route = module.routes.find((item) => item.path === this.activeRoute);
if (!route) {
this.setActiveRoute(module.routes[0]?.path);
}
break;
let module;
switch (moduleType) {
case FileType.StoreEntryModule:
module = new TangoStoreEntryModule(this, props);
this.storeEntryModule = module;
break;
case FileType.ComponentsEntryModule:
module = new TangoComponentsEntryModule(this, props);
this.componentsEntryModule = module;
break;
case FileType.RouteModule: {
module = new TangoRouteModule(this, props);
this.routeModule = module;
// check if activeRoute exists
const route = module.routes.find((item) => item.path === this.activeRoute);
if (!route) {
this.setActiveRoute(module.routes[0]?.path);
}
case FileType.JsxViewModule:
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);
break;
}
this.files.set(filename, module);
case FileType.JsxViewModule:
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.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.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();
}
/**
* 添加视图文件
* @param viewName 文件名
* @param code 代码
*/
addViewFile(viewName: string, code: string) {
const viewRoute = viewName.startsWith('/') ? viewName : `/${viewName}`;
const filename = `/src/pages/${viewName}.js`;
this.addFile(filename, code);
this.addRoute(
{
name: viewName,
path: viewRoute,
},
filename,
);
}
updateFile(filename: string, code: string, shouldFormatCode = false) {
@@ -488,50 +502,14 @@ export class Workspace extends EventTarget implements IWorkspace {
this.routeModule.removeRoute(routePath).update();
this.setActiveRoute(this.routeModule.routes[0]?.path || '/');
}
// remove appJson page
this.appJson
?.setValue('pages', (pages) => {
return (pages as IPageConfigData[]).filter((page) => page.path !== routePath);
})
.update();
this.removeFile(filename);
}
/**
* 添加新的视图文件
* @deprecated
* @param route 视图名
* @param code 视图代码
*/
addViewPage(routeConfig: string | IPageConfigData, code: string) {
const config =
typeof routeConfig === 'string'
? {
name: routeConfig,
path: routeConfig.startsWith('/') ? routeConfig : `/${routeConfig}`,
}
: routeConfig;
const filename = getFilepath(config.path, '/src/pages', '.js');
this.addFile(filename, code);
this.addRoute(config, filename);
}
/**
* 添加新的路由
*/
addRoute(routeData: IPageConfigData, importFilePath: string) {
this.routeModule?.addRoute(routeData.path, importFilePath).update();
this.appJson
?.setValue('pages', (pages) => {
(pages as IPageConfigData[]).push({
name: routeData.name || routeData.path,
path: routeData.path,
privilegeCode: getPrivilegeCode(this.packageJson?.getValue('name'), routeData.path),
});
return pages;
})
.update();
}
/**
@@ -543,18 +521,6 @@ export class Workspace extends EventTarget implements IWorkspace {
if (sourceRoutePath !== targetPageData.path) {
this.routeModule?.updateRoute(sourceRoutePath, targetPageData.path).update();
}
this.appJson
?.setValue('pages', (pages) => {
for (const page of pages as IPageConfigData[]) {
if (page.path === sourceRoutePath) {
page.path = targetPageData.path;
page.name = targetPageData.name;
break;
}
}
return pages;
})
.update();
}
/**
@@ -594,13 +560,10 @@ export class Workspace extends EventTarget implements IWorkspace {
/**
* 添加新的模型文件
* @param name 模型名
* @param code 模型代码
* @deprecated 使用 addStoreFile 代替
*/
addStoreModule(name: string, code: string) {
const filename = getFilepath(name, '/src/stores', '.js');
this.addFile(filename, code);
this.storeEntryModule?.addStore(name).update();
this.addStoreFile(name, code);
}
/**
@@ -633,15 +596,23 @@ export class Workspace extends EventTarget implements IWorkspace {
}
/**
* 根据变量路径更新模块内容
* TODO: 改名,不直观
* 根据变量路径删除状态变量
* @param variablePath
*/
removeStoreVariable(variablePath: string) {
const { storeName, variableName } = parseStoreVariablePath(variablePath);
this.removeStoreState(storeName, variableName);
}
/**
* 根据变量路径更新状态变量的值
* @param variablePath 变量路径
* @param code 变量代码
*/
updateModuleCodeByVariablePath(variablePath: string, code: string) {
if (/^stores\.\w+\.\w+$/.test(variablePath)) {
const [, storeName, stateName] = variablePath.split('.');
this.storeModules[storeName]?.updateState(stateName, code).update();
updateStoreVariable(variablePath: string, code: string) {
if (isStoreVariablePath(variablePath)) {
const { storeName, variableName } = parseStoreVariablePath(variablePath);
this.storeModules[storeName]?.updateState(variableName, code).update();
}
}
@@ -652,7 +623,7 @@ export class Workspace extends EventTarget implements IWorkspace {
* @returns
*/
getServiceFunction(serviceKey: string) {
const { name, moduleName } = this.parseServiceKey(serviceKey);
const { name, moduleName } = parseServiceVariablePath(serviceKey);
if (!name) {
return;
}
@@ -669,12 +640,10 @@ export class Workspace extends EventTarget implements IWorkspace {
*/
listServiceFunctions() {
const ret: Record<string, object> = {};
Object.values(this.serviceModules).forEach((module) => {
Object.keys(this.serviceModules).forEach((moduleName) => {
const module = this.serviceModules[moduleName];
Object.keys(module.serviceFunctions).forEach((name) => {
const serviceKey =
module.baseConfig.namespace !== 'index'
? name
: [module.baseConfig.namespace, name].join('.');
const serviceKey = moduleName === 'index' ? name : [moduleName, name].join('.');
ret[serviceKey] = module.serviceFunctions[name];
});
});
@@ -684,29 +653,27 @@ export class Workspace extends EventTarget implements IWorkspace {
/**
* 更新服务函数
*/
updateServiceFunction(payload: IServiceFunctionPayload, moduleName = 'index') {
this.serviceModules[moduleName].updateServiceFunction(payload).update();
updateServiceFunction(serviceName: string, payload: object, moduleName = 'index') {
this.serviceModules[moduleName].updateServiceFunction(serviceName, payload).update();
}
/**
* 新增服务函数,支持批量添加
*/
addServiceFunction(
payload: IServiceFunctionPayload | IServiceFunctionPayload[],
moduleName = 'index',
) {
if (Array.isArray(payload)) {
this.serviceModules[moduleName]?.addServiceFunctions(payload).update();
} else {
this.serviceModules[moduleName]?.addServiceFunction(payload).update();
}
addServiceFunction(name: string, config: object, moduleName = 'index') {
this.serviceModules[moduleName]?.addServiceFunction(name, config).update();
}
addServiceFunctions(configs: object, modName = 'index') {
this.serviceModules[modName]?.addServiceFunctions(configs).update();
}
/**
* 删除服务函数
* @param name
*/
removeServiceFunction(name: string, moduleName = 'index') {
removeServiceFunction(serviceKey: string) {
const { moduleName, name } = parseServiceVariablePath(serviceKey);
this.serviceModules[moduleName]?.deleteServiceFunction(name).update();
}
@@ -717,14 +684,29 @@ export class Workspace extends EventTarget implements IWorkspace {
this.serviceModules[moduleName]?.updateBaseConfig(config).update();
}
addDependency(data: any) {
// TODO: implement it to replace addBizComp & addServiceComp
}
/**
* 获取 package.json 中的依赖列表
* @returns
* TODO: fix this logic to merge dependencies from package.json and tango.config.json
*/
listDependencies() {
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
@@ -909,7 +891,7 @@ export class Workspace extends EventTarget implements IWorkspace {
);
importDeclarations.forEach((importDeclaration) => {
this.activeViewModule.updateImportSpecifiers(importDeclaration);
this.activeViewModule.updateImportSpecifiersLegacy(importDeclaration);
});
this.copyTempNodes.forEach((node) => {
@@ -960,7 +942,11 @@ export class Workspace extends EventTarget implements IWorkspace {
const sourcePrototype = this.getPrototype(sourceName);
const newNode = prototype2jsxElement(sourcePrototype);
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({
message: HistoryMessage.InsertNode,
data: {
@@ -980,7 +966,8 @@ export class Workspace extends EventTarget implements IWorkspace {
const sourcePrototype = this.getPrototype(sourceName);
const newNode = prototype2jsxElement(sourcePrototype);
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({
message: HistoryMessage.ReplaceNode,
data: {
@@ -998,7 +985,14 @@ export class Workspace extends EventTarget implements IWorkspace {
if (insertedPrototype) {
const newNode = prototype2jsxElement(insertedPrototype);
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({
message: HistoryMessage.InsertNode,
data: {
@@ -1054,30 +1048,40 @@ export class Workspace extends EventTarget implements IWorkspace {
const targetFile = dropTarget.node?.file;
const sourceFile = dragSource.node?.file;
// dragSourcePrototype to importDeclarations
const { source, specifiers } = prototype2importDeclarationData(
dragSourcePrototype,
targetFile.filename,
);
let isValidOperation = true;
switch (dropTarget.method) {
// 直接往目标节点的 children 里添加一个节点
case DropMethod.InsertChild: {
targetFile.insertChild(dropTarget.id, newNode, 'last', dragSourcePrototype);
targetFile
.insertChild(dropTarget.id, newNode, 'last')
.addImportSpecifiers(source, specifiers);
break;
}
case DropMethod.InsertFirstChild: {
targetFile.insertChild(dropTarget.id, newNode, 'first', dragSourcePrototype);
targetFile
.insertChild(dropTarget.id, newNode, 'first')
.addImportSpecifiers(source, specifiers);
break;
}
// 往目标节点的后边插入一个节点
case DropMethod.InsertAfter: {
targetFile.insertAfter(dropTarget.id, newNode, dragSourcePrototype);
targetFile.insertAfter(dropTarget.id, newNode).addImportSpecifiers(source, specifiers);
break;
}
// 往目标节点的前方插入一个节点
case DropMethod.InsertBefore: {
targetFile.insertBefore(dropTarget.id, newNode, dragSourcePrototype);
targetFile.insertBefore(dropTarget.id, newNode).addImportSpecifiers(source, specifiers);
break;
}
// 替换目标节点
case DropMethod.ReplaceNode: {
targetFile.replaceNode(dropTarget.id, newNode, dragSourcePrototype);
targetFile.replaceNode(dropTarget.id, newNode).addImportSpecifiers(source, specifiers);
break;
}
default:
@@ -1094,13 +1098,6 @@ export class Workspace extends EventTarget implements IWorkspace {
}
this.selectSource.clear();
this.history.push({
message: HistoryMessage.DropNode,
data: {
[targetFile.filename]: targetFile.code,
},
});
}
targetFile.update();
@@ -1109,6 +1106,19 @@ export class Workspace extends EventTarget implements IWorkspace {
}
dragSource.clear();
if (isValidOperation) {
this.history.push({
message: HistoryMessage.DropNode,
data: {
[targetFile.filename]: targetFile.code,
},
});
}
}
onFilesChange(filenams: string[]) {
// do nothing
}
/**
@@ -1185,11 +1195,11 @@ export class Workspace extends EventTarget implements IWorkspace {
if (this.files.has(sourceFilePath)) {
// 来源是文件
const file = this.files.get(sourceFilePath);
this.addFile(targetFilePath + '.js', file.cleanCode, file.type);
} else if (this.files.has(sourceFilePath + '/index.js')) {
this.addFile(`${targetFilePath}.js`, file.cleanCode, file.type);
} else if (this.files.has(`${sourceFilePath}/index.js`)) {
// 来源是目录
Array.from(this.files.keys()).forEach((key) => {
if (key.startsWith(sourceFilePath + '/')) {
if (key.startsWith(`${sourceFilePath}/`)) {
const sourceFile = this.getFile(key);
this.addFile(
targetFilePath + key.slice(sourceFilePath.length),
@@ -1202,40 +1212,4 @@ export class Workspace extends EventTarget implements IWorkspace {
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,
};
}
}
+60 -21
View File
@@ -10,11 +10,25 @@ export enum FileType {
Module = 'module',
StoreEntryModule = 'storeEntryModule',
RouteModule = 'routeModule',
BlockEntryModule = 'blockEntryModule',
ServiceModule = 'serviceModule',
StoreModule = 'storeModule',
// 组件配置文件
ComponentPrototypeModule = 'componentPrototypeModule',
// 组件运行调试入口文件,一般为 `/app.js`
ComponentDemoEntryModule = 'componentDemoEntryModule',
/**
* 本地组件目录的入口文件
*/
ComponentsEntryModule = 'componentsEntryModule',
/**
* @deprecated 已废弃
*/
BlockEntryModule = 'blockEntryModule',
// jsx 类型视图文件
JsxViewModule = 'jsxViewModule',
// json 类型视图文件
JsonViewModule = 'jsonViewModule',
// 非 js 文件
@@ -73,15 +87,57 @@ export interface ITangoViewNodeData<T = JSXElement> {
}
/**
* 导变量的解析数据
* 导变量数据
*/
export interface IImportSpecifierData {
export interface IExportSpecifierData {
/**
* 来源
*/
source: string;
/**
* exported name
*/
exportedName: string;
/**
* local name
*/
localName?: string;
}
/**
* 导入变量的来源
*/
export interface IImportSpecifierSourceData {
source: string;
isDefault?: boolean;
}
/**
* 解析的导入变量数据
*/
export interface IImportSpecifierData {
/**
* 导入名
*/
importedName?: string;
/**
* 本地名
*/
localName: string;
/**
* 类型
*/
type: 'ImportDefaultSpecifier' | 'ImportSpecifier' | 'ImportNamespaceSpecifier';
}
/**
* 解析的导入语句数据
*/
export type ImportDeclarationDataType = Record<string, IImportSpecifierData[]>;
/**
* 模块导入的参数类型
* @deprecated 使用 IImportSpecifierData 代替
*/
export interface IImportDeclarationPayload {
defaultSpecifier?: string;
@@ -89,23 +145,6 @@ export interface IImportDeclarationPayload {
sourcePath: string;
}
export interface IClassPropertyNodeData {
reference: string;
propertyName: string;
propertyBody: string;
}
/**
* 服务函数参数类型
*/
export interface IServiceFunctionPayload {
[key: string]: any;
/**
* 服务函数名
*/
name: string;
}
/**
* Store 属性类型
*/
@@ -174,7 +213,7 @@ export interface IPageConfigData {
*/
name?: string;
/**
* PMS 权限码
* 权限码
*/
privilegeCode?: string;
}
+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();
});
});
+33 -4
View File
@@ -3,26 +3,37 @@ import {
serviceConfig2Node,
isValidCode,
isValidExpressionCode,
code2expression,
} from '../src/helpers';
describe('helpers', () => {
test('isValidCode', () => {
describe('ast helpers', () => {
it('isValidCode', () => {
expect(isValidCode('() => { hello world }')).toBeFalsy();
expect(isValidCode('function() {}')).toBeFalsy();
});
test('isValidExpression', () => {
it('isValidExpression', () => {
expect(isValidExpressionCode('() => { }')).toBeTruthy();
expect(isValidExpressionCode('1')).toBeTruthy();
expect(isValidExpressionCode('a = 1')).toBeTruthy();
expect(isValidExpressionCode('1 + 1')).toBeTruthy();
expect(isValidExpressionCode('"hello"')).toBeTruthy();
expect(isValidExpressionCode('false')).toBeTruthy();
expect(isValidExpressionCode('{ bizId: "vip", type: "category" }')).toBeTruthy();
expect(isValidExpressionCode('[1,2,3]')).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({
url: '/api/backend/clientversion/appmarket/list',
method: 'POST',
@@ -40,4 +51,22 @@ describe('helpers', () => {
} as any);
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');
});
});
+10 -46
View File
@@ -25,40 +25,6 @@ describe('helpers', () => {
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', () => {
const node = code2expression(
"<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', () => {
expect(value2node(1).type).toEqual('NumericLiteral');
});
@@ -126,7 +87,6 @@ describe('helpers', () => {
});
describe('string helpers', () => {
it('value2code: empty array', () => {
expect(value2code([])).toEqual('[]');
});
@@ -151,9 +111,9 @@ describe('string helpers', () => {
it('namesToImportDeclarations', () => {
expect(
namesToImportDeclarations(['Button', 'Box', 'React'], {
Button: { package: '@music163/antd' },
Box: { package: '@music163/antd' },
React: { package: 'react', isDefault: true },
Button: { source: '@music163/antd' },
Box: { source: '@music163/antd' },
React: { source: 'react', isDefault: true },
}),
).toEqual([
{ sourcePath: '@music163/antd', specifiers: ['Button', 'Box'] },
@@ -169,8 +129,10 @@ describe('string helpers', () => {
expect(getRelativePath('/src/pages/index.js', '/src/blocks/sample-block/index.js')).toEqual(
'../blocks/sample-block/index.js',
);
// TODO: fix me
// expect(getRelativePath('/src/pages/', '/src/pages/index.js')).toEqual('./index.js');
expect(getRelativePath('/src/pages/index.js', '/src/components')).toEqual('../components');
expect(getRelativePath('/src/pages/index.js', '/src/components/input.js')).toEqual(
'../components/input.js',
);
});
it('getFilepath', () => {
@@ -183,7 +145,9 @@ describe('string helpers', () => {
expect(isFilepath('./pages/index.js')).toBeTruthy();
expect(isFilepath('../pages/index.js')).toBeTruthy();
expect(isFilepath('./pages/index.css')).toBeTruthy();
expect(isFilepath('/src/pages/index.js')).toBeTruthy();
expect(isFilepath('./src/pages/index.js')).toBeTruthy();
expect(isFilepath('./src/components')).toBeTruthy();
expect(isFilepath('../src/components')).toBeTruthy();
expect(isFilepath('path')).toBeFalsy();
expect(isFilepath('path-browserify')).toBeFalsy();
expect(isFilepath('@music/one')).toBeFalsy();
+105
View File
@@ -3,6 +3,111 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.0.0-alpha.6](https://github.com/netease/tango/compare/@music163/tango-designer@1.0.0-alpha.5...@music163/tango-designer@1.0.0-alpha.6) (2023-12-29)
### Bug Fixes
- check if store value is valid ([765ea07](https://github.com/netease/tango/commit/765ea07cd5be6528e3b326e4ecb163647774d9e5))
- refactor VariableTree & Workspace ([#83](https://github.com/netease/tango/issues/83)) ([8c07821](https://github.com/netease/tango/commit/8c07821d93cea4dfc43f81ca948b845176821184))
# [1.0.0-alpha.5](https://github.com/netease/tango/compare/@music163/tango-designer@1.0.0-alpha.4...@music163/tango-designer@1.0.0-alpha.5) (2023-12-26)
### Bug Fixes
- list overflow in expSetter ([4d6b67e](https://github.com/netease/tango/commit/4d6b67eecd02b31f01d1bc896c6eeb83f6b34b35))
# [1.0.0-alpha.4](https://github.com/netease/tango/compare/@music163/tango-designer@1.0.0-alpha.3...@music163/tango-designer@1.0.0-alpha.4) (2023-12-25)
**Note:** Version bump only for package @music163/tango-designer
# [1.0.0-alpha.2](https://github.com/netease/tango/compare/@music163/tango-designer@1.0.0-alpha.1...@music163/tango-designer@1.0.0-alpha.2) (2023-12-13)
### Bug Fixes
- update variable tree with help messages and styling & add tangoConfig schema ([5e7e285](https://github.com/netease/tango/commit/5e7e285452b46888b447991b0e8548b6392acb3a))
# [1.0.0-alpha.1](https://github.com/netease/tango/compare/@music163/tango-designer@0.5.14...@music163/tango-designer@1.0.0-alpha.1) (2023-12-12)
### Bug Fixes
- add showSearch and showGroups to SettingForm & update internal icons ([#73](https://github.com/netease/tango/issues/73)) ([a8357f4](https://github.com/netease/tango/commit/a8357f45b8b0c1f2b91a856918f6bdecdc65aa73))
- render form item without setter & enhance tools ([#76](https://github.com/netease/tango/issues/76)) ([1acfa68](https://github.com/netease/tango/commit/1acfa6864b1faad1a441facc426d8d94b6b090b5))
## [0.5.14](https://github.com/netease/tango/compare/@music163/tango-designer@0.5.13...@music163/tango-designer@0.5.14) (2023-12-04)
**Note:** Version bump only for package @music163/tango-designer
## [0.5.13](https://github.com/netease/tango/compare/@music163/tango-designer@0.5.12...@music163/tango-designer@0.5.13) (2023-12-04)
### Bug Fixes
- add filesFormatter to sandbox & fix update props with imports ([#71](https://github.com/netease/tango/issues/71)) ([138cbe9](https://github.com/netease/tango/commit/138cbe9b203b370aff42c1ae8086d69edacf35e9))
## [0.5.12](https://github.com/netease/tango/compare/@music163/tango-designer@0.5.11...@music163/tango-designer@0.5.12) (2023-11-30)
### Bug Fixes
- fix render components tree ([76a5a2c](https://github.com/netease/tango/commit/76a5a2c65920bc42b019cd1f32a3cacd0d888638))
## [0.5.11](https://github.com/netease/tango/compare/@music163/tango-designer@0.5.10...@music163/tango-designer@0.5.11) (2023-11-29)
**Note:** Version bump only for package @music163/tango-designer
## [0.5.10](https://github.com/netease/tango/compare/@music163/tango-designer@0.5.9...@music163/tango-designer@0.5.10) (2023-11-28)
### Bug Fixes
- update default theme ([14cd4c5](https://github.com/netease/tango/commit/14cd4c54a1e5eed834d01921e186bdba9bbe7922))
## [0.5.9](https://github.com/netease/tango/compare/@music163/tango-designer@0.5.8...@music163/tango-designer@0.5.9) (2023-11-28)
### Bug Fixes
- update theme ([2550411](https://github.com/netease/tango/commit/2550411c1c93037931d44aa9f7822ffe8caa900b))
## [0.5.8](https://github.com/netease/tango/compare/@music163/tango-designer@0.5.7...@music163/tango-designer@0.5.8) (2023-11-27)
### Bug Fixes
- event setter ([588861f](https://github.com/netease/tango/commit/588861f6492b45954e29ceed950567781b822492))
## [0.5.7](https://github.com/netease/tango/compare/@music163/tango-designer@0.5.6...@music163/tango-designer@0.5.7) (2023-11-23)
### Bug Fixes
- update varTreeNode style ([#69](https://github.com/netease/tango/issues/69)) ([f604996](https://github.com/netease/tango/commit/f604996e56c9e4d58ff7f80cd98c3753986f081f))
## [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
+11 -11
View File
@@ -1,6 +1,6 @@
{
"name": "@music163/tango-designer",
"version": "0.5.1",
"version": "1.0.0-alpha.6",
"description": "lowcode designer",
"keywords": [
"react"
@@ -32,13 +32,13 @@
},
"dependencies": {
"@ant-design/icons": "^4.8.0",
"@music163/request": "^0.1.0",
"@music163/tango-context": "^0.2.3",
"@music163/tango-core": "^0.2.2",
"@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",
"@music163/request": "^0.1.2",
"@music163/tango-context": "^1.0.0-alpha.5",
"@music163/tango-core": "^1.0.0-alpha.5",
"@music163/tango-helpers": "^1.0.0-alpha.1",
"@music163/tango-sandbox": "^1.0.0-alpha.5",
"@music163/tango-setting-form": "^1.0.0-alpha.5",
"@music163/tango-ui": "^1.0.0-alpha.3",
"antd": "^4.24.2",
"cash-dom": "^8.1.2",
"classnames": "^2.3.2",
@@ -53,9 +53,9 @@
"semver": "^7.3.8"
},
"devDependencies": {
"@types/color": "^3.0.4",
"@types/react-color": "^3.0.6",
"@types/react-resizable": "^3.0.4"
"@types/color": "^3.0.5",
"@types/react-color": "^3.0.9",
"@types/react-resizable": "^3.0.6"
},
"publishConfig": {
"access": "public",
@@ -1,3 +1,4 @@
export * from './drag-box';
export * from './input-kv';
export * from './variable-tree';
export * from './variable-tree-modal';
@@ -0,0 +1,45 @@
import React, { useState } from 'react';
import { ModalProps, Modal } from 'antd';
import { Box } from 'coral-system';
import { IVariableTreeNode, noop, useBoolean } from '@music163/tango-helpers';
import { VariableTree, VariableTreeProps } from './variable-tree';
interface VariableTreeModalProps extends VariableTreeProps {
trigger?: React.ReactElement;
title?: ModalProps['title'];
modalProps?: ModalProps;
}
export function VariableTreeModal({
trigger,
title,
modalProps,
onSelect = noop,
...rest
}: VariableTreeModalProps) {
const [node, setNode] = useState<IVariableTreeNode>();
const [visible, { on, off }] = useBoolean(false);
return (
<Box>
{React.cloneElement(trigger, { onClick: on })}
<Modal
title={title}
open={visible}
onCancel={off}
okButtonProps={{
disabled: !node,
}}
onOk={() => {
if (node) {
onSelect(node);
off();
}
}}
width="60%"
{...modalProps}
>
<VariableTree height={400} onSelect={setNode} {...rest} />
</Modal>
</Box>
);
}
@@ -1,631 +0,0 @@
import React, { useMemo, useState } from 'react';
import {
Tree,
Button,
Form,
Input,
Space,
ModalProps,
Modal,
Radio,
Empty,
Popconfirm,
Tooltip,
} from 'antd';
import { Box, Text, css } from 'coral-system';
import {
PlusOutlined,
FunctionOutlined,
DeleteOutlined,
CopyOutlined,
EyeOutlined,
} from '@ant-design/icons';
import {
filterTreeData,
isFunction,
isNil,
isString,
noop,
useBoolean,
} from '@music163/tango-helpers';
import {
Panel,
InputCode,
Search,
JsonView,
JsonViewProps,
CopyClipboard,
} from '@music163/tango-ui';
import { isValidExpressionCode } from '@music163/tango-core';
export interface IVariableTreeNode {
/**
* 唯一标识符
*/
key: string;
/**
* 标题
*/
title?: string;
/**
* 是否可选中
*/
selectable?: boolean;
/**
* 展示添加子节点的图标
*/
showAddChildIcon?: boolean;
/**
* 展示删除删除图标
*/
showDeleteIcon?: boolean;
/**
* 结点类型,用来展示图标
*/
type?: 'function' | 'property';
/**
* 定义的原始值
*/
raw?: any;
/**
* 子结点
*/
children?: IVariableTreeNode[];
[key: string]: any;
}
type VariableModelType = 'preview' | 'define' | 'add';
export interface EditableVariableTreeProps {
/**
* 允许的变量面板
*/
modes?: VariableModelType[];
/**
* 默认的变量面板
*/
defaultMode?: VariableModelType;
/**
* 数据源
*/
dataSource?: VariableTreeProps['dataSource'];
/**
* 点击添加变量的回调
*/
onAddVariable?: AddNodeFormProps['onSubmit'];
/**
* 预览值取值函数
*/
getPreviewValue?: (node: IVariableTreeNode) => unknown;
/**
* 选择列表项时的回调
*/
onSelect?: (data: IVariableTreeNode) => void;
/**
* 保存结点定义的回调
*/
onSave?: NodeDefineProps['onSave'];
/**
* 删除结点定义的回调
*/
onDeleteVariable?: (storeName: string, stateName: string) => void;
/**
* 删除模型时的回调
*/
onDeleteStore?: (storeName: string) => void;
/**
* 高度
*/
height?: number | string;
/**
* 搜索框的样式
*/
searchStyle?: React.CSSProperties;
/**
* 搜索框的后置结点
*/
searchAddonAfter?: React.ReactNode;
}
const previewOptions = [
{ label: '运行时', value: 'preview' },
{ label: '定义', value: 'define' },
];
export function EditableVariableTree({
height,
searchAddonAfter,
searchStyle,
dataSource,
onAddVariable = noop,
onDeleteVariable = noop,
onDeleteStore = noop,
getPreviewValue = noop,
onSelect = noop,
onSave = noop,
modes = ['add', 'preview', 'define'],
defaultMode = 'preview',
}: EditableVariableTreeProps) {
const [keyword, setKeyword] = useState('');
const [node, setNode] = useState<IVariableTreeNode>();
const [mode, setMode] = useState<EditableVariableTreeProps['defaultMode']>(defaultMode);
const hasPanelSwitch = modes.includes('define') && modes.includes('preview');
const treeData = useMemo(() => {
const pattern = new RegExp(keyword, 'ig');
return keyword
? filterTreeData(dataSource, (leaf) => pattern.test(leaf.title), 'children', true)
: dataSource;
}, [keyword, dataSource]);
return (
<Box
className="EditableVariableTree"
display="flex"
overflow="auto"
height={height}
position="relative"
>
<Box p="l" width="40%" borderRight="solid" borderColor="line.normal">
<Box mb="m" position="sticky" top="0" bg="white" zIndex={2}>
<Input.Group compact>
<Search
placeholder="请输入变量名"
onChange={(val) => setKeyword(val?.trim())}
style={searchStyle}
/>
{searchAddonAfter}
</Input.Group>
</Box>
<VariableTree
dataSource={treeData}
showViewIcon
onSelect={(item) => {
setNode(item);
mode === 'add' && setMode(defaultMode);
onSelect(item);
}}
onView={(item) => {
setNode(item);
mode === 'add' && setMode(defaultMode);
}}
onAdd={(item) => {
setNode(item);
setMode('add');
}}
onRemove={(item) => {
const [type, storeName] = item.key.split('.');
setNode(null);
onDeleteStore(storeName);
}}
/>
</Box>
{node ? (
<Panel
title={
{
preview: '变量值预览',
add: '添加变量',
define: '变量定义',
}[mode]
}
extra={
hasPanelSwitch && mode !== 'add' ? (
<Radio.Group
optionType="button"
buttonStyle="solid"
size="small"
value={mode}
onChange={(e) => setMode(e.target.value)}
options={previewOptions}
/>
) : null
}
width="60%"
borderRadius="m"
position="sticky"
top="0"
bodyProps={{ px: 'm' }}
>
{mode === 'preview' && (
<ValuePreview
value={getPreviewValue(node)}
onSelect={(valuePath) => {
return ['tango', node.key.replaceAll('.', '?.'), valuePath].join('.');
}}
/>
)}
{mode === 'define' && (
<NodeDefineForm
node={node}
onSave={onSave}
onDelete={(item) => {
const [type, storeName, stateName] = item.key.split('.');
onDeleteVariable(storeName, stateName);
setNode(null);
}}
/>
)}
{mode === 'add' && (
<AddNodeForm
parentNode={node}
onCancel={() => {
setMode(defaultMode);
setNode(null);
}}
onSubmit={(storeName, data) => {
onAddVariable(storeName, data);
setMode(defaultMode);
setNode(null);
}}
/>
)}
</Panel>
) : (
<Panel title="提示" flex="1" position="sticky" top="0">
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="请从左侧列表中选择一个变量" />
</Panel>
)}
</Box>
);
}
interface EditableVariableTreeModalProps extends EditableVariableTreeProps {
trigger?: React.ReactElement;
title?: ModalProps['title'];
modalProps?: ModalProps;
}
export function EditableVariableTreeModal({
trigger,
title,
modalProps,
onSelect = noop,
...rest
}: EditableVariableTreeModalProps) {
const [node, setNode] = useState<IVariableTreeNode>();
const [visible, { on, off }] = useBoolean(false);
return (
<Box>
{React.cloneElement(trigger, { onClick: on })}
<Modal
title={title}
open={visible}
onCancel={off}
okButtonProps={{
disabled: !node,
}}
onOk={() => {
if (node) {
onSelect(node);
off();
}
}}
width="60%"
{...modalProps}
>
<EditableVariableTree height={480} onSelect={setNode} {...rest} />
</Modal>
</Box>
);
}
const varTreeStyle = css`
overflow: auto;
.ant-tree {
font-family: Consolas, Menlo, Courier, monospace;
}
.ant-tree .ant-tree-treenode {
padding: 0;
}
.ant-tree.ant-tree-directory .ant-tree-treenode::before {
bottom: 0;
}
.ant-tree-indent-unit {
width: 12px;
}
.anticon-function {
margin-left: 4px;
color: var(--tango-colors-text2);
}
`;
interface VariableTreeProps {
dataSource?: IVariableTreeNode[];
onSelect?: (data: IVariableTreeNode) => void;
onAdd?: (data: IVariableTreeNode) => void;
onRemove?: (data: IVariableTreeNode) => void;
onCopy?: (data: IVariableTreeNode) => void;
onView?: (data: IVariableTreeNode) => void;
showDeleteIcon?: boolean;
showViewIcon?: boolean;
}
export function VariableTree({
dataSource = [],
onSelect = noop,
onAdd = noop,
onRemove = noop,
onCopy = noop,
onView = noop,
showDeleteIcon = false,
showViewIcon = false,
}: VariableTreeProps) {
return (
<Box className="VariableTree" css={varTreeStyle}>
<Tree
blockNode
showIcon={false}
defaultExpandAll
treeData={dataSource}
onSelect={(keys, detail) => {
onSelect(detail.node);
}}
titleRender={(node) => {
const isLeaf = !node.children;
if (isLeaf) {
const isDeletable = node.showDeleteIcon ?? showDeleteIcon;
return (
<Box display="flex" justifyContent="space-between" alignItems="center">
<Text>
{node.title}
{node.type === 'function' && <FunctionOutlined />}
</Text>
<Box>
{isDeletable && (
<Popconfirm
title="确认删除吗?该操作会导致引用此模型的代码报错,请谨慎操作!"
onConfirm={() => onRemove(node)}
>
<Button
type="text"
size="small"
icon={<DeleteOutlined />}
onClick={(e) => {
e.stopPropagation();
}}
/>
</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>
);
}
return (
<Box display="flex" alignItems="center" justifyContent="space-between">
<Text mr="m">{node.title}</Text>
<Box>
{node.showAddChildIcon && (
<Tooltip title={`${node.title} 中添加变量`}>
<Button
type="text"
size="small"
icon={<PlusOutlined />}
onClick={(e) => {
e.stopPropagation();
onAdd(node);
}}
/>
</Tooltip>
)}
</Box>
</Box>
);
}}
/>
</Box>
);
}
interface NodePreviewProps {
value?: unknown;
/**
* 选择预览结点的回调
*/
onSelect?: JsonViewProps['onCopy'];
}
export function ValuePreview({ value, onSelect }: NodePreviewProps) {
if (isNil(value)) {
return <span></span>;
}
if (isFunction(value)) {
return <span></span>;
}
if (typeof value === 'object') {
return <JsonView src={value} enableCopy onCopy={onSelect} />;
}
return (
<InputCode
shape="inset"
value={isString(value) ? `"${value}"` : String(value)}
editable={false}
/>
);
}
interface NodeDefineProps {
node: IVariableTreeNode;
onSave?: (code: string, node: IVariableTreeNode) => void;
onDelete?: (node: IVariableTreeNode) => void;
}
/**
* 变量值定义面板
*/
function NodeDefineForm({ node, onSave = noop, onDelete = noop }: NodeDefineProps) {
const [value, setValue] = useState('');
const [error, setError] = useState('');
const [editable, { on, off }] = useBoolean();
if (isNil(node.raw)) {
return <Empty description="缺失定义数据" />;
}
return (
<Box>
<InputCode
shape="inset"
value={node.raw}
onChange={(nextValue) => setValue(nextValue)}
editable={editable}
onBlur={() => {
if (!isValidExpressionCode(value)) {
setError('代码格式错误,请检查代码语法!');
} else {
setError('');
}
}}
/>
<Box color="red" my="m">
{error}
</Box>
<Box mt="l">
{editable ? (
<Space>
<Button
onClick={() => {
off();
setError('');
}}
>
</Button>
<Button
type="primary"
onClick={() => {
if (error) {
return;
}
onSave(value, node);
off();
}}
>
</Button>
</Space>
) : (
<Space>
<Button onClick={on}></Button>
<Popconfirm
title="确认删除此变量吗?该操作会导致引用该变量的代码报错!"
onConfirm={() => {
onDelete(node);
}}
>
<Button danger></Button>
</Popconfirm>
</Space>
)}
</Box>
</Box>
);
}
interface AddNodeFormProps {
parentNode: IVariableTreeNode;
onCancel?: React.MouseEventHandler<HTMLButtonElement>;
onSubmit?: (storeName: string, data: { name: string; initialValue: string }) => void;
}
function AddNodeForm({ parentNode, onCancel, onSubmit }: AddNodeFormProps) {
return (
<Form
layout="vertical"
autoComplete="off"
validateTrigger="onBlur"
onFinish={(values) => {
if (onSubmit && parentNode) {
const [type, storeName] = parentNode.key.split('.');
onSubmit(storeName, values);
}
}}
>
<Form.Item label="所属模型">
<Input value={parentNode?.key} disabled />
</Form.Item>
<Form.Item
label="变量名"
name="name"
rules={[
{ required: true },
{ pattern: /^\w+$/, message: '非法的变量标识符' },
{
validator(_, value) {
const found = parentNode.children.find((item) => item.title === value);
return found
? Promise.reject(new Error('和已有变量名冲突,请换一个名字!'))
: Promise.resolve();
},
},
]}
>
<Input placeholder="请输入变量名" />
</Form.Item>
<Form.Item
label="初值"
name="initialValue"
rules={[
{ required: true },
{
validator(_, value) {
return isValidExpressionCode(value)
? Promise.resolve()
: Promise.reject(new Error('代码存在语法错误,请输入合法的代码片段!'));
},
},
]}
>
<InputCode shape="inset" placeholder="请输入初值" />
</Form.Item>
<Form.Item>
<Space>
<Button onClick={onCancel}></Button>
<Button type="primary" htmlType="submit">
</Button>
</Space>
</Form.Item>
</Form>
);
}
@@ -0,0 +1,182 @@
import { isValidFunctionCode, url2serviceName } from '@music163/tango-helpers';
import { InputCode } from '@music163/tango-ui';
import { Button, Dropdown, Form, FormProps, Input, Select, Space } from 'antd';
import { Box } from 'coral-system';
import React, { useState } from 'react';
interface AddServiceFormProps extends FormProps {
initialValues?: object;
onSubmit: (values: object) => void;
onCancel: () => void;
serviceModules: object[];
serviceNames: string[];
}
export function AddServiceForm({
initialValues,
onSubmit,
onCancel,
serviceModules,
serviceNames,
...formProps
}: AddServiceFormProps) {
const isModifyMode = !!initialValues?.['name']; // 是否为更新模式
const [disabled, setDisabled] = useState(isModifyMode);
const [form] = Form.useForm();
return (
<Form
form={form}
labelCol={{ span: 6 }}
wrapperCol={{ span: 17 }}
colon={false}
layout="horizontal"
initialValues={initialValues}
onFinish={(values) => {
onSubmit(values);
}}
{...formProps}
>
<Form.Item
label="命名空间"
name="moduleName"
rules={[
{
required: true,
},
]}
>
<Select options={serviceModules} disabled={disabled || isModifyMode} />
</Form.Item>
<Form.Item
label="方法名"
name="name"
rules={[
{ required: true },
{ pattern: /^[a-z]\w+$/, message: '请输入合法的方法名称' },
!isModifyMode && {
validator(_, value) {
const isExist = serviceNames.includes(value);
return isExist
? Promise.reject(new Error('重复的方法名,请换一个!'))
: Promise.resolve();
},
},
]}
extra={
!disabled && (
<Box>
<Button
type="link"
size="small"
onClick={() => {
const url = form.getFieldValue('url');
if (!url) {
return;
}
const funcName = url2serviceName(url);
form.setFieldValue('name', funcName);
}}
>
</Button>
</Box>
)
}
>
<Input placeholder="请输入数据服务调用名称" disabled={disabled || isModifyMode} />
</Form.Item>
<Form.Item label="路径" name="url" rules={[{ required: true, type: 'url' }]}>
<Input placeholder="请输入数据服务调用名称" disabled={disabled || isModifyMode} />
</Form.Item>
<Form.Item label="方法" name="method">
<Select
options={[
{ label: 'GET', value: 'get' },
{ label: 'POST', value: 'post' },
]}
placeholder="请选择 HTTP 请求方法"
disabled={disabled}
/>
</Form.Item>
<Form.Item
label="格式化响应"
name="formatter"
tooltip="提供格式化函数,对结果进行格式化"
validateTrigger="onBlur"
rules={[
{
validator(_, value) {
if (!value) {
return Promise.resolve();
}
return isValidFunctionCode(value)
? Promise.resolve()
: Promise.reject(new Error('请提供合法的函数代码!'));
},
},
]}
extra={
!disabled && (
<Box>
<Dropdown
menu={{
items: [
{ label: '直接返回(默认)', key: 'res => res' },
{ label: '返回数据部分(云音乐标准规范)', key: 'res => res.data' },
],
onClick: ({ key }) => {
form.setFieldValue('formatter', key);
},
}}
>
<Button type="link" size="small">
使
</Button>
</Dropdown>
</Box>
)
}
>
<InputCode showLineNumbers placeholder="res => res" editable={!disabled} />
</Form.Item>
<Form.Item wrapperCol={{ offset: 6, span: 18 }}>
{isModifyMode ? (
<Space>
{!disabled ? (
<>
<Button
onClick={() => {
setDisabled(true);
}}
>
</Button>
<Button type="primary" htmlType="submit">
</Button>
</>
) : (
<Button
onClick={() => {
setDisabled(false);
}}
>
</Button>
)}
</Space>
) : (
<Space>
<Button onClick={onCancel}></Button>
<Button type="primary" htmlType="submit">
</Button>
</Space>
)}
</Form.Item>
</Form>
);
}
@@ -0,0 +1,124 @@
import React from 'react';
import { Button, Form, Input, Space } from 'antd';
import { isValidExpressionCode } from '@music163/tango-core';
import { InputCode, Panel } from '@music163/tango-ui';
import { IVariableTreeNode } from '@music163/tango-helpers';
export interface AddStoreVariableFormProps {
parentNode: IVariableTreeNode;
onCancel?: React.MouseEventHandler<HTMLButtonElement>;
onSubmit?: (storeName: string, data: { name: string; initialValue: string }) => void;
}
export function AddStoreVariableForm({
parentNode,
onCancel,
onSubmit,
}: AddStoreVariableFormProps) {
return (
<Form
layout="vertical"
autoComplete="off"
validateTrigger="onBlur"
onFinish={(values) => {
if (onSubmit && parentNode) {
const [type, storeName] = parentNode.key.split('.');
onSubmit(storeName, values);
}
}}
>
<Form.Item label="所属模型">
<Input value={parentNode?.key} disabled />
</Form.Item>
<Form.Item
label="变量名"
name="name"
rules={[
{ required: true },
{ pattern: /^\w+$/, message: '非法的变量标识符' },
{
validator(_, value) {
const found = parentNode.children.find((item) => item.title === value);
return found
? Promise.reject(new Error('和已有变量名冲突,请换一个名字!'))
: Promise.resolve();
},
},
]}
>
<Input placeholder="请输入变量名" />
</Form.Item>
<Form.Item
label="初值"
name="initialValue"
rules={[
{ required: true },
{
validator(_, value) {
return value && isValidExpressionCode(value)
? Promise.resolve()
: Promise.reject(new Error('代码存在语法错误,请输入合法的代码片段!'));
},
},
]}
>
<InputCode shape="inset" placeholder="请输入初值" />
</Form.Item>
<Form.Item>
<Space>
<Button onClick={onCancel}></Button>
<Button type="primary" htmlType="submit">
</Button>
</Space>
</Form.Item>
</Form>
);
}
export interface AddStoreFormProps {
storeNames?: string[];
onCancel?: () => void;
onSubmit?: (data: { name: string }) => void;
}
export function AddStoreForm({ storeNames = [], onCancel, onSubmit }: AddStoreFormProps) {
return (
<Panel title="添加模型" subTitle="模型可以用来组织一组变量" bodyProps={{ p: 'l' }}>
<Form
autoComplete="off"
colon={false}
onFinish={(values) => {
onSubmit && onSubmit(values);
}}
>
<Form.Item
label="模型名称"
name="name"
rules={[
{ required: true },
{ pattern: /^[a-z]\w+$/, message: '非法的变量标识符' },
{
validator(_, value) {
const found = storeNames.includes(value);
return found
? Promise.reject(new Error('已存在该模型名,请换一个!'))
: Promise.resolve();
},
},
]}
>
<Input placeholder="请输入模型名称" />
</Form.Item>
<Form.Item label=" ">
<Space>
<Button onClick={onCancel}></Button>
<Button type="primary" htmlType="submit">
</Button>
</Space>
</Form.Item>
</Form>
</Panel>
);
}
@@ -0,0 +1,360 @@
import React, { useCallback, useMemo, useState } from 'react';
import {
IVariableTreeNode,
filterTreeData,
isServiceVariablePath,
isStoreVariablePath,
noop,
parseServiceVariablePath,
} from '@music163/tango-helpers';
import { css, Box, Text } from 'coral-system';
import { Button, Popconfirm, Tooltip, Tree } from 'antd';
import {
CopyOutlined,
DeleteOutlined,
EyeOutlined,
FunctionOutlined,
PlusOutlined,
} from '@ant-design/icons';
import { CopyClipboard, Panel, Search } from '@music163/tango-ui';
import { AddStoreForm, AddStoreVariableForm } from './add-store';
import { AddServiceForm } from './add-service';
import {
NodeCommonDetail,
ValueDefine,
ValueDefineProps,
ValueDetail,
ValueDetailProps,
} from './value-detail';
import { ValuePreview } from './value-preview';
import { ServicePreview } from './service-preview';
const varTreeStyle = css`
overflow: auto;
position: relative;
.ant-tree {
font-family: Consolas, Menlo, Courier, monospace;
}
.ant-tree-node-content-wrapper.ant-tree-node-content-wrapper-normal {
width: calc(100% - 50px);
}
.ant-tree .ant-tree-treenode {
padding: 0;
}
.ant-tree.ant-tree-directory .ant-tree-treenode::before {
bottom: 0;
}
.ant-tree-indent-unit {
width: 12px;
}
`;
type SelectNodeCallback = (data: IVariableTreeNode) => void;
export interface VariableTreeProps {
defaultValueDetailMode?: ValueDetailProps['defaultMode'];
dataSource: IVariableTreeNode[];
appContext?: object;
serviceModules?: any[];
getPreviewValue?: (node: IVariableTreeNode) => unknown;
getServiceData?: (serviceKey: string) => object;
getServiceNames?: (moduleName: string) => string[];
getStoreNames?: () => string[];
onSelect?: SelectNodeCallback;
onAddStoreVariable?: (storeName: string, data: any) => void;
onAddStore?: (newStoreName: string) => void;
onAddService?: (data: object) => void;
onRemoveVariable?: (variableKey: string) => void;
onUpdateVariable?: ValueDefineProps['onSave'];
onUpdateService?: (data: object) => void;
onCopy?: (data: IVariableTreeNode) => void;
onView?: SelectNodeCallback;
height?: number | string;
showViewButton?: boolean;
}
export function VariableTree({
dataSource = [],
serviceModules = [],
appContext = {},
defaultValueDetailMode,
onSelect = noop,
onAddStoreVariable = noop,
onAddStore = noop,
onAddService = noop,
onRemoveVariable = noop,
onUpdateVariable = noop,
onUpdateService = noop,
onCopy = noop,
onView = noop,
getServiceData,
getServiceNames,
getStoreNames,
getPreviewValue = noop,
showViewButton,
...rest
}: VariableTreeProps) {
const [keyword, setKeyword] = useState('');
const [activeNode, setActiveNode] = useState<IVariableTreeNode>();
const [mode, setMode] = useState<
'detail' | 'storeVariableDetail' | 'serviceDetail' | 'addVariable' | 'addStore' | 'addService'
>();
const clear = useCallback(() => {
setActiveNode(null);
setMode(null);
}, []);
const selectNode = useCallback((node: IVariableTreeNode, callback?: SelectNodeCallback) => {
if (isStoreVariablePath(node.key)) {
setMode('storeVariableDetail');
} else if (isServiceVariablePath(node.key)) {
setMode('serviceDetail');
} else {
setMode('detail');
}
setActiveNode(node);
callback?.(node);
}, []);
const treeData = useMemo(() => {
const pattern = new RegExp(keyword, 'ig');
return keyword
? filterTreeData(dataSource, (leaf) => pattern.test(leaf.title), 'children', true)
: dataSource;
}, [keyword, dataSource]);
return (
<Box display="flex" columnGap="l" className="VariableTree" css={varTreeStyle} {...rest}>
<Box className="VariableList" width="40%">
<Box mb="m" position="sticky" top="0" bg="white" zIndex={2}>
<Search placeholder="请输入变量名" onChange={(val) => setKeyword(val?.trim())} />
</Box>
<Tree
blockNode
showIcon={false}
defaultExpandAll
treeData={treeData}
onSelect={(keys, detail) => {
selectNode(detail.node, onSelect);
}}
titleRender={(node) => {
const isLeaf = !node.children;
if (isLeaf) {
const showView = node.showViewButton ?? showViewButton;
return (
<Box display="flex" justifyContent="space-between" alignItems="center">
<Text flex="1" truncated>
{node.title}
{node.type === 'function' && (
<Text color="text3" ml="m">
<FunctionOutlined />
</Text>
)}
</Text>
<Box flex="0 0 72px" textAlign="right">
{node.showRemoveButton && (
<Popconfirm
title={`确认删除吗 ${node.title}?该操作会导致引用此模型的代码报错,请谨慎操作!`}
onConfirm={() => {
onRemoveVariable(node.key);
}}
>
<Button
type="text"
size="small"
icon={<DeleteOutlined />}
onClick={(e) => {
e.stopPropagation();
}}
/>
</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>
{showView && (
<Tooltip title="查看变量详情">
<Button
type="text"
size="small"
icon={<EyeOutlined />}
onClick={(e) => {
e.stopPropagation();
selectNode(node, onView);
}}
/>
</Tooltip>
)}
</Box>
</Box>
);
}
return (
<Box display="flex" alignItems="center" justifyContent="space-between">
<Text mr="m">{node.title}</Text>
<Box>
{/^\$?stores$/.test(node.key) && (
<Tooltip title="新建数据模型">
<Button
type="text"
size="small"
icon={<PlusOutlined />}
onClick={(e) => {
e.stopPropagation();
setMode('addStore');
}}
/>
</Tooltip>
)}
{node.showAddButton && /^stores\.[a-zA-Z0-9]+$/.test(node.key) && (
<Tooltip title={`${node.title} 中添加变量`}>
<Button
type="text"
size="small"
icon={<PlusOutlined />}
onClick={(e) => {
e.stopPropagation();
setActiveNode(node);
setMode('addVariable');
}}
/>
</Tooltip>
)}
{/^services(\.[a-zA-Z0-9]+)?$/.test(node.key) && (
<Tooltip title={`${node.title} 中添加服务函数`}>
<Button
type="text"
size="small"
icon={<PlusOutlined />}
onClick={(e) => {
e.stopPropagation();
setActiveNode(node);
setMode('addService');
}}
/>
</Tooltip>
)}
</Box>
</Box>
);
}}
/>
</Box>
<Box className="VariableDetail" flex="1" position="sticky" top="0" overflow="auto">
{mode === 'detail' && <NodeCommonDetail data={activeNode} />}
{mode === 'storeVariableDetail' && (
<ValueDetail key={activeNode.key} defaultMode={defaultValueDetailMode}>
{(previewMode) =>
previewMode === 'runtime' ? (
<ValuePreview
value={getPreviewValue(activeNode)}
onCopy={(valuePath) => {
return ['tango', activeNode.key.replaceAll('.', '?.'), valuePath].join('.');
}}
/>
) : (
<ValueDefine data={activeNode} onSave={onUpdateVariable} />
)
}
</ValueDetail>
)}
{mode === 'serviceDetail' && (
<>
<Panel shape="solid" title="服务函数配置">
<AddServiceForm
key={activeNode.key}
serviceModules={serviceModules}
serviceNames={(function () {
const { moduleName } = parseServiceVariablePath(activeNode.key);
return getServiceNames?.(moduleName) || [];
})()}
initialValues={{
...getServiceData?.(activeNode.key),
}}
onCancel={clear}
onSubmit={(values) => {
onUpdateService(values);
clear();
}}
/>
</Panel>
<Panel shape="solid" title="服务函数预览" mt="l">
<ServicePreview
key={activeNode.key}
appContext={appContext}
functionKey={activeNode.key}
/>
</Panel>
</>
)}
{mode === 'addVariable' && (
<Panel shape="solid" title="添加变量">
<AddStoreVariableForm
parentNode={activeNode}
onSubmit={(storeName, data) => {
onAddStoreVariable(storeName, data);
clear();
}}
onCancel={() => {
clear();
}}
/>
</Panel>
)}
{mode === 'addStore' && (
<AddStoreForm
storeNames={getStoreNames?.() || []}
onSubmit={({ name }) => {
onAddStore(name);
clear();
}}
onCancel={() => {
clear();
}}
/>
)}
{mode === 'addService' && (
<Panel shape="solid" title="创建服务函数">
<AddServiceForm
key={activeNode.key}
serviceModules={serviceModules}
serviceNames={activeNode.children?.map((item) => item.title) || []}
initialValues={{
moduleName: activeNode.title,
}}
onCancel={() => {
clear();
}}
onSubmit={(values) => {
onAddService(values);
clear();
}}
/>
</Panel>
)}
</Box>
</Box>
);
}
@@ -0,0 +1,93 @@
import React, { useState } from 'react';
import { Box } from 'coral-system';
import { Button, Empty } from 'antd';
import { PlayCircleOutlined } from '@ant-design/icons';
import { InputCode, Panel, JsonView } from '@music163/tango-ui';
import { isNil, logger, code2object, getValue } from '@music163/tango-helpers';
export interface ServicePreviewProps {
appContext?: any;
functionKey?: string;
}
export function ServicePreview({ appContext, functionKey }: ServicePreviewProps) {
const [payload, setPayload] = useState({});
const [result, setResult] = useState<any>();
const [error, setError] = useState('');
return (
<Box>
<Panel title="请求参数" bodyProps={{ px: 'l' }}>
<InputCode
placeholder={'添加请求参数,对象格式,如 { key: value }'}
editable
showLineNumbers
onChange={(value: string) => {
const obj = code2object(value);
setPayload(obj);
}}
/>
<Button
block
type="primary"
style={{ margin: '8px 0' }}
disabled={!appContext}
onClick={() => {
if (!appContext) {
setError('执行上下文未准备好,请关闭面板重试');
return;
}
try {
const fn = getValue(appContext, functionKey);
fn(payload).then((data: any) => {
setError('');
setResult(data);
});
} catch (err) {
setError('接口调用失败,请检查参数是否正确');
logger.error(err);
}
}}
icon={<PlayCircleOutlined />}
>
</Button>
</Panel>
<Panel title="请求响应" bodyProps={{ px: 'l' }}>
{error || (result ? <ResponseDataPreview data={result} /> : '点击预览按钮测试接口返回值')}
</Panel>
</Box>
);
}
interface ResponseDataPreviewProps {
data?: object;
}
function ResponseDataPreview({ data }: ResponseDataPreviewProps) {
let ret: React.ReactNode;
if (!isNil(data)) {
switch (typeof data) {
case 'object':
ret = <JsonView src={data as object} />;
break;
default:
ret = String(data);
break;
}
}
const initialRet = <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />;
return (
<Box
className="ResponseDataPreview"
overflow="auto"
height="auto"
minHeight={260}
marginTop={10}
>
{ret || initialRet}
</Box>
);
}
@@ -0,0 +1,122 @@
import React, { useState } from 'react';
import { IVariableTreeNode, isNil, noop, useBoolean } from '@music163/tango-helpers';
import { Empty, Space, Button, Radio, Alert } from 'antd';
import { Box } from 'coral-system';
import { InputCode, Panel } from '@music163/tango-ui';
import { isValidExpressionCode } from '@music163/tango-core';
const previewOptions = [
{ label: '运行时', value: 'runtime' },
{ label: '定义', value: 'define' },
];
export type ValueDetailModeType = 'runtime' | 'define';
export interface ValueDetailProps {
defaultMode?: ValueDetailModeType;
children: (mode: ValueDetailModeType) => React.ReactNode;
}
export function ValueDetail({ defaultMode = 'runtime', children }: ValueDetailProps) {
const [mode, setMode] = useState<ValueDetailModeType>(defaultMode);
return (
<Panel
title="变量详情"
shape="solid"
extra={
<Radio.Group
optionType="button"
buttonStyle="solid"
size="small"
value={mode}
onChange={(e) => {
setMode(e.target.value);
}}
options={previewOptions}
/>
}
>
{children(mode)}
</Panel>
);
}
export interface ValueDefineProps {
data: IVariableTreeNode;
onSave?: (variableKey: string, code: string) => void;
}
/**
* 变量值定义面板
*/
export function ValueDefine({ data, onSave = noop }: ValueDefineProps) {
const [value, setValue] = useState(data.raw);
const [error, setError] = useState('');
const [editable, { on, off }] = useBoolean();
if (isNil(data.raw)) {
return <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="缺失定义数据" />;
}
return (
<Box>
<InputCode
shape="inset"
value={value}
onChange={(nextValue) => setValue(nextValue)}
readOnly={!editable}
onBlur={() => {
if (!value) {
setError('输入内容不可为空!');
} else if (value && !isValidExpressionCode(`foo = ${value}`)) {
// 这里先保证输入是一个合法的表达式
setError('代码格式错误,请检查代码语法!');
} else {
setError('');
}
}}
/>
<Box color="red" my="m">
{error}
</Box>
<Box mt="l">
{editable ? (
<Space>
<Button
onClick={() => {
off();
setError('');
}}
>
</Button>
<Button
type="primary"
onClick={() => {
if (error) {
return;
}
onSave(data.key, value);
off();
}}
>
</Button>
</Space>
) : (
<Button onClick={on}></Button>
)}
</Box>
</Box>
);
}
export interface NodeCommonDetailProps {
data: IVariableTreeNode;
}
export function NodeCommonDetail({ data }: NodeCommonDetailProps) {
if (!data?.help) {
return <div />;
}
return <Alert type="info" message={`使用说明:${data.help}`} closable />;
}
@@ -0,0 +1,34 @@
import React from 'react';
import { isFunction, isNil, isString } from '@music163/tango-helpers';
import { InputCode, JsonView, JsonViewProps } from '@music163/tango-ui';
import { Empty } from 'antd';
interface ValuePreviewProps {
value?: unknown;
/**
* 选择预览结点的回调
*/
onCopy?: JsonViewProps['onCopy'];
}
export function ValuePreview({ value, onCopy }: ValuePreviewProps) {
if (isNil(value)) {
return <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂时没有可预览的数据" />;
}
if (isFunction(value)) {
return <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂不支持预览函数" />;
}
if (typeof value === 'object') {
return <JsonView src={value} enableCopy onCopy={onCopy} />;
}
return (
<InputCode
shape="inset"
value={isString(value) ? `"${value}"` : String(value)}
editable={false}
/>
);
}
+2
View File
@@ -36,6 +36,8 @@ export function DesignerPanel(props: DesignerPanelProps) {
height="48px"
bg="colors.custom.topNavBg"
color="colors.custom.topNavColor"
borderBottom="solid"
borderColor="colors.custom.topNavBorderColor"
className="DesignerPanelHeader"
>
<Box display="flex" alignItems="center">
+3 -3
View File
@@ -3,9 +3,9 @@ import { SystemProvider, extendTheme } from 'coral-system';
import { ConfigProvider } from 'antd';
import { TangoEngineProvider, ITangoEngineContext } from '@music163/tango-context';
import { createFromIconfontCN } from '@ant-design/icons';
import { DesignerProvider, IDesignerContext } from './context';
import { theme as baseTheme } from './theme';
import zhCN from 'antd/lib/locale/zh_CN';
import { DesignerProvider, IDesignerContext } from './context';
import defaultTheme from './themes/default';
export interface DesignerProps extends IDesignerContext, ITangoEngineContext {
/**
@@ -34,7 +34,7 @@ export function Designer(props: DesignerProps) {
iconfontScriptUrl = '//at.alicdn.com/t/c/font_2891794_lzc7rtwuzf.js',
children,
} = props;
const theme = useMemo(() => extendTheme(themeProp, baseTheme), [themeProp]);
const theme = useMemo(() => extendTheme(themeProp, defaultTheme), [themeProp]);
useEffect(() => {
createFromIconfontCN({
scriptUrl: iconfontScriptUrl,
-1
View File
@@ -72,7 +72,6 @@ export function getElementData(
const bounding = getElementBoundingData(element, relativeContainer);
const data = getElementDataProps(element);
const dnd = parseDndId(data.dnd);
// FIXME: 是否改为按需执行,仅 dropTarget 需要 display 信息,且目前 display 信息并不准确
const display = getElementCSSDisplay(element);
return {
id: dnd.id,
+1
View File
@@ -1 +1,2 @@
export * from './dom';
export * from './template';
@@ -0,0 +1,9 @@
const newStoreTemplate = `
import { defineStore } from '@music163/tango-boot';
export default defineStore({
});
`;
export const CODE_TEMPLATES = {
newStoreTemplate,
};
+2
View File
@@ -12,6 +12,8 @@ export * from './sidebar';
export * from './toolbar';
export * from './selection-menu';
export * from './widgets';
export * from './themes';
export * from './components';
export { register as registerSetter } from '@music163/tango-setting-form';
+7 -1
View File
@@ -17,6 +17,12 @@ const navigatorStyle = css`
.navigatorInput {
flex: 1;
border-color: transparent;
background-color: var(--tango-colors-fill2);
&:focus {
background-color: #fff;
}
}
`;
@@ -68,7 +74,7 @@ export class Navigator extends React.Component<NavigatorProps, NavigatorState> {
className="navigator"
bg="white"
px="l"
py="4px"
py="m"
borderBottom="solid"
borderBottomColor="line.normal"
css={navigatorStyle}
+15
View File
@@ -23,6 +23,12 @@ export type SandboxProps = Omit<CodeSandboxProps, 'files' | 'eventHandlers' | 'o
* tangoConfigJson 处理器
*/
configFormatter?: IMergeTangoConfigJsonConfig['formatter'];
/**
* 文件格式化器
* @param files 沙箱文件列表
* @returns 修改后的文件列表
*/
filesFormatter?: (files: CodeSandboxProps['files']) => CodeSandboxProps['files'];
sandboxType?: 'design' | 'preview';
mode?: 'single' | 'combined';
injectScript?: string;
@@ -41,6 +47,7 @@ const LANDING_PAGE_PATH = '/__background_landing_page__';
function useSandbox({
isPreview: isPreviewProp,
configFormatter,
filesFormatter,
onViewChange,
onMessage: onMessageProp,
onLoad: onLoadProp,
@@ -79,7 +86,12 @@ function useSandbox({
prev[filename] = { code };
return prev;
}, {});
files = fixSandboxFiles(files, workspace.entry);
if (filesFormatter) {
// 支持用户对传入的文件进行修改
files = filesFormatter(files);
}
const onMessage = (data: any) => onMessageProp && onMessageProp(data, getSandboxConfig());
const onLoad = () => onLoadProp && onLoadProp(getSandboxConfig());
@@ -119,6 +131,7 @@ const PreviewSandbox = observer(
startRoute,
injectScript,
sandboxType: 'preview',
...rest,
});
return (
@@ -166,6 +179,7 @@ const DesignSandbox = observer(
startRoute,
injectScript,
sandboxType: 'design',
...rest,
});
return (
@@ -411,6 +425,7 @@ function fixSandboxFiles(files: Record<string, { code: string }>, entry = '/src/
`,
};
}
return files;
}
@@ -4,8 +4,7 @@ import { AutoComplete } from 'antd';
import { ActionSelect } from '@music163/tango-ui';
import { FormItemComponentProps } from '@music163/tango-setting-form';
import { useWorkspace, useWorkspaceData } from '@music163/tango-context';
import { wrapCode } from '@music163/tango-helpers';
import { ExpressionModal } from './expression-setter';
import { ExpressionModal, getWrappedExpressionCode } from './expression-setter';
enum EventAction {
NoAction = 'noAction',
@@ -48,8 +47,9 @@ export function EventSetter(props: EventSetterProps) {
const handleChange = useCallback<FormItemComponentProps['onChange']>(
(nextValue: any, ...args) => {
if (nextValue !== value) {
onChange(nextValue, ...args);
const ret = getWrappedExpressionCode(nextValue);
if (ret !== value) {
onChange(ret, ...args);
}
},
[onChange, value],
@@ -84,7 +84,7 @@ export function EventSetter(props: EventSetterProps) {
onCancel={() => setExpModalVisible(false)}
onOk={(nextValue) => {
setExpModalVisible(false);
handleChange(wrapCode(nextValue));
handleChange(nextValue);
}}
dataSource={actionVariables}
/>
@@ -1,24 +1,30 @@
import React, { useState, useEffect, useCallback } from 'react';
import { Box, Text, css } from 'coral-system';
import { Modal } from 'antd';
import { isValidExpressionCode, value2expressionCode } from '@music163/tango-core';
import {
isVariableString,
isValidExpressionCode,
isWrappedByExpressionContainer,
value2expressionCode,
} from '@music163/tango-core';
import {
getVariableContent,
noop,
useBoolean,
getValue,
wrapCode,
isStoreVariablePath,
IVariableTreeNode,
} 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 { VariableTree } from '../components';
import { useSandboxQuery } from '../context';
import { CODE_TEMPLATES } from '../helpers';
import { shapeServiceValues } from '../sidebar/datasource-panel/interface-config';
export const expressionValueValidate = (value: string) => {
if (isVariableString(value)) {
if (isWrappedByExpressionContainer(value)) {
const exp = getVariableContent(value);
if (!isValidExpressionCode(exp)) {
return '表达式存在语法错误!';
@@ -27,7 +33,7 @@ export const expressionValueValidate = (value: string) => {
};
export const jsonValueValidate = (value: string) => {
if (isVariableString(value)) {
if (isWrappedByExpressionContainer(value)) {
const jsonStr = getVariableContent(value);
try {
JSON.parse(jsonStr);
@@ -37,6 +43,23 @@ export const jsonValueValidate = (value: string) => {
}
};
/**
* 返回 `{}` 包裹后的表达式代码
* @param code 原始代码
* @returns
*/
export function getWrappedExpressionCode(code: string) {
let ret;
if (!code) {
// do nothing
} else if (isWrappedByExpressionContainer(code)) {
ret = code;
} else {
ret = `{${code}}`;
}
return ret;
}
const suffixStyle = css`
.anticon-close-circle {
color: rgba(0, 0, 0, 0.25);
@@ -63,6 +86,7 @@ export function ExpressionSetter(props: ExpressionSetterProps) {
value: valueProp,
status,
allowClear = true,
newStoreTemplate,
} = props;
const [visible, { on, off }] = useBoolean();
const [inputValue, setInputValue] = useState(() => {
@@ -78,15 +102,17 @@ export function ExpressionSetter(props: ExpressionSetterProps) {
const change = useCallback(
(code: string) => {
if (!code) {
onChange(undefined);
return;
}
if (getVariableContent(code) === value2expressionCode(valueProp)) {
if (code === valueProp) {
return;
}
onChange(wrapCode(code));
const ret = getWrappedExpressionCode(code);
if (ret === valueProp) {
return;
}
onChange(ret);
},
[valueProp, onChange],
);
@@ -127,11 +153,12 @@ export function ExpressionSetter(props: ExpressionSetterProps) {
subTitle={modalTip}
placeholder={placeholder}
autoCompleteOptions={autoCompleteOptions}
newStoreTemplate={newStoreTemplate}
visible={visible}
value={inputValue}
onCancel={() => off()}
onOk={(value) => {
onChange(value);
change(value);
off();
}}
/>
@@ -150,6 +177,10 @@ export interface ExpressionModalProps {
onOk?: (value: string) => void;
dataSource?: IVariableTreeNode[];
autoCompleteOptions?: string[];
/**
* 新建 store 的模板代码
*/
newStoreTemplate?: string;
}
export function ExpressionModal({
@@ -163,18 +194,19 @@ export function ExpressionModal({
value,
dataSource,
autoCompleteOptions,
newStoreTemplate = CODE_TEMPLATES.newStoreTemplate,
}: 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 serviceModules = Object.keys(workspace.serviceModules).map((key) => ({
label: key === 'index' ? '默认模块' : key,
value: key,
}));
const sandbox = useSandboxQuery();
const evaluateContext = sandbox.window;
const handleExpInputChange = (val: string) => {
@@ -189,15 +221,12 @@ export function ExpressionModal({
<Modal
closable={false}
destroyOnClose
width="70%"
width="60%"
open={visible}
onCancel={onCancel}
onOk={() => {
onOk(exp);
}}
okButtonProps={{
disabled: !!error,
}}
bodyStyle={{
padding: 0,
}}
@@ -218,10 +247,29 @@ export function ExpressionModal({
/>
{error ? <Text color="red"></Text> : null}
</Panel>
<Panel title="从变量列表中选中" shape="solid" borderTop="0">
<EditableVariableTree
<Panel
title="从变量列表中选中"
shape="solid"
borderTop="0"
overflow="hidden"
bodyProps={{ overflow: 'hidden' }}
>
<VariableTree
height={380}
showViewButton
dataSource={dataSource || expressionVariables}
appContext={sandbox?.window['tango']}
getStoreNames={() => Object.keys(workspace.storeModules)}
serviceModules={serviceModules}
getServiceData={(serviceKey) => {
const data = workspace.getServiceFunction(serviceKey);
return {
name: data.name,
moduleName: data.moduleName,
method: 'get',
...data.config,
};
}}
onSelect={(node) => {
if (!node.key) {
return;
@@ -237,17 +285,29 @@ export function ExpressionModal({
}
setExp(str);
}}
onAddVariable={(storeName, data) => {
onAction('addStoreState', [storeName, data.name, data.initialValue]);
onAddStoreVariable={(storeName, data) => {
workspace.addStoreState(storeName, data.name, data.initialValue);
}}
onDeleteVariable={(storeName, stateName) => {
onAction('removeStoreState', [storeName, stateName]);
onUpdateVariable={(variableKey, code) => {
workspace.updateStoreVariable(variableKey, code);
}}
onDeleteStore={(storeName) => {
onAction('removeStoreModule', [storeName]);
onAddStore={(storeName) => {
workspace.addStoreFile(storeName, newStoreTemplate);
}}
onSave={(code, node) => {
onAction('updateModuleCodeByVariablePath', [node.key, code]);
onRemoveVariable={(variablePath) => {
if (isStoreVariablePath(variablePath)) {
workspace.removeStoreVariable(variablePath);
} else {
workspace.removeServiceFunction(variablePath);
}
}}
onAddService={(data) => {
const { name, moduleName, ...payload } = shapeServiceValues(data);
workspace.addServiceFunction(name, payload, moduleName);
}}
onUpdateService={(data) => {
const { name, moduleName, ...payload } = shapeServiceValues(data);
workspace.updateServiceFunction(name, payload, moduleName);
}}
getPreviewValue={(node) => {
if (!node || !node.key) {
@@ -255,7 +315,7 @@ export function ExpressionModal({
}
if (node.type === 'function') {
return node.raw;
return;
}
return getValue(evaluateContext['tango'], node.key);
+3 -1
View File
@@ -168,4 +168,6 @@ const setters: IFormItemCreateOptions[] = [
{ name: 'borderSetter', component: BorderSetter },
];
setters.forEach(register);
export function registerBuiltinSetters() {
setters.forEach(register);
}
+35 -33
View File
@@ -1,12 +1,13 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { Box } from 'coral-system';
import { Input, Tooltip } from 'antd';
import { getValue, isFunction } from '@music163/tango-helpers';
import { getValue, isFunction, isStoreVariablePath } from '@music163/tango-helpers';
import { FormItemComponentProps } from '@music163/tango-setting-form';
import { MenuOutlined } from '@ant-design/icons';
import { useWorkspace, useWorkspaceData } from '@music163/tango-context';
import { EditableVariableTreeModal } from '../components';
import { VariableTreeModal } from '../components';
import { useSandboxQuery } from '../context';
import { CODE_TEMPLATES } from '../helpers';
function object2treeData(
val: any,
@@ -52,43 +53,40 @@ function traverseTreeData(val: any, callback: (val: any) => void) {
}
}
export function ModelSetter({ value, onChange }: FormItemComponentProps) {
export function ModelSetter({
value,
onChange,
newStoreTemplate = CODE_TEMPLATES.newStoreTemplate,
}: FormItemComponentProps) {
const [inputValue, setInputValue] = useState(value);
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 map = new Map();
const list: string[] = [];
traverseTreeData(modelVariables, (node) => {
if (node.key.split('.').length > 1) {
map.set(node.key, node);
list.push(node.key);
}
});
return map;
return list;
}, [modelVariables]);
const variables = evaluateContext['tango']?.stores
? [
object2treeData(evaluateContext['tango']?.stores, 'stores', 0, 1, (keyPath, val) => {
if (keyPath.split('.').length === 2 && definedVariables.has(keyPath)) {
return {
showAddChildIcon: true,
showRemoveIcon: true,
};
}
const ret: any = {};
if (isFunction(val)) {
// 不可以同步给函数类型变量
return {
disabled: true,
type: 'function',
};
ret.disabled = true;
ret.type = 'function';
}
if (!definedVariables.includes(keyPath)) {
ret.showAddButton = false;
ret.showRemoveButton = false;
}
return ret;
}),
]
: modelVariables;
@@ -122,31 +120,35 @@ export function ModelSetter({ value, onChange }: FormItemComponentProps) {
onChange={onInputChange}
onBlur={onInputBlur}
suffix={
<EditableVariableTreeModal
<VariableTreeModal
title="同步到的变量"
trigger={
<Tooltip title="从模型列表选择" placement="topRight">
<MenuOutlined />
</Tooltip>
}
modes={['preview']}
dataSource={variables as any[]}
onSelect={(node) => {
const modelPath = node.key.split('.').slice(1).join('.');
onChange(modelPath);
}}
onAddVariable={(storeName, data) => {
onAction('addStoreState', [storeName, data.name, data.initialValue]);
onAddStoreVariable={(storeName, data) => {
workspace.addStoreState(storeName, data.name, data.initialValue);
}}
onDeleteVariable={(storeName, stateName) => {
onAction('removeStoreState', [storeName, stateName]);
onUpdateVariable={(variableKey, code) => {
workspace.updateStoreVariable(variableKey, code);
}}
onDeleteStore={(storeName) => {
onAction('removeStoreModule', [storeName]);
onAddStore={(storeName) => {
workspace.addStoreFile(storeName, newStoreTemplate);
}}
onSave={(code, node) => {
onAction('updateModuleCodeByVariablePath', [node.key, code]);
onRemoveVariable={(variablePath) => {
if (isStoreVariablePath(variablePath)) {
workspace.removeStoreVariable(variablePath);
} else {
workspace.removeServiceFunction(variablePath);
}
}}
getStoreNames={() => Object.keys(workspace.storeModules)}
getPreviewValue={(node) => {
if (!node || !node.key) {
return;
+3 -1
View File
@@ -4,7 +4,9 @@ import { SettingForm, FormModel, SettingFormProps } from '@music163/tango-settin
import { Panel } from '@music163/tango-ui';
import { clone } from '@music163/tango-helpers';
import { observer, useDesigner, useWorkspace } from '@music163/tango-context';
import './setters';
import { registerBuiltinSetters } from './setters';
registerBuiltinSetters();
export interface SettingPanelProps extends SettingFormProps {
title?: React.ReactNode;
@@ -13,7 +13,7 @@ import { QuestionCircleOutlined } from '@ant-design/icons';
import { Button, Empty, Spin, Popover } from 'antd';
import { getDragGhostElement } from '../helpers';
type MenuKeyType = 'common' | 'atom' | 'snippet' | 'block' | 'bizComp';
type MenuKeyType = 'common' | 'atom' | 'snippet' | 'bizComp' | 'localComp';
type MenuValueType = Array<{ title: string; items: string[] }>;
export type MenuDataType = PartialRecord<MenuKeyType, MenuValueType>;
@@ -26,10 +26,6 @@ export interface ComponentsPanelProps {
* 展示业务组件分类
*/
showBizComps?: boolean;
/**
* 展示基础组件分类
*/
showBlocks?: boolean;
/**
* 动态加载物料 loading,避免未加载完成用户点击空列表
*/
@@ -47,7 +43,7 @@ const localeMap = {
atom: '原子组件',
snippet: '组合',
bizComp: '业务组件',
block: '区块',
localComp: '本地组件',
};
const emptyMenuData: MenuDataType = {
@@ -74,35 +70,24 @@ export function useFlatMenuData<T>(menuData: T) {
];
}
const StyledSpin = styled(Spin)`
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
margin-left: 25px;
`;
export const ComponentsPanel = observer(
({
menuData = emptyMenuData,
showBizComps = true,
showBlocks = true,
getBizCompName = upperCamelCase,
loading = false,
}: ComponentsPanelProps) => {
const [keyword, setKeyword] = useState<string>('');
const allList = useFlatMenuData<MenuDataType>(menuData);
const workspace = useWorkspace();
const [bizCompData, blockData] = useMemo(
const [bizCompData, localCompData] = useMemo(
() => [
[{ title: '已安装业务组件', items: workspace.bizComps.map(getBizCompName) }],
[{ title: '项目区块', items: workspace.blocks }],
[{ title: '本地组件', items: workspace.localComps }],
],
[workspace.bizComps, workspace.blocks, getBizCompName],
[workspace.bizComps, workspace.localComps, getBizCompName],
);
if (loading) return <StyledSpin tip="正在加载物料列表..." spinning />;
const tabs = Object.keys(menuData).map((key) => ({
key,
label: localeMap[key],
@@ -116,11 +101,12 @@ export const ComponentsPanel = observer(
children: <MaterialList type="bizComp" data={bizCompData} />,
});
}
if (showBlocks) {
if (localCompData.length) {
tabs.push({
key: 'block',
label: localeMap.block,
children: <MaterialList type="block" data={blockData} />,
key: 'localComps',
label: localeMap.localComp,
children: <MaterialList data={localCompData} />,
});
}
const contentNode =
@@ -135,7 +121,9 @@ export const ComponentsPanel = observer(
<Box px="l" py="m" position="sticky" top="0" zIndex={1} bg="white">
<Search placeholder="搜索物料" onChange={setKeyword} />
</Box>
{!keyword ? contentNode : <MaterialList data={allList} filterKeyword={keyword} />}
<Spin spinning={loading} tip="正在加载物料列表...">
{!keyword ? contentNode : <MaterialList data={allList} filterKeyword={keyword} />}
</Spin>
</Box>
);
},
@@ -153,7 +141,7 @@ interface MaterialListProps {
/**
* 物料类型
*/
type?: 'common' | 'bizComp' | 'block';
type?: 'common' | 'bizComp' | 'localComp';
}
function MaterialList({ data, filterKeyword, type = 'common' }: MaterialListProps) {
@@ -183,7 +171,7 @@ function MaterialList({ data, filterKeyword, type = 'common' }: MaterialListProp
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="没有匹配到任何组件" />
)}
<Grid
columns={type === 'block' ? 1 : 2}
columns={type === 'localComp' ? 1 : 2}
spacing="1px"
bg="background.normal"
padding="0"
@@ -193,9 +181,7 @@ function MaterialList({ data, filterKeyword, type = 'common' }: MaterialListProp
if (!prototype) {
logger.log(`<${item}> prototype not found!`);
}
return prototype ? (
<MaterialGrid key={item} data={prototype} type={type} />
) : null;
return prototype ? <MaterialGrid key={item} data={prototype} /> : null;
})}
{addBlock && <Box bg="white" />}
</Grid>
@@ -209,7 +195,6 @@ function MaterialList({ data, filterKeyword, type = 'common' }: MaterialListProp
interface MaterialProps {
data: ComponentPrototypeType;
type: 'common' | 'atom' | 'snippet' | 'bizComp' | 'block';
}
const StyledCommonGridItem = styled.div`
@@ -227,11 +212,6 @@ const StyledCommonGridItem = styled.div`
text-overflow: ellipsis;
white-space: nowrap;
&[class~='block-item'] {
flex-direction: row;
justify-content: flex-start;
}
.material-icon {
font-size: 40px;
}
@@ -268,7 +248,7 @@ const StyledCommonGridItem = styled.div`
}
`;
function MaterialGrid({ data, type }: MaterialProps) {
function MaterialGrid({ data }: MaterialProps) {
const workspace = useWorkspace();
const handleDragStart = (e: React.DragEvent) => {
@@ -294,7 +274,6 @@ function MaterialGrid({ data, type }: MaterialProps) {
data-name={data.name}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
className={type === 'block' ? 'block-item' : null}
>
{icon.startsWith('icon-') ? (
<IconFont className="material-icon" type={data.icon || 'icon-placeholder'} />
@@ -304,7 +283,7 @@ function MaterialGrid({ data, type }: MaterialProps) {
<Text fontSize="12px" lineHeight="1.5">
{data.title}
</Text>
<Text fontSize="12px" color={type === 'block' ? 'inherit' : 'gray.50'}>
<Text fontSize="12px" color="gray.50">
{data.name}
</Text>
{data.docs || data.help ? (
@@ -4,8 +4,6 @@ import { Tabs } from '@music163/tango-ui';
import InterfaceConfig from './interface-config';
import ProxyConfig from './proxy-config';
export * from './interface-config';
export function DataSourcePanel(props: Record<string, any>) {
return (
<Box className="DataSourceView" height="100%" overflowY="auto">
@@ -1,535 +1,60 @@
import React, { useState, useEffect, useMemo } from 'react';
import React from 'react';
import { observer, useWorkspace, useWorkspaceData } from '@music163/tango-context';
import { Box, css } from 'coral-system';
import { Button, FormProps, Empty, Space, Dropdown } from 'antd';
import { PlayCircleOutlined } from '@ant-design/icons';
import { Form, InputCode, Panel, JsonView, Search } from '@music163/tango-ui';
import {
isNil,
isVariableString,
getVariableContent,
isValidFunctionCode,
logger,
code2object,
filterTreeData,
} from '@music163/tango-helpers';
import { useSandboxQuery } from '../../context';
import { Box } from 'coral-system';
import { getVariableContent, parseServiceVariablePath } from '@music163/tango-helpers';
import { isWrappedByExpressionContainer } from '@music163/tango-core';
import { VariableTree } from '../../components';
import { useSandboxQuery } from '../../context';
/*
* 服务函数的操作类型
*/
enum ServiceFunctionOperationModeType {
ADD = 'add',
UPDATE = 'update',
DELETE = 'delete',
}
/**
* 服务函数 HTTP Type
* 云音乐网关只支持 get 和 post
*/
enum ServiceFunctionMethodType {
GET = 'GET',
// PUT = 'PUT',
POST = 'POST',
// PATCH = 'PATCH',
// DELETE = 'DELETE',
}
/**
* requestType
* headers: Content-Type
*/
enum DataServiceRequestType {
'application/json' = 'json',
'application/x-www-form-urlencoded' = 'x-www-form-urlencoded',
}
/**
* 将 api 路径转换为默认的驼峰方法名
*/
export const getApiDefaultName = (url: string) =>
url
// 去除 api + 模块名前缀
// - 云音乐 api 规范为 /api/模块名/
// - 后端公技基本使用 /模块名/api/
// - 中台类服务似乎常用 /api/middle/模块名/
// TODO: 是否需要去除模块名?一般同一个应用是同一个模块名,去掉可以缩减方法名长度,但确实有模块名不一样且后面的路径完全一致的接口
// 目前的实现是去除了模块名,只干掉 /api/middle/ 和 /api/backend/ 这种常用前缀
.replace(/^\/[^/]+?\/api\/|^\/api\/middle\/|^\/api\/backend\/|^\/api\//, '')
// 去除路由参数
.replace(/\/\{.*?\}/, '')
// 忽略下划线与减号,将后面的字符转成大驼峰
.replace(/[-/_]+\w/g, (str) => str.replace(/[-/_]+/, '').toUpperCase())
// 首字母转小写
.replace(/^./, (str) => str.toLowerCase())
// 方法名以数字开头,添加 api 前缀
.replace(/^\d/, (str) => `api${str}`);
interface DataServiceViewProps {
onAdd?: (values: Record<string, string> | Array<Record<string, string>>) => void;
onUpdate?: (values: Record<string, string>) => void;
onDelete?: (values: Record<string, string>) => void;
}
// http 方法类型
const httpMethods = Object.keys(ServiceFunctionMethodType).map((key) => ({
label: key,
value: key,
}));
// requestType 类型
const requestTypes = Object.keys(DataServiceRequestType).map((key) => ({
label: key,
value: DataServiceRequestType[key],
}));
const detailFormStyle = css`
.ant-form-item {
margin-bottom: 12px;
// 移除掉不必要的属性
export function shapeServiceValues(val: any) {
const shapeValues = { ...val };
delete shapeValues.type;
// 兼容旧版,如果 formatter 包裹了 {} 则删掉首尾
if (shapeValues.formatter && isWrappedByExpressionContainer(shapeValues.formatter)) {
shapeValues.formatter = getVariableContent(shapeValues.formatter);
}
`;
return shapeValues;
}
const DataSourceView = observer(({ onAdd, onUpdate, onDelete }: DataServiceViewProps) => {
const [serviceData, setServiceData] = useState<any>();
const [keyword, setKeyword] = useState<string>();
const workspace = useWorkspace();
const DataSourceView = observer(() => {
const sandbox = useSandboxQuery();
const workspace = useWorkspace();
const { serviceVariables } = useWorkspaceData();
const serviceModules = Object.keys(workspace.serviceModules).map((key) => ({
label: key === 'index' ? '默认模块' : key,
value: key,
}));
const serviceFunctions = serviceVariables.reduce((acc, cur) => {
acc = acc.concat(cur.children.map((item: any) => item.key));
return acc;
}, []);
const dataSource = useMemo(() => {
if (!keyword) {
return serviceVariables;
}
return filterTreeData(
serviceVariables,
(leaf) => leaf.title.includes(keyword),
'children',
true,
);
}, [serviceVariables, keyword]);
const isAddMode = serviceData && !serviceData.name;
return (
<Box className="ServiceFunctionList" display="flex" borderTopColor="line.normal">
<Box width="35%" overflow="auto" borderRight="solid" borderColor="line.normal">
<Panel
height="100%"
bodyProps={{
flex: 1,
}}
>
<Box
p="m"
display="flex"
flexDirection="column"
justifyContent="center"
alignItems="center"
rowGap="m"
>
<Button
block
onClick={() => {
setServiceData({});
}}
>
</Button>
<Search placeholder="搜索服务函数" onChange={setKeyword} />
</Box>
<VariableTree
dataSource={dataSource}
showDeleteIcon
onSelect={(item) => {
setServiceData({ key: item.key, ...workspace.getServiceFunction(item.key) });
}}
onRemove={(item) => {
workspace.removeServiceFunction(item.key);
onDelete?.(item);
}}
/>
</Panel>
</Box>
<Box width="65%" overflow="auto" css={detailFormStyle}>
{serviceData && (
<>
<Panel title={isAddMode ? '新建服务函数' : '服务函数详情'}>
<ServiceDetailForm
key={serviceData.key || 'createSF'}
serviceModules={serviceModules}
serviceKeys={serviceFunctions}
initialValues={
isAddMode
? {
moduleName: 'index',
}
: {
name: serviceData.name,
moduleName: serviceData.moduleName,
method: 'get',
...serviceData.config,
}
}
onCancel={() => {
setServiceData(undefined);
}}
onSubmit={(values, mode: ServiceFunctionOperationModeType) => {
function shapeServiceValues(val: any) {
const shapeValues = { ...val };
delete shapeValues.type;
// 兼容旧版,如果 formatter 包裹了 {} 则删掉首尾
if (shapeValues.formatter && isVariableString(shapeValues.formatter)) {
shapeValues.formatter = getVariableContent(shapeValues.formatter);
}
return shapeValues;
}
// 移除掉不必要的属性
const { moduleName, ...data } = shapeServiceValues(values);
if (mode === ServiceFunctionOperationModeType.ADD) {
workspace.addServiceFunction(data, moduleName);
onAdd && onAdd(values);
} else if (mode === ServiceFunctionOperationModeType.UPDATE) {
workspace.updateServiceFunction(data, moduleName);
onUpdate && onUpdate(values);
}
}}
/>
</Panel>
{serviceData?.key ? (
<ServiceFunctionPreview
key={serviceData.key}
appContext={sandbox?.window['tango']}
functionName={serviceData.key}
/>
) : null}
</>
)}
{!serviceData && (
<Box py="xxl">
<Empty description="请从左侧选择数据服务函数或新建数据服务函数" />
</Box>
)}
</Box>
<Box className="ServiceFunctionList" p="m">
<VariableTree
dataSource={serviceVariables}
appContext={sandbox?.window['tango']}
serviceModules={serviceModules}
onRemoveVariable={(variableKey) => {
workspace.removeServiceFunction(variableKey);
}}
onAddService={(data) => {
const { name, moduleName, ...payload } = shapeServiceValues(data);
workspace.addServiceFunction(name, payload, moduleName);
}}
onUpdateService={(data) => {
const { name, moduleName, ...payload } = shapeServiceValues(data);
workspace.updateServiceFunction(name, payload, moduleName);
}}
getServiceData={(serviceKey) => {
const data = workspace.getServiceFunction(serviceKey);
return {
name: data.name,
moduleName: data.moduleName,
method: 'get',
...data.config,
};
}}
/>
</Box>
);
});
interface ServiceDetailFormProps extends FormProps {
serviceKeys?: string[];
serviceModules?: any[];
onCancel?: () => void;
onSubmit?: (values: any, mode: ServiceFunctionOperationModeType) => void;
}
function ServiceDetailForm({
serviceKeys = [],
serviceModules = [],
onCancel,
onSubmit,
initialValues,
...formProps
}: ServiceDetailFormProps) {
const isModifyMode = !!initialValues?.name; // 是否为更新模式
const [disabled, setDisabled] = useState(isModifyMode);
const [form] = Form.useForm();
// 监听服务类型变化,默认是自定义
const formType = Form.useWatch('type', form);
// 新建的时候,类型变换重置部分字段
useEffect(() => {
if (!formType) return;
!isModifyMode &&
form.setFieldsValue({
url: undefined,
method: undefined,
formatter: undefined,
});
}, [formType, form, isModifyMode]);
return (
<Form
form={form}
labelCol={6}
wrapperCol={17}
colon={false}
layout="horizontal"
initialValues={initialValues}
onFinish={(values) => {
onSubmit(
values,
isModifyMode
? ServiceFunctionOperationModeType.UPDATE
: ServiceFunctionOperationModeType.ADD,
);
setDisabled(true);
}}
{...formProps}
>
<Form.Item
label="所属模块"
name="moduleName"
rules={[
{
required: true,
},
]}
component="select"
componentProps={{
options: serviceModules,
disabled: disabled || isModifyMode,
}}
/>
<Form.Item
label="方法名"
name="name"
rules={[
{ required: true },
{ pattern: /^[a-z]\w+$/, message: '请输入合法的方法名称' },
!isModifyMode && {
validator(_, value) {
const isExist = serviceKeys.includes(
['services', form.getFieldValue('moduleName'), value].join('.'),
);
return isExist
? Promise.reject(new Error('重复的方法名,请换一个!'))
: Promise.resolve();
},
},
]}
component="input"
componentProps={{
placeholder: '请输入数据服务调用名称',
disabled: disabled || isModifyMode,
}}
extra={
!disabled &&
!isModifyMode && (
<Box>
<Button
type="link"
size="small"
disabled={!form.getFieldValue('url')}
onClick={() => {
const funcName = getApiDefaultName(form.getFieldValue('url'));
form.setFieldValue('name', funcName);
}}
>
使
</Button>
</Box>
)
}
/>
<Form.Item
label="路径"
name="url"
rules={[{ required: true, type: 'url' }]}
component="input"
componentProps={{
placeholder: '请输入数据服务调用名称',
disabled: disabled || isModifyMode,
}}
/>
<Form.Item
label="方法"
name="method"
component="select"
componentProps={{
options: httpMethods,
placeholder: '请选择 HTTP 请求方法',
disabled,
}}
/>
<Form.Item
label="编码类型"
name="requestType"
tooltip="设置请求头的 Content-Type,若不设置,则默认变为为 JSON"
component="select"
componentProps={{
options: requestTypes,
placeholder: '请求头Content-Type,默认为JSON',
disabled,
}}
/>
<Form.Item
label="格式化响应"
name="formatter"
tooltip="提供格式化函数,对结果进行格式化"
validateTrigger="onBlur"
rules={[
{
validator(_, value) {
if (!value) {
return Promise.resolve();
}
return isValidFunctionCode(value)
? Promise.resolve()
: Promise.reject(new Error('请提供合法的函数代码!'));
},
},
]}
extra={
!disabled && (
<Box>
<Dropdown
menu={{
items: [
{ label: '直接返回(默认)', key: 'res => res' },
{ label: '返回数据部分(云音乐标准规范)', key: 'res => res.data' },
],
onClick: ({ key }) => {
form.setFieldValue('formatter', key);
},
}}
>
<Button type="link" size="small">
使
</Button>
</Dropdown>
</Box>
)
}
>
<InputCode editable={!disabled} showLineNumbers placeholder="res => res" />
</Form.Item>
<Form.Item wrapperCol={{ offset: 6, span: 18 }}>
{isModifyMode ? (
<Space>
{!disabled ? (
<>
<Button
onClick={() => {
setDisabled(true);
}}
>
</Button>
<Button type="primary" htmlType="submit">
</Button>
</>
) : (
<Button
onClick={() => {
setDisabled(false);
}}
>
</Button>
)}
</Space>
) : (
<Space>
<Button onClick={onCancel}></Button>
<Button type="primary" htmlType="submit">
</Button>
</Space>
)}
</Form.Item>
</Form>
);
}
interface ServicePreviewProps {
data?: object;
}
function ServicePreview({ data }: ServicePreviewProps) {
let ret: React.ReactNode;
if (!isNil(data)) {
switch (typeof data) {
case 'object':
ret = <JsonView src={data as object} />;
break;
default:
ret = String(data);
break;
}
}
const initialRet = <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />;
return (
<Box
className="ServicePreviewPanel"
overflow="auto"
height="auto"
minHeight={260}
marginTop={10}
>
{ret || initialRet}
</Box>
);
}
interface ServiceFunctionPreviewProps {
appContext?: any;
functionName?: string;
}
function ServiceFunctionPreview({ appContext, functionName }: ServiceFunctionPreviewProps) {
const [payload, setPayload] = useState({});
const [result, setResult] = useState<any>();
const [error, setError] = useState('');
return (
<Panel
title="接口测试"
extra={
<Space>
<Button
disabled={!appContext}
size="small"
onClick={() => {
if (!appContext) {
setError('执行上下文未准备好,请关闭面板重试');
return;
}
try {
appContext.services[functionName](payload).then((data: any) => {
setResult(data);
});
} catch (err) {
setError('接口调用失败,请检查参数是否正确');
logger.error(err);
}
}}
icon={<PlayCircleOutlined />}
>
</Button>
</Space>
}
borderTop="solid"
borderTopColor="line.normal"
>
<Panel title="请求参数" bodyProps={{ px: 'l' }}>
<InputCode
placeholder={'添加请求参数,对象格式,如 { key: value }'}
editable
showLineNumbers
onChange={(value: string) => {
const obj = code2object(value);
setPayload(obj);
}}
/>
</Panel>
<Panel title="请求响应" bodyProps={{ px: 'l' }}>
{error || (result ? <ServicePreview data={result} /> : '点击预览按钮测试接口返回值')}
</Panel>
</Panel>
);
}
export default DataSourceView;
@@ -366,19 +366,19 @@ function RenderItem({
</Popconfirm>,
]
: baseNeedUpgrade
? [
<Popconfirm
title={`确认升级 ${record.name}${basePackage.version} 吗?`}
onConfirm={() => onDepUpgrade({ version: basePackage.version })}
>
<a key="upgrade"></a>
</Popconfirm>,
]
: [
<Text key="latest" color={'rgba(0, 0, 0, 0.45)'}>
{!isUndefined(baseNeedUpgrade) && '已是最新'}
</Text>,
]
? [
<Popconfirm
title={`确认升级 ${record.name}${basePackage.version} 吗?`}
onConfirm={() => onDepUpgrade({ version: basePackage.version })}
>
<a key="upgrade"></a>
</Popconfirm>,
]
: [
<Text key="latest" color={'rgba(0, 0, 0, 0.45)'}>
{!isUndefined(baseNeedUpgrade) && '已是最新'}
</Text>,
]
}
>
<List.Item.Meta
@@ -453,7 +453,7 @@ function AddDependencyModal({
const onFinishCallback = (name?: string) => {
name && message.success(`${name} 添加成功`);
// FIXME: 修复沙箱添加组件后 HMR 失效的 bug,添加完刷新
// TIP: 修复沙箱添加组件后 HMR 失效的 bug,添加完刷新
sandbox.reload();
off();
};
+2 -1
View File
@@ -4,4 +4,5 @@ export * from './components-panel';
export * from './dependency-panel';
export * from './history-panel';
export * from './variable-panel';
export * from './sidebar';
export * from './sidebar';
export * from './resizable-box';
@@ -1,14 +1,34 @@
import React, { useEffect, useMemo, useState } from 'react';
import { Space, Tree } from 'antd';
import { Dropdown, Space, Tree } from 'antd';
import { Box, css } from 'coral-system';
import { IconFont } from '@music163/tango-ui';
import { EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons';
import { EyeOutlined, EyeInvisibleOutlined, EllipsisOutlined } from '@ant-design/icons';
import { observer, useWorkspace } from '@music163/tango-context';
import { DropMethod, ITangoViewNodeData } from '@music163/tango-core';
import { parseDndId } from '@music163/tango-helpers';
import { noop, parseDndId } from '@music163/tango-helpers';
import { useSandboxQuery } from '../../context';
import { buildQueryBySlotId } from '../../helpers';
export interface ComponentsTreeProps {
/**
* 显示/隐藏组件按钮,默认显示
*/
showToggleVisibleIcon?: boolean;
/**
* 页面节点选中回调
*/
onSelect?: (nodeKey: string) => void;
/**
* 节点树更多功能菜单
*/
actionItems?: ActionItem[];
}
interface ActionItem {
item: React.ReactNode;
key: string;
}
const outlineStyle = css`
.ant-tree-title {
color: rgb(85, 85, 85);
@@ -32,6 +52,10 @@ const outlineStyle = css`
.ant-tree-switcher-noop {
display: none;
}
.material-icon {
width: 1em;
height: 1em;
}
`;
const filedNames = {
@@ -51,126 +75,159 @@ const getNodeKeys = (data: ITangoViewNodeData[]) => {
return ids;
};
const OutlineTreeNode: React.FC<any> = observer(({ node }: { node: ITangoViewNodeData }) => {
const workspace = useWorkspace();
const sandboxQuery = useSandboxQuery();
const [visible, setVisible] = useState(true);
const nodeLabel = useMemo(() => {
const { component, index } = parseDndId(node.id);
return `${component}:${index}`;
}, [node.id]);
const componentPrototype = workspace.componentPrototypes.get(node.component);
const icon = componentPrototype?.icon || 'icon-placeholder';
const OutlineTreeNode: React.FC<{ node: ITangoViewNodeData } & ComponentsTreeProps> = observer(
({ node, showToggleVisibleIcon, actionItems }) => {
const workspace = useWorkspace();
const sandboxQuery = useSandboxQuery();
const [visible, setVisible] = useState(true);
const nodeLabel = useMemo(() => {
const { component, index } = parseDndId(node.id);
return `${component}:${index}`;
}, [node.id]);
const componentPrototype = workspace.componentPrototypes.get(node.component);
const icon = componentPrototype?.icon || 'icon-placeholder';
const iconRender = icon.startsWith('icon-') ? (
<IconFont className="material-icon" type={icon} />
) : (
<img src={icon} alt={componentPrototype.name} />
);
const iconRender = icon.startsWith('icon-') ? (
<IconFont className="material-icon" type={icon} />
) : (
<img className="material-icon" src={icon} alt={componentPrototype.name} />
);
const toggleVisible = (nodeId: string) => {
sandboxQuery.getElement(buildQueryBySlotId(nodeId)).style.visibility = visible
? 'hidden'
: 'visible';
setVisible(!visible);
};
const toggleVisible = (nodeId: string) => {
sandboxQuery.getElement(buildQueryBySlotId(nodeId)).style.visibility = visible
? 'hidden'
: 'visible';
setVisible(!visible);
};
const VisibleIcon = visible ? EyeOutlined : EyeInvisibleOutlined;
return (
<Space>
{iconRender} {nodeLabel}
{/* 切换显示 */}
<VisibleIcon
className="material-icon"
onClick={(e) => {
e.stopPropagation();
toggleVisible(node.id);
}}
/>
</Space>
);
});
export const ComponentsTree = observer(() => {
const workspace = useWorkspace();
const sandboxQuery = useSandboxQuery();
const [selectedKeys, setSelectedKeys] = useState<React.Key[]>(
workspace.selectSource.selected.map((item) => item.id),
);
const file = workspace.activeViewModule;
const nodesTree = file.nodesTree as ITangoViewNodeData[];
useEffect(() => {
setSelectedKeys(workspace.selectSource.selected.map((item) => item.id));
}, [workspace.selectSource.selected]);
if (!nodesTree.length) {
const VisibleIcon = visible ? EyeOutlined : EyeInvisibleOutlined;
return (
<Box>
<code>{workspace.activeViewFile}</code>
<Space>
{iconRender} {nodeLabel}
{/* 切换显示 */}
{showToggleVisibleIcon && (
<VisibleIcon
className="material-icon"
onClick={(e) => {
e.stopPropagation();
toggleVisible(node.id);
}}
/>
)}
{/* 更多 ... 功能菜单 */}
{actionItems?.length > 0 && (
<Dropdown
menu={{
items: actionItems.map((_) => ({
key: _.key,
label: _.item,
})),
}}
trigger={['click']}
>
<EllipsisOutlined />
</Dropdown>
)}
</Space>
);
},
);
export const ComponentsTree: React.FC<ComponentsTreeProps> = observer(
({ showToggleVisibleIcon = true, onSelect = noop, actionItems }) => {
const workspace = useWorkspace();
const sandboxQuery = useSandboxQuery();
const [selectedKeys, setSelectedKeys] = useState<React.Key[]>(
workspace.selectSource.selected.map((item) => item.id),
);
const file = workspace.activeViewModule;
const nodesTree = (file?.nodesTree ?? []) as ITangoViewNodeData[];
const [expandedKeys, setExpandedKeys] = useState(getNodeKeys(nodesTree));
useEffect(() => {
setSelectedKeys(workspace.selectSource.selected.map((item) => item.id));
}, [workspace.selectSource.selected]);
useEffect(() => {
setExpandedKeys(getNodeKeys(nodesTree));
// nodeTree 更新后重新计算expandKeys
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [nodesTree?.[0]?.id]);
if (!nodesTree.length) {
return (
<Box>
<code>{workspace.activeViewFile}</code>
</Box>
);
}
return (
<Box css={outlineStyle}>
<Tree
selectedKeys={selectedKeys as string[]}
fieldNames={filedNames}
treeData={nodesTree as any[]}
onSelect={(keys) => {
const slotKey = keys?.[0] as string;
const data = sandboxQuery.getDraggableParentsData(buildQueryBySlotId(slotKey), true);
if (data && data.id) {
workspace.selectSource.select({
id: data.id,
name: data.name,
bounding: data.bounding,
parents: data.parents,
});
}
// export selected
onSelect(slotKey);
setSelectedKeys(keys);
}}
blockNode
expandedKeys={expandedKeys}
onExpand={(keys) => setExpandedKeys(keys as string[])}
draggable
onDragStart={(data) => {
const prototype = workspace.componentPrototypes.get(data.node.component);
if (!prototype) {
return;
}
const { canDrag } = prototype.rules || {};
if (canDrag && !canDrag()) {
return;
}
workspace.dragSource.set({
id: data.node.id,
name: data.node.component,
});
}}
onDrop={(data) => {
const dropKey = data.node.key as string;
let method;
if (data.dropToGap) {
// 插入节点的后面
method = DropMethod.InsertAfter;
} else {
// 作为第一个子节点
method = DropMethod.InsertFirstChild;
}
workspace.dragSource.dropTarget.set(
{
id: dropKey,
},
method,
);
workspace.dropNode();
}}
titleRender={(node) => (
<OutlineTreeNode
actionItems={actionItems}
showToggleVisibleIcon={showToggleVisibleIcon}
node={node}
/>
)}
/>
</Box>
);
}
return (
<Box css={outlineStyle}>
<Tree
selectedKeys={selectedKeys}
fieldNames={filedNames}
treeData={nodesTree as any[]}
onSelect={(keys) => {
const data = sandboxQuery.getDraggableParentsData(
buildQueryBySlotId(keys[0] as string),
true,
);
if (data && data.id) {
workspace.selectSource.select({
id: data.id,
name: data.name,
bounding: data.bounding,
parents: data.parents,
});
}
setSelectedKeys(keys);
}}
blockNode
autoExpandParent
expandedKeys={getNodeKeys(nodesTree)}
draggable
onDragStart={(data) => {
const prototype = workspace.componentPrototypes.get(data.node.component);
if (!prototype) {
return;
}
const { canDrag } = prototype.rules || {};
if (canDrag && !canDrag()) {
return;
}
workspace.dragSource.set({
id: data.node.id,
name: data.node.component,
});
}}
onDrop={(data) => {
const dropKey = data.node.key as string;
let method;
if (data.dropToGap) {
// 插入节点的后面
method = DropMethod.InsertAfter;
} else {
// 作为第一个子节点
method = DropMethod.InsertFirstChild;
}
workspace.dragSource.dropTarget.set(
{
id: dropKey,
},
method,
);
workspace.dropNode();
}}
titleRender={(node) => <OutlineTreeNode node={node} />}
/>
</Box>
);
});
},
);
@@ -2,20 +2,26 @@ import React from 'react';
import { Box } from 'coral-system';
import { CollapsePanel } from '@music163/tango-ui';
import { StateTree } from './state-tree';
import { ComponentsTree } from './components-tree';
import { ComponentsTree, ComponentsTreeProps } from './components-tree';
export interface OutlineViewProps {
export interface OutlineViewProps extends ComponentsTreeProps {
/**
* 展示状态视图
*/
showStateView?: boolean;
}
export function OutlinePanel({ showStateView = true }: OutlineViewProps) {
export function OutlinePanel({ showStateView = true, ...treeProps }: OutlineViewProps) {
return (
<Box height="100%" position="relative">
<CollapsePanel title="页面结构" stickyHeader maxHeight="90%" overflowY="auto">
<ComponentsTree />
<CollapsePanel
title="页面结构"
maxHeight={showStateView ? '90%' : '100%'}
collapsed={!showStateView ? false : undefined}
overflowY="auto"
showBottomBorder={showStateView}
>
<ComponentsTree {...treeProps} />
</CollapsePanel>
{showStateView && (
<CollapsePanel title="页面状态" stickyHeader maxHeight="90%" overflowY="auto">
+74 -18
View File
@@ -1,45 +1,101 @@
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import cx from 'classnames';
import { Box, HTMLCoralProps, css } from 'coral-system';
import { Resizable } from 'react-resizable';
import { Resizable, ResizeHandle } from 'react-resizable';
const resizeHandleStyle = css`
export interface ResizableBoxProps {
width?: number;
height?: number;
resizeHandlePosition?: 'left' | 'right' | 'top' | 'bottom';
axis?: 'x' | 'y';
children?: React.ReactNode;
className?: string;
style?: React.CSSProperties;
}
const resizeHandleStyle = (axis: ResizableBoxProps['axis'], barStyle: HTMLCoralProps<'div'>) => css`
position: absolute;
top: 0;
z-index: 999;
width: 4px;
height: 100%;
cursor: col-resize;
cursor: ${axis === 'x' ? 'col-resize' : 'row-resize'};
${barStyle &&
Object.keys(barStyle)
.map((key) => `${key}: ${barStyle[key]};`)
.join('')}
&:hover,
&:active {
background-color: var(--tango-colors-brand);
}
`;
export interface ResizableBoxProps extends HTMLCoralProps<'div'> {
width?: number;
height?: number;
resizeHandlePosition?: 'left' | 'right';
}
const getResizeHandles = (
resizeHandlePosition: ResizableBoxProps['resizeHandlePosition'],
axis: ResizableBoxProps['axis'],
) => {
const handles: ResizeHandle[] = [];
switch (axis) {
case 'x':
handles.push(resizeHandlePosition === 'right' ? 'e' : 'w');
break;
case 'y':
handles.push(resizeHandlePosition === 'bottom' ? 's' : 'n');
break;
default:
break;
}
return handles;
};
export function ResizableBox({
resizeHandlePosition = 'right',
width: widthProp,
height,
height: heightProp,
children,
className,
style,
axis = 'x',
}: ResizableBoxProps) {
const [width, setWidth] = useState(widthProp);
const barStyle = resizeHandlePosition === 'right' ? { right: '-4px' } : { left: '-4px' };
const [height, setHeight] = useState(heightProp);
const barStyle: HTMLCoralProps<'div'> = {
...(axis === 'x'
? {
height: '100%',
width: '4px',
top: '0',
[resizeHandlePosition === 'right' ? 'right' : 'left']: '-4px',
}
: {
height: '4px',
width: '100%',
left: '0',
[resizeHandlePosition === 'bottom' ? 'bottom' : 'top']: '0',
}),
};
useEffect(() => {
setWidth(widthProp);
}, [widthProp]);
useEffect(() => {
setHeight(heightProp);
}, [heightProp]);
return (
<Resizable
axis="x"
axis={axis}
width={width}
resizeHandles={getResizeHandles(resizeHandlePosition, axis)}
height={height}
minConstraints={[150, 150]}
maxConstraints={[window.innerWidth * 0.6, window.innerHeight * 0.8]}
onResize={(e, { size }) => {
setWidth(size.width);
if (axis === 'x') {
setWidth(size.width);
}
if (axis === 'y') {
setHeight(size.height);
}
}}
onResizeStart={() => {
document.body.style.pointerEvents = 'none';
@@ -49,7 +105,7 @@ export function ResizableBox({
document.body.style.pointerEvents = 'auto';
document.body.style.userSelect = 'auto';
}}
handle={<Box className="ResizeHandle" css={resizeHandleStyle} {...barStyle} />}
handle={<Box className="ResizeHandle" css={resizeHandleStyle(axis, barStyle)} />}
>
<div
className={cx('ResizableBox', className)}
+36 -26
View File
@@ -75,8 +75,10 @@ export interface SidebarPanelItemProps
}
function BaseSidebarPanel({ panelWidth: defaultPanelWidth = 280, footer, children }: SidebarProps) {
const designer = useDesigner();
const items = useMemo(() => {
const ret: Record<React.Key, SidebarPanelItemProps> = {};
const ret: Record<string, SidebarPanelItemProps> = {};
React.Children.forEach(children, (child) => {
if (child && React.isValidElement(child)) {
const widget = getWidget(['sidebar', child.key].join('.'));
@@ -90,20 +92,6 @@ function BaseSidebarPanel({ panelWidth: defaultPanelWidth = 280, footer, childre
return ret;
}, [children]);
const designer = useDesigner();
const panel = items[designer.activeSidebarPanel];
const floatPanelStyle: any = panel?.isFloat
? {
position: 'absolute',
left: '50px',
top: 0,
zIndex: 1000,
height: '100%',
boxShadow: 'var(--tango-shadows-lowRight)',
}
: {};
const panelWidth = typeof panel?.width === 'number' ? panel?.width : defaultPanelWidth;
return (
<Box display="flex" flexShrink={0} css={sidebarStyle} className="SidebarPanel">
<Box className="SidebarPanelBar" display="flex" flexDirection="column" position="relative">
@@ -142,19 +130,41 @@ function BaseSidebarPanel({ panelWidth: defaultPanelWidth = 280, footer, childre
{footer}
</Box>
</Box>
{panel ? (
<ResizableBox key={panel.label} width={panelWidth} style={floatPanelStyle}>
<SidebarPanelExpandedContent
isFloat={panel.isFloat}
closeable={panel.isFloat}
onClose={() => {
designer.closeSidebarPanel();
{Object.keys(items).map((key) => {
const panel = items[key];
const floatPanelStyle: any = panel?.isFloat
? {
position: 'absolute',
left: '50px',
top: 0,
zIndex: 1000,
height: '100%',
boxShadow: 'var(--tango-shadows-lowRight)',
}
: {};
const panelWidth = typeof panel?.width === 'number' ? panel?.width : defaultPanelWidth;
return (
<ResizableBox
key={panel.label}
width={panelWidth}
style={{
display: designer.activeSidebarPanel === key ? 'block' : 'none',
...floatPanelStyle,
}}
>
{panel.children}
</SidebarPanelExpandedContent>
</ResizableBox>
) : null}
<SidebarPanelExpandedContent
isFloat={panel.isFloat}
closeable={panel.isFloat}
onClose={() => {
designer.closeSidebarPanel();
}}
>
{panel.children}
</SidebarPanelExpandedContent>
</ResizableBox>
);
})}
</Box>
);
}
@@ -1,132 +1,61 @@
import React from 'react';
import { Button, Space } from 'antd';
import { Button } from 'antd';
import { FileAddOutlined } from '@ant-design/icons';
import { useBoolean } from '@music163/tango-helpers';
import { Panel, Form, IconButton } from '@music163/tango-ui';
import { FileAddOutlined, QuestionCircleOutlined } from '@ant-design/icons';
import { Panel } from '@music163/tango-ui';
import { observer, useWorkspace, useWorkspaceData } from '@music163/tango-context';
import { EditableVariableTree, EditableVariableTreeProps } from '../components';
import { VariableTree } from '../components';
import { CODE_TEMPLATES } from '../helpers';
import { AddStoreForm } from '../components/variable-tree/add-store';
const defaultStoreTemplate = `
import { defineStore } from '@music163/tango-boot';
export default defineStore({
});
`;
export interface VariablePanelProps extends EditableVariableTreeProps {
wrapperHeight?: number | string;
export interface VariablePanelProps {
newStoreTemplate?: string;
}
export const VariablePanel = observer(
({
wrapperHeight = '100%',
newStoreTemplate = defaultStoreTemplate,
...restProps
}: VariablePanelProps) => {
({ newStoreTemplate = CODE_TEMPLATES.newStoreTemplate }: VariablePanelProps) => {
const [isAdd, { on, off }] = useBoolean();
const workspace = useWorkspace();
const { storeVariables } = useWorkspaceData();
const storeNames = storeVariables.map((item) => item.title);
const storeNames = storeVariables.map((item) => item.title) as string[];
return (
<Panel
shape="solid"
className="ModelView"
height={wrapperHeight}
height="100%"
title="视图模型与变量管理"
subTitle={
<IconButton
tooltip="如何使用"
icon={<QuestionCircleOutlined />}
href="https://music-doc.st.netease.com/st/tango-docs/docs/guide/basic/model"
/>
}
extra={
<Button size="small" type="text" icon={<FileAddOutlined />} onClick={on}>
<Button size="small" type="primary" icon={<FileAddOutlined />} onClick={on}>
</Button>
}
bodyProps={{ p: 0 }}
bodyProps={{ p: 'm' }}
>
{isAdd ? (
<AddStoreForm
existNames={storeNames}
storeNames={storeNames}
onCancel={off}
onSubmit={(values) => {
workspace.addStoreModule(values.name, newStoreTemplate);
workspace.addStoreFile(values.name, newStoreTemplate);
off();
}}
/>
) : (
<EditableVariableTree
modes={['add', 'define']}
defaultMode="define"
<VariableTree
defaultValueDetailMode="define"
dataSource={storeVariables}
onAddVariable={(storeName, data) => {
onAddStoreVariable={(storeName, data) => {
workspace.addStoreState(storeName, data.name, data.initialValue);
}}
onDeleteVariable={(storeName, stateName) => {
workspace.removeStoreState(storeName, stateName);
onRemoveVariable={(variableKey) => {
workspace.removeStoreVariable(variableKey);
}}
onDeleteStore={(storeName) => {
workspace.removeStoreModule(storeName);
onUpdateVariable={(variableKey, code) => {
workspace.updateStoreVariable(variableKey, code);
}}
onSave={(code, node) => {
workspace.updateModuleCodeByVariablePath(node.key, code);
}}
height="100%"
{...restProps}
/>
)}
</Panel>
);
},
);
interface AddStoreFormProps {
existNames?: string[];
onCancel?: () => void;
onSubmit?: (data: { name: string }) => void;
}
function AddStoreForm({ existNames = [], onCancel, onSubmit }: AddStoreFormProps) {
return (
<Panel title="添加模型" subTitle="模型可以用来组织一组变量">
<Form
autoComplete="off"
onFinish={(values) => {
onSubmit && onSubmit(values);
}}
>
<Form.Item
label="模型名称"
name="name"
rules={[
{ required: true },
{ pattern: /^[a-z]\w+$/, message: '非法的变量标识符' },
{
validator(_, value) {
const found = existNames.includes(value);
return found
? Promise.reject(new Error('已存在该模型名,请换一个!'))
: Promise.resolve();
},
},
]}
component="input"
componentProps={{
placeholder: '请输入模型名称',
}}
/>
<Form.Item label=" ">
<Space>
<Button onClick={onCancel}></Button>
<Button type="primary" htmlType="submit">
</Button>
</Space>
</Form.Item>
</Form>
</Panel>
);
}
@@ -1,6 +1,6 @@
import { extendTheme } from 'coral-system';
export const theme = extendTheme({
export default extendTheme({
colors: {
primary: {
10: '#e8f3ff',
@@ -27,11 +27,16 @@ export const theme = extendTheme({
custom: {
topNavBg: '#222',
topNavColor: '#fff',
topNavBorderColor: '#222',
toolbarDividerColor: 'gray.60',
toolbarButtonBg: 'rgba(223, 223, 223, 0.08)',
toolbarButtonBgHover: '#4080ff',
toolbarButtonTextColor: '#f5f5f5',
toolbarButtonBgDisabled: 'rgba(223,223,223, 0.08)',
toolbarButtonBgActive: '#4080ff',
toolbarButtonTextColor: '#FFF',
toolbarButtonTextColorHover: '#FFF',
toolbarButtonTextColorDisabled: '#bfbfbf',
toolbarButtonTextColorDisabled: 'hsla(0,0%,100%,0.3)',
toolbarButtonTextColorActive: '#fff',
sidebarBg: '#fff',
sidebarExpandBg: '#fff',
sidebarItemActiveBg: '#f2f3f5',
+2
View File
@@ -0,0 +1,2 @@
export { default as themeDefault } from './default';
export { default as themeLight } from './light';
+25
View File
@@ -0,0 +1,25 @@
import { extendTheme } from 'coral-system';
export default extendTheme({
colors: {
custom: {
topNavBg: '#FFF',
topNavColor: '#333',
topNavBorderColor: 'rgb(229,230,235)',
toolbarDividerColor: 'rgb(229,230,235)',
toolbarButtonBg: 'rgb(242,243,245)',
toolbarButtonBgHover: 'rgb(229,230,235)',
toolbarButtonBgDisabled: 'rgb(247,248,250)',
toolbarButtonBgActive: 'colors.brand',
toolbarButtonTextColor: 'colors.text2',
toolbarButtonTextColorHover: 'colors.text2',
toolbarButtonTextColorDisabled: 'colors.text4',
toolbarButtonTextColorActive: '#FFF',
sidebarBg: '#fff',
sidebarExpandBg: '#fff',
sidebarItemActiveBg: '#f2f3f5',
sidebarItemHoverBg: '#f2f3f5',
viewportBg: '#f0f2f5',
},
},
});
+5 -5
View File
@@ -1,6 +1,6 @@
import React from 'react';
import { Box } from 'coral-system';
import { ToggleButton, IconFont } from '@music163/tango-ui';
import { ToggleButton, RedoOutlined, UndoOutlined } from '@music163/tango-ui';
import { observer, useWorkspace } from '@music163/tango-context';
export const HistoryTool = observer(() => {
@@ -10,24 +10,24 @@ export const HistoryTool = observer(() => {
<ToggleButton
shape="ghost"
disabled={!workspace.history.couldBack || workspace.history.index === 0}
tip="撤销"
tooltip="撤销"
onClick={() => {
workspace.history.back();
workspace.selectSource.clear();
}}
>
<IconFont type="icon-undo" />
<UndoOutlined />
</ToggleButton>
<ToggleButton
shape="ghost"
disabled={!workspace.history.couldForward}
tip="重做"
tooltip="重做"
onClick={() => {
workspace.history.forward();
workspace.selectSource.clear();
}}
>
<IconFont type="icon-redo" />
<RedoOutlined />
</ToggleButton>
</Box>
);
+17 -6
View File
@@ -1,9 +1,12 @@
import React from 'react';
import { Group } from 'coral-system';
import { ToggleButton } from '@music163/tango-ui';
import { observer, useDesigner } from '@music163/tango-context';
import { Modal } from 'antd';
import { ToggleButton, CodeOutlined } from '@music163/tango-ui';
import { observer, useDesigner, useWorkspace } from '@music163/tango-context';
import { BorderOutlined } from '@ant-design/icons';
export const ModeSwitchTool = observer(() => {
const workspace = useWorkspace();
const designer = useDesigner();
return (
<Group attached>
@@ -13,9 +16,9 @@ export const ModeSwitchTool = observer(() => {
onClick={() => {
designer.setActiveView('design');
}}
tip="设计视图"
tooltip="设计视图"
>
<BorderOutlined />
</ToggleButton>
<ToggleButton
shape="ghost"
@@ -23,10 +26,18 @@ export const ModeSwitchTool = observer(() => {
onClick={() => {
designer.setActiveView('code'); // 切换到源码视图
designer.setActiveSidebarPanel(''); // 关闭左侧面板
if (workspace.activeFile !== workspace.activeViewFile) {
Modal.confirm({
title: '当前打开的文件与视图不匹配,是否切换到当前视图对应的文件?',
onOk: () => {
workspace.setActiveFile(workspace.activeViewFile);
},
});
}
}}
tip="源码视图"
tooltip="源码视图"
>
<CodeOutlined />
</ToggleButton>
</Group>
);
+1 -1
View File
@@ -17,7 +17,7 @@ export const PreviewTool = observer(() => {
designer.setActiveView('design');
}
}}
tip="预览"
tooltip="预览"
>
<EyeOutlined />
</ToggleButton>

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