Compare commits

...

54 Commits

Author SHA1 Message Date
wwsun 0d0479e3e5 chore(release): publish
- @music163/tango-context@1.1.6
 - @music163/tango-core@1.4.0
 - @music163/tango-designer@1.4.0
 - @music163/tango-helpers@1.2.1
 - @music163/tango-sandbox@1.0.9
 - @music163/tango-setting-form@1.2.9
 - @music163/tango-ui@1.4.0
2024-08-01 14:28:53 +08:00
wwsun 67d602a3ec fix: update types 2024-08-01 14:26:37 +08:00
Wells 8dd289cd7d feat: add isError and errorMessage to File & add isAstSynced to Module (#190)
* feat: update file without sync ast

* feat: parse and display file errors

* fix: update

* fix: refactor toolbar

* fix: add activeViews config to toolbarItem

* fix: update icon
2024-07-30 20:54:05 +08:00
wwsun 82bc5db4d8 chore(release): publish
- @music163/tango-context@1.1.5
 - @music163/tango-core@1.3.3
 - @music163/tango-designer@1.3.3
 - @music163/tango-sandbox@1.0.8
 - @music163/tango-setting-form@1.2.8
 - @music163/tango-ui@1.3.3
2024-07-12 13:51:34 +08:00
Wells 85c053b3db fix: update designer config (#186)
* docs: add section to generate local https ca

* fix: support disable format code

* fix: disable autoRemoveUnusedImports in CodeEditor

* fix: update toggle view setting
2024-07-12 10:52:12 +08:00
wwsun 32f5667cae docs: update readme 2024-07-03 15:44:25 +08:00
Wells 1d0fb4f274 fix: selection bug without prototype & update ToggleButton style (#180)
* fix: update toggleButton arrow style

* fix: check if prototype exists in selection tool

* fix: update state tree data
2024-06-26 16:54:38 +08:00
Wells 0011cdca0f chore(release): publish
- @music163/tango-context@1.1.4
 - @music163/tango-core@1.3.2
 - @music163/tango-designer@1.3.2
 - @music163/tango-sandbox@1.0.7
 - @music163/tango-setting-form@1.2.7
 - @music163/tango-ui@1.3.2
2024-06-20 15:45:08 +08:00
Wells 0f3f0afdfa fix: update designer style (#176)
* fix: remove useless codes

* fix: show insert children icon only for hasChildren node

* fix: update dependency

* fix: update styles

* fix: show sibling action for nodes except page & update drag icon for DragPanel

* fix: update ToggleButton style

* fix: add showDropdownIcon to ToggleButton

* fix: update codeSetter style

* fix: add builtin imageSetter

* fix: update PopOut icon
2024-06-20 15:42:49 +08:00
BoBoooooo b53a9f7eef fix: drag-panel add resizeable & height prop (#175)
* fix: drag-panel add resizeable & height prop

* fix: add className
2024-06-18 16:56:00 +08:00
Wells 5336c2ec28 docs: update 2024-06-11 09:17:36 +00:00
Wells cdbd2351a2 docs: update README.zh-CN.md 2024-06-11 17:04:28 +08:00
Wells 1f03977f7d docs: add some examples 2024-06-11 17:01:41 +08:00
864907600cc a2cc418a73 fix: fix global hotkey triggering on content editable elements (#174) 2024-06-06 14:57:41 +08:00
Wells 239c0b0a28 chore(release): publish
- @music163/tango-context@1.1.3
 - @music163/tango-core@1.3.1
 - @music163/tango-designer@1.3.1
 - @music163/tango-sandbox@1.0.6
 - @music163/tango-setting-form@1.2.6
 - @music163/tango-ui@1.3.1
2024-06-05 18:17:02 +08:00
Wells ae04850bb2 fix: set value as jsxElement children (#173) 2024-06-05 18:11:57 +08:00
864907600cc ca740c83ae fix: fix context menu error on macOS (#170)
* fix: fix context menu error on macOS

* fix: adjust context menu icon style
2024-06-03 15:57:58 +08:00
Wells 00bf52a411 chore(release): publish
- @music163/tango-context@1.1.2
 - @music163/tango-core@1.3.0
 - @music163/tango-designer@1.3.0
 - @music163/tango-helpers@1.2.0
 - @music163/tango-sandbox@1.0.5
 - @music163/tango-setting-form@1.2.5
 - @music163/tango-ui@1.3.0
2024-06-03 09:51:00 +08:00
864907600cc c1306e24e0 fix: use addEventListener to bind iframe event handlers (#169)
* feat: use addEventListener to bind iframe event handlers

* fix: types
2024-05-31 17:44:55 +08:00
864907600cc 28040fc006 feat: add context menu (#161)
* feat: add more actions menu on SelectionTools

* feat: add view source context action

* feat: normalize hotkey by platform

* feat: add context menu to components tree

* feat: add context menu to viewport
2024-05-31 17:31:58 +08:00
Wells 3f3981bb9d fix: parse app entry file & pass correct routerType to sandbox (#168)
* fix: parse app entry file & pass correct routerType to sandbox

* fix: export entry module
2024-05-31 14:54:12 +08:00
Wells 253ceca969 chore(release): publish
- @music163/tango-designer@1.2.3
 - @music163/tango-setting-form@1.2.4
 - @music163/tango-ui@1.2.3
2024-05-30 17:05:52 +08:00
BoBoooooo a0ed57ff4f fix: optimize variable panel scroll ui & onCancel bug (#166) 2024-05-30 14:06:12 +08:00
BoBoooooo f83c6bc0f6 fix: popover re-click position bug & drag panel drag bug (#165)
* fix: popover reClick position bug

* fix: drag event lose when drag over iframe
2024-05-29 15:26:00 +08:00
Wells bb92210594 chore(release): publish
- @music163/tango-designer@1.2.2
 - @music163/tango-setting-form@1.2.3
 - @music163/tango-ui@1.2.2
2024-05-27 10:18:03 +08:00
Wells 163ecedff3 fix: export usePreviewSandboxQuery & add builtin sandboxQuery (#163)
* fix: export usePreviewSandboxQuery

* fix: update ui

* fix: update ui
2024-05-24 18:28:04 +08:00
Wells 22a2e9bdd1 chore(release): publish
- @music163/tango-designer@1.2.1
 - @music163/tango-setting-form@1.2.2
 - @music163/tango-ui@1.2.1
2024-05-22 14:53:57 +08:00
Wells 696f82bd26 style: update 2024-05-22 14:52:44 +08:00
BoBoooooo a0484e8f64 fix: check local component prototypes (#160)
* fix: prototype undefined bug

* fix: logic bug

* fix: components grid item ui

* fix: ui bug

* fix: update local file
2024-05-22 11:56:52 +08:00
Wells cf31ef19f4 chore(release): publish
- @music163/tango-context@1.1.1
 - @music163/tango-core@1.2.0
 - @music163/tango-designer@1.2.0
 - @music163/tango-helpers@1.1.1
 - @music163/tango-sandbox@1.0.4
 - @music163/tango-setting-form@1.2.1
 - @music163/tango-ui@1.2.0
2024-05-21 15:06:42 +08:00
864907600cc fe31246483 feat: add select parent node of selected node (#158)
* feat: add select parent node of selected node

* feat: move select parent node to SelectSource

* fix: change icon & order

---------

Co-authored-by: ccloli <8115912+ccloli@users.noreply.github.com>
2024-05-20 20:15:28 +08:00
BoBoooooo 9daf3872e7 feat: event-setter & expression-setter use popover (#159) 2024-05-20 16:57:37 +08:00
BoBoooooo f17ccbb7f6 fix: support add components with popover (#155)
* feat: add drag panel & popover component

* feat: update web ide version to 1.3.11

* feat: add components-popover

* fix: update sidebar default width

* feat: add global components popover

* fix: _menuData default value

* feat: add addComponent event

* revert: remove isCollapsed prop

* fix: update local icon font url
2024-05-20 16:13:34 +08:00
BoBoooooo f854f75b8a feat: supoort set default active view (#157) 2024-05-20 15:27:32 +08:00
BoBoooooo df3021f75e feat: add history forward & back shortcut key (#154) 2024-05-20 15:26:46 +08:00
BoBoooooo 8bf53a76f8 fix: prototype2code & go back history error (#156)
* fix: snippet import bug

* fix: history could back logic bug

* fix: editor add left border when in dual mode

* fix: setting-form title overflow bug
2024-05-20 15:26:07 +08:00
Wells 791fbb162a fix: enhance code value validate in SettingForm (#152)
* fix: update register setter strategy & add getSetter

* fix: handle invalid code to prop value

* fix: add validate logic to code setters

* fix: update code validate

* fix: update ui of settingForm header
2024-05-17 18:20:08 +08:00
Wells 704277b8f0 chore(release): publish
- @music163/tango-context@1.1.0
 - @music163/tango-core@1.1.0
 - @music163/tango-designer@1.1.0
 - @music163/tango-helpers@1.1.0
 - @music163/tango-sandbox@1.0.3
 - @music163/tango-setting-form@1.2.0
 - @music163/tango-ui@1.1.0
2024-05-17 11:13:19 +08:00
Wells 1fc22a4535 fix: support quick set tid in SettingForm (#151)
* fix: support quick set tid

* fix: shape return type of parseDndId
2024-05-17 10:53:18 +08:00
Wells ffaa276b5c feat: refactor parse attribute value (#149)
* refactor: update attribute value parsing logic

* fix: refactor toggle setter logic

* fix: enhance basic setters

* fix: move basic value setter to setting-form package

* fix: add setter type

* fix: update

* feat: support toggle formObject to codeSetter

* fix: update invalid setter notice

* fix: update node2value

* fix: trigger onChange for formObject

* docs: update

* chore: up

* feat: improve code2value function and fix related issues

* Revert "chore: up"

This reverts commit a87d8fcce4.

* chore: update npm dependencies to latest versions
2024-05-16 13:03:41 +08:00
Wells 56d0877507 fix: parse module with module alias (#147) 2024-05-11 14:25:45 +08:00
Wells 897fe0a5a1 chore(release): publish
- @music163/tango-context@1.0.2
 - @music163/tango-core@1.0.2
 - @music163/tango-designer@1.0.3
 - @music163/tango-helpers@1.0.0
 - @music163/tango-sandbox@1.0.2
 - @music163/tango-setting-form@1.1.0
 - @music163/tango-ui@1.0.3
2024-05-09 10:54:49 +08:00
Wells b52bbad547 fix: disable toggle variable for expSetter & add isValid to workspace (#146)
* fix: add isValid to test if valid project

* fix: disable toggle variable for expSetter
2024-05-08 20:11:22 +08:00
864907600cc f7c837a8a9 feat: support validate on formItem (#145)
Co-authored-by: ccloli <8115912+ccloli@users.noreply.github.com>
2024-05-08 19:57:32 +08:00
Wells f64d178ab8 chore: up 2024-05-06 15:32:10 +08:00
maxwls f2dcc942ed chore: support start playground in different os (#139)
Co-authored-by: 王龙生 <wanglongsheng@pansoft.com>
2024-05-06 15:31:09 +08:00
Wells ac48a8880e fix: update setting form styles & sort deprecated props to the bottom (#142)
* fix: sort the deprecated props to the bottom

* fix: merge undefined props

* fix: update form label color
2024-05-06 15:30:29 +08:00
Wells 0f1711868d chore(release): publish
- @music163/tango-context@1.0.1
 - @music163/tango-core@1.0.1
 - @music163/tango-designer@1.0.2
 - @music163/tango-helpers@1.0.0-alpha.7
 - @music163/tango-sandbox@1.0.1
 - @music163/tango-setting-form@1.0.2
 - @music163/tango-ui@1.0.2
2024-04-22 11:36:47 +08:00
Wells 06cd3b3a18 chore: generate setting-form docs 2024-04-22 11:34:20 +08:00
Wells 2664613ab2 fix: allow reload state tree & display pageStore in variable tree (#135)
* fix: update state tree display

* fix: allow reload state tree

* fix: update action props

* fix: display pageStore in variable tree
2024-04-19 19:55:40 +08:00
Wells e39a913a87 chore(release): publish
- @music163/tango-designer@1.0.1
 - @music163/tango-setting-form@1.0.1
 - @music163/tango-ui@1.0.1
2024-04-16 15:29:34 +08:00
Wells 52864b5400 fix: variable tree remove button event handling (#134)
* docs: add StrictMode

* fix: variable tree remove button event handling
2024-04-16 15:25:09 +08:00
BoBoooooo 27166ec949 fix: render-setter expression value bug (#133)
* Create sync.yml

* Update sync.yml

* fix: render-setter expression value bug

* ci: remove sync.yml
2024-04-16 10:33:54 +08:00
Wells 1a8d15c9da chore: up 2024-04-15 14:47:52 +08:00
160 changed files with 7896 additions and 25938 deletions
+1
View File
@@ -9,3 +9,4 @@ coverage/
.umi-production/ .umi-production/
.log .log
.eslintcache .eslintcache
*.pem
+26 -15
View File
@@ -31,23 +31,17 @@ You can view the detailed usage guide through the following links:
- Out-of-the-box front-end low-code designer, providing flexible and easy-to-use designer React components - Out-of-the-box front-end low-code designer, providing flexible and easy-to-use designer React components
- Developed using TypeScript, providing complete type definition files - Developed using TypeScript, providing complete type definition files
## 🎯 Milestone ## 💡 Examples
```mermaid You can use tango to build lowcode builders, for example:
gantt
dateFormat YYYY-MM-DD
title Tango 1.0 Milestones
excludes weekends
%% (`excludes` accepts specific dates in YYYY-MM-DD format, days of the week ("sunday") or "weekends", but not the word "weekdays".)
section Builder Release | Preview | Description |
1.0 Alpha : m1, 2023-10-01, 2024-01-31 | ---------------------------------------------------------------------------------------------- | ----------------------- |
1.0 RC : m2, 2024-02-01, 2024-04-30 | ![image](https://github.com/NetEase/tango/assets/6984035/56a71741-4536-4336-803a-d81d51128a76) | Internal App Builder |
1.0 Stable : m3, 2024-04-30, 2024-12-31 | ![dashboard app](public/dashboard-builder.png) | Dashboard App Builder |
``` | ![image](https://github.com/NetEase/tango/assets/6984035/790d88e1-d716-4f99-868a-31dda25e9fb1) | Mobile App Builder |
| ![rn app](public/rn-builder.png) | ReactNative App Builder |
> [!NOTE] | ![mail app](public/mail-builder.png) | Email Builder |
> Alpha versions will continue to optimize some implementation protocols and continuously integrate new features from the internal version of NetEase; RC versions will provide a stable parsing protocol and implementation; The 1.0 official version is planned to be released in Q2 2024. After the release of the 1.0 official version, we will continue to iterate on versions and gradually incorporate core capabilities such as process design, component library, and AIGC from the internal version.
## 🌐 Compatibility ## 🌐 Compatibility
@@ -76,6 +70,23 @@ yarn
yarn start yarn start
``` ```
### Local Https certificate
If you need to use https in the local development environment, you can use the following command to generate a certificate:
```bash
brew install mkcert
# install the local certificate
mkcert -install
# enter the playground app directory
cd apps/playground
# generate a certificate
mkcert local.netease.com # or change to your own domain name
```
## 💬 Community ## 💬 Community
Join NetEase Tango Community to share your ideas, suggestions, or questions and connect with other users and contributors. Join NetEase Tango Community to share your ideas, suggestions, or questions and connect with other users and contributors.
+30 -19
View File
@@ -26,28 +26,22 @@
## ✨ 特性 ## ✨ 特性
- 经历网易云音乐内网生产环境的实际检验,可灵活集成应用于低代码平台,本地开发工具等 - 经历网易云音乐内网生产环境的实际检验,可灵活集成应用于低代码平台,本地开发工具等
- 基于源码 AST 驱动,无私有 DSL 和协议 - 提供基于源码 AST 驱动的低代码引擎,无私有 DSL 和协议
- 提供实时出码能力,支持源码进,源码出 - 提供实时出码能力,支持源码进,源码出,适配多种源码开发场景
- 开箱即用的前端低代码设计器,提供灵活易用的设计器 React 组件 - 提供开箱即用的前端低代码设计器,提供灵活易用的设计器 React 组件
- 使用 TypeScript 开发,提供完整的类型定义文件 - 使用 TypeScript 开发,提供完整的类型定义文件和完善的文档支持
## 🎯 里程碑 ## 💡 例子
```mermaid 你可以使用 Tango 快速构建多种类型的基于源码的低代码搭建工具,例如:
gantt
dateFormat YYYY-MM-DD
title Tango 1.0 Milestones
excludes weekends
%% (`excludes` accepts specific dates in YYYY-MM-DD format, days of the week ("sunday") or "weekends", but not the word "weekdays".)
section Builder Release | 预览图 | 说明 |
1.0 Alpha : m1, 2023-10-01, 2024-01-31 | ---------------------------------------------------------------------------------------------- | -------------------- |
1.0 RC : m2, 2024-02-01, 2024-04-30 | ![image](https://github.com/NetEase/tango/assets/6984035/56a71741-4536-4336-803a-d81d51128a76) | 中后台系统搭建 |
1.0 Stable : m3, 2024-04-30, 2024-12-31 | ![dashboard app](public/dashboard-builder.png) | 仪表盘应用搭建 |
``` | ![image](https://github.com/NetEase/tango/assets/6984035/790d88e1-d716-4f99-868a-31dda25e9fb1) | H5活动页面搭建 |
| ![rn app](public/rn-builder.png) | ReactNative 应用搭建 |
> [!NOTE] | ![mail app](public/mail-builder.png) | 营销类邮件搭建 |
> Alpha 版本会持续对一些实现协议进行优化,并持续融合网易内网版本中的新增特性;RC 版本将会提供基本稳定的解析协议和实现;1.0 正式版本计划在 2024 年Q2发布。1.0 正式版发布后我们将会持续进行版本迭代,并逐步融入内网版本中的流程设计、组件库、AIGC 等核心基础能力。
## 🌐 兼容环境 ## 🌐 兼容环境
@@ -76,6 +70,23 @@ yarn
yarn start yarn start
``` ```
### 本地 https 证书
如果需要在本地开发环境中使用 https,可以使用以下命令生成证书:
```bash
brew install mkcert
# 将 mkcert 添加到本地根 CA,仅在本地生效
mkcert -install
# 进入 playground 应用目录
cd apps/playground
# 为网站生成一个由 mkcert 签名的证书
mkcert local.netease.com
```
## 💬 社区讨论 ## 💬 社区讨论
参与 NetEase Tango 的社区,以分享您的想法、建议或问题,并与其他用户和贡献者建立联系。 参与 NetEase Tango 的社区,以分享您的想法、建议或问题,并与其他用户和贡献者建立联系。
-28
View File
@@ -1,28 +0,0 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC4sAqDE3nhtSWD
YqV4mlglnzIs2AioZr2uxfj/OKrpE1ZML/dflJyc5NGUpOZWV+W1vRc70wWVNFxB
Si8nrYrrseoID7Ehxxe6+J1m/JsEbhZWHwV3M1iGW7JD7VhujaDpSLg67wMTMpSW
3BUS+0NT+MNfCisp+u/ls4fXPCTgXiWcjMULm95oLlTeYwurPHfxwn9f9hh85I5I
K3dI7w8MsQN7EFNrJz3UfrNLpmARMtSUBMwBYqrCitoUxRT0q7KYCru7kHxGPB6A
SQrFpSpOWT79vp8GBSp9g3axhRsK+aEgzQ0QGrFPHBwBP/kWgmdB1URc/K7STSC9
xyMNZOM7AgMBAAECggEATbyDYDKVbaRymr+tyHqmuYctdmSkGEXOdz8LFFoEzj/+
ZekWpVuHJB7H/FoAPb2XiYyCuAKVafz1C+IuwPOx8o0bZbM9KmuDmIZGsm+GzDGO
I0fBZC/vhfYYrbC8NSRV43thWCT8VVtH4cvW7vtllnWxvlIoYu4lhbiwZ68AEpO1
8PlJsOhnagfkFpG8LehSDs9if5L+Ig0geQEcUY3pGUM9LKYsRoxFv6Vh8wQku1XD
YOxLvLWDPqJ75m816t50RARe3e6eDJ+ukzDCoDjmLP8uhf1sk1FcL5aOgUNU3VOi
P6sIH5uzuD0MA3FXx2mItYLOc44r4uaFJZiRUMEXGQKBgQDCjUYblE4rh0nKFkqx
AhgfcRgpgLbP42OwFtD0u20RXdZAEJDRDQ43NdOgjFp2B50k75LBPIoFXYUZdzKj
l7S2FH5O9Cc6hEgZGI4hCiYd+nQVI1mxv820YnzZJN4oooxyL+B5RlnW5cDT7LJN
11Q5PRewkM+XpfbE3lKUjTNcDwKBgQDzBS+j8T9GJSzV2xgA+mpGHe1RjDq6yQtk
Rl5GWpflW4Nr3/58BbmCWNA8WP0NOWutJhrkltwLCqJC8OZX/WHnmcmq2k+UAfQQ
z98p+xcMWOTcl5XIbveuhYoZb2udt9WVJLcpRVxVhEWcBrtHsrpScZXsKYf/TaZ1
JtCjPU5KFQKBgGXj/UqmYkYzxXZ8NEP998pHvcLGsXew/F2VpXv2yQlmXrYQtvd1
YfOSUjJsL1hPZoKYogBoB1UGBJYwsimxYyhVFU2eWwkvjF7wWEd/fDUJHVCQwgHw
lPga+I303BDmCK4o2uRI7vY4P8P92+gelPKhR03mmYIvnky+rvsz9CkLAoGAVerl
du6Z0jDecxUsnVvrKrL17jjHorXlYyRFvFXXEe2SvxbIIAzeEylXJZ7IiF5epS6t
n1d+oCC4UTZeGYmpjXofhFn+fHNcWH1FhtAQy/q5nwuwltY0luz7cBamU3Jk/n+m
id7N0CYdfhYbLDQSD4e822ureyV2zkBUzwGjpikCgYEAoCVbhkxVrQHRmxLC2jDx
pM94W1ij9TqblfGg3OMpPXjpTdlknatWjBw7JkoyxYiYJq5Gi2p4QV6HHUvS1osj
BB+4A99/PD9A0ICoJglz8SXlGe/895zwHwpKT9f0zjizQrEd2KCPf75URa+t2FGY
B2GkXsbChRWVrey5+uMzUD8=
-----END PRIVATE KEY-----
-25
View File
@@ -1,25 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIEQjCCAqqgAwIBAgIRAIeLJ2jwt4MSKycHFmFXL6YwDQYJKoZIhvcNAQELBQAw
fTEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMSkwJwYDVQQLDCB3d3N1
bkBzdW4tbWJwLmxvY2FsIChTVU4gV2Vpd2VpKTEwMC4GA1UEAwwnbWtjZXJ0IHd3
c3VuQHN1bi1tYnAubG9jYWwgKFNVTiBXZWl3ZWkpMB4XDTIyMDcyODAxNTIxMFoX
DTI0MTAyODAxNTIxMFowVDEnMCUGA1UEChMebWtjZXJ0IGRldmVsb3BtZW50IGNl
cnRpZmljYXRlMSkwJwYDVQQLDCB3d3N1bkBzdW4tbWJwLmxvY2FsIChTVU4gV2Vp
d2VpKTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALiwCoMTeeG1JYNi
pXiaWCWfMizYCKhmva7F+P84qukTVkwv91+UnJzk0ZSk5lZX5bW9FzvTBZU0XEFK
Lyetiuux6ggPsSHHF7r4nWb8mwRuFlYfBXczWIZbskPtWG6NoOlIuDrvAxMylJbc
FRL7Q1P4w18KKyn67+Wzh9c8JOBeJZyMxQub3mguVN5jC6s8d/HCf1/2GHzkjkgr
d0jvDwyxA3sQU2snPdR+s0umYBEy1JQEzAFiqsKK2hTFFPSrspgKu7uQfEY8HoBJ
CsWlKk5ZPv2+nwYFKn2DdrGFGwr5oSDNDRAasU8cHAE/+RaCZ0HVRFz8rtJNIL3H
Iw1k4zsCAwEAAaNmMGQwDgYDVR0PAQH/BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUF
BwMBMB8GA1UdIwQYMBaAFDILZR+SW32Ug0HkAKrH1KT2a1ttMBwGA1UdEQQVMBOC
EWxvY2FsLm5ldGVhc2UuY29tMA0GCSqGSIb3DQEBCwUAA4IBgQCEdeV2Likl4MUt
wWeN/X3tAR3L7mkl7e4f0CQ4v2lOs8cekOP7p3ZFk5nSyVbxda3usPB0OtbVTibw
qZ1n/TMRb5epHroXIMleqngP82zFZNg+WVyHm/ZAml8gVD0VZjYM1Tb+U9+SrgGw
lvZndbuUXa3h/d0irPH90KsdEeAR1SqwzAzvLO2JgvBuimHRceQWcTiiGWDZOyii
tux4U+pEzh0Dia40uFozFAoYZPDUpZ+gsiQ3MR3HOrAu2OR0a+CfelDXZNLR3C4z
cBKr53ajAbHstnpxKJh9IfeKph44PrbDo6HMe1XPprl7idoEesX3Ma3SBgm4lmId
cfEOUWNIYOgtOT5bflXNNyzXOgYVjpLnjwg4EOASO/XIjhvqdjIyCu7HJMIcZ9oo
Aob2exeluv5LVwic1UJQDl2McDBIgJ8R32oEN4skCKIBSbWlxWyjnxL5I5WkJF4y
vlSx8bKAjVr8map0C3rWISuZdgheJkF96qjwhVSxOv++p6srq6M=
-----END CERTIFICATE-----
+2 -2
View File
@@ -4,7 +4,7 @@
"private": true, "private": true,
"author": "Wells <ww.sun@outlook.com>", "author": "Wells <ww.sun@outlook.com>",
"scripts": { "scripts": {
"dev": "HOST=local.netease.com PORT=8001 umi dev", "dev": "cross-env HOST=local.netease.com PORT=8001 umi dev",
"build": "umi build", "build": "umi build",
"postinstall": "umi setup", "postinstall": "umi setup",
"setup": "umi setup", "setup": "umi setup",
@@ -15,6 +15,6 @@
"@music163/antd": "^0.2.4", "@music163/antd": "^0.2.4",
"antd": "^4.24.2", "antd": "^4.24.2",
"coral-system": "^1.0.5", "coral-system": "^1.0.5",
"umi": "^4.0.89" "umi": "^4.2.3"
} }
} }
+12 -2
View File
@@ -5,6 +5,7 @@ import { BranchesOutlined, MenuOutlined, QuestionCircleOutlined } from '@ant-des
import { registerSetter } from '@music163/tango-designer'; import { registerSetter } from '@music163/tango-designer';
import type { IVariableTreeNode } from '@music163/tango-helpers'; import type { IVariableTreeNode } from '@music163/tango-helpers';
import { FooSetter } from '../components'; import { FooSetter } from '../components';
import { ToggleButton } from '@music163/tango-ui';
export * from './mock-files'; export * from './mock-files';
@@ -46,8 +47,17 @@ registerSetter({
// 平台 Logo // 平台 Logo
export function Logo() { export function Logo() {
return ( return (
<Box width="50px" display="flex" alignItems="center" justifyContent="center" fontSize="20px"> <Box width="50px" display="flex" alignItems="center" justifyContent="center" fontSize="14px">
<MenuOutlined /> <ToggleButton
shape="text"
items={[
{ label: '回到首页', key: 'home' },
{ label: '文档', key: 'doc' },
{ label: '设置', key: 'setting' },
]}
>
<MenuOutlined />
</ToggleButton>
</Box> </Box>
); );
} }
+2 -3
View File
@@ -2,7 +2,7 @@ const packageJson = {
name: 'demo', name: 'demo',
private: true, private: true,
dependencies: { dependencies: {
'@music163/tango-mail': '0.1.1', '@music163/tango-mail': '0.2.4',
'@music163/tango-boot': '0.2.5', '@music163/tango-boot': '0.2.5',
react: '17.0.2', react: '17.0.2',
'react-dom': '17.0.2', 'react-dom': '17.0.2',
@@ -44,7 +44,7 @@ const tangoConfigJson = {
}, },
'@music163/tango-mail': { '@music163/tango-mail': {
description: 'TangoMail 基础物料', description: 'TangoMail 基础物料',
version: '0.1.1', version: '0.2.4',
library: 'TangoMail', library: 'TangoMail',
type: 'baseDependency', type: 'baseDependency',
resources: ['https://unpkg.com/@music163/tango-mail@{{version}}/dist/index.js'], resources: ['https://unpkg.com/@music163/tango-mail@{{version}}/dist/index.js'],
@@ -116,7 +116,6 @@ const WelcomeEmail = () => (
</Section> </Section>
<Text style={paragraph}> <Text style={paragraph}>
Best, Best,
<br />
The Koala team The Koala team
</Text> </Text>
<Hr style={hr} /> <Hr style={hr} />
+57 -6
View File
@@ -2,8 +2,8 @@ const packageJson = {
name: 'demo', name: 'demo',
private: true, private: true,
dependencies: { dependencies: {
'@music163/antd': '0.2.2', '@music163/antd': '0.2.5',
'@music163/tango-boot': '0.2.5', '@music163/tango-boot': '0.3.5',
react: '17.0.2', react: '17.0.2',
'react-dom': '17.0.2', 'react-dom': '17.0.2',
'prop-types': '15.7.2', 'prop-types': '15.7.2',
@@ -48,7 +48,7 @@ const tangoConfigJson = {
}, },
'@music163/tango-boot': { '@music163/tango-boot': {
description: '云音乐低代码运行时框架', description: '云音乐低代码运行时框架',
version: '0.2.5', version: '0.3.5',
library: 'TangoBoot', library: 'TangoBoot',
type: 'baseDependency', type: 'baseDependency',
resources: ['https://unpkg.com/@music163/tango-boot@{{version}}/dist/boot.js'], resources: ['https://unpkg.com/@music163/tango-boot@{{version}}/dist/boot.js'],
@@ -56,11 +56,12 @@ const tangoConfigJson = {
}, },
'@music163/antd': { '@music163/antd': {
description: '云音乐低代码中后台应用基础物料', description: '云音乐低代码中后台应用基础物料',
version: '0.2.2', version: '0.2.6',
library: 'TangoAntd', library: 'TangoAntd',
type: 'baseDependency', type: 'baseDependency',
resources: [ resources: [
'https://unpkg.com/@music163/antd@{{version}}/dist/index.js', 'https://unpkg.com/@music163/antd@{{version}}/dist/index.js',
// 'http://localhost:9002/designer.js',
'https://unpkg.com/antd@4.24.13/dist/antd.css', 'https://unpkg.com/antd@4.24.13/dist/antd.css',
], ],
designerResources: [ designerResources: [
@@ -85,6 +86,7 @@ export function registerComponentPrototype(proto) {
const routesCode = ` const routesCode = `
import Index from "./pages/list"; import Index from "./pages/list";
import Detail from "./pages/detail";
const routes = [ const routes = [
{ {
@@ -92,6 +94,11 @@ const routes = [
exact: true, exact: true,
component: Index component: Index
}, },
{
path: '/detail',
exact: true,
component: Detail
},
]; ];
export default routes; export default routes;
@@ -150,20 +157,33 @@ import {
Input, Input,
FormilyForm, FormilyForm,
FormilyFormItem, FormilyFormItem,
Table,
} from "@music163/antd"; } from "@music163/antd";
import { Space } from "@music163/antd"; import { Space } from "@music163/antd";
import { LocalButton } from "../components"; import { LocalButton } from "../components";
import { OutButton } from "../components";
class App extends React.Component { class App extends React.Component {
render() { render() {
return ( return (
<Page title={tango.stores.app.title}> <Page title={tango.stores.app.title} subTitle={111}>
<Section tid="section0" />
<Section tid="section1" title="Section Title"> <Section tid="section1" title="Section Title">
your input: <Input tid="input1" defaultValue="hello" /> your input: <Input tid="input1" defaultValue="hello" />
copy input: <Input value={tango.page.input1?.value} /> copy input: <Input value={tango.page.input1?.value} />
<Table
columns={[
{ title: "姓名", dataIndex: "name", key: "name" },
{ title: "年龄", dataIndex: "age", key: "age" },
{ title: "住址", dataIndex: "address", key: "address" },
]}
tid="table1"
/>
</Section> </Section>
<Section tid="section2"> <Section tid="section2">
<Space tid="space1"> <Space tid="space1">
<LocalButton /> <LocalButton />
<OutButton />
<Button tid="button1">button</Button> <Button tid="button1">button</Button>
</Space> </Space>
</Section> </Section>
@@ -173,7 +193,10 @@ class App extends React.Component {
<FormilyFormItem name="select1" component="Select" label="表单项" /> <FormilyFormItem name="select1" component="Select" label="表单项" />
</FormilyForm> </FormilyForm>
</Section> </Section>
<Section title="原生 DOM" tid="section3"> <Section title="原生 DOM" tid="section4">
<h1 style={{ ...{ color: "red" }, fontSize: 64 }}>
hello world
</h1>
<div <div
style={{ style={{
border: "1px solid #ccc", border: "1px solid #ccc",
@@ -234,6 +257,23 @@ class App extends React.Component {
export default definePage(App); export default definePage(App);
`; `;
export const emptyPageCode = `
import React from "react";
import { definePage } from "@music163/tango-boot";
import {
Page,
Section,
} from "@music163/antd";
function App() {
return (<Page title="Detail Page">
<Section></Section>
</Page>)
}
export default definePage(App);
`;
const componentsButtonCode = ` const componentsButtonCode = `
import React from 'react'; import React from 'react';
import { registerComponentPrototype } from '../utils'; import { registerComponentPrototype } from '../utils';
@@ -253,6 +293,14 @@ registerComponentPrototype({
}); });
`; `;
const outButtonCode = `
import React from 'react';
export default function OutButton(props) {
return <button {...props}>Out button (from other file)</button>
}
`;
const componentsInputCode = ` const componentsInputCode = `
import React from 'react'; import React from 'react';
import { registerComponentPrototype } from '../utils'; import { registerComponentPrototype } from '../utils';
@@ -275,6 +323,7 @@ registerComponentPrototype({
const componentsEntryCode = ` const componentsEntryCode = `
export { default as LocalButton } from './button'; export { default as LocalButton } from './button';
export { default as LocalInput } from './input'; export { default as LocalInput } from './input';
export { default as OutButton } from './out-button';
`; `;
const storeApp = ` const storeApp = `
@@ -373,7 +422,9 @@ export const sampleFiles = [
{ filename: '/src/style.css', code: cssCode }, { filename: '/src/style.css', code: cssCode },
{ filename: '/src/index.js', code: entryCode }, { filename: '/src/index.js', code: entryCode },
{ filename: '/src/pages/list.js', code: viewHomeCode }, { filename: '/src/pages/list.js', code: viewHomeCode },
{ filename: '/src/pages/detail.js', code: emptyPageCode },
{ filename: '/src/components/button.js', code: componentsButtonCode }, { filename: '/src/components/button.js', code: componentsButtonCode },
{ filename: '/src/components/out-button.js', code: outButtonCode },
{ filename: '/src/components/input.js', code: componentsInputCode }, { filename: '/src/components/input.js', code: componentsInputCode },
{ filename: '/src/components/index.js', code: componentsEntryCode }, { filename: '/src/components/index.js', code: componentsEntryCode },
{ filename: '/src/routes.js', code: routesCode }, { filename: '/src/routes.js', code: routesCode },
+35 -11
View File
@@ -45,14 +45,18 @@ const Snippet2ColumnLayout: IComponentPrototype = {
const Snippet3ColumnLayout: IComponentPrototype = { const Snippet3ColumnLayout: IComponentPrototype = {
name: 'Snippet3ColumnLayout', name: 'Snippet3ColumnLayout',
title: '三列布局', title: '三列布局',
icon: 'icon-column3', icon: 'icon-column-3',
type: 'snippet', type: 'snippet',
package: '@music163/antd', package: '@music163/antd',
// FIXME: Column 组件 需要更新为 defineComponent
initChildren: ` initChildren: `
<Columns columns={12}> <Columns columns={12}>
<Column colSpan={4}></Column> <Column colSpan={4}>
<Column colSpan={4}></Column> </Column>
<Column colSpan={4}></Column> <Column colSpan={4}>
</Column>
<Column colSpan={4}>
</Column>
</Columns> </Columns>
`, `,
relatedImports: ['Columns', 'Column'], relatedImports: ['Columns', 'Column'],
@@ -76,13 +80,9 @@ const SnippetButtonGroup: IComponentPrototype = {
// hack some prototypes // hack some prototypes
basePrototypes['Section'].siblingNames = [ basePrototypes['Section'].siblingNames = [
'SnippetButtonGroup', 'SnippetButtonGroup',
'Section', 'Snippet2ColumnLayout',
'Section', 'Snippet3ColumnLayout',
'Section', 'SnippetSuccessResult',
'Section',
'Section',
'Section',
'Section',
]; ];
export const nativeDomPrototypes = () => { export const nativeDomPrototypes = () => {
@@ -174,6 +174,30 @@ const prototypes: Dict<IComponentPrototype> = {
package: '@music163/antd', package: '@music163/antd',
hasChildren: true, hasChildren: true,
siblingNames: ['Box'], siblingNames: ['Box'],
props: [
{
name: 'aaa',
title: 'aaa',
setter: 'textSetter',
},
{
name: 'bbb',
title: 'bbb',
setter: 'textSetter',
deprecated: true,
},
{
name: 'ccc',
title: 'ccc',
setter: 'textSetter',
deprecated: true,
},
{
name: 'd',
title: 'd',
setter: 'textSetter',
},
],
}, },
Columns: { Columns: {
name: 'Columns', name: 'Columns',
+1 -5
View File
@@ -3,9 +3,5 @@ import { Outlet } from 'umi';
import './index.less'; import './index.less';
export default function Layout() { export default function Layout() {
return ( return <Outlet />;
<div>
<Outlet />
</div>
);
} }
+82 -39
View File
@@ -1,5 +1,5 @@
import { Box } from 'coral-system'; import { Box } from 'coral-system';
import { Button, Space } from 'antd'; import { Button, Form, Input, Modal, Space } from 'antd';
import { import {
Designer, Designer,
DesignerPanel, DesignerPanel,
@@ -15,43 +15,17 @@ import {
} from '@music163/tango-designer'; } from '@music163/tango-designer';
import { createEngine, Workspace } from '@music163/tango-core'; import { createEngine, Workspace } from '@music163/tango-core';
import prototypes from '../helpers/prototypes'; import prototypes from '../helpers/prototypes';
import { Logo, ProjectDetail, bootHelperVariables, sampleFiles } from '../helpers'; import { Logo, ProjectDetail, bootHelperVariables, emptyPageCode, sampleFiles } from '../helpers';
import { import {
ApiOutlined, ApiOutlined,
AppstoreAddOutlined, AppstoreAddOutlined,
BuildOutlined, BuildOutlined,
ClusterOutlined,
FunctionOutlined, FunctionOutlined,
PlusOutlined,
createFromIconfontCN, createFromIconfontCN,
} from '@ant-design/icons'; } from '@ant-design/icons';
import { Action, PackageOutlined } from '@music163/tango-ui';
// 1. 实例化工作区 import { useState } from 'react';
const workspace = new Workspace({
entry: '/src/index.js',
files: sampleFiles,
prototypes,
});
// inject workspace to window for debug
(window as any).__workspace__ = workspace;
// 2. 引擎初始化
const engine = createEngine({
workspace,
});
// @ts-ignore
window.__workspace__ = workspace;
// 3. 沙箱初始化
const sandboxQuery = new DndQuery({
context: 'iframe',
});
// 4. 图标库初始化(物料面板和组件树使用了 iconfont 里的图标)
createFromIconfontCN({
scriptUrl: '//at.alicdn.com/t/c/font_2891794_6d4hj5u0bjx.js',
});
const menuData = { const menuData = {
common: [ common: [
@@ -63,6 +37,7 @@ const menuData = {
'Columns', 'Columns',
'Column', 'Column',
'Box', 'Box',
'Text',
'Space', 'Space',
'Typography', 'Typography',
'Title', 'Title',
@@ -79,13 +54,49 @@ const menuData = {
title: 'Formily表单', title: 'Formily表单',
items: ['FormilyForm', 'FormilyFormItem', 'FormilySubmit', 'FormilyReset'], items: ['FormilyForm', 'FormilyFormItem', 'FormilySubmit', 'FormilyReset'],
}, },
{
title: '数据展示',
items: ['Comment'],
},
], ],
}; };
// 1. 实例化工作区
const workspace = new Workspace({
entry: '/src/index.js',
files: sampleFiles,
prototypes,
});
// inject workspace to window for debug
(window as any).__workspace__ = workspace;
// 2. 引擎初始化
const engine = createEngine({
workspace,
menuData,
defaultActiveView: 'design', // dual code design
});
// @ts-ignore
window.__workspace__ = workspace;
// 3. 沙箱初始化
const sandboxQuery = new DndQuery({
context: 'iframe',
});
// 4. 图标库初始化(物料面板和组件树使用了 iconfont 里的图标)
createFromIconfontCN({
scriptUrl: '//at.alicdn.com/t/c/font_2891794_151xsllxqd7.js',
});
/** /**
* 5. 平台初始化,访问 https://local.netease.com:6006/ * 5. 平台初始化,访问 https://local.netease.com:6006/
*/ */
export default function App() { export default function App() {
const [showNewPageModal, setShowNewPageModal] = useState(false);
const [form] = Form.useForm();
return ( return (
<Designer <Designer
theme={themeLight} theme={themeLight}
@@ -102,11 +113,19 @@ export default function App() {
actions={ actions={
<Box px="l"> <Box px="l">
<Toolbar> <Toolbar>
<Toolbar.Item key="routeSwitch" placement="left" /> <Toolbar.Item key="routeSwitch" placement="left" activeViews={['design']} />
<Toolbar.Item key="history" placement="left" /> <Toolbar.Item key="addPage" placement="left" activeViews={['design']}>
<Toolbar.Item key="preview" placement="left" /> <Action
tooltip="添加页面"
shape="outline"
icon={<PlusOutlined />}
onClick={() => setShowNewPageModal(true)}
/>
</Toolbar.Item>
<Toolbar.Item key="history" placement="left" activeViews={['design']} />
<Toolbar.Item key="preview" placement="left" activeViews={['design']} />
<Toolbar.Item key="togglePanel" placement="right" activeViews={['design']} />
<Toolbar.Item key="modeSwitch" placement="right" /> <Toolbar.Item key="modeSwitch" placement="right" />
<Toolbar.Item key="togglePanel" placement="right" />
<Toolbar.Separator /> <Toolbar.Separator />
<Toolbar.Item placement="right"> <Toolbar.Item placement="right">
<Space> <Space>
@@ -114,6 +133,30 @@ export default function App() {
</Space> </Space>
</Toolbar.Item> </Toolbar.Item>
</Toolbar> </Toolbar>
<Modal
title="添加新页面"
open={showNewPageModal}
onCancel={() => setShowNewPageModal(false)}
footer={null}
>
<Form
form={form}
onFinish={(values) => {
workspace.addViewFile(values.name, emptyPageCode);
setShowNewPageModal(false);
}}
layout="vertical"
>
<Form.Item label="文件名" name="name" required rules={[{ required: true }]}>
<Input placeholder="请输入文件名" />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">
</Button>
</Form.Item>
</Form>
</Modal>
</Box> </Box>
} }
> >
@@ -138,12 +181,15 @@ export default function App() {
<Sidebar.Item <Sidebar.Item
key="dependency" key="dependency"
label="依赖" label="依赖"
icon={<ClusterOutlined />} icon={<PackageOutlined />}
isFloat isFloat
width={800} width={800}
/> />
</Sidebar> </Sidebar>
<WorkspacePanel> <WorkspacePanel>
<WorkspaceView mode="code">
<CodeEditor />
</WorkspaceView>
<WorkspaceView mode="design"> <WorkspaceView mode="design">
<Sandbox <Sandbox
onMessage={(e) => { onMessage={(e) => {
@@ -165,9 +211,6 @@ export default function App() {
navigatorExtra={<Button size="small">hello world</Button>} navigatorExtra={<Button size="small">hello world</Button>}
/> />
</WorkspaceView> </WorkspaceView>
<WorkspaceView mode="code">
<CodeEditor />
</WorkspaceView>
</WorkspacePanel> </WorkspacePanel>
<SettingPanel /> <SettingPanel />
</DesignerPanel> </DesignerPanel>
+2 -1
View File
@@ -45,7 +45,7 @@ const sandboxQuery = new DndQuery({
// 4. 图标库初始化(物料面板和组件树使用了 iconfont 里的图标) // 4. 图标库初始化(物料面板和组件树使用了 iconfont 里的图标)
createFromIconfontCN({ createFromIconfontCN({
scriptUrl: '//at.alicdn.com/t/c/font_2891794_cou9i7556tl.js', scriptUrl: '//at.alicdn.com/t/c/font_2891794_151xsllxqd7.js',
}); });
/** /**
@@ -112,6 +112,7 @@ export default function App() {
if (sandboxWindow.TangoMail) { if (sandboxWindow.TangoMail) {
if (sandboxWindow.TangoMail.menuData) { if (sandboxWindow.TangoMail.menuData) {
setMenuData(sandboxWindow.TangoMail.menuData); setMenuData(sandboxWindow.TangoMail.menuData);
engine.designer.setMenuData(sandboxWindow.TangoMail.menuData);
} }
if (sandboxWindow.TangoMail.prototypes) { if (sandboxWindow.TangoMail.prototypes) {
workspace.setComponentPrototypes(sandboxWindow.TangoMail.prototypes); workspace.setComponentPrototypes(sandboxWindow.TangoMail.prototypes);
+2 -2
View File
@@ -16,8 +16,8 @@
"dependencies": { "dependencies": {
"@music163/tango-setting-form": "*", "@music163/tango-setting-form": "*",
"@music163/tango-ui": "*", "@music163/tango-ui": "*",
"mobx": "6.12.0", "mobx": "6.12.3",
"mobx-react-lite": "4.0.5" "mobx-react-lite": "4.0.7"
}, },
"devDependencies": { "devDependencies": {
"@ant-design/icons": "^4.8.0", "@ant-design/icons": "^4.8.0",
+485 -279
View File
@@ -1,9 +1,7 @@
import React from 'react'; import React from 'react';
import { FormModel, SettingForm, register } from '@music163/tango-setting-form'; import { FormModel, SettingForm, register } from '@music163/tango-setting-form';
import { IComponentPrototype } from '@music163/tango-helpers'; import { IComponentPrototype } from '@music163/tango-helpers';
import { BorderSetter, DisplaySetter } from '@music163/tango-designer/src/setters/style-setter'; import { BUILT_IN_SETTERS } from '@music163/tango-designer/src/setters';
import { JsxSetter } from '@music163/tango-designer/src/setters/jsx-setter';
import { RenderSetter, TableCellSetter } from '@music163/tango-designer/src/setters/render-setter';
import { Box } from 'coral-system'; import { Box } from 'coral-system';
import { JsonView } from '@music163/tango-ui'; import { JsonView } from '@music163/tango-ui';
import { toJS } from 'mobx'; import { toJS } from 'mobx';
@@ -11,272 +9,18 @@ import { observer } from 'mobx-react-lite';
import { Card } from 'antd'; import { Card } from 'antd';
import { createFromIconfontCN } from '@ant-design/icons'; import { createFromIconfontCN } from '@ant-design/icons';
// 这里按需注入,因为部分 setter 依赖 Designer 的上下文 const BLACK_LIST = ['codeSetter', 'eventSetter', 'modelSetter', 'routerSetter'];
register({
name: 'borderSetter',
component: BorderSetter,
});
register({ BUILT_IN_SETTERS.filter((setter) => !BLACK_LIST.includes(setter.name)).forEach(register);
name: 'displaySetter',
component: DisplaySetter,
});
register({
name: 'jsxSetter',
component: JsxSetter,
});
register({
name: 'renderSetter',
component: RenderSetter,
});
register({
name: 'tableCellSetter',
component: TableCellSetter,
});
createFromIconfontCN({ createFromIconfontCN({
scriptUrl: '//at.alicdn.com/t/c/font_2891794_cou9i7556tl.js', scriptUrl: '//at.alicdn.com/t/c/font_2891794_151xsllxqd7.js',
}); });
export default { export default {
title: 'SettingForm', title: 'SettingForm',
}; };
const prototype: IComponentPrototype = {
name: 'Test',
exportType: 'namedExport',
title: '测试',
icon: 'icon-test',
type: 'element',
category: 'basic',
package: '@music163/antd',
hasChildren: false,
props: [
{
name: 'text',
title: 'textSetter',
setter: 'textSetter',
tip: '这是一个文本属性',
docs: 'https://music-one.fn.netease.com/docs/button',
deprecated: '使用 text2 替代',
},
{
name: 'display',
title: 'displaySetter',
setter: 'displaySetter',
},
{
name: 'border',
title: 'borderSetter',
setter: 'borderSetter',
},
{
name: 'onClick',
title: 'eventSetter',
tip: '当点击按钮时',
setter: 'eventSetter',
group: 'event',
},
{
name: 'children',
title: 'jsxSetter',
setter: 'jsxSetter',
},
{
name: 'render',
title: 'renderSetter',
setter: 'renderSetter',
},
{
name: 'cell',
title: 'tableCellSetter',
setter: 'tableCellSetter',
},
{
name: 'router',
title: 'routerSetter',
setter: 'routerSetter',
},
{
name: 'object',
title: '对象属性',
tip: '一个嵌套的对象',
props: [
{
name: 'name',
title: 'Name',
setter: 'textSetter',
},
{
name: 'age',
title: 'Age',
setter: 'numberSetter',
},
{
name: 'address',
title: 'Address',
props: [
{
name: 'city',
title: 'City',
setter: 'textSetter',
},
],
},
],
getVisible: (form) => {
return form.getValue('text') !== 'test';
},
},
{
name: 'expression',
title: 'expressionSetter',
setter: 'expressionSetter',
},
{
name: 'model',
title: 'modelSetter',
setter: 'modelSetter',
},
{
name: 'image',
title: 'imageSetter',
setter: 'imageSetter',
},
{
name: 'css',
title: 'cssSetter',
setter: 'cssSetter',
},
{
name: 'extra',
title: 'jsxSetter',
setter: 'jsxSetter',
},
{
name: 'icon',
title: 'iconSetter',
setter: 'iconSetter',
},
{
name: 'iconType',
title: 'iconTypeSetter',
setter: 'iconTypeSetter',
},
{
name: 'onClick2',
title: 'actionSetter',
tip: '当点击按钮时',
setter: 'actionSetter',
group: 'event',
},
{
name: 'disabled',
title: 'boolSetter',
tip: 'disabled 是否禁用',
defaultValue: false,
setter: 'boolSetter',
},
{
name: 'size',
title: 'choiceSetter',
tip: 'size 按钮的尺寸',
defaultValue: 'medium',
setter: 'choiceSetter',
setterProps: {
options: [
{ label: '小', value: 'small' },
{ label: '中', value: 'medium' },
{ label: '大', value: 'large' },
],
},
},
{
name: 'color',
title: 'colorSetter',
setter: 'colorSetter',
},
{
name: 'columns',
title: 'columnSetter',
setter: 'columnSetter',
},
{
name: 'date',
title: 'dateSetter',
setter: 'dateSetter',
},
{
name: 'dateRange',
title: 'dateRangeSetter',
setter: 'dateRangeSetter',
},
{
name: 'enum',
title: 'enumSetter',
setter: 'enumSetter',
setterProps: {},
},
{
name: 'time',
title: 'timeSetter',
setter: 'timeSetter',
},
{
name: 'timeRange',
title: 'timeRangeSetter',
setter: 'timeRangeSetter',
},
{
name: 'dataSource',
title: 'jsonSetter 不推荐',
setter: 'jsonSetter',
},
{
name: 'listSetter',
title: 'listSetter',
setter: 'listSetter',
},
{
name: 'count',
title: 'numberSetter',
setter: 'numberSetter',
},
{
name: 'options',
title: 'optionSetter',
setter: 'optionSetter',
},
{
name: 'type',
title: 'pickerSetter',
defaultValue: 'solid',
setter: 'pickerSetter',
setterProps: {
options: [
{ label: '文本型', value: 'text' },
{ label: '实体型', value: 'solid' },
{ label: '幽灵', value: 'ghost' },
],
},
},
{
name: 'title',
title: 'textSetter',
setter: 'textSetter',
},
{
name: 'invalid',
title: 'invalidSetter',
setter: 'invalidSetter',
},
],
};
/** /**
* 表单值预览 * 表单值预览
*/ */
@@ -285,28 +29,22 @@ const FormValuePreview = observer(({ model }: { model: FormModel }) => {
return <JsonView src={data} />; return <JsonView src={data} />;
}); });
export function Basic() { interface SettingFormDemoProps {
const model = new FormModel( initValues?: object;
{ prototype?: IComponentPrototype;
router: 'www.163.com', }
expression: `{ foo: 'foo' }`,
object: {
name: 'Alice',
},
image:
'https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/13270238619/2cc5/0782/1d6e/009b96bf90c557b9bbde09b1687a2c80.png',
},
{ onChange: console.log },
);
function SettingFormDemo({ initValues, prototype }: SettingFormDemoProps) {
const model = new FormModel(initValues, { onChange: console.log });
return ( return (
<Box display="flex"> <Box display="flex">
<Box flex="0 0 400px" overflow="hidden"> <Box flex="0 0 320px" overflow="hidden">
<SettingForm <SettingForm
model={model} model={model}
prototype={prototype} prototype={prototype}
showIdentifier={{ showIdentifier={{
identifierKey: 'tid', identifierKey: 'tid',
getIdentifier: () => `time${Date.now()}`,
}} }}
/> />
</Box> </Box>
@@ -319,25 +57,493 @@ export function Basic() {
); );
} }
const prototypeHasBasicProps: IComponentPrototype = {
name: 'Sample',
title: '演示组件',
package: 'sample-pkg',
type: 'element',
docs: 'https://4x-ant-design.antgroup.com/components/slider-cn',
props: [
{
name: 'code',
title: 'codeSetter',
setter: 'codeSetter',
},
{
name: 'text',
title: 'textSetter',
setter: 'textSetter',
},
{
name: 'text2',
title: 'textAreaSetter',
setter: 'textAreaSetter',
},
{
name: 'src',
title: 'imageSetter',
setter: 'imageSetter',
},
{
name: 'number',
title: 'numberSetter',
setter: 'numberSetter',
},
{
name: 'number2',
title: 'sliderSetter',
setter: 'sliderSetter',
},
{
name: 'bool',
title: 'boolSetter',
setter: 'boolSetter',
},
{
name: 'enum',
title: 'enumSetter',
setter: 'enumSetter',
},
{
name: 'list',
title: 'listSetter',
setter: 'listSetter',
},
],
};
export function Basic() {
return (
<SettingFormDemo
initValues={{
bool: true,
enum: {
aaa: 'aaa',
bbb: 'bbb',
ccc: 'ccc',
},
list: [{ key: 1 }, { key: 2 }],
}}
prototype={prototypeHasBasicProps}
/>
);
}
export function DeprecatedProp() {
return (
<SettingFormDemo
prototype={{
name: 'Deprecated',
package: 'sample-pkg',
type: 'element',
props: [
{
name: 'number',
title: 'numberSetter',
setter: 'numberSetter',
tip: '这是一个文本属性',
docs: 'https://4x-ant-design.antgroup.com/components/slider-cn',
deprecated: '使用 text2 替代',
},
{
name: 'number1',
title: 'sliderSetter',
setter: 'sliderSetter',
},
],
}}
/>
);
}
export function Validate() {
return (
<SettingFormDemo
prototype={{
name: 'Validate',
package: 'sample-pkg',
type: 'element',
props: [
{
name: 'number',
title: 'numberSetter',
setter: 'numberSetter',
validate: (value) => {
if (!value && value !== 0) {
return '必填';
}
if (value < 0) {
return '必须大于 0';
}
if (value > 10) {
return '必须小于等于 10';
}
},
},
],
}}
/>
);
}
export function ObjectSetter() {
return (
<SettingFormDemo
prototype={{
name: 'Object',
package: 'sample-pkg',
type: 'element',
props: [
{
name: 'text',
title: 'textSetter',
setter: 'textSetter',
},
{
name: 'object',
title: 'objectSetter',
setter: 'objectSetter',
props: [
{
name: 'text',
title: 'textSetter',
setter: 'textSetter',
},
{
name: 'number',
title: 'numberSetter',
setter: 'numberSetter',
},
],
},
],
}}
/>
);
}
export function InitValues() {
return (
<SettingFormDemo
initValues={{
bool: true,
bool1: '{{true}}',
style: {
background: 'red',
},
object: {
text: 'text',
number: 10,
},
object1: {
text: 'text',
number: '{{tango.stores.user?.age}}',
},
// 只会有一种情况传下来的是字符串,就是用户代码里存在 rest operator,这时候不需要额外处理,提示用户就使用代码模式
object2: '{{{ text: "text22", number: 22, ...{ extra: "some" } }}}',
list: [{ key: 'aaa' }, { key: 'bbb' }], // list object
list1: "{{[{ key: 'aaa' }, { key: 'bbb' }]}}", // raw code
}}
prototype={{
name: 'InitValues',
package: 'sample-pkg',
type: 'element',
props: [
{
name: 'bool',
title: 'value初始化',
setter: 'boolSetter',
},
{
name: 'bool1',
title: 'value初始化',
setter: 'boolSetter',
},
{
name: 'bool2',
title: '无初值',
setter: 'boolSetter',
},
{
name: 'style',
title: 'codeSetter',
setter: 'codeSetter',
},
{
name: 'object',
props: [
{
name: 'text',
title: 'text',
setter: 'textSetter',
},
{
name: 'number',
title: 'number',
setter: 'numberSetter',
},
],
},
{
name: 'object1',
props: [
{
name: 'text',
title: 'text',
setter: 'textSetter',
},
{
name: 'number',
title: 'number',
setter: 'numberSetter',
},
],
},
{
name: 'object2',
props: [
{
name: 'text',
title: 'text',
setter: 'textSetter',
},
{
name: 'number',
title: 'number',
setter: 'numberSetter',
},
],
},
{
name: 'list',
title: 'listSetter',
setter: 'listSetter',
},
{
name: 'list1',
title: 'listSetter',
setter: 'listSetter',
},
],
}}
/>
);
}
export function Lite() { export function Lite() {
return ( return (
<Box width={320} border="solid"> <Box width={320} border="solid" borderColor="line2">
<SettingForm <SettingForm
showSearch={false} showSearch={false}
showGroups={false} showGroups={false}
showItemSubtitle={false} showItemSubtitle={false}
prototype={prototype} prototype={prototypeHasBasicProps}
disableSwitchExpressionSetter disableSwitchExpressionSetter
/> />
</Box> </Box>
); );
} }
export function NoExpressionSwitch() { export function HideToggleCode() {
const model = new FormModel({}); const model = new FormModel({});
return ( return (
<Box> <Box width={320} border="solid" borderColor="line2">
<SettingForm model={model} prototype={prototype} disableSwitchExpressionSetter /> <SettingForm model={model} prototype={prototypeHasBasicProps} disableSwitchExpressionSetter />
</Box> </Box>
); );
} }
const prototypeHasExtraProps: IComponentPrototype = {
name: 'ExtraProps',
type: 'element',
package: '@music163/antd',
props: [
{
name: 'choice',
title: 'choiceSetter',
setter: 'choiceSetter',
options: [
{ label: '选项1', value: '1' },
{ label: '选项2', value: '2' },
{ label: '选项3', value: '3' },
],
},
{
name: 'picker',
title: 'pickerSetter',
setter: 'pickerSetter',
options: [
{ label: '选项1', value: '1' },
{ label: '选项2', value: '2' },
{ label: '选项3', value: '3' },
],
},
{
name: 'actionList',
title: 'actionListSetter',
setter: 'actionListSetter',
},
{
name: 'list',
title: 'listSetter',
setter: 'listSetter',
},
{
name: 'options',
title: 'optionSetter',
setter: 'optionSetter',
},
{
name: 'columns',
title: 'tableColumnsSetter',
setter: 'tableColumnsSetter',
},
{
name: 'css',
title: 'cssSetter',
setter: 'cssSetter',
},
{
name: 'date',
title: 'dateSetter',
setter: 'dateSetter',
},
{
name: 'dateRange',
title: 'dateRangeSetter',
setter: 'dateRangeSetter',
},
{
name: 'time',
title: 'timeSetter',
setter: 'timeSetter',
},
{
name: 'time',
title: 'timeRangeSetter',
setter: 'timeRangeSetter',
},
{
name: 'enum',
title: 'enumSetter',
setter: 'enumSetter',
},
{
name: 'event',
title: 'eventSetter',
setter: 'eventSetter',
},
{
name: 'json',
title: 'jsonSetter',
setter: 'jsonSetter',
},
{
name: 'jsx',
title: 'jsxSetter',
setter: 'jsxSetter',
},
{
name: 'render',
title: 'renderPropsSetter',
setter: 'renderPropsSetter',
},
{
name: 'cell',
title: 'tableCellSetter',
setter: 'tableCellSetter',
},
{
name: 'expandable',
title: 'tableExpandableSetter',
setter: 'tableExpandableSetter',
},
{
name: 'router',
title: 'routerSetter',
setter: 'routerSetter',
},
],
};
export function ExtraSetters() {
return (
<SettingFormDemo
initValues={{
router: 'www.163.com',
expression: `{ foo: 'foo' }`,
object: {
name: 'Alice',
},
image:
'https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/13270238619/2cc5/0782/1d6e/009b96bf90c557b9bbde09b1687a2c80.png',
}}
prototype={prototypeHasExtraProps}
/>
);
}
export function StyleProps() {
return (
<SettingFormDemo
prototype={{
name: 'Box',
title: 'Box',
type: 'element',
package: '@music163/antd',
props: [
{
name: 'style',
title: 'styleSetter',
setter: 'styleSetter',
},
{
name: 'display',
title: 'displaySetter',
setter: 'displaySetter',
},
{
name: 'flexDirection',
title: 'flexDirectionSetter',
setter: 'flexDirectionSetter',
},
{
name: 'flexGap',
title: 'flexGapSetter',
setter: 'flexGapSetter',
},
{
name: 'flexJustifyContent',
title: 'flexJustifyContentSetter',
setter: 'flexJustifyContentSetter',
},
{
name: 'flexAlignItems',
title: 'flexAlignItemsSetter',
setter: 'flexAlignItemsSetter',
},
{
name: 'spacing',
title: 'spacingSetter',
setter: 'spacingSetter',
},
{
name: 'color',
title: 'colorSetter',
setter: 'colorSetter',
},
{
name: 'bg',
title: 'bgSetter',
setter: 'bgSetter',
},
{
name: 'border',
title: 'borderSetter',
setter: 'borderSetter',
},
],
}}
/>
);
}
@@ -0,0 +1,82 @@
import React from 'react';
import { DragPanel } from '@music163/tango-ui';
import { Box, Text } from 'coral-system';
import { Button } from 'antd';
export default {
title: 'UI/DragPanel',
};
export function Basic() {
return (
<>
<DragPanel
title="弹层标题"
extra="右上角内容"
width={350}
body={<Box p="m"></Box>}
footer={<Text></Text>}
>
<Button></Button>
</DragPanel>
<DragPanel
title="弹层标题"
extra="右上角内容"
width={350}
body={<Box p="m"></Box>}
footer={<Text></Text>}
>
<Button
style={{
position: 'absolute',
bottom: 20,
right: 20,
}}
>
</Button>
</DragPanel>
</>
);
}
export function FooterCustom() {
return (
<DragPanel
title="弹层标题"
extra="右上角内容"
width={350}
body={<Box p="m"></Box>}
footer={(close) => (
<Box display="flex" justifyContent="space-between">
<Text></Text>
<Button size="small" onClick={() => close()}>
</Button>
</Box>
)}
>
<Button></Button>
</DragPanel>
);
}
export function ResizeablePanel() {
return (
<DragPanel
title="弹层标题"
resizeable
extra="右上角内容"
width={350}
height={400}
body={
<Box p="m" textAlign="center" background="#fa8484">
</Box>
}
footer={<Text></Text>}
>
<Button></Button>
</DragPanel>
);
}
+1
View File
@@ -40,6 +40,7 @@
"@typescript-eslint/parser": "^6.13.2", "@typescript-eslint/parser": "^6.13.2",
"conventional-changelog-cli": "^4.1.0", "conventional-changelog-cli": "^4.1.0",
"copyfiles": "^2.4.1", "copyfiles": "^2.4.1",
"cross-env": "^7.0.3",
"eslint": "^8.55.0", "eslint": "^8.55.0",
"eslint-config-ali": "^14.0.2", "eslint-config-ali": "^14.0.2",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
+36
View File
@@ -3,6 +3,42 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.1.6](https://github.com/netease/tango/compare/@music163/tango-context@1.1.5...@music163/tango-context@1.1.6) (2024-08-01)
**Note:** Version bump only for package @music163/tango-context
## [1.1.5](https://github.com/netease/tango/compare/@music163/tango-context@1.1.4...@music163/tango-context@1.1.5) (2024-07-12)
**Note:** Version bump only for package @music163/tango-context
## [1.1.4](https://github.com/netease/tango/compare/@music163/tango-context@1.1.3...@music163/tango-context@1.1.4) (2024-06-20)
**Note:** Version bump only for package @music163/tango-context
## [1.1.3](https://github.com/netease/tango/compare/@music163/tango-context@1.1.2...@music163/tango-context@1.1.3) (2024-06-05)
**Note:** Version bump only for package @music163/tango-context
## [1.1.2](https://github.com/netease/tango/compare/@music163/tango-context@1.1.1...@music163/tango-context@1.1.2) (2024-06-03)
**Note:** Version bump only for package @music163/tango-context
## [1.1.1](https://github.com/netease/tango/compare/@music163/tango-context@1.1.0...@music163/tango-context@1.1.1) (2024-05-21)
**Note:** Version bump only for package @music163/tango-context
# [1.1.0](https://github.com/netease/tango/compare/@music163/tango-context@1.0.2...@music163/tango-context@1.1.0) (2024-05-17)
### Features
- refactor parse attribute value ([#149](https://github.com/netease/tango/issues/149)) ([ffaa276](https://github.com/netease/tango/commit/ffaa276b5c205ed962d37e2fdb358a703f8fad01))
## [1.0.1](https://github.com/netease/tango/compare/@music163/tango-context@1.0.0...@music163/tango-context@1.0.1) (2024-04-22)
### Bug Fixes
- allow reload state tree & display pageStore in variable tree ([#135](https://github.com/netease/tango/issues/135)) ([2664613](https://github.com/netease/tango/commit/2664613ab263aead2a5239fa012454fc7fd5ff99))
# [1.0.0-alpha.10](https://github.com/netease/tango/compare/@music163/tango-context@1.0.0-alpha.9...@music163/tango-context@1.0.0-alpha.10) (2024-04-09) # [1.0.0-alpha.10](https://github.com/netease/tango/compare/@music163/tango-context@1.0.0-alpha.9...@music163/tango-context@1.0.0-alpha.10) (2024-04-09)
**Note:** Version bump only for package @music163/tango-context **Note:** Version bump only for package @music163/tango-context
+4 -4
View File
@@ -1,6 +1,6 @@
{ {
"name": "@music163/tango-context", "name": "@music163/tango-context",
"version": "1.0.0", "version": "1.1.6",
"description": "react context for tango-apps", "description": "react context for tango-apps",
"keywords": [ "keywords": [
"react", "react",
@@ -31,9 +31,9 @@
"react": ">= 16.8" "react": ">= 16.8"
}, },
"dependencies": { "dependencies": {
"@music163/tango-core": "^1.0.0", "@music163/tango-core": "^1.4.0",
"@music163/tango-helpers": "^1.0.0-alpha.6", "@music163/tango-helpers": "^1.2.1",
"mobx-react-lite": "4.0.5" "mobx-react-lite": "4.0.7"
}, },
"publishConfig": { "publishConfig": {
"access": "public", "access": "public",
+7
View File
@@ -35,8 +35,14 @@ export const useWorkspaceData = () => {
const modelVariables: IVariableTreeNode[] = []; // 绑定变量列表 const modelVariables: IVariableTreeNode[] = []; // 绑定变量列表
const storeActionVariables: IVariableTreeNode[] = []; // 模型中的所有 actions const storeActionVariables: IVariableTreeNode[] = []; // 模型中的所有 actions
const storeVariables: IVariableTreeNode[] = []; // 模型中的所有变量 const storeVariables: IVariableTreeNode[] = []; // 模型中的所有变量
const pageStoreVariables: IVariableTreeNode[] = []; // 页面中的组件实例变量
const serviceVariables: IVariableTreeNode[] = []; // 服务中的所有变量 const serviceVariables: IVariableTreeNode[] = []; // 服务中的所有变量
pageStoreVariables.push({
title: 'pageStore',
key: 'pageStore',
});
Object.values(workspace.storeModules).forEach((file) => { Object.values(workspace.storeModules).forEach((file) => {
const prefix = `stores.${file.name}`; const prefix = `stores.${file.name}`;
const states: IVariableTreeNode[] = file.states.map((item) => ({ const states: IVariableTreeNode[] = file.states.map((item) => ({
@@ -109,6 +115,7 @@ export const useWorkspaceData = () => {
} }
let expressionVariables: IVariableTreeNode[] = [ let expressionVariables: IVariableTreeNode[] = [
buildVariableOptions('当前页面组件实例', '$pageStore', pageStoreVariables),
buildVariableOptions('数据模型', '$stores', storeVariables), buildVariableOptions('数据模型', '$stores', storeVariables),
buildVariableOptions('服务函数', '$services', serviceVariables), buildVariableOptions('服务函数', '$services', serviceVariables),
]; ];
+65
View File
@@ -3,6 +3,71 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.4.0](https://github.com/netease/tango/compare/@music163/tango-core@1.3.3...@music163/tango-core@1.4.0) (2024-08-01)
### Bug Fixes
- update types ([67d602a](https://github.com/netease/tango/commit/67d602a3ec6b7f74156bb78fda6f3b4bc2676e55))
### Features
- add isError and errorMessage to File & add isAstSynced to Module ([#190](https://github.com/netease/tango/issues/190)) ([8dd289c](https://github.com/netease/tango/commit/8dd289cd7daba628e54dc6b8929f22a1e2245160))
## [1.3.3](https://github.com/netease/tango/compare/@music163/tango-core@1.3.2...@music163/tango-core@1.3.3) (2024-07-12)
### Bug Fixes
- update designer config ([#186](https://github.com/netease/tango/issues/186)) ([85c053b](https://github.com/netease/tango/commit/85c053b3db6d652b41c9873dba1366315174833f))
## [1.3.2](https://github.com/netease/tango/compare/@music163/tango-core@1.3.1...@music163/tango-core@1.3.2) (2024-06-20)
### Bug Fixes
- update designer style ([#176](https://github.com/netease/tango/issues/176)) ([0f3f0af](https://github.com/netease/tango/commit/0f3f0afdfa8aee2532a97c5c2e92ef4230397d86))
## [1.3.1](https://github.com/netease/tango/compare/@music163/tango-core@1.3.0...@music163/tango-core@1.3.1) (2024-06-05)
### Bug Fixes
- set value as jsxElement children ([#173](https://github.com/netease/tango/issues/173)) ([ae04850](https://github.com/netease/tango/commit/ae04850bb251f392575a72ca5fd960249ea9d06b))
# [1.3.0](https://github.com/netease/tango/compare/@music163/tango-core@1.2.0...@music163/tango-core@1.3.0) (2024-06-03)
### Bug Fixes
- parse app entry file & pass correct routerType to sandbox ([#168](https://github.com/netease/tango/issues/168)) ([3f3981b](https://github.com/netease/tango/commit/3f3981bb9db874b738a67fd449c579c35c58d08b))
### Features
- add context menu ([#161](https://github.com/netease/tango/issues/161)) ([28040fc](https://github.com/netease/tango/commit/28040fc00604339a40ad3216b76baf7de93a13e0))
# [1.2.0](https://github.com/netease/tango/compare/@music163/tango-core@1.1.0...@music163/tango-core@1.2.0) (2024-05-21)
### Bug Fixes
- enhance code value validate in SettingForm ([#152](https://github.com/netease/tango/issues/152)) ([791fbb1](https://github.com/netease/tango/commit/791fbb162a7147243924e01f54e9c0b586f14438))
- prototype2code & go back history error ([#156](https://github.com/netease/tango/issues/156)) ([8bf53a7](https://github.com/netease/tango/commit/8bf53a76f8a71eaf261ea68b9ee44e5bf19893aa))
- support add components with popover ([#155](https://github.com/netease/tango/issues/155)) ([f17ccbb](https://github.com/netease/tango/commit/f17ccbb7f645f8047ecd96d9f3f2185048a3b726))
### Features
- add select parent node of selected node ([#158](https://github.com/netease/tango/issues/158)) ([fe31246](https://github.com/netease/tango/commit/fe3124648325e72abfc58da8b2f8ff83301d40b8))
- supoort set default active view ([#157](https://github.com/netease/tango/issues/157)) ([f854f75](https://github.com/netease/tango/commit/f854f75b8a8c25384d290bd76d6b8bb6fb69f22d))
# [1.1.0](https://github.com/netease/tango/compare/@music163/tango-core@1.0.2...@music163/tango-core@1.1.0) (2024-05-17)
### Bug Fixes
- parse module with module alias ([#147](https://github.com/netease/tango/issues/147)) ([56d0877](https://github.com/netease/tango/commit/56d0877507b0138877eac6f36db288147b43c7d9))
### Features
- refactor parse attribute value ([#149](https://github.com/netease/tango/issues/149)) ([ffaa276](https://github.com/netease/tango/commit/ffaa276b5c205ed962d37e2fdb358a703f8fad01))
## [1.0.1](https://github.com/netease/tango/compare/@music163/tango-core@1.0.0...@music163/tango-core@1.0.1) (2024-04-22)
**Note:** Version bump only for package @music163/tango-core
# [1.0.0-alpha.10](https://github.com/netease/tango/compare/@music163/tango-core@1.0.0-alpha.9...@music163/tango-core@1.0.0-alpha.10) (2024-04-09) # [1.0.0-alpha.10](https://github.com/netease/tango/compare/@music163/tango-core@1.0.0-alpha.9...@music163/tango-core@1.0.0-alpha.10) (2024-04-09)
### Bug Fixes ### Bug Fixes
+3 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "@music163/tango-core", "name": "@music163/tango-core",
"version": "1.0.0", "version": "1.4.0",
"description": "tango core", "description": "tango core",
"author": "wwsun <ww.sun@outlook.com>", "author": "wwsun <ww.sun@outlook.com>",
"homepage": "", "homepage": "",
@@ -28,10 +28,10 @@
"@babel/parser": "^7.23.5", "@babel/parser": "^7.23.5",
"@babel/traverse": "^7.23.5", "@babel/traverse": "^7.23.5",
"@babel/types": "^7.23.5", "@babel/types": "^7.23.5",
"@music163/tango-helpers": "^1.0.0-alpha.6", "@music163/tango-helpers": "^1.2.1",
"@types/babel__generator": "^7.6.7", "@types/babel__generator": "^7.6.7",
"@types/babel__traverse": "^7.20.4", "@types/babel__traverse": "^7.20.4",
"mobx": "6.12.0", "mobx": "6.12.3",
"path-browserify": "^1.0.1" "path-browserify": "^1.0.1"
}, },
"peerDependencies": { "peerDependencies": {
+14 -1
View File
@@ -1,4 +1,5 @@
import { Designer, Engine, SimulatorNameType } from './models'; import { MenuDataType } from '@music163/tango-helpers';
import { Designer, DesignerViewType, Engine, SimulatorNameType } from './models';
import { IWorkspace } from './models/interfaces'; import { IWorkspace } from './models/interfaces';
interface ICreateEngineOptions { interface ICreateEngineOptions {
@@ -6,6 +7,10 @@ interface ICreateEngineOptions {
* 自定义工作区 * 自定义工作区
*/ */
workspace?: IWorkspace; workspace?: IWorkspace;
/**
* 菜单信息
*/
menuData?: MenuDataType;
/** /**
* 默认的模拟器模式 * 默认的模拟器模式
*/ */
@@ -14,6 +19,10 @@ interface ICreateEngineOptions {
* 默认激活的侧边栏 * 默认激活的侧边栏
*/ */
defaultActiveSidebarPanel?: string; defaultActiveSidebarPanel?: string;
/**
* 默认激活的视图
*/
defaultActiveView?: DesignerViewType;
} }
/** /**
@@ -23,15 +32,19 @@ interface ICreateEngineOptions {
*/ */
export function createEngine({ export function createEngine({
workspace, workspace,
defaultActiveView = 'design',
defaultSimulatorMode = 'desktop', defaultSimulatorMode = 'desktop',
defaultActiveSidebarPanel = '', defaultActiveSidebarPanel = '',
menuData,
}: ICreateEngineOptions) { }: ICreateEngineOptions) {
const engine = new Engine({ const engine = new Engine({
workspace, workspace,
designer: new Designer({ designer: new Designer({
workspace, workspace,
simulator: defaultSimulatorMode, simulator: defaultSimulatorMode,
activeView: defaultActiveView,
activeSidebarPanel: defaultActiveSidebarPanel, activeSidebarPanel: defaultActiveSidebarPanel,
menuData,
}), }),
}); });
+2 -1
View File
@@ -35,11 +35,12 @@ export function isTangoVariable(name: string) {
return /^tango\??\.(stores|services)\??\./.test(name) && name.split('.').length > 2; return /^tango\??\.(stores|services)\??\./.test(name) && name.split('.').length > 2;
} }
const templatePattern = /^{(.+)}$/s; const templatePattern = /^{(.+)}$/;
/** /**
* 判断给定字符串是否被表达式容器`{expCode}`包裹 * 判断给定字符串是否被表达式容器`{expCode}`包裹
* @param code * @param code
* @deprecated 新版改为 {{code}} 作为容器,使用 isWrappedCode 代替
*/ */
export function isWrappedByExpressionContainer(code: string, isStrict = true) { export function isWrappedByExpressionContainer(code: string, isStrict = true) {
if (isStrict && isValidExpressionCode(code)) { if (isStrict && isValidExpressionCode(code)) {
+50 -44
View File
@@ -3,7 +3,7 @@
*/ */
import generator, { GeneratorOptions } from '@babel/generator'; import generator, { GeneratorOptions } from '@babel/generator';
import * as t from '@babel/types'; import * as t from '@babel/types';
import { logger } from '@music163/tango-helpers'; import { Dict, logger, wrapCode } from '@music163/tango-helpers';
import { formatCode } from '../string'; import { formatCode } from '../string';
const defaultGeneratorOptions: GeneratorOptions = { const defaultGeneratorOptions: GeneratorOptions = {
@@ -25,19 +25,7 @@ export function ast2code(ast: t.Node, options: GeneratorOptions = defaultGenerat
return code; return code;
} }
const bracketPattern = /^\(.+\)$/s; const bracketPattern = /^\(.+\)$/;
/**
* 是否被 () 包裹
*
* @example ({ foo: 'foo' }) -> true
* @example { foo: 'foo' } -> false
*
* @param str 目标字符串
*/
function isWrappingWithBrackets(str: string) {
return bracketPattern.test(str);
}
/** /**
* 将表达式生成为块级代码 * 将表达式生成为块级代码
@@ -54,7 +42,7 @@ export function expression2code(node: t.Expression) {
const isWrappingExpression = t.isObjectExpression(node) || t.isFunctionExpression(node); const isWrappingExpression = t.isObjectExpression(node) || t.isFunctionExpression(node);
if (isWrappingExpression && isWrappingWithBrackets(ret)) { if (isWrappingExpression && bracketPattern.test(ret)) {
// 如果是对象,输出包含 ({}),则去掉首尾的括号 // 如果是对象,输出包含 ({}),则去掉首尾的括号
ret = ret.slice(1, -1); ret = ret.slice(1, -1);
} }
@@ -156,10 +144,10 @@ export function node2code(node: t.Node) {
/** /**
* 将 t.Node 生成为 js 值 * 将 t.Node 生成为 js 值
* @param node ast node * @param node ast node
* @param hasExpressionWrapper 是否包裹表达式 * @param isWrapCode 是否包裹代码,例如 code -> {{code}}
* @returns a plain javascript value * @returns a plain javascript value
*/ */
export function node2value(node: t.Node, hasExpressionWrapper = true): any { export function node2value(node: t.Node, isWrapCode = true): any {
let ret; let ret;
switch (node.type) { switch (node.type) {
case 'StringLiteral': case 'StringLiteral':
@@ -171,39 +159,50 @@ export function node2value(node: t.Node, hasExpressionWrapper = true): any {
case 'NullLiteral': case 'NullLiteral':
ret = null; ret = null;
break; break;
case 'Identifier': // {data} case 'Identifier': // {{data}}
case 'MemberExpression': // {this.props.data} case 'MemberExpression': // {{this.props.data}}
case 'OptionalMemberExpression': // {a?.b} case 'OptionalMemberExpression': // {{a?.b}}
case 'UnaryExpression': // {!false} case 'UnaryExpression': // {{!false}}
case 'ArrowFunctionExpression': // {() => {}} case 'ArrowFunctionExpression': // {{() => {}}}
case 'TemplateLiteral': // {`hello ${text}`} case 'TemplateLiteral': // {{`hello ${text}`}}
case 'ConditionalExpression': // {a ? 'foo' : 'bar'} case 'ConditionalExpression': // {{a ? 'foo' : 'bar'}}
case 'LogicalExpression': // { a || b} case 'LogicalExpression': // {{ a || b}}
case 'BinaryExpression': // { a + b} case 'BinaryExpression': // {{ a + b}}
case 'TaggedTemplateExpression': // {css``} case 'TaggedTemplateExpression': // {{css``}}
case 'CallExpression': // {[1,2,3].map(fn)} case 'CallExpression': // {{[1,2,3].map(fn)}}
case 'JSXElement': // {<Box>hello</Box>} case 'JSXElement': // {{<Box>hello</Box>}}
case 'JSXFragment': // <><Box /></> case 'JSXFragment': // {{<><Box /></>}}
ret = expression2code(node); ret = expression2code(node);
if (hasExpressionWrapper) { if (isWrapCode) {
ret = `{${ret}}`; ret = wrapCode(ret);
} }
break; break;
case 'ObjectExpression': { case 'ObjectExpression': {
ret = node.properties.reduce((prev, propertyNode) => { const isSimpleObject = node.properties.every(
if (propertyNode.type === 'ObjectProperty') { (propertyNode) => propertyNode.type === 'ObjectProperty',
const key = keyNode2value(propertyNode.key); );
const value = node2value(propertyNode.value, hasExpressionWrapper); if (isSimpleObject) {
// key 可能是字符串,也可能是数字 // simple object: { key1, key2, key3 }
prev[key] = value; ret = node.properties.reduce<Dict>((prev, propertyNode) => {
if (propertyNode.type === 'ObjectProperty') {
const key = keyNode2value(propertyNode.key);
const value = node2value(propertyNode.value, isWrapCode);
prev[key] = value; // key 可能是字符串,也可能是数字
}
return prev;
}, {});
} else {
// mixed object, object property maybe SpreadElement or ObjectMethod, e.g. { key1, fn() {}, ...obj1 }
ret = expression2code(node);
if (wrapCode) {
ret = wrapCode(ret);
} }
// FIXME: property is a SpreadElement }
return prev;
}, {});
break; break;
} }
case 'ArrayExpression': { case 'ArrayExpression': {
ret = node.elements.map((elementNode) => node2value(elementNode, hasExpressionWrapper)); // FIXME: 有可能会解析失败
ret = node.elements.map((elementNode) => node2value(elementNode, isWrapCode));
break; break;
} }
default: default:
@@ -214,7 +213,7 @@ export function node2value(node: t.Node, hasExpressionWrapper = true): any {
} }
/** /**
* jsx 属性值节点转为 js value * jsx prop value 节点转为 js value
*/ */
export function jsxAttributeValueNode2value(node: t.Node): any { export function jsxAttributeValueNode2value(node: t.Node): any {
// e.g. <Checkbox checked /> 此时没有 value node // e.g. <Checkbox checked /> 此时没有 value node
@@ -232,9 +231,16 @@ export function jsxAttributeValueNode2value(node: t.Node): any {
// <Foo bar={[]}> // <Foo bar={[]}>
ret = jsxAttributeValueNode2value(node.expression); ret = jsxAttributeValueNode2value(node.expression);
break; break;
default: case 'ArrayExpression': {
// 数组统一处理为 code
ret = expression2code(node);
ret = wrapCode(ret);
break;
}
default: {
ret = node2value(node); ret = node2value(node);
break; break;
}
} }
return ret; return ret;
+27 -30
View File
@@ -5,12 +5,12 @@ import { parse, parseExpression, ParserOptions } from '@babel/parser';
import * as t from '@babel/types'; import * as t from '@babel/types';
import { import {
logger, logger,
isValidObjectString,
getVariableContent, getVariableContent,
isPlainObject, isPlainObject,
Dict, Dict,
isWrappedCode,
getCodeOfWrappedCode,
} from '@music163/tango-helpers'; } from '@music163/tango-helpers';
import { isWrappedByExpressionContainer } from '../assert';
// @see https://babeljs.io/docs/en/babel-parser#pluginss // @see https://babeljs.io/docs/en/babel-parser#pluginss
const babelParserConfig: ParserOptions = { const babelParserConfig: ParserOptions = {
@@ -68,11 +68,7 @@ export function isValidExpressionCode(code: string) {
* @returns * @returns
*/ */
export function code2ast(code: string): t.File { export function code2ast(code: string): t.File {
try { return parse(code, babelParserConfig);
return parse(code, babelParserConfig);
} catch (err) {
logger.error('[code2ast failed!]', err);
}
} }
/** /**
@@ -97,8 +93,7 @@ export function code2expression(code: string) {
try { try {
expNode = t.cloneNode(parseExpression(code, babelParserConfig), false, true); expNode = t.cloneNode(parseExpression(code, babelParserConfig), false, true);
} catch (err) { } catch (err) {
logger.error('invalid code', err); logger.error('code2expression failed, invalid code:', code);
// expNode = t.identifier('undefined');
} }
return expNode; return expNode;
} }
@@ -109,9 +104,6 @@ export function code2expression(code: string) {
* @returns File * @returns File
*/ */
export function expressionCode2ast(code: string) { export function expressionCode2ast(code: string) {
if (isWrappedByExpressionContainer(code)) {
code = getVariableContent(code);
}
const node = code2expression(code); const node = code2expression(code);
return t.file(t.program([t.blockStatement([t.expressionStatement(node)])])); return t.file(t.program([t.blockStatement([t.expressionStatement(node)])]));
} }
@@ -132,18 +124,19 @@ export function value2node(
| t.Expression { | t.Expression {
let ret; let ret;
switch (typeof value) { switch (typeof value) {
case 'number':
ret = t.numericLiteral(value);
break;
case 'string': case 'string':
if (isWrappedByExpressionContainer(value)) { if (isWrappedCode(value)) {
// 再检查是否是表达式容器,例如 {this.foo}, {1} // 再检查是否是代码 {{code}},例如 {{this.foo}}, {{1}}
const innerString = getVariableContent(value); const innerCode = getCodeOfWrappedCode(value);
ret = code2expression(innerString); ret = code2expression(innerCode);
} else { } else {
// 否则当成字符串处理
ret = t.stringLiteral(value); ret = t.stringLiteral(value);
} }
break; break;
case 'number':
ret = t.numericLiteral(value);
break;
case 'boolean': case 'boolean':
ret = t.booleanLiteral(value); ret = t.booleanLiteral(value);
break; break;
@@ -167,7 +160,7 @@ export function value2node(
ret = t.identifier('undefined'); ret = t.identifier('undefined');
break; break;
default: { default: {
logger.error(`value2node: unsupport value <${value}>`); logger.error(`value2node: value <${value}> transform failed!`);
break; break;
} }
} }
@@ -178,7 +171,7 @@ export function value2node(
* 将 js 普通对象解析为 t.Node * 将 js 普通对象解析为 t.Node
*/ */
export function object2node( export function object2node(
obj: object, obj: Dict,
getValueNode: (value: any, key?: string) => t.Expression = value2node, getValueNode: (value: any, key?: string) => t.Expression = value2node,
) { ) {
if (!isPlainObject(obj)) { if (!isPlainObject(obj)) {
@@ -196,21 +189,23 @@ export function code2jsxAttributeValueNode(code: string) {
return t.jsxExpressionContainer(code2expression(code)); return t.jsxExpressionContainer(code2expression(code));
} }
/**
* FIXME: 统一处理为 code2jsxAttributeValueNode
* 将 js value 转为 JSXAttributeValueNode
* @param value js value, or wrapped code
* @returns 返回 JSXAttributeValueNode,转换失败返回Node为 {undefined}
*/
export function value2jsxAttributeValueNode(value: any) { export function value2jsxAttributeValueNode(value: any) {
let ret; let ret;
switch (typeof value) { switch (typeof value) {
// FIXME: 重构这个逻辑,是不是统一当成 code 处理
case 'string': { case 'string': {
if (value.length > 1) { if (value.length > 1) {
value = value.trim(); value = value.trim();
} }
if (isValidObjectString(value)) { if (isWrappedCode(value)) {
// 先检查是否是对象字符串 const innerCode = getCodeOfWrappedCode(value);
ret = t.jsxExpressionContainer(code2expression(value)); const node = code2expression(innerCode);
} else if (isWrappedByExpressionContainer(value)) { ret = t.jsxExpressionContainer(node || t.identifier('undefined'));
// 再检查是否是表达式容器,例如 {this.foo}, {1}
const innerString = getVariableContent(value);
ret = t.jsxExpressionContainer(code2expression(innerString));
} else { } else {
ret = t.stringLiteral(value); ret = t.stringLiteral(value);
} }
@@ -227,10 +222,12 @@ export function value2jsxChildrenValueNode(value: any) {
let ret: t.JSXElement | t.JSXFragment | t.JSXExpressionContainer | t.JSXSpreadChild | t.JSXText; let ret: t.JSXElement | t.JSXFragment | t.JSXExpressionContainer | t.JSXSpreadChild | t.JSXText;
switch (typeof value) { switch (typeof value) {
case 'string': case 'string':
if (isWrappedByExpressionContainer(value)) { if (isWrappedCode(value)) {
// 代码模式
const innerString = getVariableContent(value); const innerString = getVariableContent(value);
ret = t.jsxExpressionContainer(code2expression(innerString)); ret = t.jsxExpressionContainer(code2expression(innerString));
} else { } else {
// 普通的文本
ret = t.jsxText(value); ret = t.jsxText(value);
} }
break; break;
+23 -2
View File
@@ -391,8 +391,10 @@ export function updateJSXElementAttribute(node: t.JSXElement, attrName: string,
isExist = true; isExist = true;
// @ts-ignore // @ts-ignore
if (t.isJSXAttribute(jsxAttributeNode)) { if (t.isJSXAttribute(jsxAttributeNode)) {
// 更新 jsx 属性值 <Foo bar="1" /> 的模式
jsxAttributeNode.value = value2jsxAttributeValueNode(attrValue); jsxAttributeNode.value = value2jsxAttributeValueNode(attrValue);
} else if (name === 'children') { } else if (name === 'children') {
// 更新 jsx children 的情况 <Foo>children</Foo>
node.children = value2jsxChildrenValueNode(attrValue); node.children = value2jsxChildrenValueNode(attrValue);
} }
} }
@@ -1007,7 +1009,7 @@ export function serviceConfig2Node(payload: object) {
}); });
} }
export function updateServiceConfigToServiceFile(ast: t.File, config: Dict<object>) { export function updateServiceConfigToServiceFile(ast: t.File, config: Dict<Dict>) {
traverse(ast, { traverse(ast, {
CallExpression(path) { CallExpression(path) {
const calleeName = keyNode2value(path.node.callee) as string; const calleeName = keyNode2value(path.node.callee) as string;
@@ -1015,7 +1017,7 @@ export function updateServiceConfigToServiceFile(ast: t.File, config: Dict<objec
const configNode = path.node.arguments[0]; const configNode = path.node.arguments[0];
if (t.isObjectExpression(configNode)) { if (t.isObjectExpression(configNode)) {
const newPropertiesNodeMap = Object.keys(config).reduce((properties, key) => { const newPropertiesNodeMap = Object.keys(config).reduce<Dict>((properties, key) => {
const serviceConfig = config[key]; const serviceConfig = config[key];
const property = t.objectProperty(t.identifier(key), serviceConfig2Node(serviceConfig)); const property = t.objectProperty(t.identifier(key), serviceConfig2Node(serviceConfig));
properties[key] = property; properties[key] = property;
@@ -1425,3 +1427,22 @@ export function traverseFile(ast: t.File) {
imports, imports,
}; };
} }
/**
* app.js 入口文件解析
* @param ast
*/
export function traverseEntryFile(ast: t.File) {
let appConfig: any;
traverse(ast, {
CallExpression(path) {
const { node } = path;
const calleeName = keyNode2value(node.callee) as string;
if (calleeName === 'runApp') {
appConfig = node2value(node.arguments[0]);
path.stop();
}
},
});
return appConfig;
}
+39 -36
View File
@@ -1,27 +1,8 @@
import { getVariableContent } from '@music163/tango-helpers'; import { getCodeOfWrappedCode, isWrappedCode } from '@music163/tango-helpers';
import { value2node, expression2code, isValidExpressionCode } from './ast'; import { value2node, expression2code, code2expression, node2value } from './ast';
import { isWrappedByExpressionContainer } from './assert';
/** /**
* js value 转为代码字符串 * js value 转为代码字符串
*/
export function value2code(value: any) {
const node = value2node(value);
const code = expression2code(node);
return code;
}
/**
* 是否是字符串代码
* @param code
* @returns
*/
function isStringCode(code: string) {
return /^".*"$/.test(code?.trim());
}
/**
* js value 转为表达式代码
* @example 1 => 1 * @example 1 => 1
* @example hello => "hello" * @example hello => "hello"
* @example { foo: bar } => {{ foo: bar }} * @example { foo: bar } => {{ foo: bar }}
@@ -30,34 +11,56 @@ function isStringCode(code: string) {
* @param val js value * @param val js value
* @returns 表达式代码 * @returns 表达式代码
*/ */
export function value2expressionCode(val: any) { export function value2code(val: any) {
if (!val) return ''; if (val === undefined) {
return '';
}
if (val === null) {
return 'null';
}
let ret; let ret;
switch (typeof val) { switch (typeof val) {
case 'string': { case 'string': {
if (isValidExpressionCode(val)) { if (isWrappedCode(val)) {
ret = val; ret = getCodeOfWrappedCode(val);
} else if (isWrappedByExpressionContainer(val, false)) {
ret = getVariableContent(val);
} else if (isStringCode(val)) {
ret = val;
} else { } else {
ret = `"${val}"`; ret = `"${val}"`;
} }
break; break;
} }
case 'boolean':
case 'function':
case 'number': case 'number':
ret = String(val); ret = String(val);
break; break;
case 'object': default: {
ret = value2code(val); // other cases, including array, object, null, undefined
break; const node = value2node(val);
default: ret = expression2code(node);
ret = '';
break; break;
}
} }
return ret; return ret;
} }
export const value2expressionCode = value2code;
/**
* 代码字符串转为具体的 js value
* @example `() => {}` 返回 undefined
*
* @param rawCode 代码字符串
* @returns 返回解析后的 js value,包括:string, number, boolean, simpleObject, simpleArray
*/
export function code2value(rawCode: string) {
const node = code2expression(rawCode);
const value = node2value(node);
if (isWrappedCode(value)) {
// 能转的就转,转不能的就返回空
return;
}
return value;
}
+1 -1
View File
@@ -11,7 +11,7 @@ export function namesToImportDeclarations(
names: string[], names: string[],
nameMap: Dict<IImportSpecifierSourceData>, nameMap: Dict<IImportSpecifierSourceData>,
) { ) {
const map = {}; const map: Dict = {};
names.forEach((name) => { names.forEach((name) => {
const mod = nameMap[name]; const mod = nameMap[name];
if (mod) { if (mod) {
+41 -26
View File
@@ -3,20 +3,24 @@ import {
IComponentProp, IComponentProp,
IComponentPrototype, IComponentPrototype,
Dict, Dict,
isNil,
logger, logger,
uuid, uuid,
isWrappedCode,
getCodeOfWrappedCode,
wrapCodeWithJSXExpressionContainer,
} from '@music163/tango-helpers'; } from '@music163/tango-helpers';
import { getRelativePath, isFilepath } from './string'; import { getRelativePath, isFilepath } from './string';
import type { IImportDeclarationPayload, IImportSpecifierData } from '../types'; import type { IImportDeclarationPayload, IImportSpecifierData } from '../types';
import { code2expression } from './ast'; import { code2expression } from './ast';
import { isWrappedByExpressionContainer } from './assert'; import { isWrappedByExpressionContainer } from './assert';
import { value2code } from './code-helpers';
export function prototype2importDeclarationData( export function prototype2importDeclarationData(
prototype: IComponentPrototype, prototype: IComponentPrototype,
relativeFilepath?: string, relativeFilepath?: string,
): { source: string; specifiers: IImportSpecifierData[] } { ): { source: string; specifiers: IImportSpecifierData[] } {
let source = prototype.package; let source = prototype.package;
const isSnippet = prototype.type === 'snippet';
if (relativeFilepath && isFilepath(source)) { if (relativeFilepath && isFilepath(source)) {
source = getRelativePath(relativeFilepath, source); source = getRelativePath(relativeFilepath, source);
} }
@@ -32,12 +36,15 @@ export function prototype2importDeclarationData(
type: 'ImportDefaultSpecifier', type: 'ImportDefaultSpecifier',
}); });
} else { } else {
[prototype.name, ...(prototype.relatedImports || [])].forEach((item) => { // 忽略代码片段 name
specifiers.push({ [...(isSnippet ? [] : [prototype.name]), ...(prototype.relatedImports || [])].forEach(
localName: item, (item) => {
type: 'ImportSpecifier', specifiers.push({
}); localName: item,
}); type: 'ImportSpecifier',
});
},
);
} }
return { return {
@@ -84,48 +91,56 @@ export function getImportDeclarationPayloadByPrototype(
/** /**
* 基于 key-value 生成 prop={value} 字符串 * 基于 key-value 生成 prop={value} 字符串
* @param key * @example { name: 'foo', initValue: false } >> name={false}
* @param value * @example { name: 'foo', initValue: 1 } >> name={1}
* @example { name: 'foo', initValue: () => {} } >> name={()=>{}}
* @example { name: 'foo', initValue: { foo: 'bar' } } >> name={{ foo: 'bar' }}
* @example { name: 'foo', initValue: [{ foo: 'bar' }] } >> name={[{ foo: 'bar' }]}
* @example { name: 'foo', initValue: 'bar' } >> name="bar"
* @example { name: 'foo', initValue: '{() => {}}' } >> name={()=>{}}
* @example { name: 'foo', initValue: '{{() => {}}}' } >> name={() => {}}
* @example { name: 'foo', initValue: '{bar}' } >> name={bar}
* @returns * @returns
*/ */
function getPropKeyValuePair(item: IComponentProp, generateValue: (...args: any[]) => string) { export function propDataToKeyValueString(
item: IComponentProp,
generateValue?: (...args: any[]) => string,
) {
const key = item.name; const key = item.name;
let value = item.initValue; let value = item.initValue;
if (!value && item.autoInitValue) { if (!value && item.autoInitValue) {
value = generateValue(3); value = generateValue?.(3) || uuid(key, 3);
} }
if (isNil(value)) { if (value === undefined) {
return; return;
} }
switch (typeof value) { switch (typeof value) {
case 'number': case 'number':
case 'boolean': { case 'boolean':
value = `{${value}}`; case 'function': {
value = wrapCodeWithJSXExpressionContainer(String(value));
break; break;
} }
case 'object': { case 'object': {
// TIP: bugfix 如果 object 里有 jsx 或者 function 会失败
try { try {
value = `{${JSON.stringify(value)}}`; value = wrapCodeWithJSXExpressionContainer(value2code(value));
} catch (err) { } catch (err) {
logger.error(err); logger.error(err);
} }
break; break;
} }
case 'function': {
value = `{${(value as object).toString()}}`;
break;
}
case 'string': { case 'string': {
if (!isWrappedByExpressionContainer(value)) { if (isWrappedCode(value)) {
// 不是变量字符串 const innerCode = getCodeOfWrappedCode(value);
value = `"${value}"`; value = wrapCodeWithJSXExpressionContainer(innerCode);
} else if (isWrappedByExpressionContainer(value)) {
// TIP: 兼容旧版逻辑,如果是变量字符串,无需处理
} else { } else {
// 如果是变量字符串,无需处理 value = `"${value}"`;
} }
break; break;
} }
@@ -155,11 +170,11 @@ export function prototype2code(prototype: IComponentPrototype, extraProps?: Dict
})) }))
: []; : [];
// merge extraProps to props // merge extraProps to props
const props = [...prototype.props, ...propList]; const props = [...(prototype.props || []), ...propList];
const keys = const keys =
props.reduce((acc, item) => { props.reduce((acc, item) => {
const pair = getPropKeyValuePair(item, (fractionDigits: number) => const pair = propDataToKeyValueString(item, (fractionDigits: number) =>
uuid(prototype.name, fractionDigits), uuid(prototype.name, fractionDigits),
); );
return pair ? ` ${acc} ${pair}` : acc; return pair ? ` ${acc} ${pair}` : acc;
+2 -2
View File
@@ -1,9 +1,9 @@
import { parseDndId, uuid } from '@music163/tango-helpers'; import { Dict, parseDndId, uuid } from '@music163/tango-helpers';
// { id: 1, component, props: { id: 1 }, children: [ { id: 2, component } ] } // { id: 1, component, props: { id: 1 }, children: [ { id: 2, component } ] }
export function deepCloneNode(obj: any, component?: string): any { export function deepCloneNode(obj: any, component?: string): any {
function deepCloneObject() { function deepCloneObject() {
const target = {}; const target: Dict = {};
for (const key in obj) { for (const key in obj) {
if (Object.hasOwn(obj, key)) { if (Object.hasOwn(obj, key)) {
target[key] = deepCloneNode(obj[key], component); target[key] = deepCloneNode(obj[key], component);
+1 -1
View File
@@ -13,7 +13,7 @@ export class TangoComponentsEntryModule extends TangoModule {
constructor(workspace: IWorkspace, props: IFileConfig) { constructor(workspace: IWorkspace, props: IFileConfig) {
super(workspace, props, false); super(workspace, props, false);
this.update(props.code, false, false); this.update(props.code, true, false);
makeObservable(this, { makeObservable(this, {
_code: observable, _code: observable,
_cleanCode: observable, _cleanCode: observable,
+117 -3
View File
@@ -1,5 +1,6 @@
import { action, computed, makeObservable, observable, toJS } from 'mobx'; import { action, computed, makeObservable, observable, toJS } from 'mobx';
import { IWorkspace } from './interfaces'; import { IWorkspace } from './interfaces';
import { MenuDataType } from '@music163/tango-helpers';
export type SimulatorNameType = 'desktop' | 'phone'; export type SimulatorNameType = 'desktop' | 'phone';
@@ -19,7 +20,15 @@ interface IViewportBounding {
interface IDesignerOptions { interface IDesignerOptions {
workspace: IWorkspace; workspace: IWorkspace;
simulator?: SimulatorNameType | ISimulatorType; simulator?: SimulatorNameType | ISimulatorType;
/**
* 菜单配置
*/
menuData: MenuDataType;
activeSidebarPanel?: string; activeSidebarPanel?: string;
/**
* 默认激活的视图模式
*/
activeView?: DesignerViewType;
} }
const ISimulatorTypes: Record<string, ISimulatorType> = { const ISimulatorTypes: Record<string, ISimulatorType> = {
@@ -64,6 +73,26 @@ export class Designer {
*/ */
_showSmartWizard = false; _showSmartWizard = false;
/**
* 是否显示添加组件面板
*/
_showAddComponentPopover = false;
/**
* 添加组件面板的位置
*/
_addComponentPopoverPosition = { clientX: 0, clientY: 0 };
/**
* 是否显示右键菜单
*/
_showContextMenu = false;
/**
* 右键菜单在 iframe 上的位置
*/
_contextMenuPosition = { clientX: 0, clientY: 0 };
/** /**
* 是否显示右侧面板 * 是否显示右侧面板
*/ */
@@ -75,9 +104,9 @@ export class Designer {
_isPreview = false; _isPreview = false;
/** /**
* 默认展开的侧边栏 * 菜单列表
*/ */
defaultActiveSidebarPanel?: string; _menuData?: MenuDataType = null;
private readonly workspace: IWorkspace; private readonly workspace: IWorkspace;
@@ -109,10 +138,39 @@ export class Designer {
return this._showRightPanel; return this._showRightPanel;
} }
get showAddComponentPopover() {
return this._showAddComponentPopover;
}
get addComponentPopoverPosition() {
return this._addComponentPopoverPosition;
}
get showContextMenu() {
return this._showContextMenu;
}
get contextMenuPosition() {
return this._contextMenuPosition;
}
get menuData() {
return this._menuData ?? ([] as MenuDataType);
}
constructor(options: IDesignerOptions) { constructor(options: IDesignerOptions) {
this.workspace = options.workspace; this.workspace = options.workspace;
const { simulator, activeSidebarPanel: defaultActiveSidebarPanel } = options; const {
simulator,
menuData,
activeSidebarPanel: defaultActiveSidebarPanel,
activeView: defaultActiveView,
} = options;
if (menuData) {
this.setMenuData(menuData);
}
// 默认设计器模式 // 默认设计器模式
if (simulator) { if (simulator) {
@@ -124,6 +182,11 @@ export class Designer {
this.setActiveSidebarPanel(defaultActiveSidebarPanel); this.setActiveSidebarPanel(defaultActiveSidebarPanel);
} }
// 默认激活的视图
if (defaultActiveView) {
this.setActiveView(defaultActiveView);
}
makeObservable(this, { makeObservable(this, {
_simulator: observable, _simulator: observable,
_viewport: observable, _viewport: observable,
@@ -131,6 +194,11 @@ export class Designer {
_activeSidebarPanel: observable, _activeSidebarPanel: observable,
_showSmartWizard: observable, _showSmartWizard: observable,
_showRightPanel: observable, _showRightPanel: observable,
_showAddComponentPopover: observable,
_addComponentPopoverPosition: observable,
_showContextMenu: observable,
_contextMenuPosition: observable,
_menuData: observable,
_isPreview: observable, _isPreview: observable,
simulator: computed, simulator: computed,
viewport: computed, viewport: computed,
@@ -139,6 +207,11 @@ export class Designer {
isPreview: computed, isPreview: computed,
showRightPanel: computed, showRightPanel: computed,
showSmartWizard: computed, showSmartWizard: computed,
showAddComponentPopover: computed,
addComponentPopoverPosition: computed,
showContextMenu: computed,
contextMenuPosition: computed,
menuData: computed,
setSimulator: action, setSimulator: action,
setViewport: action, setViewport: action,
setActiveView: action, setActiveView: action,
@@ -147,6 +220,8 @@ export class Designer {
toggleRightPanel: action, toggleRightPanel: action,
toggleSmartWizard: action, toggleSmartWizard: action,
toggleIsPreview: action, toggleIsPreview: action,
toggleAddComponentPopover: action,
toggleContextMenu: action,
}); });
} }
@@ -164,6 +239,13 @@ export class Designer {
setActiveView(view: DesignerViewType) { setActiveView(view: DesignerViewType) {
this._activeView = view; this._activeView = view;
if (view === 'dual') {
this.setActiveSidebarPanel('');
this.toggleRightPanel(false);
this.toggleIsPreview(true);
} else if (view === 'design') {
this.toggleIsPreview(false);
}
} }
setActiveSidebarPanel(panel: string) { setActiveSidebarPanel(panel: string) {
@@ -173,6 +255,11 @@ export class Designer {
this._activeSidebarPanel = ''; this._activeSidebarPanel = '';
} }
} }
setMenuData(menuData: MenuDataType) {
this._menuData = menuData;
}
closeSidebarPanel() { closeSidebarPanel() {
this._activeSidebarPanel = ''; this._activeSidebarPanel = '';
} }
@@ -185,10 +272,37 @@ export class Designer {
this._showRightPanel = value ?? !this._showRightPanel; this._showRightPanel = value ?? !this._showRightPanel;
} }
/**
* 显示添加组件面板
* @param value 是否显示
* @param position 坐标
*/
toggleAddComponentPopover(
value: boolean,
position: {
clientX: number;
clientY: number;
} = this.addComponentPopoverPosition,
) {
this._showAddComponentPopover = value;
this._addComponentPopoverPosition = position;
}
toggleIsPreview(value: boolean) { toggleIsPreview(value: boolean) {
this._isPreview = value ?? !this._isPreview; this._isPreview = value ?? !this._isPreview;
if (value) { if (value) {
this.workspace.selectSource.clear(); this.workspace.selectSource.clear();
} }
} }
toggleContextMenu(
value: boolean,
position: {
clientX: number;
clientY: number;
} = this.contextMenuPosition,
) {
this._showContextMenu = value;
this._contextMenuPosition = position;
}
} }
+18
View File
@@ -0,0 +1,18 @@
import { traverseEntryFile } from '../helpers';
import { IFileConfig } from '../types';
import { IWorkspace } from './interfaces';
import { TangoModule } from './module';
export class AppEntryModule extends TangoModule {
routerType: string;
constructor(workspace: IWorkspace, props: IFileConfig) {
super(workspace, props, false);
this.update(props.code, true, false);
}
_analysisAst() {
const config = traverseEntryFile(this.ast);
this.routerType = config?.router?.type;
}
}
+11
View File
@@ -24,6 +24,16 @@ export class TangoFile {
*/ */
lastModified: number; lastModified: number;
/**
* 文件解析是否出错
*/
isError: boolean;
/**
* 文件解析错误消息
*/
errorMessage: string;
_code: string; _code: string;
_cleanCode: string; _cleanCode: string;
@@ -40,6 +50,7 @@ export class TangoFile {
this.filename = props.filename; this.filename = props.filename;
this.type = props.type; this.type = props.type;
this.lastModified = Date.now(); this.lastModified = Date.now();
this.isError = false;
// 这里主要是为了解决 umi ts 编译错误的问题,@see https://github.com/umijs/umi/issues/7594 // 这里主要是为了解决 umi ts 编译错误的问题,@see https://github.com/umijs/umi/issues/7594
if (isSyncCode) { if (isSyncCode) {
+1 -1
View File
@@ -58,7 +58,7 @@ export class TangoHistory {
} }
get couldBack() { get couldBack() {
return this._records.length > 0 && this._index > -1; return this._records.length > 0 && this._index > 0;
} }
get couldForward() { get couldForward() {
+1
View File
@@ -7,6 +7,7 @@ export * from './drag-source';
export * from './history'; export * from './history';
export * from './file'; export * from './file';
export * from './module'; export * from './module';
export * from './entry-module';
export * from './route-module'; export * from './route-module';
export * from './service-module'; export * from './service-module';
export * from './store-module'; export * from './store-module';
+29 -1
View File
@@ -10,12 +10,14 @@ import {
IPageConfigData, IPageConfigData,
IImportSpecifierSourceData, IImportSpecifierSourceData,
IImportSpecifierData, IImportSpecifierData,
IFileError,
} from '../types'; } from '../types';
import { TangoFile, TangoJsonFile } from './file'; import { TangoFile, TangoJsonFile } from './file';
import { TangoRouteModule } from './route-module'; import { TangoRouteModule } from './route-module';
import { TangoStoreModule } from './store-module'; import { TangoStoreModule } from './store-module';
import { TangoServiceModule } from './service-module'; import { TangoServiceModule } from './service-module';
import { IdGenerator } from '../helpers'; import { IdGenerator } from '../helpers';
import { AppEntryModule } from './entry-module';
export interface IViewFile { export interface IViewFile {
readonly workspace: IWorkspace; readonly workspace: IWorkspace;
@@ -152,6 +154,10 @@ export interface IWorkspace {
* 解析后的 tango.config.json 文件,如果要获取项目配置,推荐使用 projectConfig 获取 * 解析后的 tango.config.json 文件,如果要获取项目配置,推荐使用 projectConfig 获取
*/ */
tangoConfigJson: TangoJsonFile; tangoConfigJson: TangoJsonFile;
/**
* app.js 入口文件解析后的模块
*/
appEntryModule: AppEntryModule;
/** /**
* 解析后的路由模块 * 解析后的路由模块
*/ */
@@ -187,7 +193,18 @@ export interface IWorkspace {
renameFile: (oldFilename: string, newFilename: string) => void; renameFile: (oldFilename: string, newFilename: string) => void;
renameFolder: (oldFoldername: string, newFoldername: string) => void; renameFolder: (oldFoldername: string, newFoldername: string) => void;
updateFile: (filename: string, code: string, shouldFormatCode?: boolean) => void; /**
* 更新文件
* @param filename 文件名
* @param code 代码
* @param isSyncAst 是否同步 ast
*/
updateFile: (filename: string, code: string, isSyncAst?: boolean) => void;
/**
* 检查并同步文件的 ast
*/
syncFiles: () => void;
listFiles: () => Record<string, string>; listFiles: () => Record<string, string>;
getFile: (filename: string) => TangoFile; getFile: (filename: string) => TangoFile;
@@ -285,9 +302,20 @@ export interface IWorkspace {
* 解析后的项目配置信息 * 解析后的项目配置信息
*/ */
get projectConfig(): ITangoConfigJson; get projectConfig(): ITangoConfigJson;
/**
* 当前活动的视图文件
*/
get activeViewModule(): IViewFile; get activeViewModule(): IViewFile;
get pages(): any[]; get pages(): any[];
get bizComps(): string[]; get bizComps(): string[];
get baseComps(): string[]; get baseComps(): string[];
get localComps(): string[]; get localComps(): string[];
get fileErrors(): IFileError[];
/**
* 是否是有效的项目
* - 包含 tango.config.json
* - 包含视图模块
* - 没有文件错误
*/
get isValid(): boolean;
} }
+60 -18
View File
@@ -21,6 +21,11 @@ import { IWorkspace } from './interfaces';
export class TangoModule extends TangoFile { export class TangoModule extends TangoFile {
ast: t.File; ast: t.File;
/**
* ast 是否与 code 保持同步
*/
isAstSynced: boolean;
/** /**
* 导入的依赖列表 * 导入的依赖列表
*/ */
@@ -33,23 +38,53 @@ export class TangoModule extends TangoFile {
/** /**
* 基于最新的 ast 进行同步 * 基于最新的 ast 进行同步
* @param code 如果传入 code,则基于 code 进行同步 * @param code 如果传入 code,则基于 code 进行同步
* @param isFormatCode 是否格式化代码 * @param isSyncAst 是否同步 ast
* @param refreshWorkspace 是否刷新 workspace * @param isRefreshWorkspace 是否刷新 workspace
*/ */
update(code?: string, isFormatCode = true, refreshWorkspace = true) { update(code?: string, isSyncAst = true, isRefreshWorkspace = true) {
this.lastModified = Date.now(); this.lastModified = Date.now();
if (isNil(code)) {
this._syncByAst(); try {
} else { if (isNil(code)) {
this._syncByCode(code, isFormatCode); this._syncByAst();
} else {
this._syncByCode(code, isSyncAst);
}
if (isSyncAst) {
this._analysisAst();
}
this.isAstSynced = isSyncAst;
this.isError = false;
this.errorMessage = undefined;
this.workspace.onFilesChange([this.filename]);
if (isRefreshWorkspace) {
this.workspace.refresh([this.filename]);
}
} catch (err: any) {
this.isError = true;
this.errorMessage = err.message;
} }
}
this._analysisAst(); /**
* 基于当前的代码重新生成 ast
this.workspace.onFilesChange([this.filename]); */
updateAst() {
if (refreshWorkspace) { if (!this.isAstSynced) {
this.workspace.refresh([this.filename]); try {
this.ast = code2ast(this._code);
this._analysisAst();
this.isAstSynced = true;
this.isError = false;
this.errorMessage = undefined;
} catch (err: any) {
this.isAstSynced = false;
this.isError = true;
this.errorMessage = err.message;
}
} }
} }
@@ -75,22 +110,26 @@ export class TangoModule extends TangoFile {
/** /**
* 基于输入的源码进行同步 * 基于输入的源码进行同步
* @param code 源码 * @param code 源码
* @param isFormatCode 是否格式化代码 * @param isSyncAst 是否同步 ast
* @returns * @returns
*/ */
_syncByCode(code: string, isFormatCode = true) { _syncByCode(code: string, isSyncAst = true) {
if (code === this._code) { if (code === this._code) {
return; return;
} }
// 提前格式化代码 // 提前格式化代码
if (isFormatCode) { try {
code = formatCode(code); code = formatCode(code);
} catch (err) {
// err ignored, format code failed
} }
this._code = code; this._code = code;
this._cleanCode = code; this._cleanCode = code;
this.ast = code2ast(code); if (isSyncAst) {
this.ast = code2ast(code);
}
} }
_analysisAst() { _analysisAst() {
@@ -105,14 +144,17 @@ export class TangoModule extends TangoFile {
export class TangoJsModule extends TangoModule { export class TangoJsModule extends TangoModule {
constructor(workspace: IWorkspace, props: IFileConfig) { constructor(workspace: IWorkspace, props: IFileConfig) {
super(workspace, props, false); super(workspace, props, false);
this.update(props.code, false, false); this.update(props.code, true, false);
makeObservable(this, { makeObservable(this, {
_code: observable, _code: observable,
_cleanCode: observable, _cleanCode: observable,
isError: observable,
errorMessage: observable,
code: computed, code: computed,
cleanCode: computed, cleanCode: computed,
update: action, update: action,
updateAst: action,
}); });
} }
} }
+5 -1
View File
@@ -1,4 +1,5 @@
import { action, computed, makeObservable, observable, toJS } from 'mobx'; import { action, computed, makeObservable, observable, toJS } from 'mobx';
import { logger } from '@music163/tango-helpers';
import { import {
traverseRouteFile, traverseRouteFile,
addRouteToRouteFile, addRouteToRouteFile,
@@ -27,10 +28,13 @@ export class TangoRouteModule extends TangoModule {
_routes: observable, _routes: observable,
_code: observable, _code: observable,
_cleanCode: observable, _cleanCode: observable,
isError: observable,
errorMessage: observable,
routes: computed, routes: computed,
code: computed, code: computed,
cleanCode: computed, cleanCode: computed,
update: action, update: action,
updateAst: action,
}); });
} }
@@ -74,7 +78,7 @@ export class TangoRouteModule extends TangoModule {
*/ */
removeRoute(route: string) { removeRoute(route: string) {
if (route === '/') { if (route === '/') {
console.warn('index route should not be removed!'); logger.warn('index route should not be removed!');
return; return;
} }
const record = this.getRouteByRoutePath(route); const record = this.getRouteByRoutePath(route);
+14
View File
@@ -109,6 +109,20 @@ export class SelectSource {
this._start = null; this._start = null;
} }
/**
* 选中当前选中节点的父节点
*/
selectParent() {
const parents = this.first?.parents || [];
if (parents.length) {
const [parent, ...rest] = parents;
this.select({
...parent,
parents: rest,
});
}
}
setStart(data: StartDataType) { setStart(data: StartDataType) {
this._start = data; this._start = data;
} }
@@ -49,11 +49,14 @@ export class TangoServiceModule extends TangoModule {
_baseConfig: observable, _baseConfig: observable,
_code: observable, _code: observable,
_cleanCode: observable, _cleanCode: observable,
isError: observable,
errorMessage: observable,
serviceFunctions: computed, serviceFunctions: computed,
baseConfig: computed, baseConfig: computed,
cleanCode: computed, cleanCode: computed,
code: computed, code: computed,
update: action, update: action,
updateAst: action,
}); });
} }
+3
View File
@@ -31,10 +31,13 @@ export class TangoStoreEntryModule extends TangoModule {
_stores: observable, _stores: observable,
_code: observable, _code: observable,
_cleanCode: observable, _cleanCode: observable,
isError: observable,
errorMessage: observable,
stores: computed, stores: computed,
code: computed, code: computed,
cleanCode: computed, cleanCode: computed,
update: action, update: action,
updateAst: action,
}); });
} }
+5 -1
View File
@@ -138,10 +138,14 @@ export class TangoViewModule extends TangoModule implements IViewFile {
_code: observable, _code: observable,
_cleanCode: observable, _cleanCode: observable,
isError: observable,
errorMessage: observable,
code: computed, code: computed,
cleanCode: computed, cleanCode: computed,
update: action, update: action,
updateAst: action,
}); });
} }
@@ -320,7 +324,7 @@ export class TangoViewModule extends TangoModule implements IViewFile {
updateNodeAttributes(nodeId: string, config: Record<string, any>, relatedImports?: string[]) { updateNodeAttributes(nodeId: string, config: Record<string, any>, relatedImports?: string[]) {
if (relatedImports && relatedImports.length) { if (relatedImports && relatedImports.length) {
// 导入依赖的组件 // 导入依赖的组件
const newImportData = relatedImports.reduce((prev, name) => { const newImportData = relatedImports.reduce<Dict<IImportSpecifierData[]>>((prev, name) => {
const proto = this.workspace.getPrototype(name); const proto = this.workspace.getPrototype(name);
const { source, specifiers } = prototype2importDeclarationData(proto, this.filename); const { source, specifiers } = prototype2importDeclarationData(proto, this.filename);
const existSpecifiers: IImportSpecifierData[] = prev[source]; const existSpecifiers: IImportSpecifierData[] = prev[source];
+71 -22
View File
@@ -11,6 +11,7 @@ import {
parseServiceVariablePath, parseServiceVariablePath,
parseStoreVariablePath, parseStoreVariablePath,
uniq, uniq,
setValue,
} from '@music163/tango-helpers'; } from '@music163/tango-helpers';
import { import {
prototype2jsxElement, prototype2jsxElement,
@@ -24,10 +25,10 @@ import {
import { DropMethod } from './drop-target'; import { DropMethod } from './drop-target';
import { HistoryMessage, TangoHistory } from './history'; import { HistoryMessage, TangoHistory } from './history';
import { TangoNode } from './node'; import { TangoNode } from './node';
import { TangoJsModule } from './module'; import { TangoJsModule, TangoModule } from './module';
import { TangoFile, TangoJsonFile, TangoLessFile } from './file'; import { TangoFile, TangoJsonFile, TangoLessFile } from './file';
import { IWorkspace } from './interfaces'; import { IWorkspace } from './interfaces';
import { IFileConfig, FileType, ITangoConfigPackages, IPageConfigData } from '../types'; import { IFileConfig, FileType, ITangoConfigPackages, IPageConfigData, IFileError } from '../types';
import { SelectSource } from './select-source'; import { SelectSource } from './select-source';
import { DragSource } from './drag-source'; import { DragSource } from './drag-source';
import { TangoRouteModule } from './route-module'; import { TangoRouteModule } from './route-module';
@@ -35,6 +36,7 @@ import { TangoStoreEntryModule, TangoStoreModule } from './store-module';
import { TangoServiceModule } from './service-module'; import { TangoServiceModule } from './service-module';
import { TangoViewModule } from './view-module'; import { TangoViewModule } from './view-module';
import { TangoComponentsEntryModule } from './component-module'; import { TangoComponentsEntryModule } from './component-module';
import { AppEntryModule } from './entry-module';
export interface IWorkspaceOptions { export interface IWorkspaceOptions {
/** /**
@@ -97,6 +99,11 @@ export class Workspace extends EventTarget implements IWorkspace {
*/ */
activeViewFile: string; activeViewFile: string;
/**
* 应用入口模块
*/
appEntryModule: AppEntryModule;
/** /**
* 路由配置模块 * 路由配置模块
*/ */
@@ -144,6 +151,10 @@ export class Workspace extends EventTarget implements IWorkspace {
*/ */
private copyTempNodes: TangoNode[]; private copyTempNodes: TangoNode[];
get isValid() {
return !!this.tangoConfigJson && !!this.activeViewModule && this.fileErrors.length === 0;
}
/** /**
* 项目配置,返回解析后的 tango.config.json 文件 * 项目配置,返回解析后的 tango.config.json 文件
*/ */
@@ -209,6 +220,19 @@ export class Workspace extends EventTarget implements IWorkspace {
return Object.keys(this.componentsEntryModule?.exportList || {}); return Object.keys(this.componentsEntryModule?.exportList || {});
} }
get fileErrors() {
const errors: IFileError[] = [];
this.files.forEach((file) => {
if (file.isError) {
errors.push({
filename: file.filename,
message: file.errorMessage,
});
}
});
return errors;
}
constructor(options?: IWorkspaceOptions) { constructor(options?: IWorkspaceOptions) {
super(); super();
this.history = new TangoHistory(this); this.history = new TangoHistory(this);
@@ -242,6 +266,8 @@ export class Workspace extends EventTarget implements IWorkspace {
activeViewFile: observable, activeViewFile: observable,
pages: computed, pages: computed,
bizComps: computed, bizComps: computed,
fileErrors: computed,
isValid: computed,
setActiveRoute: action, setActiveRoute: action,
setActiveFile: action, setActiveFile: action,
addFile: action, addFile: action,
@@ -322,6 +348,9 @@ export class Workspace extends EventTarget implements IWorkspace {
* @param fileType 模块类型 * @param fileType 模块类型
*/ */
addFile(filename: string, code: string, fileType?: FileType) { addFile(filename: string, code: string, fileType?: FileType) {
if (!fileType && filename === this.entry) {
fileType = FileType.AppEntryModule;
}
const moduleType = fileType || inferFileType(filename); const moduleType = fileType || inferFileType(filename);
const props = { const props = {
filename, filename,
@@ -331,6 +360,10 @@ export class Workspace extends EventTarget implements IWorkspace {
let module; let module;
switch (moduleType) { switch (moduleType) {
case FileType.AppEntryModule:
module = new AppEntryModule(this, props);
this.appEntryModule = module;
break;
case FileType.StoreEntryModule: case FileType.StoreEntryModule:
module = new TangoStoreEntryModule(this, props); module = new TangoStoreEntryModule(this, props);
this.storeEntryModule = module; this.storeEntryModule = module;
@@ -418,10 +451,16 @@ export class Workspace extends EventTarget implements IWorkspace {
); );
} }
updateFile(filename: string, code: string, shouldFormatCode = false) { updateFile(filename: string, code: string, isSyncAst = true) {
const file = this.getFile(filename); const file = this.getFile(filename);
file.update(code); if (file instanceof TangoModule) {
if (shouldFormatCode && file instanceof TangoViewModule) { file.update(code, isSyncAst);
} else {
file.update(code);
}
const shouldFormat = this.projectConfig?.designerConfig?.autoFormatCode;
if (shouldFormat && file instanceof TangoViewModule) {
file.removeUnusedImportSpecifiers().update(); file.removeUnusedImportSpecifiers().update();
} }
this.history.push({ this.history.push({
@@ -432,12 +471,15 @@ export class Workspace extends EventTarget implements IWorkspace {
}); });
} }
/** syncFiles() {
* 删除工作区的文件 this.files.forEach((file) => {
* @param filename if (file instanceof TangoModule) {
*/ file.updateAst();
}
});
}
removeFile(filename: string) { removeFile(filename: string) {
// TODO: refactor visitFile to share this logic
if (this.files.get(filename)) { if (this.files.get(filename)) {
// 如果是文件,直接删除 // 如果是文件,直接删除
this.files.delete(filename); this.files.delete(filename);
@@ -493,7 +535,7 @@ export class Workspace extends EventTarget implements IWorkspace {
* @returns { [filename]: fileCode } * @returns { [filename]: fileCode }
*/ */
listFiles() { listFiles() {
const ret = {}; const ret: Dict<string> = {};
this.files.forEach((file) => { this.files.forEach((file) => {
ret[file.filename] = file.cleanCode; ret[file.filename] = file.cleanCode;
}); });
@@ -721,6 +763,7 @@ export class Workspace extends EventTarget implements IWorkspace {
* 更新依赖,没有就添加 * 更新依赖,没有就添加
* @param name * @param name
* @param version * @param version
* FIXME: 参数3的设置需要重新考虑下
*/ */
updateDependency( updateDependency(
name: string, name: string,
@@ -743,15 +786,12 @@ export class Workspace extends EventTarget implements IWorkspace {
if (!packages) { if (!packages) {
return undefined; return undefined;
} }
if (options?.package || packages[name]) {
packages[name] = { setValue(packages, name, {
type: packages[name]?.type, ...packages[name], // 保留原有的配置
// 如果没有传入 package 信息,则沿用 tango.config.json 中已记录的数据 version, // 更新版本号
// 如果传入了,则全量使用传入的信息,适配可能在某个版本中如 resources 等字段被删除的情况 ...options?.package, // 更新 package 配置
...(options?.package || packages[name]), });
version,
};
}
return packages; return packages;
}) })
@@ -1226,8 +1266,17 @@ export class Workspace extends EventTarget implements IWorkspace {
let filename: string; let filename: string;
this.routeModule?.routes.forEach((route) => { this.routeModule?.routes.forEach((route) => {
if (isPathnameMatchRoute(routePath, route.path) && route.importPath) { if (isPathnameMatchRoute(routePath, route.path) && route.importPath) {
const absolutePath = route.importPath.replace('.', '/src'); if (route.importPath.startsWith('@/')) {
filename = this.getRealViewFilePath(absolutePath); filename = route.importPath;
const alias = this.tangoConfigJson.getValue('sandbox.alias') || {};
if (alias['@']) {
filename = filename.replace('@', alias['@']);
}
filename = this.getRealViewFilePath(filename);
} else {
const absolutePath = route.importPath.replace('.', '/src');
filename = this.getRealViewFilePath(absolutePath);
}
} }
}); });
return filename; return filename;
+6
View File
@@ -8,6 +8,7 @@ export type SimulatorMode = 'desktop' | 'tablet' | 'phone';
export enum FileType { export enum FileType {
// js 文件 // js 文件
Module = 'module', Module = 'module',
AppEntryModule = 'appEntryModule',
StoreEntryModule = 'storeEntryModule', StoreEntryModule = 'storeEntryModule',
RouteModule = 'routeModule', RouteModule = 'routeModule',
ServiceModule = 'serviceModule', ServiceModule = 'serviceModule',
@@ -56,6 +57,11 @@ export interface IFileConfig {
type?: FileType; type?: FileType;
} }
export interface IFileError {
filename: string;
message: string;
}
/** /**
* 视图节点数据类型 * 视图节点数据类型
*/ */
+1 -16
View File
@@ -1,4 +1,4 @@
import { isTangoVariable, isWrappedByExpressionContainer } from '../src/helpers'; import { isTangoVariable } from '../src/helpers';
describe('assert', () => { describe('assert', () => {
it('isTangoVariable', () => { it('isTangoVariable', () => {
@@ -7,19 +7,4 @@ describe('assert', () => {
expect(isTangoVariable('tango.stores.app?.name')).toBeTruthy(); expect(isTangoVariable('tango.stores.app?.name')).toBeTruthy();
// expect(isTangoVariable('tango.copyToClipboard')).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();
});
}); });
+28 -5
View File
@@ -1,19 +1,28 @@
import { Identifier } from '@babel/types';
import { import {
object2node, object2node,
serviceConfig2Node, serviceConfig2Node,
isValidCode, isValidCode,
isValidExpressionCode, isValidExpressionCode,
code2expression, code2expression,
value2jsxAttributeValueNode,
} from '../src/helpers'; } from '../src/helpers';
describe('ast helpers', () => { describe('ast helpers', () => {
it('isValidCode', () => { it('isValidCode', () => {
expect(isValidCode('')).toBeTruthy();
expect(isValidCode('1')).toBeTruthy();
expect(isValidCode('"hello"')).toBeTruthy();
expect(isValidCode('<div>hello</div>')).toBeTruthy();
expect(isValidCode('function fn() {}')).toBeTruthy();
// invalid function body
expect(isValidCode('() => { hello world }')).toBeFalsy(); expect(isValidCode('() => { hello world }')).toBeFalsy();
// invalid function
expect(isValidCode('function() {}')).toBeFalsy(); expect(isValidCode('function() {}')).toBeFalsy();
}); });
it('isValidExpression', () => { it('isValidExpressionCode', () => {
expect(isValidExpressionCode('() => { }')).toBeTruthy();
expect(isValidExpressionCode('1')).toBeTruthy(); expect(isValidExpressionCode('1')).toBeTruthy();
expect(isValidExpressionCode('a = 1')).toBeTruthy(); expect(isValidExpressionCode('a = 1')).toBeTruthy();
expect(isValidExpressionCode('1 + 1')).toBeTruthy(); expect(isValidExpressionCode('1 + 1')).toBeTruthy();
@@ -22,8 +31,11 @@ describe('ast helpers', () => {
expect(isValidExpressionCode('{ bizId: "vip", type: "category" }')).toBeTruthy(); expect(isValidExpressionCode('{ bizId: "vip", type: "category" }')).toBeTruthy();
expect(isValidExpressionCode('[1,2,3]')).toBeTruthy(); expect(isValidExpressionCode('[1,2,3]')).toBeTruthy();
expect(isValidExpressionCode('<div>hello</div>')).toBeTruthy(); expect(isValidExpressionCode('<div>hello</div>')).toBeTruthy();
expect(isValidExpressionCode('<div>hello</div>')).toBeTruthy(); expect(isValidExpressionCode('tango.stores.app.title')).toBeTruthy();
expect(isValidExpressionCode('"hello" + window.location.path')).toBeTruthy();
expect(isValidExpressionCode('() => { }')).toBeTruthy();
expect(isValidExpressionCode('')).toBeFalsy();
expect(isValidExpressionCode('{1}')).toBeFalsy(); expect(isValidExpressionCode('{1}')).toBeFalsy();
expect(isValidExpressionCode('{"1"}')).toBeFalsy(); expect(isValidExpressionCode('{"1"}')).toBeFalsy();
expect(isValidExpressionCode('{ 1+1 }')).toBeFalsy(); expect(isValidExpressionCode('{ 1+1 }')).toBeFalsy();
@@ -54,8 +66,7 @@ describe('ast helpers', () => {
it('code2expression', () => { it('code2expression', () => {
expect(code2expression('')).toBeUndefined(); expect(code2expression('')).toBeUndefined();
expect(code2expression('{tango.stores.app}')).toBeUndefined(); expect(code2expression('tango.stores.app').type).toBe('MemberExpression');
expect(code2expression('{ type: window.bar }').type).toEqual('ObjectExpression'); expect(code2expression('{ type: window.bar }').type).toEqual('ObjectExpression');
expect(code2expression('() => {};').type).toEqual('ArrowFunctionExpression'); expect(code2expression('() => {};').type).toEqual('ArrowFunctionExpression');
expect(code2expression('<Button>hello</Button>').type).toBe('JSXElement'); expect(code2expression('<Button>hello</Button>').type).toBe('JSXElement');
@@ -69,4 +80,16 @@ describe('ast helpers', () => {
`; `;
expect(code2expression(arrayCode).type).toEqual('ArrayExpression'); expect(code2expression(arrayCode).type).toEqual('ArrayExpression');
}); });
it('value2jsxAttributeValueNode', () => {
expect(value2jsxAttributeValueNode('hello').type).toBe('StringLiteral');
expect(value2jsxAttributeValueNode(true).type).toBe('JSXExpressionContainer');
expect(value2jsxAttributeValueNode(1).type).toBe('JSXExpressionContainer');
expect(value2jsxAttributeValueNode('{{{ foo: "foo"}}}').type).toBe('JSXExpressionContainer');
// invalid code will return undefined
const node: any = value2jsxAttributeValueNode('{{tango.xx+}}');
expect(node.type).toBe('JSXExpressionContainer');
expect((node.expression as Identifier).name).toBe('undefined');
});
}); });
+37 -10
View File
@@ -17,6 +17,7 @@ import {
getJSXElementAttributes, getJSXElementAttributes,
inferFileType, inferFileType,
deepCloneNode, deepCloneNode,
code2value,
} from '../src/helpers'; } from '../src/helpers';
import { FileType } from '../src/types'; import { FileType } from '../src/types';
@@ -27,15 +28,18 @@ describe('helpers', () => {
it('parse jsxElement attributes', () => { it('parse jsxElement attributes', () => {
const node = code2expression( const node = code2expression(
"<XColumn dataIndex='col' enumMap={{ 1: '已解决', 2: '未解决' }} />", "<Foo id={tango.user.id} num={1} str='col' enumMap={{ 1: '已解决', 2: '未解决' }} list={[{ key: 1 }, { key: 2 }]} />",
); );
const attributes = getJSXElementAttributes(node as JSXElement); const attributes = getJSXElementAttributes(node as JSXElement);
expect(attributes).toEqual({ expect(attributes).toEqual({
dataIndex: 'col', id: '{{tango.user.id}}',
num: 1,
str: 'col',
enumMap: { enumMap: {
1: '已解决', 1: '已解决',
2: '未解决', 2: '未解决',
}, },
list: '{{[{ key: 1 }, { key: 2 }]}}',
}); });
}); });
@@ -80,15 +84,22 @@ describe('helpers', () => {
it('expressionCode2ast', () => { it('expressionCode2ast', () => {
expect(expressionCode2ast('<Button>hello</Button>').type).toEqual('File'); expect(expressionCode2ast('<Button>hello</Button>').type).toEqual('File');
expect(expressionCode2ast('{<Button>hello</Button>}').type).toEqual('File');
expect(expressionCode2ast('() => <Button>hello</Button>').type).toEqual('File'); expect(expressionCode2ast('() => <Button>hello</Button>').type).toEqual('File');
expect(expressionCode2ast('{() => <Button>hello</Button>}').type).toEqual('File');
}); });
}); });
describe('string helpers', () => { describe('string helpers', () => {
it('value2code: empty array', () => { it('value2code: empty array', () => {
expect(value2code([])).toEqual('[]'); expect(value2code([])).toEqual('[]');
expect(value2code({})).toEqual('{}');
expect(value2code(() => {})).toEqual('() => {}');
expect(value2code(true)).toEqual('true');
expect(value2code(false)).toEqual('false');
expect(value2code(1)).toEqual('1');
expect(value2code('hello')).toEqual('"hello"');
expect(value2code('{{window.tango}}')).toEqual('window.tango');
expect(value2code('{{() => {}}}')).toEqual('() => {}');
expect(value2code('{{1111}}')).toEqual('1111');
}); });
it('value2code: array', () => { it('value2code: array', () => {
@@ -201,10 +212,26 @@ describe('schema helpers', () => {
}, },
], ],
}; };
const cloned = deepCloneNode(schema); it('deepCloneNode', () => {
expect(cloned.props.id).toBe(schema.props.id); const cloned = deepCloneNode(schema);
expect(cloned.children[0].props.id).toBe(schema.children[0].props.id); expect(cloned.props.id).toBe(schema.props.id);
expect(cloned.children[0].props.id).toBe(schema.children[0].props.id);
expect(cloned.id).not.toBe(schema.id); expect(cloned.id).not.toBe(schema.id);
expect(cloned.children[0].id).not.toBe(schema.children[0].id); expect(cloned.children[0].id).not.toBe(schema.children[0].id);
});
});
describe('code helper', () => {
it('code2value', () => {
expect(code2value(`1`)).toEqual(1);
expect(code2value(`false`)).toEqual(false);
expect(code2value(`"foo"`)).toEqual('foo');
expect(code2value(`{ foo: "foo" }`)).toEqual({ foo: 'foo' });
expect(code2value(`[{ foo: "foo" }]`)).toEqual([{ foo: 'foo' }]);
expect(code2value(`{ foo: "foo", ...{ bar: "bar"} }`)).toBe(undefined);
expect(code2value(`() => {}`)).toBe(undefined);
expect(code2value(`tango.stores.app.name`)).toBe(undefined);
expect(code2value(`window`)).toBe(undefined);
expect(code2value(`<div>hello</div>`)).toBe(undefined);
});
}); });
+45
View File
@@ -0,0 +1,45 @@
import { propDataToKeyValueString } from '../src/helpers';
describe('prototype helpers', () => {
it('propDataToKeyValueString', () => {
// basic
expect(propDataToKeyValueString({ name: 'foo', initValue: 'bar' })).toEqual('foo="bar"');
expect(propDataToKeyValueString({ name: 'foo', initValue: 1 })).toEqual('foo={1}');
expect(propDataToKeyValueString({ name: 'foo', initValue: false })).toEqual('foo={false}');
expect(propDataToKeyValueString({ name: 'foo', initValue: null })).toEqual('foo={null}');
expect(propDataToKeyValueString({ name: 'foo', initValue: [] })).toEqual('foo={[]}');
expect(propDataToKeyValueString({ name: 'foo', initValue: {} })).toEqual('foo={{}}');
expect(propDataToKeyValueString({ name: 'foo', initValue: () => {} })).toEqual(
'foo={() => {}}',
);
expect(propDataToKeyValueString({ name: 'foo', initValue: { foo: 'bar' } })).toEqual(
'foo={{ foo: "bar" }}',
);
expect(propDataToKeyValueString({ name: 'foo', initValue: [{ foo: 'bar' }] })).toEqual(
'foo={[{ foo: "bar" }]}',
);
// wrapped code
expect(
propDataToKeyValueString({ name: 'foo', initValue: '{{<Placeholder text="放置替换" />}}' }),
).toEqual('foo={<Placeholder text="放置替换" />}');
expect(propDataToKeyValueString({ name: 'foo', initValue: '{{tango}}' })).toEqual(
'foo={tango}',
);
expect(propDataToKeyValueString({ name: 'foo', initValue: '{{"bar"}}' })).toEqual(
'foo={"bar"}',
);
expect(propDataToKeyValueString({ name: 'foo', initValue: '{{() => {}}}' })).toEqual(
'foo={() => {}}',
);
// compatible with old version
expect(propDataToKeyValueString({ name: 'foo', initValue: '{() => {}}' })).toEqual(
'foo={() => {}}',
);
expect(
propDataToKeyValueString({ name: 'foo', initValue: '{<Placeholder text="放置替换" />}' }),
).toEqual('foo={<Placeholder text="放置替换" />}');
// expect(propDataToKeyValueString({ name: 'foo', initValue: '{tango}' })).toEqual('foo={tango}');
});
});
File diff suppressed because it is too large Load Diff
+96
View File
@@ -3,6 +3,102 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.4.0](https://github.com/netease/tango/compare/@music163/tango-designer@1.3.3...@music163/tango-designer@1.4.0) (2024-08-01)
### Bug Fixes
- update types ([67d602a](https://github.com/netease/tango/commit/67d602a3ec6b7f74156bb78fda6f3b4bc2676e55))
### Features
- add isError and errorMessage to File & add isAstSynced to Module ([#190](https://github.com/netease/tango/issues/190)) ([8dd289c](https://github.com/netease/tango/commit/8dd289cd7daba628e54dc6b8929f22a1e2245160))
## [1.3.3](https://github.com/netease/tango/compare/@music163/tango-designer@1.3.2...@music163/tango-designer@1.3.3) (2024-07-12)
### Bug Fixes
- selection bug without prototype & update ToggleButton style ([#180](https://github.com/netease/tango/issues/180)) ([1d0fb4f](https://github.com/netease/tango/commit/1d0fb4f274a5b767962c54583ab5863329d5bcc5))
- update designer config ([#186](https://github.com/netease/tango/issues/186)) ([85c053b](https://github.com/netease/tango/commit/85c053b3db6d652b41c9873dba1366315174833f))
## [1.3.2](https://github.com/netease/tango/compare/@music163/tango-designer@1.3.1...@music163/tango-designer@1.3.2) (2024-06-20)
### Bug Fixes
- drag-panel add resizeable & height prop ([#175](https://github.com/netease/tango/issues/175)) ([b53a9f7](https://github.com/netease/tango/commit/b53a9f7eefc748ba8a7c94270c3a374b9d6ed211))
- fix global hotkey triggering on content editable elements ([#174](https://github.com/netease/tango/issues/174)) ([a2cc418](https://github.com/netease/tango/commit/a2cc418a73edfd9b7fe8aad456ae775a49df2166))
- update designer style ([#176](https://github.com/netease/tango/issues/176)) ([0f3f0af](https://github.com/netease/tango/commit/0f3f0afdfa8aee2532a97c5c2e92ef4230397d86))
## [1.3.1](https://github.com/netease/tango/compare/@music163/tango-designer@1.3.0...@music163/tango-designer@1.3.1) (2024-06-05)
### Bug Fixes
- fix context menu error on macOS ([#170](https://github.com/netease/tango/issues/170)) ([ca740c8](https://github.com/netease/tango/commit/ca740c83ae7f72a75c7db90d2e137a7e9a032326))
# [1.3.0](https://github.com/netease/tango/compare/@music163/tango-designer@1.2.3...@music163/tango-designer@1.3.0) (2024-06-03)
### Bug Fixes
- parse app entry file & pass correct routerType to sandbox ([#168](https://github.com/netease/tango/issues/168)) ([3f3981b](https://github.com/netease/tango/commit/3f3981bb9db874b738a67fd449c579c35c58d08b))
### Features
- add context menu ([#161](https://github.com/netease/tango/issues/161)) ([28040fc](https://github.com/netease/tango/commit/28040fc00604339a40ad3216b76baf7de93a13e0))
## [1.2.3](https://github.com/netease/tango/compare/@music163/tango-designer@1.2.2...@music163/tango-designer@1.2.3) (2024-05-30)
### Bug Fixes
- optimize variable panel scroll ui & onCancel bug ([#166](https://github.com/netease/tango/issues/166)) ([a0ed57f](https://github.com/netease/tango/commit/a0ed57ff4f332bd88749b8b1925bdb29319e4d13))
## [1.2.2](https://github.com/netease/tango/compare/@music163/tango-designer@1.2.1...@music163/tango-designer@1.2.2) (2024-05-27)
### Bug Fixes
- export usePreviewSandboxQuery & add builtin sandboxQuery ([#163](https://github.com/netease/tango/issues/163)) ([163eced](https://github.com/netease/tango/commit/163ecedff3ed7baee59e200a8cc60dcc63a24e48))
## [1.2.1](https://github.com/netease/tango/compare/@music163/tango-designer@1.2.0...@music163/tango-designer@1.2.1) (2024-05-22)
### Bug Fixes
- check local component prototypes ([#160](https://github.com/netease/tango/issues/160)) ([a0484e8](https://github.com/netease/tango/commit/a0484e8f64f20f67c30c25c3d5ce65de549b3e04))
# [1.2.0](https://github.com/netease/tango/compare/@music163/tango-designer@1.1.0...@music163/tango-designer@1.2.0) (2024-05-21)
### Bug Fixes
- enhance code value validate in SettingForm ([#152](https://github.com/netease/tango/issues/152)) ([791fbb1](https://github.com/netease/tango/commit/791fbb162a7147243924e01f54e9c0b586f14438))
- prototype2code & go back history error ([#156](https://github.com/netease/tango/issues/156)) ([8bf53a7](https://github.com/netease/tango/commit/8bf53a76f8a71eaf261ea68b9ee44e5bf19893aa))
- support add components with popover ([#155](https://github.com/netease/tango/issues/155)) ([f17ccbb](https://github.com/netease/tango/commit/f17ccbb7f645f8047ecd96d9f3f2185048a3b726))
### Features
- add history forward & back shortcut key ([#154](https://github.com/netease/tango/issues/154)) ([df3021f](https://github.com/netease/tango/commit/df3021f75e057e229756b429321c14d181223698))
- add select parent node of selected node ([#158](https://github.com/netease/tango/issues/158)) ([fe31246](https://github.com/netease/tango/commit/fe3124648325e72abfc58da8b2f8ff83301d40b8))
- event-setter & expression-setter use popover ([#159](https://github.com/netease/tango/issues/159)) ([9daf387](https://github.com/netease/tango/commit/9daf3872e743fcf706184877020bcbf1f75ffd25))
# [1.1.0](https://github.com/netease/tango/compare/@music163/tango-designer@1.0.3...@music163/tango-designer@1.1.0) (2024-05-17)
### Bug Fixes
- support quick set tid in SettingForm ([#151](https://github.com/netease/tango/issues/151)) ([1fc22a4](https://github.com/netease/tango/commit/1fc22a4535a8bdc12a018a41ebd5ab908fc46817))
### Features
- refactor parse attribute value ([#149](https://github.com/netease/tango/issues/149)) ([ffaa276](https://github.com/netease/tango/commit/ffaa276b5c205ed962d37e2fdb358a703f8fad01))
## [1.0.2](https://github.com/netease/tango/compare/@music163/tango-designer@1.0.1...@music163/tango-designer@1.0.2) (2024-04-22)
### Bug Fixes
- allow reload state tree & display pageStore in variable tree ([#135](https://github.com/netease/tango/issues/135)) ([2664613](https://github.com/netease/tango/commit/2664613ab263aead2a5239fa012454fc7fd5ff99))
## [1.0.1](https://github.com/netease/tango/compare/@music163/tango-designer@1.0.0...@music163/tango-designer@1.0.1) (2024-04-16)
### Bug Fixes
- render-setter expression value bug ([#133](https://github.com/netease/tango/issues/133)) ([27166ec](https://github.com/netease/tango/commit/27166ec94976979cb59130788399d90977cf0aec))
- variable tree remove button event handling ([#134](https://github.com/netease/tango/issues/134)) ([52864b5](https://github.com/netease/tango/commit/52864b540095025232ae237134d6be5701d20549))
# [1.0.0-alpha.18](https://github.com/netease/tango/compare/@music163/tango-designer@1.0.0-alpha.17...@music163/tango-designer@1.0.0-alpha.18) (2024-04-09) # [1.0.0-alpha.18](https://github.com/netease/tango/compare/@music163/tango-designer@1.0.0-alpha.17...@music163/tango-designer@1.0.0-alpha.18) (2024-04-09)
### Bug Fixes ### Bug Fixes
+11 -11
View File
@@ -1,6 +1,6 @@
{ {
"name": "@music163/tango-designer", "name": "@music163/tango-designer",
"version": "1.0.0", "version": "1.4.0",
"description": "lowcode designer", "description": "lowcode designer",
"keywords": [ "keywords": [
"react" "react"
@@ -32,25 +32,25 @@
}, },
"dependencies": { "dependencies": {
"@ant-design/icons": "^4.8.0", "@ant-design/icons": "^4.8.0",
"@music163/request": "^0.1.2", "@music163/request": "^0.2.0",
"@music163/tango-context": "^1.0.0", "@music163/tango-context": "^1.1.6",
"@music163/tango-core": "^1.0.0", "@music163/tango-core": "^1.4.0",
"@music163/tango-helpers": "^1.0.0-alpha.6", "@music163/tango-helpers": "^1.2.1",
"@music163/tango-sandbox": "^1.0.0", "@music163/tango-sandbox": "^1.0.9",
"@music163/tango-setting-form": "^1.0.0", "@music163/tango-setting-form": "^1.2.9",
"@music163/tango-ui": "^1.0.0", "@music163/tango-ui": "^1.4.0",
"antd": "^4.24.2", "antd": "^4.24.2",
"cash-dom": "^8.1.2", "cash-dom": "^8.1.2",
"classnames": "^2.3.2", "classnames": "^2.5.1",
"color": "^4.2.3", "color": "^4.2.3",
"coral-system": "^1.0.5", "coral-system": "^1.0.5",
"cssjson": "^2.1.3", "cssjson": "^2.1.3",
"date-fns": "^2.29.2", "date-fns": "^2.29.2",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"moment": "^2.29.4", "moment": "^2.30.1",
"react-color": "^2.19.3", "react-color": "^2.19.3",
"react-resizable": "^3.0.5", "react-resizable": "^3.0.5",
"semver": "^7.3.8" "semver": "^7.6.2"
}, },
"devDependencies": { "devDependencies": {
"@types/color": "^3.0.5", "@types/color": "^3.0.5",
@@ -0,0 +1,146 @@
import React, { useCallback, useMemo, useState } from 'react';
import { Box } from 'coral-system';
import { observer, useDesigner, useWorkspace } from '@music163/tango-context';
import { IconFont, DragPanel } from '@music163/tango-ui';
import { ComponentsPanel, ComponentsPanelProps } from '../sidebar';
import { IComponentPrototype } from '@music163/tango-helpers';
interface ComponentsPopoverProps {
// 添加组件位置
type?: 'inner' | 'before' | 'after';
// 弹出方式 手动触发/DOM 触发
isControlled?: boolean;
title?: string;
prototype?: IComponentPrototype;
children?: React.ReactNode;
}
export const ComponentsPopover = observer(
({
type = 'inner',
title = '添加组件',
isControlled = false,
children,
...popoverProps
}: ComponentsPopoverProps) => {
const [layout, setLayout] = useState<ComponentsPanelProps['layout']>('grid');
const workspace = useWorkspace();
const designer = useDesigner();
const { addComponentPopoverPosition, showAddComponentPopover } = designer;
const selectedNode = workspace.selectSource.selected?.[0];
const selectedNodeId = selectedNode?.codeId ?? '未选中';
const prototype =
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
workspace.componentPrototypes.get(selectedNode?.name) ?? ({} as IComponentPrototype);
// 推荐使用的子组件
const insertedList = useMemo(
() =>
Array.isArray(prototype?.childrenName)
? prototype?.childrenName
: [prototype?.childrenName].filter(Boolean),
[prototype?.childrenName],
);
// 推荐使用的代码片段
const siblingList = useMemo(() => prototype?.siblingNames ?? [], [prototype.siblingNames]);
const tipsTextMap = useMemo(
() => ({
before: `点击,在 ${selectedNodeId} 的前方添加节点`,
after: `点击,在 ${selectedNodeId} 的后方添加节点`,
inner: `点击,在 ${selectedNodeId} 内部添加节点`,
}),
[selectedNodeId],
);
const handleSelect = useCallback(
(name: string) => {
switch (type) {
case 'before':
workspace.insertBeforeSelectedNode(name);
break;
case 'after':
workspace.insertAfterSelectedNode(name);
break;
case 'inner':
workspace.insertToSelectedNode(name);
break;
default:
break;
}
},
[type, workspace],
);
const changeLayout = useCallback(() => {
setLayout(layout === 'grid' ? 'line' : 'grid');
}, [layout]);
const menuData = useMemo(() => {
const menuList = JSON.parse(JSON.stringify(designer.menuData));
const commonList = menuList['common'] ?? [];
if (commonList?.length && siblingList?.length) {
commonList.unshift({
title: '代码片段',
items: siblingList,
});
}
if (commonList?.length && insertedList?.length) {
commonList.unshift({
title: '推荐使用',
items: insertedList,
});
}
return menuList;
}, [insertedList, siblingList, designer.menuData]);
const innerTypeProps =
// 手动触发 适用于 点击添加组件
type === 'inner' && isControlled
? {
open: showAddComponentPopover,
onOpenChange: (open: boolean) => designer.toggleAddComponentPopover(open),
left: addComponentPopoverPosition.clientX,
top: addComponentPopoverPosition.clientY,
}
: {};
return (
<DragPanel
{...innerTypeProps}
title={title}
extra={
<Box fontSize="12px">
{layout === 'grid' ? (
<IconFont type="icon-liebiaoitem" onClick={changeLayout} />
) : (
<IconFont type="icon-grid1" onClick={changeLayout} />
)}
</Box>
}
footer={tipsTextMap[type]}
width="330px"
height="450px"
resizeable
maskClosable
body={
<ComponentsPanel
isScope
showBizComps={false}
menuData={menuData}
layout={layout}
onItemSelect={handleSelect}
/>
}
{...popoverProps}
>
{children}
</DragPanel>
);
},
);
@@ -0,0 +1,130 @@
import React from 'react';
import { getWidget } from '../widgets';
import { Menu, MenuProps } from 'antd';
import { observer, useWorkspace } from '@music163/tango-context';
import { ISelectedItemData, isString } from '@music163/tango-helpers';
import { Box, css } from 'coral-system';
import { IconFont } from '@music163/tango-ui';
const contextMenuStyle = css`
.ant-dropdown-menu {
width: 240px;
}
.ant-dropdown-menu-title-content {
padding-left: 22px;
}
.ant-dropdown-menu-item-icon {
font-size: 14px;
width: 14px;
}
.ant-dropdown-menu-item-icon + .ant-dropdown-menu-title-content {
padding-left: 0;
}
.ant-dropdown-menu-submenu-popup {
padding: 0;
margin-top: -4px;
}
`;
const ParentNodesMenuItem = ({
record,
onClick,
key,
}: {
record: ISelectedItemData;
onClick: () => any;
key?: string;
}) => {
const workspace = useWorkspace();
const componentPrototype = workspace.componentPrototypes.get(record.name);
const icon = componentPrototype?.icon || 'icon-placeholder';
const iconRender = icon.startsWith('icon-') ? (
<IconFont className="material-icon" type={icon} />
) : (
<img className="material-icon" src={icon} alt={componentPrototype.name} />
);
return (
<Menu.Item key={key || record.id} icon={iconRender} onClick={onClick}>
{record.name}
{!!record.codeId && ` (${record.codeId})`}
</Menu.Item>
);
};
const ParentNodesMenu = observer(() => {
const workspace = useWorkspace();
const selectSource = workspace.selectSource;
const parents = selectSource.first?.parents;
if (!parents?.length) {
return null;
}
return (
<Menu.SubMenu key="parentNodes" title="选取父节点">
{parents.map((item, index) => (
<ParentNodesMenuItem
key={item.id}
record={item}
onClick={() => {
selectSource.select({
...item,
parents: parents.slice(index + 1),
});
}}
/>
))}
</Menu.SubMenu>
);
});
export interface ContextMenuProps extends MenuProps {
/**
* 动作列表,默认列出全部
*/
actions?: Array<string | React.ReactElement>;
/**
* 是否显示父节点选项
*/
showParents?: boolean;
className?: string;
style?: React.CSSProperties;
menuStyle?: React.CSSProperties;
}
export const ContextMenu = observer(
({
showParents,
actions: actionsProp,
className,
style,
menuStyle,
...rest
}: ContextMenuProps) => {
const actions = actionsProp || Object.keys(getWidget('contextMenu'));
const menus = actions.map((item) => {
if (isString(item)) {
const widget = getWidget(['contextMenu', item].join('.'));
return widget ? React.createElement(widget) : null;
}
return item;
});
if (showParents) {
menus.unshift(<ParentNodesMenu />);
}
return (
<Box display="inline-block" css={contextMenuStyle} className={className} style={style}>
<Menu activeKey={null} {...rest} style={menuStyle}>
{menus}
</Menu>
</Box>
);
},
);
@@ -0,0 +1,68 @@
import React, { useState } from 'react';
import { Box, css } from 'coral-system';
import { observer, useWorkspace } from '@music163/tango-context';
import { CloseOutlined } from '@ant-design/icons';
const errorMessageStyle = css`
padding: 24px;
white-space: pre-wrap;
`;
/**
* 文件错误提示浮层
*/
export const FileErrorsOverlay = observer(() => {
const [isVisible, setIsVisible] = useState<boolean>(true);
const workspace = useWorkspace();
const handleClose = () => {
setIsVisible(false);
};
if (workspace.fileErrors.length === 0) {
return null;
}
if (!isVisible) {
return null;
}
return (
<Box
className="WorkspaceErrorOverlay"
position="absolute"
top="0"
left="0"
right="0"
height="100%"
bg="rgba(244, 244, 244, 0.9)"
color="red"
zIndex={1999}
>
<Box position="relative" p="4">
<CloseOutlined
style={{
position: 'absolute',
top: 12,
right: 12,
color: '#000',
}}
onClick={handleClose}
/>
<Box css={errorMessageStyle}>
<Box fontSize="20px" mb="l">
</Box>
{workspace.fileErrors.map((fileError) => (
<Box key={fileError.filename} borderBottom="solid" borderBottomColor="line2" py="l">
<Box fontWeight="bold">{fileError.filename}</Box>
<Box as="code" display="block">
{fileError.message}
</Box>
</Box>
))}
</Box>
</Box>
</Box>
);
});
@@ -1,4 +1,7 @@
export * from './context-menu';
export * from './drag-box'; export * from './drag-box';
export * from './input-kv'; export * from './input-kv';
export * from './variable-tree'; export * from './variable-tree';
export * from './variable-tree-modal'; export * from './variable-tree-modal';
export * from './components-popover';
export * from './file-errors-overlay';
@@ -1,11 +1,11 @@
import { isValidFunctionCode, url2serviceName } from '@music163/tango-helpers'; import { Dict, isValidFunctionCode, url2serviceName } from '@music163/tango-helpers';
import { InputCode } from '@music163/tango-ui'; import { InputCode } from '@music163/tango-ui';
import { Button, Dropdown, Form, FormProps, Input, Select, Space } from 'antd'; import { Button, Dropdown, Form, FormProps, Input, Select, Space } from 'antd';
import { Box } from 'coral-system'; import { Box } from 'coral-system';
import React, { useState } from 'react'; import React, { useState } from 'react';
interface AddServiceFormProps extends FormProps { interface AddServiceFormProps extends FormProps {
initialValues?: object; initialValues?: Dict;
onSubmit: (values: object) => void; onSubmit: (values: object) => void;
onCancel: () => void; onCancel: () => void;
serviceModules: object[]; serviceModules: object[];
@@ -166,7 +166,7 @@ export function VariableTree(props: VariableTreeProps) {
return ( return (
<Box display="flex" columnGap="l" className="VariableTree" css={varTreeStyle} {...rest}> <Box display="flex" columnGap="l" className="VariableTree" css={varTreeStyle} {...rest}>
<Box className="VariableList" width="40%"> <Box className="VariableList" width="40%" overflow="auto">
{renderHeaderExtra?.(props, state)} {renderHeaderExtra?.(props, state)}
<Box mb="m" position="sticky" top="0" bg="white" zIndex={2}> <Box mb="m" position="sticky" top="0" bg="white" zIndex={2}>
<Search placeholder="请输入变量名" onChange={(val) => setKeyword(val?.trim())} /> <Search placeholder="请输入变量名" onChange={(val) => setKeyword(val?.trim())} />
@@ -203,8 +203,13 @@ export function VariableTree(props: VariableTreeProps) {
<Box flex="0 0 72px" textAlign="right"> <Box flex="0 0 72px" textAlign="right">
{node.showRemoveButton && ( {node.showRemoveButton && (
<Popconfirm <Popconfirm
zIndex={99999}
title={`确认删除吗 ${node.title}?该操作会导致引用此模型的代码报错,请谨慎操作!`} title={`确认删除吗 ${node.title}?该操作会导致引用此模型的代码报错,请谨慎操作!`}
onConfirm={() => { onCancel={(e) => {
e.stopPropagation();
}}
onConfirm={(e) => {
e.stopPropagation();
if (isStoreVariablePath(node.key)) { if (isStoreVariablePath(node.key)) {
onRemoveStoreVariable(node.key); onRemoveStoreVariable(node.key);
} else { } else {
@@ -332,7 +337,14 @@ export function VariableTree(props: VariableTreeProps) {
}} }}
/> />
) : ( ) : (
<ValueDefine data={activeNode} onSave={onUpdateStoreVariable} /> <ValueDefine
data={activeNode}
onSave={(variableKey, code) => {
onUpdateStoreVariable(variableKey, code);
// 更新当前节点 code
setActiveNode((pre) => ({ ...pre, raw: code }));
}}
/>
) )
} }
</ValueDetail> </ValueDetail>
@@ -3,7 +3,8 @@ import { Box } from 'coral-system';
import { Button, Empty } from 'antd'; import { Button, Empty } from 'antd';
import { PlayCircleOutlined } from '@ant-design/icons'; import { PlayCircleOutlined } from '@ant-design/icons';
import { InputCode, Panel, JsonView } from '@music163/tango-ui'; import { InputCode, Panel, JsonView } from '@music163/tango-ui';
import { isNil, logger, code2object, getValue } from '@music163/tango-helpers'; import { isNil, logger, getValue } from '@music163/tango-helpers';
import { code2value } from '@music163/tango-core';
export interface ServicePreviewProps { export interface ServicePreviewProps {
appContext?: any; appContext?: any;
@@ -22,7 +23,7 @@ export function ServicePreview({ appContext, functionKey }: ServicePreviewProps)
editable editable
showLineNumbers showLineNumbers
onChange={(value: string) => { onChange={(value: string) => {
const obj = code2object(value); const obj = code2value(value); // 转为 object 对象
setPayload(obj); setPayload(obj);
}} }}
/> />
@@ -84,6 +84,8 @@ export function ValueDefine({ data, onSave = noop }: ValueDefineProps) {
onClick={() => { onClick={() => {
off(); off();
setError(''); setError('');
// 重置到原始值
setValue(data.raw);
}} }}
> >
@@ -0,0 +1,19 @@
import React from 'react';
import { useWorkspace, observer } from '@music163/tango-context';
import { ContextAction } from '@music163/tango-ui';
import { CopyOutlined } from '@ant-design/icons';
export const CopyNodeContextAction = observer(() => {
const workspace = useWorkspace();
return (
<ContextAction
icon={<CopyOutlined />}
hotkey="Command+C"
onClick={() => {
workspace.copySelectedNode();
}}
>
</ContextAction>
);
});
@@ -0,0 +1,19 @@
import React from 'react';
import { useWorkspace, observer } from '@music163/tango-context';
import { ContextAction } from '@music163/tango-ui';
import { DeleteOutlined } from '@ant-design/icons';
export const DeleteNodeContextAction = observer(() => {
const workspace = useWorkspace();
return (
<ContextAction
icon={<DeleteOutlined />}
hotkey="Backspace"
onClick={() => {
workspace.removeSelectedNode();
}}
>
</ContextAction>
);
});
@@ -0,0 +1,4 @@
export * from './copy-node';
export * from './delete-node';
export * from './paste-node';
export * from './view-source';
@@ -0,0 +1,19 @@
import React from 'react';
import { useWorkspace, observer } from '@music163/tango-context';
import { ContextAction } from '@music163/tango-ui';
import { SnippetsOutlined } from '@ant-design/icons';
export const PasteNodeContextAction = observer(() => {
const workspace = useWorkspace();
return (
<ContextAction
icon={<SnippetsOutlined />}
hotkey="Command+V"
onClick={() => {
workspace.pasteSelectedNode();
}}
>
</ContextAction>
);
});
@@ -0,0 +1,18 @@
import React from 'react';
import { useDesigner, observer } from '@music163/tango-context';
import { ContextAction } from '@music163/tango-ui';
import { CodeOutlined } from '@ant-design/icons';
export const ViewSourceContextAction = observer(() => {
const designer = useDesigner();
return (
<ContextAction
icon={<CodeOutlined />}
onClick={() => {
designer.setActiveView('code');
}}
>
</ContextAction>
);
});
+11 -3
View File
@@ -6,10 +6,14 @@ export interface IDesignerContext<S = any> {
* 沙箱查询实例 * 沙箱查询实例
*/ */
sandboxQuery: DndQuery; sandboxQuery: DndQuery;
/**
* 预览沙箱查询实例
*/
previewSandboxQuery: DndQuery;
/** /**
* 远程服务 * 远程服务
*/ */
remoteServices?: Record<string, S>; remoteServices: Record<string, S>;
} }
const [DesignerProvider, useDesigner] = createContext<IDesignerContext>({ const [DesignerProvider, useDesigner] = createContext<IDesignerContext>({
@@ -19,9 +23,13 @@ const [DesignerProvider, useDesigner] = createContext<IDesignerContext>({
export { DesignerProvider }; export { DesignerProvider };
export const useSandboxQuery = () => { export const useSandboxQuery = () => {
return useDesigner()?.sandboxQuery; return useDesigner().sandboxQuery;
};
export const usePreviewSandboxQuery = () => {
return useDesigner().previewSandboxQuery;
}; };
export const useRemoteServices = () => { export const useRemoteServices = () => {
return useDesigner()?.remoteServices; return useDesigner().remoteServices;
}; };
+23 -3
View File
@@ -5,8 +5,18 @@ import { TangoEngineProvider, ITangoEngineContext } from '@music163/tango-contex
import zhCN from 'antd/lib/locale/zh_CN'; import zhCN from 'antd/lib/locale/zh_CN';
import { DesignerProvider, IDesignerContext } from './context'; import { DesignerProvider, IDesignerContext } from './context';
import defaultTheme from './themes/default'; import defaultTheme from './themes/default';
import { DndQuery } from './dnd';
import { DESIGN_SANDBOX_ID, PREVIEW_SANDBOX_ID } from './helpers';
export interface DesignerProps extends IDesignerContext, ITangoEngineContext { const builtinSandboxQuery = new DndQuery({
context: `#${DESIGN_SANDBOX_ID}`,
});
const builtinPreviewSandboxQuery = new DndQuery({
context: `#${PREVIEW_SANDBOX_ID}`,
});
export interface DesignerProps extends Partial<IDesignerContext>, ITangoEngineContext {
/** /**
* 主题包 * 主题包
*/ */
@@ -20,13 +30,23 @@ export interface DesignerProps extends IDesignerContext, ITangoEngineContext {
* @returns * @returns
*/ */
export function Designer(props: DesignerProps) { export function Designer(props: DesignerProps) {
const { engine, config, theme: themeProp, sandboxQuery, remoteServices = {}, children } = props; const {
engine,
config,
theme: themeProp,
sandboxQuery = builtinSandboxQuery,
previewSandboxQuery = builtinPreviewSandboxQuery,
remoteServices = {},
children,
} = props;
const theme = useMemo(() => extendTheme(themeProp, defaultTheme), [themeProp]); const theme = useMemo(() => extendTheme(themeProp, defaultTheme), [themeProp]);
return ( return (
<SystemProvider theme={theme} prefix="--tango"> <SystemProvider theme={theme} prefix="--tango">
<ConfigProvider locale={zhCN}> <ConfigProvider locale={zhCN}>
<TangoEngineProvider value={{ engine, config }}> <TangoEngineProvider value={{ engine, config }}>
<DesignerProvider value={{ sandboxQuery, remoteServices }}>{children}</DesignerProvider> <DesignerProvider value={{ sandboxQuery, previewSandboxQuery, remoteServices }}>
{children}
</DesignerProvider>
</TangoEngineProvider> </TangoEngineProvider>
</ConfigProvider> </ConfigProvider>
</SystemProvider> </SystemProvider>
+6 -1
View File
@@ -12,7 +12,6 @@ export const DRAGGABLE_SELECTOR = `[${SLOT.dnd}]`;
interface DndQueryOptions { interface DndQueryOptions {
/** /**
* DOM 查询上下文选择器 * DOM 查询上下文选择器
* TODO: 是不是可以合并成一个 API
*/ */
context?: string; context?: string;
/** /**
@@ -63,6 +62,9 @@ export class DndQuery {
return false; return false;
} }
/**
* 沙箱内的 window 对象
*/
get window() { get window() {
if (this.context && 'defaultView' in this.context) { if (this.context && 'defaultView' in this.context) {
return (this.context as unknown as Document).defaultView; return (this.context as unknown as Document).defaultView;
@@ -71,6 +73,9 @@ export class DndQuery {
return window; return window;
} }
/**
* 沙箱内的全局滚动偏移
*/
get scrollTop() { get scrollTop() {
if (this.context && 'documentElement' in this.context) { if (this.context && 'documentElement' in this.context) {
return (this.context as unknown as Document).documentElement.scrollTop; return (this.context as unknown as Document).documentElement.scrollTop;
+3 -1
View File
@@ -1,8 +1,10 @@
import { Dict } from '@music163/tango-helpers';
/** /**
* 快捷键 * 快捷键
*/ */
export class Hotkey { export class Hotkey {
private readonly hotkeyMap = {}; private readonly hotkeyMap: Dict = {};
constructor(hotkeys: Record<string, Function>) { constructor(hotkeys: Record<string, Function>) {
Object.keys(hotkeys).forEach((hotkey) => { Object.keys(hotkeys).forEach((hotkey) => {
+79 -5
View File
@@ -58,6 +58,15 @@ export function useDnd({
'command+v,ctrl+v': () => { 'command+v,ctrl+v': () => {
workspace.pasteSelectedNode(); workspace.pasteSelectedNode();
}, },
'command+arrowup,ctrl+arrowup': () => {
workspace.selectSource.selectParent();
},
'command+z,ctrl+z': () => {
workspace.history.back();
},
'command+shift+z,ctrl+shift+z': () => {
workspace.history.forward();
},
}); });
}, [workspace]); }, [workspace]);
@@ -68,6 +77,9 @@ export function useDnd({
if (designer.isPreview) { if (designer.isPreview) {
return; return;
} }
if (designer.showContextMenu) {
designer.toggleContextMenu(false);
}
const point = sandboxQuery.getRelativePoint({ x: e.clientX, y: e.clientY }); const point = sandboxQuery.getRelativePoint({ x: e.clientX, y: e.clientY });
selectSource.setStart({ selectSource.setStart({
@@ -84,10 +96,10 @@ export function useDnd({
const onMouseMove = (e: React.MouseEvent) => { const onMouseMove = (e: React.MouseEvent) => {
const point = sandboxQuery.getRelativePoint({ x: e.clientX, y: e.clientY }); const point = sandboxQuery.getRelativePoint({ x: e.clientX, y: e.clientY });
setElementStyle('.SelectionMask', { setElementStyle('.SelectionMask', {
width: Math.abs(selectSource.start?.point.x - point.x) + 'px', width: `${Math.abs(selectSource.start?.point.x - point.x)}px`,
height: Math.abs(selectSource.start?.point.y - point.y) + 'px', height: `${Math.abs(selectSource.start?.point.y - point.y)}px`,
left: Math.min(selectSource.start?.point.x, point.x) + 'px', left: `${Math.min(selectSource.start?.point.x, point.x)}px`,
top: Math.min(selectSource.start?.point.y, point.y) + 'px', top: `${Math.min(selectSource.start?.point.y, point.y)}px`,
}); });
}; };
@@ -119,6 +131,9 @@ export function useDnd({
}; };
const onClick = (e: React.MouseEvent) => { const onClick = (e: React.MouseEvent) => {
if (designer.showContextMenu) {
designer.toggleContextMenu(false);
}
const data = sandboxQuery.getDraggableParentsData(e.target as HTMLElement, true); const data = sandboxQuery.getDraggableParentsData(e.target as HTMLElement, true);
if (data && data.id) { if (data && data.id) {
selectSource.select(data); selectSource.select(data);
@@ -371,7 +386,8 @@ export function useDnd({
}; };
const onKeyDown = (e: React.KeyboardEvent) => { const onKeyDown = (e: React.KeyboardEvent) => {
const isInputElement = isInputElements((e.target as HTMLElement).nodeName); const target = e.target as HTMLElement;
const isInputElement = target.isContentEditable || isInputElements(target.nodeName);
// 禁用输入事件的触发的快捷键逻辑 // 禁用输入事件的触发的快捷键逻辑
if (!isInputElement) { if (!isInputElement) {
const key = getHotkey(e); const key = getHotkey(e);
@@ -379,6 +395,56 @@ export function useDnd({
} }
}; };
const onContextMenu = (event: React.MouseEvent) => {
if (designer.showContextMenu) {
designer.toggleContextMenu(false);
}
// 按下其他按键时,视为用户有特殊操作,此时不展示右键菜单
if (event.ctrlKey || event.altKey || event.metaKey || event.shiftKey) {
return;
}
const { clientX, clientY } = event;
let target;
if (workspace.selectSource.isSelected) {
for (const item of workspace.selectSource.selected) {
if (
// 如果选中的节点是页面根节点(无 parents),则忽略
item.parents?.length &&
item.bounding &&
clientX >= item.bounding.left &&
clientX <= item.bounding.left + item.bounding.width &&
clientY >= item.bounding.top &&
clientY <= item.bounding.top + item.bounding.height
) {
// 右键坐标已经在当前选中组件的选区内,直接展示右键菜单
target = item;
break;
}
}
}
// 否则,根据右键的元素选中最接近的组件
if (!target) {
target = sandboxQuery.getDraggableParentsData(event.target as HTMLElement, true);
}
if (target && target.id) {
if (!target.parents?.length) {
// 页面根节点不展示右键菜单操作
return;
}
// 右键时高亮选中当前元素
// 以防之前选区有多个元素,即便已经是选中的元素也再选中一遍
event.preventDefault();
selectSource.select(target);
// 在下一周期再展示右键菜单,以让先前的菜单先被销毁
requestAnimationFrame(() => {
designer.toggleContextMenu(true, {
clientX,
clientY,
});
});
}
};
const onTango = (e: CustomEvent) => { const onTango = (e: CustomEvent) => {
const detail = e.detail || {}; const detail = e.detail || {};
@@ -406,6 +472,13 @@ export function useDnd({
// 打开智能向导弹窗 // 打开智能向导弹窗
designer.toggleSmartWizard(true); designer.toggleSmartWizard(true);
break; break;
case 'addComponent':
// 打开添加组件面板
designer.toggleAddComponentPopover(true, {
clientX: (e.detail.meta as any).clientX + 40,
clientY: (e.detail.meta as any).clientY + 110,
});
break;
default: default:
break; break;
} }
@@ -421,6 +494,7 @@ export function useDnd({
onDragEnd, onDragEnd,
onScroll, onScroll,
onKeyDown, onKeyDown,
onContextMenu,
onTango, onTango,
...selectHandler, ...selectHandler,
}; };
+80 -102
View File
@@ -1,9 +1,7 @@
import React, { useRef, useEffect, useCallback } from 'react'; import React, { useRef, useEffect, useCallback } from 'react';
import { Box } from 'coral-system'; import { Box } from 'coral-system';
import { MultiEditor, MultiEditorProps } from '@music163/tango-ui'; import { MultiEditor, MultiEditorProps } from '@music163/tango-ui';
import { observer, useWorkspace } from '@music163/tango-context'; import { observer, useDesigner, useWorkspace } from '@music163/tango-context';
import { isValidCode } from '@music163/tango-core';
import { Modal } from 'antd';
const ideConfig = { const ideConfig = {
// disableFileOps: { // disableFileOps: {
@@ -21,108 +19,88 @@ const ideConfig = {
// disableSetting: true, // disableSetting: true,
}; };
export interface CodeEditorProps extends Partial<MultiEditorProps> { export type CodeEditorProps = Partial<MultiEditorProps>;
/**
* 是否自动清楚未使用的导入
*/
autoRemoveUnusedImports?: boolean;
}
export const CodeEditor = observer( export const CodeEditor = observer((props: CodeEditorProps) => {
({ autoRemoveUnusedImports = true, ...rest }: CodeEditorProps) => { const editorRef = useRef(null);
const editorRef = useRef(null); const workspace = useWorkspace();
const workspace = useWorkspace(); const designer = useDesigner();
const files = workspace.listFiles(); const files = workspace.listFiles();
const activeFile = workspace.activeFile; const activeFile = workspace.activeFile;
let loc: any; // 记录视图代码的选中位置 let loc: any; // 记录视图代码的选中位置
const selectNode = workspace.selectSource.firstNode; const selectNode = workspace.selectSource.firstNode;
if (selectNode && activeFile === workspace.activeViewFile) { if (selectNode && activeFile === workspace.activeViewFile) {
loc = selectNode.loc; loc = selectNode.loc;
} }
useEffect(() => { useEffect(() => {
editorRef.current?.refresh(files, activeFile, loc); editorRef.current?.refresh(files, activeFile, loc);
}, [files, activeFile, loc]); }, [files, activeFile, loc]);
const fileSave = useCallback( const fileSave = useCallback(
(path: string, value: string) => { (path: string, value: string) => {
if (!isJsFile(path)) { workspace.updateFile(path, value, false);
// 非 js 文件直接保存 },
workspace.updateFile(path, value, autoRemoveUnusedImports); [workspace],
return; );
const handleFileChange = useCallback(
(type: string, info: any) => {
switch (type) {
case 'addFile':
workspace.addFile(info.path, info.value);
break;
case 'deleteFile':
case 'deleteFolder':
workspace.removeFile(info.path);
break;
case 'renameFile':
workspace.renameFile(info.path, info.newpath);
workspace.setActiveFile(info.newpath);
break;
case 'renameFolder':
workspace.renameFolder(info.path, info.newpath);
break;
case 'addFolder':
default:
break;
}
},
[workspace],
);
const handlePathChange = useCallback(
(path: string) => {
workspace.setActiveFile(path);
},
[workspace],
);
const borderStyle =
designer.activeView === 'dual'
? {
borderLeft: 'solid 1px var(--tango-colors-line2)',
} }
: {};
// js 文件需要先检查语法,只有语法正确才会保存 return (
if (isValidCode(value)) { <Box display="flex" flexDirection="row" height="100%" bg="white" {...borderStyle}>
workspace.updateFile(path, value, autoRemoveUnusedImports); <MultiEditor
} else { ref={editorRef}
Modal.confirm({ options={{
title: '检测到代码中存在语法错误,暂时无法将代码同步给设计器,是否回退到安全代码?', fontSize: 14,
onOk: () => { automaticLayout: true,
editorRef.current?.refresh(files, activeFile); }}
}, ideConfig={ideConfig}
onCancel: () => {}, onFileSave={fileSave}
}); onPathChange={handlePathChange}
} onFileChange={handleFileChange}
}, defaultPath={activeFile}
[workspace, autoRemoveUnusedImports, activeFile, files], defaultTheme="GithubLightDefault"
); defaultFiles={files}
{...props}
const handleFileChange = useCallback( />
(type: string, info: any) => { </Box>
switch (type) { );
case 'addFile': });
workspace.addFile(info.path, info.value);
break;
case 'deleteFile':
case 'deleteFolder':
workspace.removeFile(info.path);
break;
case 'renameFile':
workspace.renameFile(info.path, info.newpath);
workspace.setActiveFile(info.newpath);
break;
case 'renameFolder':
workspace.renameFolder(info.path, info.newpath);
break;
case 'addFolder':
default:
break;
}
},
[workspace],
);
const handlePathChange = useCallback(
(path: string) => {
workspace.setActiveFile(path);
},
[workspace],
);
return (
<Box display="flex" flexDirection="row" height="100%" bg="white">
<MultiEditor
ref={editorRef}
options={{
fontSize: 14,
automaticLayout: true,
}}
ideConfig={ideConfig}
onFileSave={fileSave}
onPathChange={handlePathChange}
onFileChange={handleFileChange}
defaultPath={activeFile}
defaultTheme="GithubLightDefault"
defaultFiles={files}
{...rest}
/>
</Box>
);
},
);
function isJsFile(path: string) {
return /.jsx?$/.test(path);
}
+15 -3
View File
@@ -7,6 +7,20 @@ import {
parseDndId, parseDndId,
} from '@music163/tango-helpers'; } from '@music163/tango-helpers';
// -----------
// CONSTANTS
// -----------
export const DRAG_GHOST_ID = 'dragGhost';
export const DESIGN_SANDBOX_ID = 'sandbox-container';
export const PREVIEW_SANDBOX_ID = 'preview-sandbox-container';
// -----------
// DOM Helpers
// -----------
export function buildQueryBySlotId(id: string) { export function buildQueryBySlotId(id: string) {
return `[${SLOT.dnd}="${id}"]`; return `[${SLOT.dnd}="${id}"]`;
} }
@@ -15,8 +29,6 @@ export function getElement(selector: Selector) {
return $(selector).get(0); return $(selector).get(0);
} }
export const DRAG_GHOST_ID = 'dragGhost';
export const getDragGhostElement = () => getElement(`#${DRAG_GHOST_ID}`); export const getDragGhostElement = () => getElement(`#${DRAG_GHOST_ID}`);
export function setElementStyle(selector: Selector, style: any) { export function setElementStyle(selector: Selector, style: any) {
@@ -75,7 +87,7 @@ export function getElementData(
const display = getElementCSSDisplay(element); const display = getElementCSSDisplay(element);
return { return {
id: dnd.id, id: dnd.id,
codeId: dnd.index, codeId: dnd.codeId,
name: dnd.component || element.tagName.toLowerCase(), name: dnd.component || element.tagName.toLowerCase(),
filename: dnd.filename, filename: dnd.filename,
bounding, bounding,
+15 -6
View File
@@ -1,13 +1,14 @@
import React, { useImperativeHandle, ForwardedRef, useRef, useState, useEffect } from 'react'; import React, { useImperativeHandle, ForwardedRef, useRef, useState, useEffect } from 'react';
import { Box } from 'coral-system'; import { Box } from 'coral-system';
import { CodeSandbox, CodeSandboxProps } from '@music163/tango-sandbox'; import { CodeSandbox, CodeSandboxProps } from '@music163/tango-sandbox';
import { getValue, isFunction, logger, pick, setValue } from '@music163/tango-helpers'; import { Dict, getValue, isFunction, logger, pick, setValue } from '@music163/tango-helpers';
import { observer, useWorkspace, useDesigner } from '@music163/tango-context'; import { observer, useWorkspace, useDesigner } from '@music163/tango-context';
import { Simulator, Viewport } from '../simulator'; import { Simulator, Viewport } from '../simulator';
import { useSandboxQuery } from '../context'; import { useSandboxQuery } from '../context';
import { DndQuery, useDnd } from '../dnd'; import { DndQuery, useDnd } from '../dnd';
import { Navigator } from './navigator'; import { Navigator } from './navigator';
import { SelectionToolsProps } from '../simulator/selection'; import { SelectionToolsProps } from '../simulator/selection';
import { DESIGN_SANDBOX_ID, PREVIEW_SANDBOX_ID } from '../helpers';
interface ISandboxEventHandlerConfig { interface ISandboxEventHandlerConfig {
sandboxQuery?: DndQuery; sandboxQuery?: DndQuery;
@@ -48,6 +49,10 @@ export interface CombinedSandboxRef {
const LANDING_PAGE_PATH = '/__background_landing_page__'; const LANDING_PAGE_PATH = '/__background_landing_page__';
function fixRouterMode(routerType: string): CodeSandboxProps['routerMode'] {
return routerType === 'hash' ? 'hash' : undefined;
}
function useSandbox({ function useSandbox({
isPreview: isPreviewProp, isPreview: isPreviewProp,
configFormatter, configFormatter,
@@ -62,6 +67,7 @@ function useSandbox({
const workspace = useWorkspace(); const workspace = useWorkspace();
const designer = useDesigner(); const designer = useDesigner();
const sandboxQuery = useSandboxQuery(); const sandboxQuery = useSandboxQuery();
const isPreview = isPreviewProp ?? designer.isPreview; const isPreview = isPreviewProp ?? designer.isPreview;
// 组件不一定会立即刷新,因此 isActive 需要实时获取 // 组件不一定会立即刷新,因此 isActive 需要实时获取
@@ -82,7 +88,7 @@ function useSandbox({
onViewChange: (data) => onViewChange && onViewChange(data, getSandboxConfig()), onViewChange: (data) => onViewChange && onViewChange(data, getSandboxConfig()),
}); });
let files = Array.from(workspace.files.keys()).reduce((prev, filename) => { let files = Array.from(workspace.files.keys()).reduce<Dict>((prev, filename) => {
let code = workspace.getFile(filename).code; let code = workspace.getFile(filename).code;
if (filename === '/tango.config.json') { if (filename === '/tango.config.json') {
code = mergeTangoConfigJson(code, { isPreview, injectScript, formatter: configFormatter }); code = mergeTangoConfigJson(code, { isPreview, injectScript, formatter: configFormatter });
@@ -103,23 +109,26 @@ function useSandbox({
// 根据当前 workspace 状态与组件传入的状态是否一致,控制是否需要切换到空白路由 // 根据当前 workspace 状态与组件传入的状态是否一致,控制是否需要切换到空白路由
const display = isActive ? 'block' : 'none'; const display = isActive ? 'block' : 'none';
const routePath = isActive ? startRoute || workspace.activeRoute : LANDING_PAGE_PATH; const routePath = isActive ? startRoute || workspace.activeRoute : LANDING_PAGE_PATH;
const routerMode = fixRouterMode(workspace.appEntryModule?.routerType);
const sandboxProps = isPreview const sandboxProps = isPreview
? { ? {
files, files,
eventHandlers: pick(dndHandlers, ['onTango']), eventHandlers: pick(dndHandlers, ['onTango']),
onMessage, onMessage,
onLoad,
display, display,
startRoute: routePath, startRoute: routePath,
onLoad, routerMode,
} }
: { : {
files, files,
eventHandlers: dndHandlers as any, eventHandlers: dndHandlers as any,
onMessage, onMessage,
onLoad,
display, display,
startRoute: routePath, startRoute: routePath,
onLoad, routerMode,
}; };
return sandboxProps; return sandboxProps;
@@ -140,7 +149,7 @@ const PreviewSandbox = observer(
return ( return (
<Box display={display} width="100%" height="100%"> <Box display={display} width="100%" height="100%">
<CodeSandbox ref={ref} iframeId="preview-sandbox-container" {...sandboxProps} {...rest} /> <CodeSandbox ref={ref} iframeId={PREVIEW_SANDBOX_ID} {...sandboxProps} {...rest} />
</Box> </Box>
); );
}, },
@@ -188,7 +197,7 @@ const DesignSandbox = observer(
return ( return (
<Box display={display} width="100%" height="100%"> <Box display={display} width="100%" height="100%">
<CodeSandbox ref={ref} {...sandboxProps} {...rest} /> <CodeSandbox ref={ref} iframeId={DESIGN_SANDBOX_ID} {...sandboxProps} {...rest} />
</Box> </Box>
); );
}, },
@@ -1,3 +1,5 @@
export * from './copy-node'; export * from './copy-node';
export * from './delete-node'; export * from './delete-node';
export * from './more-actions';
export * from './select-parent-node';
export * from './view-source'; export * from './view-source';
@@ -0,0 +1,16 @@
import React from 'react';
import { observer } from '@music163/tango-context';
import { SelectAction } from '@music163/tango-ui';
import { MoreOutlined } from '@ant-design/icons';
import { Dropdown } from 'antd';
import { ContextMenu } from '../components';
export const MoreActionsAction = observer(() => {
return (
<Dropdown placement="bottomRight" overlay={<ContextMenu showParents />}>
<SelectAction tooltip="更多">
<MoreOutlined />
</SelectAction>
</Dropdown>
);
});
@@ -0,0 +1,18 @@
import React from 'react';
import { useWorkspace, observer } from '@music163/tango-context';
import { IconFont, SelectAction } from '@music163/tango-ui';
export const SelectParentNodeAction = observer(() => {
const workspace = useWorkspace();
return (
<SelectAction
tooltip="选中父节点"
onClick={() => {
workspace.selectSource.selectParent();
}}
>
<IconFont type="icon-huiche1" rotate={90} />
</SelectAction>
);
});
+10 -11
View File
@@ -2,9 +2,8 @@ import React, { useCallback, useEffect, useState } from 'react';
import { Box } from 'coral-system'; import { Box } from 'coral-system';
// @ts-ignore // @ts-ignore
import { toJSON } from 'cssjson'; import { toJSON } from 'cssjson';
import { InputNumber, Space } from 'antd'; import { InputNumber, Slider, Space } from 'antd';
import { FormItemComponentProps } from '@music163/tango-setting-form'; import { FormItemComponentProps } from '@music163/tango-setting-form';
import { SliderSetter } from './number-setter';
import { import {
BgSetter, BgSetter,
BorderSetter, BorderSetter,
@@ -16,10 +15,11 @@ import {
FlexJustifyContentSetter, FlexJustifyContentSetter,
SpacingSetter, SpacingSetter,
} from './style-setter'; } from './style-setter';
import { wrapCode } from '@music163/tango-helpers';
const getRawCssValue = (value: string) => { const getRawCssValue = (value: string) => {
if (value && value.startsWith('{css`')) { if (value && value.startsWith('{{css`')) {
return value.split('').slice(5, -2).join('').trim(); return value.split('').slice(6, -3).join('').trim();
} }
return value; return value;
}; };
@@ -51,7 +51,8 @@ const cssPattern = (string: string, name: string): any => {
}; };
/** /**
* 废弃 * coral-system css prop
* @example 提供 css-in-js 代码支持,例如 css`background: red;`
* @deprecated 使用嵌套属性代替 * @deprecated 使用嵌套属性代替
*/ */
export function CssSetter(props: FormItemComponentProps<string>) { export function CssSetter(props: FormItemComponentProps<string>) {
@@ -82,16 +83,14 @@ export function CssSetter(props: FormItemComponentProps<string>) {
useEffect(() => { useEffect(() => {
if (contentValue || contentValue === '') { if (contentValue || contentValue === '') {
onChange(`{css\`${contentValue}\`}`, { onChange(wrapCode(`css\`${contentValue}\``));
// relatedImports: ['']
});
} }
}, [contentValue]); }, [contentValue]);
const isFlex = ['flex', 'inline-flex'].includes(display); const isFlex = ['flex', 'inline-flex'].includes(display);
return ( return (
<Box> <Box bg="fill1">
<ItemGroup title="布局方式"> <ItemGroup title="布局方式">
<DisplaySetter <DisplaySetter
value={toJSON(contentValue).attributes?.display} value={toJSON(contentValue).attributes?.display}
@@ -204,12 +203,12 @@ export function CssSetter(props: FormItemComponentProps<string>) {
/> />
</ItemGroup> </ItemGroup>
<ItemGroup title="透明度"> <ItemGroup title="透明度">
<SliderSetter <Slider
max={1} max={1}
min={0} min={0}
step={0.1} step={0.1}
value={toJSON(contentValue).attributes?.opacity || 1} value={toJSON(contentValue).attributes?.opacity || 1}
onChange={(v) => { onChange={(v: number) => {
changeStyle(v, 'opacity'); changeStyle(v, 'opacity');
}} }}
/> />
+25 -5
View File
@@ -25,13 +25,18 @@ const style = {
width: '100%', width: '100%',
}; };
export function DateSetter({ value, onChange, format = 'YYYY-MM-DD', ...rest }: FormItemComponentProps<string>) { export function DateSetter({
value,
onChange,
format = 'YYYY-MM-DD',
...rest
}: FormItemComponentProps<string>) {
return ( return (
<DatePicker <DatePicker
{...rest} {...rest}
format={format} format={format}
style={style} style={style}
value={toMoment(value, format)} value={value ? toMoment(value, format) : undefined}
onChange={(val, str) => { onChange={(val, str) => {
onChange && onChange(str); onChange && onChange(str);
}} }}
@@ -46,7 +51,12 @@ function toMoments(value: string[], format: string): moment.Moment[] {
return []; return [];
} }
export function DateRangeSetter({ value, onChange, format = 'YYYY-MM-DD', ...rest }: FormItemComponentProps<string[]>) { export function DateRangeSetter({
value,
onChange,
format = 'YYYY-MM-DD',
...rest
}: FormItemComponentProps<string[]>) {
return ( return (
<DatePicker.RangePicker <DatePicker.RangePicker
{...rest} {...rest}
@@ -60,7 +70,12 @@ export function DateRangeSetter({ value, onChange, format = 'YYYY-MM-DD', ...res
); );
} }
export function TimeSetter({ value, onChange, format = 'HH:mm:ss', ...rest }: FormItemComponentProps<string>) { export function TimeSetter({
value,
onChange,
format = 'HH:mm:ss',
...rest
}: FormItemComponentProps<string>) {
return ( return (
<TimePicker <TimePicker
{...rest} {...rest}
@@ -74,7 +89,12 @@ export function TimeSetter({ value, onChange, format = 'HH:mm:ss', ...rest }: Fo
); );
} }
export function TimeRangeSetter({ value, onChange, format = 'HH:mm:ss', ...rest }: FormItemComponentProps<string[]>) { export function TimeRangeSetter({
value,
onChange,
format = 'HH:mm:ss',
...rest
}: FormItemComponentProps<string[]>) {
return ( return (
<TimePicker.RangePicker <TimePicker.RangePicker
{...rest} {...rest}
+44 -39
View File
@@ -1,10 +1,12 @@
import React, { useCallback, useState } from 'react'; import React, { useCallback, useMemo, useState } from 'react';
import { css, Box } from 'coral-system'; import { css, Box, Text } from 'coral-system';
import { AutoComplete } from 'antd'; import { AutoComplete } from 'antd';
import { ActionSelect } from '@music163/tango-ui'; import { ActionSelect } from '@music163/tango-ui';
import { FormItemComponentProps } from '@music163/tango-setting-form'; import { FormItemComponentProps } from '@music163/tango-setting-form';
import { useWorkspace, useWorkspaceData } from '@music163/tango-context'; import { useWorkspace, useWorkspaceData } from '@music163/tango-context';
import { ExpressionModal, getWrappedExpressionCode } from './expression-setter'; import { Dict, wrapCode } from '@music163/tango-helpers';
import { ExpressionPopover } from './expression-setter';
import { value2code } from '@music163/tango-core';
enum EventAction { enum EventAction {
NoAction = 'noAction', NoAction = 'noAction',
@@ -23,15 +25,6 @@ const wrapperStyle = css`
} }
`; `;
const options = [
{ label: '无动作', value: EventAction.NoAction },
{ label: '打印事件', value: EventAction.ConsoleLog },
{ label: '绑定 JS 表达式', value: EventAction.BindExpression },
{ label: '打开页面', value: EventAction.NavigateTo },
{ label: '打开弹窗', value: EventAction.OpenModal },
{ label: '关闭弹窗', value: EventAction.CloseModal },
];
export type EventSetterProps = FormItemComponentProps<string>; export type EventSetterProps = FormItemComponentProps<string>;
/** /**
@@ -41,20 +34,47 @@ export function EventSetter(props: EventSetterProps) {
const { value, onChange, modalTitle } = props; const { value, onChange, modalTitle } = props;
const [type, setType] = useState<EventAction>(); // 事件类型 const [type, setType] = useState<EventAction>(); // 事件类型
const [temp, setTemp] = useState(''); // 二级暂存值 const [temp, setTemp] = useState(''); // 二级暂存值
const [expModalVisible, setExpModalVisible] = useState(false); // 弹窗是否显示
const { actionVariables, routeOptions } = useWorkspaceData(); const { actionVariables, routeOptions } = useWorkspaceData();
const workspace = useWorkspace(); const workspace = useWorkspace();
const modalOptions = workspace.activeViewModule.listModals() || []; const modalOptions = workspace.activeViewModule.listModals() || [];
const code = value2code(value);
const handleChange = useCallback<FormItemComponentProps['onChange']>( const handleChange = useCallback<FormItemComponentProps['onChange']>(
(nextValue: any, ...args) => { (nextValue: any, ...args) => {
const ret = getWrappedExpressionCode(nextValue); if (!nextValue) {
if (ret !== value) { onChange(undefined);
onChange(ret, ...args); }
if (nextValue !== code) {
onChange(wrapCode(nextValue), ...args);
} }
}, },
[onChange, value], [onChange, code],
);
const options = useMemo(
() => [
{ label: '无动作', value: EventAction.NoAction },
{ label: '打印事件', value: EventAction.ConsoleLog },
{
label: (
<ExpressionPopover
title={modalTitle}
value={value}
onOk={(nextValue) => {
handleChange(nextValue);
}}
dataSource={actionVariables}
>
<Text> JS </Text>
</ExpressionPopover>
),
value: EventAction.BindExpression,
},
{ label: '打开页面', value: EventAction.NavigateTo },
{ label: '打开弹窗', value: EventAction.OpenModal },
{ label: '关闭弹窗', value: EventAction.CloseModal },
],
[modalTitle, value, actionVariables, handleChange],
); );
const onAction = (key: string) => { const onAction = (key: string) => {
@@ -62,9 +82,6 @@ export function EventSetter(props: EventSetterProps) {
setTemp(''); // 重置二级选项值 setTemp(''); // 重置二级选项值
switch (key) { switch (key) {
case EventAction.BindExpression:
setExpModalVisible(true);
break;
case EventAction.ConsoleLog: case EventAction.ConsoleLog:
handleChange('(...args) => console.log(...args)'); handleChange('(...args) => console.log(...args)');
break; break;
@@ -76,23 +93,11 @@ export function EventSetter(props: EventSetterProps) {
} }
}; };
const actionText = getActionText(type, temp, value); const actionText = getActionText(type, temp, code);
const inputValue = value;
return ( return (
<Box css={wrapperStyle}> <Box css={wrapperStyle}>
<ActionSelect options={options} onSelect={onAction} text={actionText} /> <ActionSelect options={options} onSelect={onAction} text={actionText} />
<ExpressionModal
title={modalTitle}
value={inputValue}
visible={expModalVisible}
onCancel={() => setExpModalVisible(false)}
onOk={(nextValue) => {
setExpModalVisible(false);
handleChange(nextValue);
}}
dataSource={actionVariables}
/>
{type === EventAction.NavigateTo && ( {type === EventAction.NavigateTo && (
<AutoComplete <AutoComplete
placeholder="选择或输入页面路由" placeholder="选择或输入页面路由"
@@ -136,18 +141,18 @@ export function EventSetter(props: EventSetterProps) {
); );
} }
const handlerMap = { const handlerMap: Dict = {
[EventAction.OpenModal]: 'openModal', [EventAction.OpenModal]: 'openModal',
[EventAction.CloseModal]: 'closeModal', [EventAction.CloseModal]: 'closeModal',
[EventAction.NavigateTo]: 'navigateTo', [EventAction.NavigateTo]: 'navigateTo',
}; };
function getActionText(type: EventAction, temp: string, value: any) { function getActionText(type: EventAction, temp: string, fallbackCode: string) {
let text; let text;
if (handlerMap[type]) { if (handlerMap[type]) {
text = getExpressionValue(type, temp); text = getExpressionValue(type, temp);
} else if (value) { } else if (fallbackCode) {
text = value; text = fallbackCode;
} }
text = text || '请选择'; text = text || '请选择';
return text; return text;
@@ -156,6 +161,6 @@ function getActionText(type: EventAction, temp: string, value: any) {
function getExpressionValue(type: EventAction, value = '') { function getExpressionValue(type: EventAction, value = '') {
const handler = handlerMap[type]; const handler = handlerMap[type];
if (handler) { if (handler) {
return `{() => tango.${handler}("${value}")}`; return `() => tango.${handler}("${value}")`;
} }
} }
@@ -1,26 +0,0 @@
import React from 'react';
import { SingleMonacoEditor } from '@music163/tango-ui';
import { Box } from 'coral-system';
import { FormItemComponentProps } from '@music163/tango-setting-form/src/form-item';
export function ExpressionSetter({ value, onChange }: FormItemComponentProps) {
return (
<Box height="120px" border="solid" borderColor="line.normal" borderRadius="s">
<SingleMonacoEditor
defaultValue={value}
onBlur={(newValue) => {
if (newValue !== value) {
onChange(`{${newValue}}`);
}
}}
language="javascript"
options={{
lineNumbers: 'off',
minimap: {
enabled: false,
},
}}
/>
</Box>
);
}
@@ -1,26 +1,10 @@
import React, { useState, useEffect, useCallback } from 'react'; import React, { useState, useEffect, useCallback } from 'react';
import { Box, Text, css } from 'coral-system'; import { Box, Text, css } from 'coral-system';
import { Dropdown, Modal } from 'antd'; import { Dropdown, Button } from 'antd';
import { import { isValidExpressionCode } from '@music163/tango-core';
isValidExpressionCode, import { getValue, IVariableTreeNode, noop } from '@music163/tango-helpers';
isWrappedByExpressionContainer, import { CloseCircleFilled, MenuOutlined } from '@ant-design/icons';
value2expressionCode, import { Panel, InputCode, Action, DragPanel, PopOutOutlined } from '@music163/tango-ui';
} from '@music163/tango-core';
import {
getVariableContent,
noop,
useBoolean,
getValue,
IVariableTreeNode,
} from '@music163/tango-helpers';
import {
CloseCircleFilled,
ExpandAltOutlined,
InfoOutlined,
KeyOutlined,
MenuOutlined,
} from '@ant-design/icons';
import { Panel, InputCode, Action, CodeOutlined } from '@music163/tango-ui';
import { FormItemComponentProps } from '@music163/tango-setting-form'; import { FormItemComponentProps } from '@music163/tango-setting-form';
import { useWorkspace, useWorkspaceData } from '@music163/tango-context'; import { useWorkspace, useWorkspaceData } from '@music163/tango-context';
import { VariableTree } from '../components'; import { VariableTree } from '../components';
@@ -29,42 +13,19 @@ import { CODE_TEMPLATES } from '../helpers';
import { shapeServiceValues } from '../sidebar/datasource-panel/interface-config'; import { shapeServiceValues } from '../sidebar/datasource-panel/interface-config';
export const expressionValueValidate = (value: string) => { export const expressionValueValidate = (value: string) => {
if (isWrappedByExpressionContainer(value)) { if (!isValidExpressionCode(value)) {
const exp = getVariableContent(value); return '表达式存在语法错误!';
if (!isValidExpressionCode(exp)) {
return '表达式存在语法错误!';
}
} }
}; };
export const jsonValueValidate = (value: string) => { export const jsonValueValidate = (value: string) => {
if (isWrappedByExpressionContainer(value)) { try {
const jsonStr = getVariableContent(value); JSON.parse(value);
try { } catch (e) {
JSON.parse(jsonStr); return '不是合法的 JSON 语法!';
} catch (e) {
return '不是合法的 JSON 语法!';
}
} }
}; };
/**
* 返回 `{}` 包裹后的表达式代码
* @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` const suffixStyle = css`
display: flex; display: flex;
align-items: center; align-items: center;
@@ -90,23 +51,19 @@ export function ExpressionSetter(props: ExpressionSetterProps) {
modalTitle, modalTitle,
modalTip, modalTip,
autoCompleteOptions, autoCompleteOptions,
placeholder = '输入JS代码', placeholder = '在这里输入JS代码',
value: valueProp, value: valueProp,
status, status,
allowClear = true, allowClear = true,
newStoreTemplate, newStoreTemplate,
showOptionsDropDown = true, showOptionsDropDown = true,
} = props; } = props;
const [visible, { on, off }] = useBoolean(); // const codeValue = getCodeOfWrappedCode(valueProp);
const [inputValue, setInputValue] = useState(() => { const [inputValue, setInputValue] = useState(valueProp);
return value2expressionCode(valueProp);
});
const sandbox = useSandboxQuery();
const evaluateContext = sandbox.window;
// when receive new value, sync state // when receive new value, sync state
useEffect(() => { useEffect(() => {
setInputValue(value2expressionCode(valueProp)); setInputValue(valueProp);
}, [valueProp]); }, [valueProp]);
const change = useCallback( const change = useCallback(
@@ -114,18 +71,14 @@ export function ExpressionSetter(props: ExpressionSetterProps) {
if (code === valueProp) { if (code === valueProp) {
return; return;
} }
onChange(code);
const ret = getWrappedExpressionCode(code);
if (ret === valueProp) {
return;
}
onChange(ret);
}, },
[valueProp, onChange], [valueProp, onChange],
); );
const sandbox = useSandboxQuery();
const evaluateContext = sandbox.window;
return ( return (
<Box className="ExpressionSetter"> <Box className="ExpressionSetter">
{/* 同时支持下拉框展示 */} {/* 同时支持下拉框展示 */}
@@ -161,11 +114,16 @@ export function ExpressionSetter(props: ExpressionSetterProps) {
<Action tooltip="使用预设代码片段" icon={<MenuOutlined />} size="small" /> <Action tooltip="使用预设代码片段" icon={<MenuOutlined />} size="small" />
</Dropdown> </Dropdown>
)} )}
<Action <ExpressionPopover
tooltip="打开表达式变量选择面板" title={modalTitle}
icon={<ExpandAltOutlined />} subTitle={modalTip}
onClick={on} placeholder={placeholder}
size="small" autoCompleteOptions={autoCompleteOptions}
newStoreTemplate={newStoreTemplate}
value={inputValue}
onOk={(value) => {
change(value);
}}
/> />
</Box> </Box>
} }
@@ -179,32 +137,16 @@ export function ExpressionSetter(props: ExpressionSetterProps) {
status={status} status={status}
maxHeight="200px" maxHeight="200px"
/> />
<ExpressionModal
title={modalTitle}
subTitle={modalTip}
placeholder={placeholder}
autoCompleteOptions={autoCompleteOptions}
newStoreTemplate={newStoreTemplate}
visible={visible}
value={inputValue}
onCancel={() => off()}
onOk={(value) => {
change(value);
off();
}}
/>
</Box> </Box>
); );
} }
export interface ExpressionModalProps { export interface ExpressionPopoverProps {
title?: string; title?: string;
subTitle?: string; subTitle?: string;
placeholder?: string; placeholder?: string;
visible?: boolean;
defaultValue?: string; defaultValue?: string;
value?: string; value?: string;
onCancel?: () => void;
onOk?: (value: string) => void; onOk?: (value: string) => void;
dataSource?: IVariableTreeNode[]; dataSource?: IVariableTreeNode[];
autoCompleteOptions?: string[]; autoCompleteOptions?: string[];
@@ -212,25 +154,27 @@ export interface ExpressionModalProps {
* 新建 store 的模板代码 * 新建 store 的模板代码
*/ */
newStoreTemplate?: string; newStoreTemplate?: string;
children?: React.ReactNode;
} }
export function ExpressionModal({ export function ExpressionPopover({
title, title,
subTitle, subTitle,
placeholder, placeholder,
visible,
onCancel = noop,
onOk = noop, onOk = noop,
defaultValue, defaultValue,
value, value,
dataSource, dataSource,
autoCompleteOptions, autoCompleteOptions,
newStoreTemplate = CODE_TEMPLATES.newStoreTemplate, newStoreTemplate = CODE_TEMPLATES.newStoreTemplate,
}: ExpressionModalProps) { children,
}: ExpressionPopoverProps) {
const [exp, setExp] = useState(value ?? defaultValue); const [exp, setExp] = useState(value ?? defaultValue);
const [error, setError] = useState(''); const [error, setError] = useState('');
const workspace = useWorkspace(); const workspace = useWorkspace();
const selectNodePath = workspace.selectSource.selected[0]?.codeId;
const { expressionVariables } = useWorkspaceData(); const { expressionVariables } = useWorkspaceData();
const serviceModules = Object.keys(workspace.serviceModules).map((key) => ({ const serviceModules = Object.keys(workspace.serviceModules).map((key) => ({
label: key === 'index' ? '默认模块' : key, label: key === 'index' ? '默认模块' : key,
@@ -238,7 +182,7 @@ export function ExpressionModal({
})); }));
const sandbox = useSandboxQuery(); const sandbox = useSandboxQuery();
const evaluateContext = sandbox.window; const evaluateContext: any = sandbox.window;
const handleExpInputChange = (val: string) => { const handleExpInputChange = (val: string) => {
setExp(val?.trim()); setExp(val?.trim());
@@ -249,109 +193,159 @@ export function ExpressionModal({
}, [value]); }, [value]);
return ( return (
<Modal <DragPanel
closable={false} width={700}
destroyOnClose height={615}
width="60%" resizeable
open={visible} title={`使用代码设置 ${selectNodePath} 组件的${title}`}
onCancel={onCancel} onOpenChange={(open) => {
onOk={() => { if (!open) {
onOk(exp); setExp(undefined);
}} setError('');
bodyStyle={{ }
padding: 0,
}} }}
body={
<Box display="flex" flexDirection="column">
<Box p="m">
<InputCode
shape="inset"
minHeight="56px"
maxHeight="160px"
value={exp}
placeholder={placeholder}
onChange={handleExpInputChange}
onBlur={() => {
if (exp) {
setError(expressionValueValidate(exp));
}
}}
autoCompleteContext={evaluateContext}
autoCompleteOptions={autoCompleteOptions}
/>
{error ? (
<Text color="red" fontSize="12px" as="p">
</Text>
) : null}
{subTitle && (
<Text fontSize="12px" color="text3" as="p">
{subTitle}
</Text>
)}
<Text fontSize="12px" color="text3" as="p">
javascript 使 jsx
</Text>
</Box>
<Panel
title="从变量列表中选中"
shape="solid"
borderLeft="0"
borderRight="0"
flex={1}
bodyProps={{
style: {
overflow: 'hidden',
},
}}
>
<VariableTree
height="100%"
showViewButton
dataSource={dataSource || expressionVariables}
appContext={evaluateContext['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;
}
if (node.key.split('.').length < 2) {
return;
}
let str;
if (/^(stores|services)\./.test(node.key)) {
str = `tango.${node.key.replaceAll('.', '?.')}`;
} else {
str = `${node.key}`;
}
setExp(str);
}}
onAddStoreVariable={(storeName, data) => {
workspace.addStoreState(storeName, data.name, data.initialValue);
}}
onUpdateStoreVariable={(variableKey, code) => {
workspace.updateStoreVariable(variableKey, code);
}}
onAddStore={(storeName) => {
workspace.addStoreFile(storeName, newStoreTemplate);
}}
onRemoveStoreVariable={(variableKey) => {
workspace.removeStoreVariable(variableKey);
}}
onRemoveService={(serviceKey) => {
workspace.removeServiceFunction(serviceKey);
}}
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) {
return;
}
if (node.type === 'function') {
return;
}
return getValue(evaluateContext['tango'], node.key);
}}
/>
</Panel>
</Box>
}
footer={(close) => (
<Box display="flex" width="100%" justifyContent="flex-end" gap="5px">
<Button
type="primary"
size="small"
onClick={() => {
if (!error) {
onOk(exp);
close();
}
}}
>
</Button>
<Button size="small" onClick={close}>
</Button>
</Box>
)}
> >
<Panel title={`${title} 设置为引用变量或自定义表达式`} subTitle={subTitle} shape="solid"> {children || (
<InputCode <Action
shape="inset" tooltip="弹出代码编辑和变量选择面板"
minHeight="56px" tooltipProps={{ placement: 'topLeft' }}
maxHeight="200px" icon={<PopOutOutlined />}
value={exp} size="small"
placeholder={placeholder}
onChange={handleExpInputChange}
onBlur={() => {
setError(expressionValueValidate(exp));
}}
autoCompleteContext={evaluateContext}
autoCompleteOptions={autoCompleteOptions}
/> />
{error ? <Text color="red"></Text> : null} )}
</Panel> </DragPanel>
<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;
}
if (node.key.split('.').length < 2) {
return;
}
let str;
if (/^(stores|services)\./.test(node.key)) {
str = `{tango.${node.key.replaceAll('.', '?.')}}`;
} else {
str = `{${node.key}}`;
}
setExp(str);
}}
onAddStoreVariable={(storeName, data) => {
workspace.addStoreState(storeName, data.name, data.initialValue);
}}
onUpdateStoreVariable={(variableKey, code) => {
workspace.updateStoreVariable(variableKey, code);
}}
onAddStore={(storeName) => {
workspace.addStoreFile(storeName, newStoreTemplate);
}}
onRemoveStoreVariable={(variableKey) => {
workspace.removeStoreVariable(variableKey);
}}
onRemoveService={(serviceKey) => {
workspace.removeServiceFunction(serviceKey);
}}
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) {
return;
}
if (node.type === 'function') {
return;
}
return getValue(evaluateContext['tango'], node.key);
}}
/>
</Panel>
</Modal>
); );
} }
+40 -33
View File
@@ -25,44 +25,46 @@ import {
FlexAlignItemsSetter, FlexAlignItemsSetter,
FlexDirectionSetter, FlexDirectionSetter,
} from './style-setter'; } from './style-setter';
import { BoolSetter } from './bool-setter';
import { ChoiceSetter } from './choice-setter'; import { ChoiceSetter } from './choice-setter';
import { NumberSetter, SliderSetter } from './number-setter'; import { isValidExpressionCode } from '@music163/tango-core';
import { TextAreaSetter, TextSetter } from './text-setter';
const setters: IFormItemCreateOptions[] = [ const codeValidate: IFormItemCreateOptions['validate'] = (value, field) => {
if (!value) return;
const rawCode = field.detail.rawCode;
if (!rawCode) return;
return isValidExpressionCode(rawCode) ? '' : '请输入合法的 Javascript 代码片段';
};
const jsonValidate: IFormItemCreateOptions['validate'] = (value, field) => {
if (!value) return;
try {
JSON.parse(field.detail.rawCode);
return;
} catch (e) {
return '请输入合法的 JSON 字符串';
}
};
export const BUILT_IN_SETTERS: IFormItemCreateOptions[] = [
{ {
name: 'boolSetter', name: 'codeSetter',
component: BoolSetter, alias: ['expSetter', 'expressionSetter'],
},
{
name: 'choiceSetter',
component: ChoiceSetter,
},
{
name: 'expSetter',
alias: ['expressionSetter'],
component: ExpressionSetter, component: ExpressionSetter,
type: 'code',
validate: codeValidate,
}, },
{ {
name: 'numberSetter', name: 'radioGroupSetter',
component: NumberSetter, alias: ['choiceSetter'],
}, component: ChoiceSetter,
{
name: 'textSetter',
component: TextSetter,
},
{ name: 'textAreaSetter', component: TextAreaSetter },
{
name: 'sliderSetter',
component: SliderSetter,
}, },
{ {
name: 'actionListSetter', name: 'actionListSetter',
component: ActionListSetter, component: ActionListSetter,
}, },
{ {
name: 'columnSetter', name: 'tableColumnsSetter',
alias: ['columnSetter'], // 兼容
component: ColumnSetter, component: ColumnSetter,
}, },
{ {
@@ -94,18 +96,17 @@ const setters: IFormItemCreateOptions[] = [
alias: ['actionSetter', 'functionSetter', 'callbackSetter'], alias: ['actionSetter', 'functionSetter', 'callbackSetter'],
component: EventSetter, component: EventSetter,
}, },
{
name: 'expressionSetter',
alias: ['expSetter'],
component: ExpressionSetter,
},
{ {
name: 'jsonSetter', name: 'jsonSetter',
component: JSONSetter, component: JSONSetter,
type: 'code',
validate: jsonValidate,
}, },
{ {
name: 'jsxSetter', name: 'jsxSetter',
component: JsxSetter, component: JsxSetter,
type: 'code',
validate: codeValidate,
}, },
{ {
name: 'listSetter', name: 'listSetter',
@@ -120,20 +121,26 @@ const setters: IFormItemCreateOptions[] = [
component: OptionSetter, component: OptionSetter,
}, },
{ {
name: 'pickerSetter', name: 'selectSetter',
alias: ['pickerSetter'],
component: PickerSetter, component: PickerSetter,
}, },
{ {
name: 'renderPropsSetter', name: 'renderPropsSetter',
component: RenderSetter, component: RenderSetter,
type: 'code',
validate: codeValidate,
}, },
{ {
name: 'tableCellSetter', name: 'tableCellSetter',
component: TableCellSetter, component: TableCellSetter,
type: 'code',
}, },
{ {
name: 'tableExpandableSetter', name: 'tableExpandableSetter',
component: TableExpandableSetter, component: TableExpandableSetter,
type: 'code',
validate: codeValidate,
}, },
{ {
name: 'routerSetter', name: 'routerSetter',
@@ -169,5 +176,5 @@ const setters: IFormItemCreateOptions[] = [
]; ];
export function registerBuiltinSetters() { export function registerBuiltinSetters() {
setters.forEach(register); BUILT_IN_SETTERS.forEach(register);
} }
@@ -1,4 +1,4 @@
import React from 'react'; import React, { useState } from 'react';
import { Box } from 'coral-system'; import { Box } from 'coral-system';
import { SingleMonacoEditor } from '@music163/tango-ui'; import { SingleMonacoEditor } from '@music163/tango-ui';
import { FormItemComponentProps } from '@music163/tango-setting-form'; import { FormItemComponentProps } from '@music163/tango-setting-form';
@@ -6,14 +6,18 @@ import { FormItemComponentProps } from '@music163/tango-setting-form';
/** /**
* JSON Setter * JSON Setter
*/ */
export function JSONSetter({ value, onChange }: FormItemComponentProps) { export function JSONSetter({ value: valueProp, onChange }: FormItemComponentProps) {
const [value, setValue] = useState(valueProp || '');
return ( return (
<Box height="120px" border="solid" borderColor="line.normal" borderRadius="s"> <Box height="120px" border="solid" borderColor="line.normal" borderRadius="s">
<SingleMonacoEditor <SingleMonacoEditor
defaultValue={value} defaultValue={value}
onChange={(newValue) => {
setValue(newValue);
}}
onBlur={(newValue) => { onBlur={(newValue) => {
if (newValue !== value) { if (newValue !== value) {
onChange(`{${newValue}}`); onChange(newValue);
} }
}} }}
language="json" language="json"
+16 -5
View File
@@ -1,5 +1,4 @@
import React from 'react'; import React, { useEffect, useState } from 'react';
import { isPlainString } from '@music163/tango-helpers';
import { ActionSelect, InputCode } from '@music163/tango-ui'; import { ActionSelect, InputCode } from '@music163/tango-ui';
import { FormItemComponentProps } from '@music163/tango-setting-form'; import { FormItemComponentProps } from '@music163/tango-setting-form';
import { Box } from 'coral-system'; import { Box } from 'coral-system';
@@ -54,24 +53,36 @@ const defaultGetTemplate = (key: string) => {
*/ */
export function JsxSetter(props: JsxSetterProps) { export function JsxSetter(props: JsxSetterProps) {
const { showInput, getTemplate = defaultGetTemplate, value, onChange } = props; const { showInput, getTemplate = defaultGetTemplate, value, onChange } = props;
const [inputValue, setInputValue] = useState(value);
useEffect(() => {
setInputValue(value);
}, [value]);
return ( return (
<Box> <Box>
<ActionSelect <ActionSelect
showInput={showInput} showInput={showInput}
defaultInputValue={isPlainString(value) ? value : undefined} defaultInputValue={value}
options={options} options={options}
text="设置此区域为" text="设置此区域为"
onInputChange={onChange} onInputChange={onChange}
onSelect={(key) => { onSelect={(key) => {
const [tpl, deps] = getTemplate(key); const [tpl, deps] = getTemplate(key);
if (tpl) { if (tpl) {
onChange(`{${tpl}}`, { relatedImports: deps }); onChange(tpl, { relatedImports: deps });
} else { } else {
onChange(undefined); onChange(undefined);
} }
}} }}
/> />
{value && <InputCode value={value} readOnly editable={false} />} {value && (
<InputCode
value={value}
onChange={(val) => setInputValue(val)}
onBlur={() => {
onChange(inputValue);
}}
/>
)}
</Box> </Box>
); );
} }
@@ -3,10 +3,11 @@ import { css, Box } from 'coral-system';
import { Form, Input, Button, Popover } from 'antd'; import { Form, Input, Button, Popover } from 'antd';
import { DeleteOutlined, EditOutlined, HolderOutlined } from '@ant-design/icons'; import { DeleteOutlined, EditOutlined, HolderOutlined } from '@ant-design/icons';
import { FormItemComponentProps } from '@music163/tango-setting-form'; import { FormItemComponentProps } from '@music163/tango-setting-form';
import { Dict } from '@music163/tango-helpers';
import { DragBox } from '../components'; import { DragBox } from '../components';
function copyWithoutUndefined(source: Record<string, any>) { function copyWithoutUndefined(source: Dict) {
const ret = {}; const ret: Dict = {};
for (const key in source) { for (const key in source) {
if (source[key] !== undefined) { if (source[key] !== undefined) {
ret[key] = source[key]; ret[key] = source[key];
@@ -234,6 +235,7 @@ function NewOptionForm({ fields = [], initialValues = {}, onSubmit }: NewOptionF
label={item.label} label={item.label}
name={item.name} name={item.name}
required={item.required} required={item.required}
rules={[{ required: item.required }]}
valuePropName={item.valuePropName} valuePropName={item.valuePropName}
extra={item.extra} extra={item.extra}
style={{ style={{
@@ -260,7 +262,7 @@ interface ListSetterProps extends FormItemComponentProps<any[]> {
addBtnText?: string; addBtnText?: string;
getListItemKey?: (item: any) => React.Key; getListItemKey?: (item: any) => React.Key;
renderItem?: (item: any) => React.ReactNode; renderItem?: (item: any) => React.ReactNode;
}; }
const defaultListItemFormFields: ListSetterItemProps['formFields'] = [ const defaultListItemFormFields: ListSetterItemProps['formFields'] = [
{ label: 'key', name: 'key', required: true }, { label: 'key', name: 'key', required: true },
@@ -60,7 +60,7 @@ export function ModelSetter({
}: FormItemComponentProps) { }: FormItemComponentProps) {
const [inputValue, setInputValue] = useState(value); const [inputValue, setInputValue] = useState(value);
const { modelVariables } = useWorkspaceData(); const { modelVariables } = useWorkspaceData();
const evaluateContext = useSandboxQuery().window || {}; const evaluateContext: any = useSandboxQuery().window || {};
const workspace = useWorkspace(); const workspace = useWorkspace();
const definedVariables = useMemo(() => { const definedVariables = useMemo(() => {
const list: string[] = []; const list: string[] = [];
+23 -12
View File
@@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { ActionSelect, InputCode } from '@music163/tango-ui'; import { ActionSelect, InputCode } from '@music163/tango-ui';
import { FormItemComponentProps } from '@music163/tango-setting-form'; import { FormItemComponentProps } from '@music163/tango-setting-form';
import { Box } from 'coral-system'; import { Box } from 'coral-system';
import { Dict } from '@music163/tango-helpers';
interface IRenderOption { interface IRenderOption {
label: string; label: string;
@@ -16,6 +17,11 @@ export interface RenderSetterProps {
fallbackOption?: IRenderOption; fallbackOption?: IRenderOption;
} }
const defaultOptions: IRenderOption[] = [
{ label: '取消自定义', value: '' },
{ label: '自定义渲染', value: 'Box', render: '() => <Box></Box>' },
];
/** /**
* Render Props Setters * Render Props Setters
*/ */
@@ -23,17 +29,16 @@ export function RenderSetter({
value, value,
onChange, onChange,
text = '自定义渲染为', text = '自定义渲染为',
options = [], options = defaultOptions,
fallbackOption, fallbackOption,
}: FormItemComponentProps & RenderSetterProps) { }: FormItemComponentProps & RenderSetterProps) {
const [inputValue, setInputValue] = useState(value); const [inputValue, setInputValue] = useState(value || '');
useEffect(() => { useEffect(() => {
setInputValue(value); setInputValue(value);
}, [value]); }, [value]);
const optionsMap = useMemo(() => { const optionsMap = useMemo(() => {
return options.reduce((prev, cur) => { return options.reduce<Dict>((prev, cur) => {
prev[cur.value] = cur; prev[cur.value] = cur;
return prev; return prev;
}, {}); }, {});
@@ -53,9 +58,9 @@ export function RenderSetter({
return ( return (
<Box> <Box>
<ActionSelect text={text} options={options} onSelect={onSelect} /> <ActionSelect text={text} options={options} onSelect={onSelect} />
{value && ( {inputValue && (
<InputCode <InputCode
value={value} value={inputValue}
onChange={(val) => setInputValue(val)} onChange={(val) => setInputValue(val)}
onBlur={() => onChange(inputValue)} onBlur={() => onChange(inputValue)}
/> />
@@ -65,17 +70,22 @@ export function RenderSetter({
} }
const getRender = (content: string, type?: 'tableCell' | 'tableExpandable') => { const getRender = (content: string, type?: 'tableCell' | 'tableExpandable') => {
let code;
switch (type) { switch (type) {
case 'tableCell': case 'tableCell':
return `{(value, record, index) => ${content}}`; code = `(value, record, index) => ${content}`;
break;
case 'tableExpandable': case 'tableExpandable':
return `{{ code = `{
expandedRowRender: (record) => ${content}, expandedRowRender: (record) => ${content},
rowExpandable: (record) => true rowExpandable: (record) => true
}}`; }`;
break;
default: default:
return `{() => ${content}}`; code = `() => ${content}`;
break;
} }
return code;
}; };
const tableCellOptions: RenderSetterProps['options'] = [ const tableCellOptions: RenderSetterProps['options'] = [
@@ -120,6 +130,7 @@ export function TableCellSetter(props: FormItemComponentProps) {
return <RenderSetter options={tableCellOptions} {...props} />; return <RenderSetter options={tableCellOptions} {...props} />;
} }
// FIXME: 应该直接用 props 嵌套的模式
export function TableExpandableSetter(props: FormItemComponentProps) { export function TableExpandableSetter(props: FormItemComponentProps) {
return <RenderSetter options={tableExpandableOptions} text="配置表格可展开行" {...props} />; return <RenderSetter options={tableExpandableOptions} text="配置表格可展开行" {...props} />;
} }
@@ -1,11 +1,17 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Box } from 'coral-system'; import { Box } from 'coral-system';
import { Input, Radio } from 'antd'; import { Input, Radio } from 'antd';
import { isValidUrl } from '@music163/tango-helpers'; import { Dict, isValidUrl } from '@music163/tango-helpers';
import { FormItemComponentProps } from '@music163/tango-setting-form'; import { FormItemComponentProps } from '@music163/tango-setting-form';
import { useWorkspaceData } from '@music163/tango-context'; import { useWorkspaceData } from '@music163/tango-context';
import { PickerSetter } from './picker-setter'; import { PickerSetter } from './picker-setter';
const routerModeMap: Dict = {
link: '链接',
router: '路由',
both: '',
};
export function RouterSetter(props: FormItemComponentProps) { export function RouterSetter(props: FormItemComponentProps) {
const [type, setType] = useState<'input' | 'select'>(() => { const [type, setType] = useState<'input' | 'select'>(() => {
return isValidUrl(props.value) ? 'input' : 'select'; return isValidUrl(props.value) ? 'input' : 'select';
@@ -37,7 +43,7 @@ export function RouterSetter(props: FormItemComponentProps) {
)} )}
{type === 'input' && ( {type === 'input' && (
<Input <Input
placeholder={`请输入任意${{ link: '链接', router: '路由', both: '' }[displayType]}地址`} placeholder={`请输入任意${routerModeMap[displayType]}地址`}
{...props} {...props}
value={input} value={input}
onChange={(e) => setInput(e.target.value)} onChange={(e) => setInput(e.target.value)}
@@ -7,8 +7,6 @@ import { BgColorsOutlined, EyeInvisibleOutlined, FileImageOutlined } from '@ant-
import { SingleMonacoEditor, LineSolidOutlined, LineDashedOutlined } from '@music163/tango-ui'; import { SingleMonacoEditor, LineSolidOutlined, LineDashedOutlined } from '@music163/tango-ui';
import { FormItemComponentProps } from '@music163/tango-setting-form'; import { FormItemComponentProps } from '@music163/tango-setting-form';
import { ChoiceSetter } from './choice-setter'; import { ChoiceSetter } from './choice-setter';
import { TextSetter } from './text-setter';
// import { ImageSetter } from './image-setter';
function getRawCssValue(value: string) { function getRawCssValue(value: string) {
if (value && value.startsWith('{css`')) { if (value && value.startsWith('{css`')) {
@@ -17,12 +15,13 @@ function getRawCssValue(value: string) {
return value; return value;
} }
// TODO: style object setter
/** /**
* 不稳定,暂不推荐使用 * 不稳定,暂不推荐使用
*/ */
export function CssCodeSetter({ value, onChange }: FormItemComponentProps<string>) { export function CssCodeSetter({ value, onChange }: FormItemComponentProps<string>) {
const contentValue = getRawCssValue(value); const contentValue = getRawCssValue(value);
// TODO: relatedImports
return ( return (
<Popover <Popover
placement="leftBottom" placement="leftBottom"
@@ -34,9 +33,7 @@ export function CssCodeSetter({ value, onChange }: FormItemComponentProps<string
value={contentValue} value={contentValue}
onBlur={(newCode) => { onBlur={(newCode) => {
if (newCode != contentValue) { if (newCode != contentValue) {
onChange(`{css\`${newCode}\`}`, { onChange(`{css\`${newCode}\`}`, {});
// relatedImports: ['']
});
} }
}} }}
/> />
@@ -239,9 +236,10 @@ export function BgSetter({ value, onChange }: FormItemComponentProps<string>) {
<Box mt="m"> <Box mt="m">
{mode === 'color' && <ColorSetter value={value} onChange={onChange} />} {mode === 'color' && <ColorSetter value={value} onChange={onChange} />}
{mode === 'image' && ( {mode === 'image' && (
<TextSetter <Input
value={getImageUrl(value)} value={getImageUrl(value)}
onChange={(imgUrl) => { onChange={(e) => {
const imgUrl = e.target.value;
onChange(imgUrl ? `url(${imgUrl})` : undefined); onChange(imgUrl ? `url(${imgUrl})` : undefined);
}} }}
/> />
+12 -3
View File
@@ -2,11 +2,15 @@ import React from 'react';
import { Box } from 'coral-system'; import { Box } from 'coral-system';
import { SettingForm, FormModel, SettingFormProps } from '@music163/tango-setting-form'; import { SettingForm, FormModel, SettingFormProps } from '@music163/tango-setting-form';
import { Panel } from '@music163/tango-ui'; import { Panel } from '@music163/tango-ui';
import { clone } from '@music163/tango-helpers'; import { clone, Dict, parseDndId } from '@music163/tango-helpers';
import { observer, useDesigner, useWorkspace } from '@music163/tango-context'; import { observer, useDesigner, useWorkspace } from '@music163/tango-context';
import { registerBuiltinSetters } from './setters'; import { registerBuiltinSetters } from './setters';
registerBuiltinSetters(); let registered = false;
if (!registered) {
registerBuiltinSetters();
registered = true;
}
export interface SettingPanelProps extends SettingFormProps { export interface SettingPanelProps extends SettingFormProps {
title?: React.ReactNode; title?: React.ReactNode;
@@ -30,7 +34,7 @@ export const SettingPanel = observer((props: SettingPanelProps) => {
} }
const getSettingValue = (attributes: Record<string, string>) => { const getSettingValue = (attributes: Record<string, string>) => {
const ret = {}; const ret: Dict = {};
const keys = Object.keys(attributes); const keys = Object.keys(attributes);
for (const key of keys) { for (const key of keys) {
if (/^data-/.test(key)) { if (/^data-/.test(key)) {
@@ -103,6 +107,11 @@ export const SettingPanel = observer((props: SettingPanelProps) => {
prototype={prototype} prototype={prototype}
showIdentifier={{ showIdentifier={{
identifierKey: 'tid', identifierKey: 'tid',
getIdentifier: () => {
// 直接拿当前的虚拟 id 作为 tid
const { codeId } = parseDndId(workspace.selectSource.first.id);
return codeId;
},
}} }}
{...props} {...props}
/> />
@@ -1,21 +1,30 @@
import React, { useMemo, useState } from 'react'; import React, { useMemo, useState } from 'react';
import { Box, Grid, Text } from 'coral-system'; import { Box, Grid, Text } from 'coral-system';
import styled from 'styled-components'; import styled, { css } from 'styled-components';
import { import {
Dict,
IComponentPrototype, IComponentPrototype,
MenuDataType,
MenuValueType,
createContext,
logger, logger,
PartialRecord,
upperCamelCase, upperCamelCase,
} from '@music163/tango-helpers'; } from '@music163/tango-helpers';
import { CollapsePanel, IconFont, Search, Tabs } from '@music163/tango-ui'; import { CollapsePanel, IconFont, Search, Tabs } from '@music163/tango-ui';
import { observer, useWorkspace } from '@music163/tango-context'; import { observer, useWorkspace } from '@music163/tango-context';
import { QuestionCircleOutlined } from '@ant-design/icons'; import { QuestionCircleOutlined } from '@ant-design/icons';
import { Button, Empty, Spin, Popover } from 'antd'; import { Button, Empty, Spin, Popover, TabsProps } from 'antd';
import { getDragGhostElement } from '../helpers'; import { getDragGhostElement } from '../helpers';
type MenuKeyType = 'common' | 'atom' | 'snippet' | 'bizComp' | 'localComp'; type IComponentsPanelContext = {
type MenuValueType = Array<{ title: string; items: string[] }>; isScope: boolean;
export type MenuDataType = PartialRecord<MenuKeyType, MenuValueType>; onItemSelect: (name: string) => void;
layout: 'grid' | 'line';
};
const [ComponentsPanelProvider, usePanelContext] = createContext<IComponentsPanelContext>({
name: 'ComponentsPanelContext',
});
export interface ComponentsPanelProps { export interface ComponentsPanelProps {
/** /**
@@ -36,9 +45,29 @@ export interface ComponentsPanelProps {
* @returns * @returns
*/ */
getBizCompName?: (name: string) => string; getBizCompName?: (name: string) => string;
/**
* 是否局部模式 (快捷添加组件面板中使用)
*/
isScope?: boolean;
/**
* 组件选中回调
*/
onItemSelect?: (name: string) => void;
/**
* 自定义样式
*/
style?: React.CSSProperties;
/**
* tabProps
*/
tabProps?: TabsProps;
/**
* 布局模式,默认网格布局
*/
layout?: 'grid' | 'line';
} }
const localeMap = { const localeMap: Dict = {
common: '基础组件', common: '基础组件',
atom: '原子组件', atom: '原子组件',
snippet: '组合', snippet: '组合',
@@ -72,10 +101,15 @@ export function useFlatMenuData<T>(menuData: T) {
export const ComponentsPanel = observer( export const ComponentsPanel = observer(
({ ({
isScope = false,
menuData = emptyMenuData, menuData = emptyMenuData,
showBizComps = true, showBizComps = true,
getBizCompName = upperCamelCase, getBizCompName = upperCamelCase,
loading = false, loading = false,
style,
tabProps,
onItemSelect,
layout = 'grid',
}: ComponentsPanelProps) => { }: ComponentsPanelProps) => {
const [keyword, setKeyword] = useState<string>(''); const [keyword, setKeyword] = useState<string>('');
const allList = useFlatMenuData<MenuDataType>(menuData); const allList = useFlatMenuData<MenuDataType>(menuData);
@@ -91,7 +125,7 @@ export const ComponentsPanel = observer(
const tabs = Object.keys(menuData).map((key) => ({ const tabs = Object.keys(menuData).map((key) => ({
key, key,
label: localeMap[key], label: localeMap[key],
children: <MaterialList data={menuData[key]} />, children: <MaterialList data={(menuData as any)[key]} />,
})); }));
if (showBizComps) { if (showBizComps) {
@@ -109,22 +143,44 @@ export const ComponentsPanel = observer(
children: <MaterialList data={localCompData} />, children: <MaterialList data={localCompData} />,
}); });
} }
const contentNode = const contentNode =
tabs.length === 1 ? ( tabs.length === 1 ? (
tabs[0].children tabs[0].children
) : ( ) : (
<Tabs centered isTabBarSticky tabBarStickyOffset={48} items={tabs} /> <Tabs
size={isScope ? 'small' : 'middle'}
centered
isTabBarSticky
tabBarStickyOffset={48}
items={tabs}
{...tabProps}
/>
); );
return ( return (
<Box className="ComponentsView" overflowY="auto"> <ComponentsPanelProvider
<Box px="l" py="m" position="sticky" top="0" zIndex={1} bg="white"> value={{
<Search placeholder="搜索物料" onChange={setKeyword} /> isScope,
onItemSelect,
layout,
}}
>
<Box className="ComponentsView" overflowY="auto" style={style}>
<Box px="l" py="m" position="sticky" top="0" zIndex={3} bg="white">
<Search
style={{
borderRadius: '4px',
}}
placeholder="搜索物料"
onChange={setKeyword}
/>
</Box>
<Spin spinning={loading} tip="正在加载物料列表...">
{!keyword ? contentNode : <MaterialList data={allList} filterKeyword={keyword} />}
</Spin>
</Box> </Box>
<Spin spinning={loading} tip="正在加载物料列表..."> </ComponentsPanelProvider>
{!keyword ? contentNode : <MaterialList data={allList} filterKeyword={keyword} />}
</Spin>
</Box>
); );
}, },
); );
@@ -146,6 +202,9 @@ interface MaterialListProps {
function MaterialList({ data, filterKeyword, type = 'common' }: MaterialListProps) { function MaterialList({ data, filterKeyword, type = 'common' }: MaterialListProps) {
const workspace = useWorkspace(); const workspace = useWorkspace();
const { layout } = usePanelContext();
const isGrid = layout === 'grid';
return ( return (
<Box className="ComponentsViewList"> <Box className="ComponentsViewList">
{data.map((cate) => { {data.map((cate) => {
@@ -166,15 +225,21 @@ function MaterialList({ data, filterKeyword, type = 'common' }: MaterialListProp
title={cate.title} title={cate.title}
borderBottom="solid" borderBottom="solid"
borderColor="line.normal" borderColor="line.normal"
showBottomBorder={false}
bodyProps={{
padding: isGrid ? '4px 12px 12px' : '0',
}}
> >
{!items.length && ( {!items.length && (
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="没有匹配到任何组件" /> <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="没有匹配到任何组件" />
)} )}
<Grid <Grid
columns={type === 'localComp' ? 1 : 2} templateColumns={isGrid ? 'repeat(auto-fit,minmax(55px,1fr))' : '1fr'}
spacing="1px" spacing="1px"
bg="background.normal" bg="background.normal"
padding="0" padding="0"
gap={isGrid ? '12px 8px' : '0'}
backgroundColor="white"
> >
{items.map((item) => { {items.map((item) => {
const prototype = workspace.componentPrototypes.get(item); const prototype = workspace.componentPrototypes.get(item);
@@ -202,9 +267,8 @@ const StyledCommonGridItem = styled.div`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: start;
padding: 8px; cursor: ${(props) => (props.draggable ? 'grab' : 'pointer')};
cursor: move;
text-align: center; text-align: center;
color: var(--tango-colors-text-body); color: var(--tango-colors-text-body);
background-color: #fff; background-color: #fff;
@@ -213,7 +277,23 @@ const StyledCommonGridItem = styled.div`
white-space: nowrap; white-space: nowrap;
.material-icon { .material-icon {
font-size: 40px; display: flex;
align-items: center;
justify-content: center;
font-size: 36px;
background: #f9f9f9;
border-radius: 4px;
border: 1px solid #ebebeb;
width: 100%;
height: 52px;
position: relative;
transition: 0.15s ease-in-out;
transition-property: transform;
will-change: transform;
img {
width: unset;
height: 85%;
}
} }
.info { .info {
@@ -225,19 +305,13 @@ const StyledCommonGridItem = styled.div`
.anticon-question-circle { .anticon-question-circle {
display: none; display: none;
font-size: 13px;
position: absolute; position: absolute;
top: 8px; top: 4px;
right: 8px; right: 4px;
}
img {
height: 40px;
width: 40px;
} }
&:hover { &:hover {
box-shadow: 0 0 10px rgb(0 0 0 / 10%);
> span { > span {
color: var(--tango-colors-brand); color: var(--tango-colors-brand);
} }
@@ -245,11 +319,26 @@ const StyledCommonGridItem = styled.div`
.anticon-question-circle { .anticon-question-circle {
display: inline-block; display: inline-block;
} }
.material-icon {
border-color: #c7c7c7;
}
}
`;
const GridLineItemStyle = css`
cursor: pointer;
user-select: none;
&:hover {
background-color: var(--tango-colors-fill2);
} }
`; `;
function MaterialGrid({ data }: MaterialProps) { function MaterialGrid({ data }: MaterialProps) {
const workspace = useWorkspace(); const workspace = useWorkspace();
const { isScope, onItemSelect, layout } = usePanelContext();
const isLine = layout === 'line';
const handleDragStart = (e: React.DragEvent) => { const handleDragStart = (e: React.DragEvent) => {
e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.effectAllowed = 'move';
@@ -266,28 +355,92 @@ function MaterialGrid({ data }: MaterialProps) {
workspace.dragSource.clear(); workspace.dragSource.clear();
}; };
const handleSelect = () => {
onItemSelect?.(data.name);
};
const icon = data.icon || 'icon-placeholder'; const icon = data.icon || 'icon-placeholder';
const iconNode = icon.startsWith('icon-') ? (
<IconFont className="material-icon" type={data.icon || 'icon-placeholder'} />
) : (
<Box className="material-icon">
<img src={icon} alt={data.name} />
</Box>
);
if (isLine) {
return (
<Box
key={data.name}
display="flex"
columnGap="m"
px="l"
py="m"
fontSize="12px"
css={GridLineItemStyle}
onClick={handleSelect}
>
<Box
p="m"
fontSize="32px"
width="46px"
height="46px"
background="fill1"
border="1px solid"
borderRadius="4px"
borderColor="line2"
display="flex"
alignItems="center"
justifyContent="center"
>
{iconNode}
</Box>
<Box flex={1}>
<Box fontWeight="500" display="flex" justifyContent="space-between">
<Text flex={1}>{data.title}</Text>
{data.docs ? (
<Popover
zIndex={9999}
placement="right"
title={data.title}
content={<CommonMaterialInfoBox docs={data.docs} />}
>
<QuestionCircleOutlined />
</Popover>
) : null}
</Box>
<Box color="text2" fontStyle="italic">
{data.help ?? data.title}
</Box>
</Box>
</Box>
);
}
return ( return (
<StyledCommonGridItem <StyledCommonGridItem
draggable key={data.name}
draggable={!isScope}
data-name={data.name} data-name={data.name}
onDragStart={handleDragStart} onDragStart={handleDragStart}
onDragEnd={handleDragEnd} onDragEnd={handleDragEnd}
onClick={handleSelect}
> >
{icon.startsWith('icon-') ? ( {iconNode}
<IconFont className="material-icon" type={data.icon || 'icon-placeholder'} /> <Text fontSize="12px" marginTop="4px">
) : ( {data.title ?? data.name}
<img src={icon} alt={data.name} />
)}
<Text fontSize="12px" lineHeight="1.5">
{data.title}
</Text> </Text>
<Text fontSize="12px" color="gray.50"> <Text fontSize="10px" color="gray.50">
{data.name} {data.name}
</Text> </Text>
{data.docs || data.help ? ( {data.docs || data.help ? (
<Popover placement="right" title={data.title} content={<CommonMaterialInfoBox {...data} />}> <Popover
zIndex={9999}
placement="right"
title={data.title}
content={<CommonMaterialInfoBox {...data} />}
>
<QuestionCircleOutlined /> <QuestionCircleOutlined />
</Popover> </Popover>
) : null} ) : null}
@@ -295,7 +448,7 @@ function MaterialGrid({ data }: MaterialProps) {
); );
} }
function CommonMaterialInfoBox({ help, docs }: IComponentPrototype) { function CommonMaterialInfoBox({ help, docs }: Pick<IComponentPrototype, 'help' | 'docs'>) {
return ( return (
<Box maxWidth={300}> <Box maxWidth={300}>
{!!help && <Box mb="m">{help}</Box>} {!!help && <Box mb="m">{help}</Box>}
@@ -1,8 +1,6 @@
import React from 'react'; import React from 'react';
import { observer, useWorkspace, useWorkspaceData } from '@music163/tango-context'; import { observer, useWorkspace, useWorkspaceData } from '@music163/tango-context';
import { Box } from 'coral-system'; import { Box } from 'coral-system';
import { getVariableContent } from '@music163/tango-helpers';
import { isWrappedByExpressionContainer } from '@music163/tango-core';
import { VariableTree } from '../../components'; import { VariableTree } from '../../components';
import { useSandboxQuery } from '../../context'; import { useSandboxQuery } from '../../context';
@@ -10,10 +8,6 @@ import { useSandboxQuery } from '../../context';
export function shapeServiceValues(val: any) { export function shapeServiceValues(val: any) {
const shapeValues = { ...val }; const shapeValues = { ...val };
delete shapeValues.type; delete shapeValues.type;
// 兼容旧版,如果 formatter 包裹了 {} 则删掉首尾
if (shapeValues.formatter && isWrappedByExpressionContainer(shapeValues.formatter)) {
shapeValues.formatter = getVariableContent(shapeValues.formatter);
}
return shapeValues; return shapeValues;
} }
@@ -30,7 +24,7 @@ const DataSourceView = observer(() => {
<Box className="ServiceFunctionList" p="m"> <Box className="ServiceFunctionList" p="m">
<VariableTree <VariableTree
dataSource={serviceVariables} dataSource={serviceVariables}
appContext={sandbox?.window['tango']} appContext={(sandbox?.window as any)['tango']}
serviceModules={serviceModules} serviceModules={serviceModules}
onRemoveService={(variableKey) => { onRemoveService={(variableKey) => {
workspace.removeServiceFunction(variableKey); workspace.removeServiceFunction(variableKey);
@@ -4,6 +4,7 @@ import { ConfigGroup, ConfigItem } from '@music163/tango-ui';
import { Box, css } from 'coral-system'; import { Box, css } from 'coral-system';
import { Input, Switch, Form, Button, message, FormProps } from 'antd'; import { Input, Switch, Form, Button, message, FormProps } from 'antd';
import { CloseCircleOutlined, CloseOutlined } from '@ant-design/icons'; import { CloseCircleOutlined, CloseOutlined } from '@ant-design/icons';
import { Dict } from '@music163/tango-helpers';
export default observer(() => { export default observer(() => {
const workspace = useWorkspace(); const workspace = useWorkspace();
@@ -63,7 +64,7 @@ function ProxyManage({
initialValues: initialValuesProp = {}, initialValues: initialValuesProp = {},
onSave, onSave,
}: { }: {
initialValues?: object; initialValues?: Dict;
onSave?: (values: any) => void; onSave?: (values: any) => void;
}) { }) {
const initialValues = useMemo(() => { const initialValues = useMemo(() => {
@@ -84,7 +85,7 @@ function ProxyManage({
{...proxyManageFormStyle} {...proxyManageFormStyle}
initialValues={initialValues} initialValues={initialValues}
onFinish={(values) => { onFinish={(values) => {
const proxy = values.proxy.reduce((prev: object, { path, ...rest }: any) => { const proxy = values.proxy.reduce((prev: any, { path, ...rest }: any) => {
prev[path] = rest; prev[path] = rest;
return prev; return prev;
}, {}); }, {});
@@ -68,17 +68,6 @@ export const DependencyPanel = observer(
[dependencies, baseDependencies, bizDependencies, packages, packageNames], [dependencies, baseDependencies, bizDependencies, packages, packageNames],
); );
// 业务组件依赖
const bizDeps = useMemo(
() =>
bizDependencies?.map((name: string) => ({
name,
version: dependencies[name] ?? packages?.[name]?.version,
package: packages?.[name],
})) || [],
[bizDependencies, dependencies, packages],
);
// 基础包 // 基础包
const baseDeps = useMemo( const baseDeps = useMemo(
() => () =>
@@ -8,6 +8,7 @@ import { DropMethod, ITangoViewNodeData } from '@music163/tango-core';
import { noop, parseDndId } from '@music163/tango-helpers'; import { noop, parseDndId } from '@music163/tango-helpers';
import { useSandboxQuery } from '../../context'; import { useSandboxQuery } from '../../context';
import { buildQueryBySlotId } from '../../helpers'; import { buildQueryBySlotId } from '../../helpers';
import { ContextMenu } from '../../components';
export interface ComponentsTreeProps { export interface ComponentsTreeProps {
/** /**
@@ -81,8 +82,8 @@ const OutlineTreeNode: React.FC<{ node: ITangoViewNodeData } & ComponentsTreePro
const sandboxQuery = useSandboxQuery(); const sandboxQuery = useSandboxQuery();
const [visible, setVisible] = useState(true); const [visible, setVisible] = useState(true);
const nodeLabel = (() => { const nodeLabel = (() => {
const { index } = parseDndId(node.id); const { codeId } = parseDndId(node.id);
return index; return codeId;
})(); })();
const componentPrototype = workspace.componentPrototypes.get(node.component); const componentPrototype = workspace.componentPrototypes.get(node.component);
const icon = componentPrototype?.icon || 'icon-placeholder'; const icon = componentPrototype?.icon || 'icon-placeholder';
@@ -143,6 +144,18 @@ export const ComponentsTree: React.FC<ComponentsTreeProps> = observer(
const file = workspace.activeViewModule; const file = workspace.activeViewModule;
const nodesTree = (file?.nodesTree ?? []) as ITangoViewNodeData[]; const nodesTree = (file?.nodesTree ?? []) as ITangoViewNodeData[];
const [expandedKeys, setExpandedKeys] = useState(getNodeKeys(nodesTree)); const [expandedKeys, setExpandedKeys] = useState(getNodeKeys(nodesTree));
const [contextMenuOpen, setContextMenuOpen] = useState(false);
const handleSelect = (keys: React.Key[]) => {
const slotKey = keys?.[0] as string;
const data = sandboxQuery.getDraggableParentsData(buildQueryBySlotId(slotKey), true);
if (data && data.id) {
workspace.selectSource.select(data);
}
// export selected
onSelect(slotKey);
setSelectedKeys(keys);
};
useEffect(() => { useEffect(() => {
setSelectedKeys(workspace.selectSource.selected.map((item) => item.id)); setSelectedKeys(workspace.selectSource.selected.map((item) => item.id));
@@ -164,64 +177,83 @@ export const ComponentsTree: React.FC<ComponentsTreeProps> = observer(
return ( return (
<Box css={outlineStyle}> <Box css={outlineStyle}>
<Tree <Dropdown
selectedKeys={selectedKeys as string[]} placement="bottomLeft"
fieldNames={filedNames} trigger={['contextMenu']}
treeData={nodesTree as any[]} open={contextMenuOpen}
onSelect={(keys) => { onOpenChange={(val) => {
const slotKey = keys?.[0] as string; if (!val) {
const data = sandboxQuery.getDraggableParentsData(buildQueryBySlotId(slotKey), true); setContextMenuOpen(val);
if (data && data.id) {
workspace.selectSource.select(data);
} }
// export selected
onSelect(slotKey);
setSelectedKeys(keys);
}} }}
blockNode overlay={<ContextMenu onClick={() => setContextMenuOpen(false)} showParents={false} />}
expandedKeys={expandedKeys} >
onExpand={(keys) => setExpandedKeys(keys as string[])} <Tree
draggable selectedKeys={selectedKeys as string[]}
onDragStart={(data) => { fieldNames={filedNames}
const prototype = workspace.componentPrototypes.get(data.node.component); treeData={nodesTree as any[]}
if (!prototype) { onSelect={handleSelect}
return; blockNode
} expandedKeys={expandedKeys}
const { canDrag } = prototype.rules || {}; onExpand={(keys) => setExpandedKeys(keys as string[])}
if (canDrag && !canDrag()) { draggable
return; onDragStart={(data) => {
} const prototype = workspace.componentPrototypes.get(data.node.component);
workspace.dragSource.set({ if (!prototype) {
id: data.node.id, return;
name: data.node.component, }
}); const { canDrag } = prototype.rules || {};
}} if (canDrag && !canDrag()) {
onDrop={(data) => { return;
const dropKey = data.node.key as string; }
let method; workspace.dragSource.set({
if (data.dropToGap) { id: data.node.id,
// 插入节点的后面 name: data.node.component,
method = DropMethod.InsertAfter; });
} else { }}
// 作为第一个子节点 onDrop={(data) => {
method = DropMethod.InsertFirstChild; const dropKey = data.node.key as string;
} let method;
workspace.dragSource.dropTarget.set( if (data.dropToGap) {
{ // 插入节点的后面
id: dropKey, method = DropMethod.InsertAfter;
}, } else {
method, // 作为第一个子节点
); method = DropMethod.InsertFirstChild;
workspace.dropNode(); }
}} workspace.dragSource.dropTarget.set(
titleRender={(node) => ( {
<OutlineTreeNode id: dropKey,
actionItems={actionItems} },
showToggleVisibleIcon={showToggleVisibleIcon} method,
node={node} );
/> workspace.dropNode();
)} }}
/> onRightClick={({ event, node }) => {
// 按下其他按键时,视为用户有特殊操作,此时不展示右键菜单
if (event.ctrlKey || event.altKey || event.metaKey || event.shiftKey) {
setContextMenuOpen(false);
return;
}
// 顶层的 Page 组件不展示右键菜单
if (node.component === 'Page') {
setContextMenuOpen(false);
return;
}
event.preventDefault();
// 由于部分操作是基于 selectSource 实现的,因此右键时默认选中当前项
handleSelect([node.key]);
setContextMenuOpen(true);
}}
titleRender={(node) => (
<OutlineTreeNode
actionItems={actionItems}
showToggleVisibleIcon={showToggleVisibleIcon}
node={node}
/>
)}
/>
</Dropdown>
</Box> </Box>
); );
}, },
@@ -23,11 +23,7 @@ export function OutlinePanel({ showStateView = true, ...treeProps }: OutlineView
> >
<ComponentsTree {...treeProps} /> <ComponentsTree {...treeProps} />
</CollapsePanel> </CollapsePanel>
{showStateView && ( {showStateView && <StateTree />}
<CollapsePanel title="页面状态" stickyHeader maxHeight="90%" overflowY="auto">
<StateTree />
</CollapsePanel>
)}
</Box> </Box>
); );
} }
@@ -1,11 +1,12 @@
import React, { useEffect, useReducer } from 'react'; import React, { useEffect, useReducer } from 'react';
import { observer } from '@music163/tango-context'; import { observer } from '@music163/tango-context';
import { isFunction, isObject, pick } from '@music163/tango-helpers'; import { Dict, isFunction, isObject, pick } from '@music163/tango-helpers';
import { JsonView } from '@music163/tango-ui'; import { CollapsePanel, IconButton, JsonView } from '@music163/tango-ui';
import { ReloadOutlined } from '@ant-design/icons';
import { useSandboxQuery } from '../../context'; import { useSandboxQuery } from '../../context';
function processTango(obj: any = {}, index = 0) { function processTango(obj: Dict = {}, index = 0) {
const ret = {}; const ret: Dict = {};
Object.keys(obj).forEach((key) => { Object.keys(obj).forEach((key) => {
const val = obj[key]; const val = obj[key];
if (index > 2) { if (index > 2) {
@@ -24,16 +25,18 @@ function processTango(obj: any = {}, index = 0) {
export const StateTree = observer(() => { export const StateTree = observer(() => {
const sandboxQuery = useSandboxQuery(); const sandboxQuery = useSandboxQuery();
const [, forceUpdate] = useReducer((x) => x + 1, 0); const [, forceUpdate] = useReducer((x) => x + 1, 0);
const tangoContext = pick(sandboxQuery.window['tango'] || {}, [ const tangoContext = pick((sandboxQuery.window as any)['tango'] || {}, [
'stores', 'stores',
'page', 'pageStore',
'services', 'services',
'config', 'config',
'routeData',
]); ]);
const callback = () => { const callback = () => {
forceUpdate(); forceUpdate();
}; };
useEffect(() => { useEffect(() => {
// FIXME: 优化这段代码,应该监听 tangoContext 的变化,来刷新
const settingForm = document.querySelector('.SettingPanel'); const settingForm = document.querySelector('.SettingPanel');
const ob = new MutationObserver(callback); const ob = new MutationObserver(callback);
if (settingForm) { if (settingForm) {
@@ -48,13 +51,31 @@ export const StateTree = observer(() => {
}; };
}, []); }, []);
return ( return (
<JsonView <CollapsePanel
src={processTango(tangoContext)} title="页面状态"
collapsed={1} stickyHeader
name="tango" maxHeight="90%"
displayObjectSize={false} overflowY="auto"
indentWidth={2} extra={
enableCopy <IconButton
/> tooltip="同步状态"
size="small"
icon={<ReloadOutlined />}
onClick={(e) => {
e.stopPropagation();
forceUpdate();
}}
/>
}
>
<JsonView
src={processTango(tangoContext)}
collapsed={1}
name="tango"
displayObjectSize={false}
indentWidth={2}
enableCopy
/>
</CollapsePanel>
); );
}); });
@@ -19,7 +19,7 @@ const resizeHandleStyle = (axis: ResizableBoxProps['axis'], barStyle: HTMLCoralP
cursor: ${axis === 'x' ? 'col-resize' : 'row-resize'}; cursor: ${axis === 'x' ? 'col-resize' : 'row-resize'};
${barStyle && ${barStyle &&
Object.keys(barStyle) Object.keys(barStyle)
.map((key) => `${key}: ${barStyle[key]};`) .map((key) => `${key}: ${(barStyle as any)[key]};`)
.join('')} .join('')}
&:hover, &:hover,
&:active { &:active {
+29 -19
View File
@@ -3,7 +3,7 @@ import cx from 'classnames';
import { Box, Text, css, HTMLCoralProps } from 'coral-system'; import { Box, Text, css, HTMLCoralProps } from 'coral-system';
import { Badge } from 'antd'; import { Badge } from 'antd';
import { ReactComponentProps } from '@music163/tango-helpers'; import { ReactComponentProps } from '@music163/tango-helpers';
import { observer, useDesigner } from '@music163/tango-context'; import { observer, useDesigner, useWorkspace } from '@music163/tango-context';
import { ResizableBox } from './resizable-box'; import { ResizableBox } from './resizable-box';
import { getWidget } from '../widgets'; import { getWidget } from '../widgets';
@@ -19,15 +19,22 @@ const sidebarStyle = css`
list-style: none; list-style: none;
user-select: none; user-select: none;
transition: all 0.3s ease; transition: all 0.3s ease;
&.disabled {
pointer-events: none;
}
} }
.SidebarPanelBarListItem { .SidebarPanelBarListItem {
color: var(--tango-colors-text2);
&.active { &.active {
background-color: var(--tango-colors-custom-sidebarItemActiveBg); color: var(--tango-colors-brand);
background-color: var(--tango-colors-custom-sidebarItemActiveBg, --tango-colors-fill2);
} }
&:hover { &:hover {
background-color: var(--tango-colors-custom-sidebarItemHoverBg); background-color: var(--tango-colors-custom-sidebarItemHoverBg, --tango-colors-fill2);
} }
} }
`; `;
@@ -74,8 +81,9 @@ export interface SidebarPanelItemProps
widgetProps?: object; widgetProps?: object;
} }
function BaseSidebarPanel({ panelWidth: defaultPanelWidth = 280, footer, children }: SidebarProps) { function BaseSidebarPanel({ panelWidth: defaultPanelWidth = 266, footer, children }: SidebarProps) {
const designer = useDesigner(); const designer = useDesigner();
const workspace = useWorkspace();
const items = useMemo(() => { const items = useMemo(() => {
const ret: Record<string, SidebarPanelItemProps> = {}; const ret: Record<string, SidebarPanelItemProps> = {};
@@ -92,10 +100,23 @@ function BaseSidebarPanel({ panelWidth: defaultPanelWidth = 280, footer, childre
return ret; return ret;
}, [children]); }, [children]);
if (designer.activeView !== 'design') {
return null;
}
return ( return (
<Box display="flex" flexShrink={0} css={sidebarStyle} className="SidebarPanel"> <Box display="flex" flexShrink={0} css={sidebarStyle} className="SidebarPanel">
<Box className="SidebarPanelBar" display="flex" flexDirection="column" position="relative"> <Box className="SidebarPanelBar" display="flex" flexDirection="column" position="relative">
<Box as="ul" flex="0" p="0" m="0" textAlign="center" className="SidebarPanelBarList"> <Box
as="ul"
flex="0"
p="0"
m="0"
textAlign="center"
className={cx('SidebarPanelBarList', {
disabled: workspace.fileErrors.length > 0,
})}
>
{Object.keys(items).map((key) => { {Object.keys(items).map((key) => {
const item = items[key]; const item = items[key];
const isActive = key === designer.activeSidebarPanel; const isActive = key === designer.activeSidebarPanel;
@@ -112,7 +133,6 @@ function BaseSidebarPanel({ panelWidth: defaultPanelWidth = 280, footer, childre
icon={item.icon} icon={item.icon}
label={item.label} label={item.label}
showBadge={item.showBadge} showBadge={item.showBadge}
isActive={isActive}
/> />
</li> </li>
); );
@@ -183,10 +203,6 @@ BaseSidebarPanel.Item = function ({
export const Sidebar = observer(BaseSidebarPanel); export const Sidebar = observer(BaseSidebarPanel);
interface SidebarPanelBarItemProps { interface SidebarPanelBarItemProps {
/**
* 是否选中
*/
isActive?: boolean;
/** /**
* 侧边栏图标 * 侧边栏图标
*/ */
@@ -212,15 +228,9 @@ interface SidebarPanelBarItemProps {
}; };
} }
function SidebarPanelBarItem({ function SidebarPanelBarItem({ icon: iconProp, label, showBadge }: SidebarPanelBarItemProps) {
isActive,
icon: iconProp,
label,
showBadge,
}: SidebarPanelBarItemProps) {
const color = isActive ? 'brand' : 'text.body';
let icon = ( let icon = (
<Text fontSize="24px" lineHeight={1} color={color}> <Text fontSize="24px" lineHeight={1}>
{iconProp} {iconProp}
</Text> </Text>
); );
@@ -241,7 +251,7 @@ function SidebarPanelBarItem({
py="12px" py="12px"
> >
{icon} {icon}
<Text fontSize="12px" mt="s" color={color}> <Text fontSize="12px" mt="s">
{label} {label}
</Text> </Text>
</Box> </Box>

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