Compare commits

...

59 Commits

Author SHA1 Message Date
Wells
35e258b32a chore(release): publish
- @music163/tango-context@1.0.0-alpha.6
 - @music163/tango-core@1.0.0-alpha.6
 - @music163/tango-designer@1.0.0-alpha.7
 - @music163/tango-helpers@1.0.0-alpha.2
 - @music163/tango-sandbox@1.0.0-alpha.6
 - @music163/tango-setting-form@1.0.0-alpha.6
 - @music163/tango-ui@1.0.0-alpha.4
2024-01-16 15:31:02 +08:00
Wells
62d403f80a fix: 优化变量树实现 (#90)
* docs: update milestone

* fix: add new properties and methods to interfaces and models

* chore: add CI workflow for pull requests

* chore: add prebuild step to CI workflow
2024-01-16 15:28:55 +08:00
Wells
73aded3672 chore(release): publish
- @music163/tango-context@1.0.0-alpha.5
 - @music163/tango-core@1.0.0-alpha.5
 - @music163/tango-designer@1.0.0-alpha.6
 - @music163/tango-helpers@1.0.0-alpha.1
 - @music163/tango-sandbox@1.0.0-alpha.5
 - @music163/tango-setting-form@1.0.0-alpha.5
 - @music163/tango-ui@1.0.0-alpha.3
2023-12-29 17:41:58 +08:00
Wells
8c07821d93 fix: refactor VariableTree & Workspace (#83)
* fix: refactor VariableTree

* fix: update

* fix: update

* fix: update

* fix: update

* fix: refactor IVariableTreeNode

* fix: update

* fix: refactor workspace

---------

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

* feat: parse local components

* fix: update

* refactor: module exports to exportList

* fix: update workspace

---------

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

* chore: add spec package

* fix: update schema

* chore: disabled umi https2

---------

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

* fix: update toggle-button style

* fix: update tools

* fix: remove useless code

---------

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

* chore: update release scripts

* chore: up

* chore: update lerna config

* chore(release): publish

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

---------

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

* feat: add showSearch and showGroups to SettingForm

* fix: update icons

* fix: use antd form

---------

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

* fix: add filesFormatter to sandbox

* fix: update node attributes with imports

---------

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

* fix: remove appJson parse logic

* style: remove comments

---------

Co-authored-by: wwsun <ww.sww@outlook.com>
2023-11-29 14:43:06 +08:00
wwsun
5ca1118f88 chore(release): publish
- @music163/tango-designer@0.5.10
2023-11-28 14:55:20 +08:00
wwsun
14cd4c54a1 fix: update default theme 2023-11-28 14:51:10 +08:00
wwsun
7f46137c56 chore(release): publish
- @music163/tango-designer@0.5.9
 - @music163/tango-setting-form@0.2.12
 - @music163/tango-ui@0.1.12
2023-11-28 13:51:35 +08:00
wwsun
1db52948db fix: update type 2023-11-28 13:49:44 +08:00
wwsun
2550411c1c fix: update theme 2023-11-28 13:45:17 +08:00
wwsun
640859d36e chore(release): publish
- @music163/tango-context@0.2.8
 - @music163/tango-core@0.2.7
 - @music163/tango-designer@0.5.8
 - @music163/tango-sandbox@0.1.12
 - @music163/tango-setting-form@0.2.11
2023-11-27 14:02:30 +08:00
wwsun
d91763379c fix: parse tango variables in view file 2023-11-27 13:55:53 +08:00
wwsun
588861f649 fix: event setter 2023-11-27 13:46:45 +08:00
wwsun
91b53da38e fix: list service functions 2023-11-24 09:50:14 +08:00
wwsun
9b0ada94a0 chore(release): publish
- @music163/tango-designer@0.5.7
2023-11-23 18:50:57 +08:00
Wells
f604996e56 fix: update varTreeNode style (#69)
Co-authored-by: wwsun <ww.sww@outlook.com>
2023-11-23 18:46:56 +08:00
wwsun
fb26540f27 chore(release): publish
- @music163/tango-context@0.2.7
 - @music163/tango-core@0.2.6
 - @music163/tango-designer@0.5.6
 - @music163/tango-helpers@0.1.8
 - @music163/tango-sandbox@0.1.11
 - @music163/tango-setting-form@0.2.10
 - @music163/tango-ui@0.1.11
2023-11-23 15:56:48 +08:00
Wells
558f2cd0a6 fix: refactor update import specifiers (#68)
* fix: refactor update import declaration

* fix: rename module methods

* fix: update types

* fix: update

---------

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

* test: update

* fix: test if code has expression wrapper

* fix: parse code to expression node

* fix: update long text style in variableTree

---------

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

* fix: refactor workspace

* fix: refactor viewModule

* fix: update interfaces

* fix: always switch to the view matched code file

* fix: keep sidebar state

* test: update

---------

Co-authored-by: wwsun <ww.sww@outlook.com>
2023-11-16 14:31:10 +08:00
wwsun
089a482f75 fix: parse variables from view 2023-11-14 11:30:45 +08:00
wwsun
afb94d6a30 fix: fix sandbox props 2023-11-14 11:10:55 +08:00
wwsun
b9403cfec6 fix: isVariableString 2023-11-07 20:20:07 +08:00
wwsun
6cf673e7f6 fix: allow override existing file via addFile 2023-11-07 17:23:02 +08:00
wwsun
653386dcc0 fix: update types 2023-11-06 19:21:35 +08:00
BoBoooooo
208e275bb1 fix: export isJSXElementById from traverse (#54)
* feat: traverse helper add findJSXParentsById func
* fix: update findJSXParentsById def
* feat: export isJSXElementById from traverse
2023-11-06 11:31:33 +08:00
139 changed files with 8540 additions and 6104 deletions

View File

@@ -2,11 +2,12 @@
"extends": ["eslint-config-ali/typescript/react", "prettier"],
"ignorePatterns": ["**/dist/**/*", "**/lib/**/*", "**/node_modules/**/*", "scripts/**/*"],
"rules": {
"@typescript-eslint/consistent-type-definitions": "off",
"@typescript-eslint/dot-notation": "off",
"@typescript-eslint/no-unused-vars": "warn",
"import/no-cycle": "off",
"no-nested-ternary": "off",
"no-useless-return":"off",
"no-useless-return": "off",
"no-param-reassign": "off",
"prefer-destructuring": "off"
}

49
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,49 @@
name: CI
# Event设置为main分支的pull request事件
# 这里的main分支相当于master分支github项目新建是把main设置为默认分支我懒得改了所以就保持这样吧
on:
pull_request:
branches: main
jobs:
# 只需要定义一个job并命名为CI
CI:
runs-on: ubuntu-latest
steps:
# 拉取项目代码
- name: Checkout repository
uses: actions/checkout@v2
# 给当前环境下载node
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: '18.x'
# 检查缓存
# 如果key命中缓存则直接将缓存的文件还原到 path 目录,从而减少流水线运行时间
# 若 key 没命中缓存时在当前Job成功完成时将自动创建一个新缓存
- name: Cache
# 缓存命中结果会存储在steps.[id].outputs.cache-hit里该变量在继后的step中可读
id: cache-dependencies
uses: actions/cache@v3
with:
# 缓存文件目录的路径
path: |
**/node_modules
# key中定义缓存标志位的生成方式。runner.OS指当前环境的系统。外加对yarn.lock内容生成哈希码作为key值如果yarn.lock改变则代表依赖有变化。
# 这里用yarn.lock而不是package.json是因为package.json中还有version和description之类的描述项目但和依赖无关的属性
key: ${{runner.OS}}-${{hashFiles('**/yarn.lock')}}
# 安装依赖
- name: Installing Dependencies
# 如果缓存标志位没命中则执行该step。否则就跳过该step
if: steps.cache-dependencies.outputs.cache-hit != 'true'
run: yarn install
# 预构建
- name: Prebuild
run: yarn build
# 运行代码扫描
- name: Running Lint
# 通过前面章节定义的命令行执行代码扫描
run: yarn eslint
# 运行自动化测试
- name: Running Test
# 通过前面章节定义的命令行执行自动化测试
run: yarn test

2
.npmrc Normal file
View File

@@ -0,0 +1,2 @@
registry=https://registry.npmmirror.com/

View File

@@ -31,32 +31,34 @@ 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
- Developed using TypeScript, providing complete type definition files
## 🌐 Compatibility
- Modern browsersChrome >= 80, Edge >= 80, last 2 safari versions, last 2 firefox versions
## 🎯 Milestone
```mermaid
gantt
dateFormat YYYY-MM-DD
title Tango Release Milestone
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
Alpha version :active, des1, 2023-08-10, 2023-08-30
Beta version : des2, 2023-09-01, 2023-09-30
1.0 RC : des3, after des2, 40d
1.0 version : des4, after des3, 21d
1.0 Alpha : m1, 2023-10-01, 2024-01-31
1.0 RC : m2, 2024-02-01, 2024-04-30
1.0 Stable : m3, 2024-04-30, 2024-12-31
```
> [!NOTE]
> 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
- Modern browsersChrome >= 80, Edge >= 80, last 2 safari versions, last 2 firefox versions
## 💻 Development
### Recommended Development Environment
### Environment
- Node.js >= 16.0.0
- Yarn >= 1.22.0
- Node `>= 18`
- Yarn `>= 1.22 && < 2`
### Development Quick Start
@@ -96,6 +98,17 @@ Thanks to the NetEase Cloud Music Front-end team, Public Technology team, Live B
Thank you to CodeSandbox for providing the [Sandpack](https://sandpack.codesandbox.io/) project, which provides powerful online code execution capabilities for Tango.
## 📣 Product Promotion
![](https://p5.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/31629770956/da9e/3a74/4e00/7c69cf46a713f1b008bd1243b5b1ab1c.png)
Don't waste your time restoring the UI, try the NetEase Cloud Music "Seal D2C" development tool! Easily turn your design into code, support React, RN, Vue, WeChat apps and other multi-end scenarios, what you see is what you get!
Experience "Seal D2C" now:
- I'm a Figma user: <https://www.figma.com/community/plugin/1174548852019950797/seal-figma-to-code-d2c/>
- I'm a MasterGo user: <https://mastergo.com/community/plugin/98956774428196/>
## 📄 License
This project is licensed under the terms of the [MIT license](./LICENSE)

View File

@@ -31,32 +31,34 @@
- 开箱即用的前端低代码设计器,提供灵活易用的设计器 React 组件
- 使用 TypeScript 开发,提供完整的类型定义文件
## 🌐 兼容环境
- 现代浏览器Chrome >= 80, Edge >= 80, last 2 safari versions, last 2 firefox versions
## 🎯 里程碑
```mermaid
gantt
dateFormat YYYY-MM-DD
title Tango Release Milestone
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
Alpha version :active, des1, 2023-08-10, 2023-08-30
Beta version : des2, 2023-09-01, 2023-09-30
1.0 RC : des3, after des2, 40d
1.0 version : des4, after des3, 21d
1.0 Alpha : m1, 2023-10-01, 2024-01-31
1.0 RC : m2, 2024-02-01, 2024-04-30
1.0 Stable : m3, 2024-04-30, 2024-12-31
```
> [!NOTE]
> Alpha 版本会持续对一些实现协议进行优化并持续融合网易内网版本中的新增特性RC 版本将会提供基本稳定的解析协议和实现1.0 正式版本计划在 2024 年Q2发布。1.0 正式版发布后我们将会持续进行版本迭代并逐步融入内网版本中的流程设计、组件库、AIGC 等核心基础能力。
## 🌐 兼容环境
- 现代浏览器Chrome >= 80, Edge >= 80, last 2 safari versions, last 2 firefox versions
## 💻 开发
### 推荐开发环境
- Node.js >= 16.0.0
- Yarn >= 1.22.0
- Node `>= 18`
- Yarn `>= 1.22 && < 2`
### 本地开发调试方法
@@ -97,6 +99,17 @@ yarn start
感谢 CodeSandbox 提供的 [Sandpack](https://sandpack.codesandbox.io/) 项目,为 Tango 提供了强大的基于浏览器的代码构建与执行能力。
## 📣 产品推广
![](https://p5.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/31629770956/da9e/3a74/4e00/7c69cf46a713f1b008bd1243b5b1ab1c.png)
不要再浪费时间还原 UI 啦,快来试试网易云音乐「海豹 D2C」研发工具吧轻松将设计稿转为代码支持 React、RN、Vue、微信小程序等多端场景所见即所得
立即体验「海豹 D2C」
- 我是 Figma 用户:<https://www.figma.com/community/plugin/1174548852019950797/seal-figma-to-code-d2c/>
- 我是 MasterGo 用户:<https://mastergo.com/community/plugin/98956774428196/>
## 📄 开源协议
此项目遵循 [MIT 开源协议](./LICENSE)

View File

@@ -1,2 +0,0 @@
PORT=6006
HOST=local.netease.com

9
apps/playground/.gitignore vendored Normal file
View File

@@ -0,0 +1,9 @@
/node_modules
/.env.local
/.umirc.local.ts
/config/config.local.ts
/src/.umi
/src/.umi-production
/src/.umi-test
/dist
.swc

View File

@@ -1,33 +1,17 @@
import path from 'path';
import { defineConfig } from 'umi';
const resolvePackageIndex = (relativeEntry: string) =>
path.resolve(__dirname, '../../../packages/', relativeEntry);
path.resolve(__dirname, '../../packages/', relativeEntry);
export default {
export default defineConfig({
routes: [
{
exact: true,
path: '/',
component: 'index',
name: '首页',
},
{ path: '/', component: 'index' },
{ path: '/docs', component: 'docs' },
],
devServer: {
host: 'local.netease.com',
port: 8008,
https: {
key: path.resolve(__dirname, 'local.netease.com-key.pem'),
cert: path.resolve(__dirname, 'local.netease.com.pem'),
},
headers: { 'Origin-Agent-Cluster': '?0' },
},
targets: {
chrome: 79,
firefox: false,
safari: false,
edge: false,
ios: false,
},
npmClient: 'yarn',
mfsu: false,
codeSplitting: false,
alias: {
'@music163/tango-helpers': resolvePackageIndex('helpers/src/index.ts'),
'@music163/tango-core': resolvePackageIndex('core/src/index.ts'),
@@ -44,6 +28,21 @@ export default {
moment: 'moment',
antd: 'antd',
},
headScripts: [
'https://cdn.jsdelivr.net/npm/react@18.2.0/umd/react.development.js',
'https://cdn.jsdelivr.net/npm/react-dom@18.2.0/umd/react-dom.development.js',
'https://cdn.jsdelivr.net/npm/react-is@18.2.0/umd/react-is.production.min.js',
'https://cdn.jsdelivr.net/npm/moment/min/moment-with-locales.js',
'https://cdn.jsdelivr.net/npm/styled-components@5.3.11/dist/styled-components.js',
'https://cdn.jsdelivr.net/npm/antd@4.24.13/dist/antd-with-locales.min.js',
'https://cdn.jsdelivr.net/npm/prettier@2.6.0/standalone.js',
'https://cdn.jsdelivr.net/npm/prettier@2.6.0/parser-babel.js',
],
https: {
key: path.resolve(__dirname, 'local.netease.com-key.pem'),
cert: path.resolve(__dirname, 'local.netease.com.pem'),
http2: false,
},
chainWebpack: (config: any) => {
// @see https://github.com/graphql/graphql-js/issues/1272#issuecomment-393903706
config.module
@@ -61,4 +60,5 @@ export default {
config.module.rule('js').include.add(resolvePackageIndex('ui/src'));
return config;
},
};
monorepoRedirect: {},
});

View File

@@ -1,5 +0,0 @@
# `playground`
搭建平台演示示例
## 说明

View File

@@ -2,24 +2,23 @@
"name": "playground",
"version": "0.0.0",
"private": true,
"description": "playground of tango-apps",
"license": "MIT",
"author": "wwsun <ww.sun@outlook.com>",
"files": [
"lib"
],
"author": "Wells <ww.sun@outlook.com>",
"scripts": {
"build": "COMPRESS=none umi build",
"start": "umi dev"
"dev": "HOST=local.netease.com PORT=8001 umi dev",
"build": "umi build",
"postinstall": "umi setup",
"setup": "umi setup",
"start": "npm run dev"
},
"dependencies": {
"@ant-design/icons": "^4.8.0",
"@music163/tango-designer": "*",
"@music163/tango-helpers": "*",
"antd": "^4.24.2",
"coral-system": "^1.0.5"
"coral-system": "^1.0.5",
"umi": "^4.0.89"
},
"devDependencies": {
"umi": "^3.5.24"
"@types/react": "^18.0.33",
"@types/react-dom": "^18.0.11",
"typescript": "^5.0.3"
}
}

10
apps/playground/plugin.ts Normal file
View File

@@ -0,0 +1,10 @@
import type { IApi } from 'umi';
export default (api: IApi) => {
api.addMiddlewares(() => {
return (req, res, next) => {
res.setHeader('Origin-Agent-Cluster', '?0');
next();
};
});
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 KiB

View File

@@ -6,29 +6,30 @@ import { registerSetter } from '@music163/tango-designer';
import type { ComponentPrototypeType } from '@music163/tango-helpers';
import { FooSetter } from '../components';
export * from './mock-files';
export const bootHelperVariables = [
{
key: '$helpers',
title: '工具函数',
children: [
{
title: 'setStoreValue',
title: '设置变量值',
key: '() => tango.setStoreValue("variableName", "variableValue")',
type: 'function',
help: '设置状态值tango.setStoreValue("variableName", "variableValue")',
},
{
title: 'getStoreValue',
title: '获取变量值',
key: '() => tango.getStoreValue("variableName")',
type: 'function',
help: '获取状态值',
},
{ title: 'openModal', key: '() => tango.openModal("")', type: 'function' },
{ title: 'closeModal', key: '() => tango.closeModal("")', type: 'function' },
{ title: 'navigateTo', key: '() => tango.navigateTo("/")', type: 'function' },
{ title: 'showToast', key: '() => tango.showToast("hello")', type: 'function' },
{ title: 'formatDate', key: '() => tango.formatDate("2022-12-12")', type: 'function' },
{ title: 'formatNumber', key: '() => tango.formatDate(9999)', type: 'function' },
{ title: '打开弹窗', key: '() => tango.openModal("")', type: 'function' },
{ title: '关闭弹窗', key: '() => tango.closeModal("")', type: 'function' },
{ title: '切换路由', key: '() => tango.navigateTo("/")', type: 'function' },
{
title: 'copyToClipboard',
title: '拷贝到剪贴板',
key: '() => tango.copyToClipboard("hello")',
type: 'function',
},

View File

@@ -17,37 +17,43 @@ const tangoConfigJson = {
version: '17.0.2',
library: 'React',
type: 'dependency',
resources: ['https://unpkg.com/react@{{version}}/umd/react.development.js'],
resources: ['https://cdn.jsdelivr.net/npm/react@{{version}}/umd/react.development.js'],
},
'react-dom': {
version: '17.0.2',
library: 'ReactDOM',
type: 'dependency',
resources: ['https://unpkg.com/react-dom@{{version}}/umd/react-dom.development.js'],
resources: [
'https://cdn.jsdelivr.net/npm/react-dom@{{version}}/umd/react-dom.development.js',
],
},
'react-is': {
version: '16.13.1',
library: 'ReactIs',
type: 'dependency',
resources: ['https://unpkg.com/react-is@{{version}}/umd/react-is.production.min.js'],
resources: [
'https://cdn.jsdelivr.net/npm/react-is@{{version}}/umd/react-is.production.min.js',
],
},
'styled-components': {
version: '5.3.5',
library: 'styled',
type: 'dependency',
resources: ['https://unpkg.com/styled-components@{{version}}/dist/styled-components.min.js'],
resources: [
'https://cdn.jsdelivr.net/npm/styled-components@{{version}}/dist/styled-components.min.js',
],
},
moment: {
version: '2.29.4',
library: 'moment',
type: 'dependency',
resources: ['https://unpkg.com/moment@{{version}}/moment.js'],
resources: ['https://cdn.jsdelivr.net/npm/moment@{{version}}/moment.js'],
},
'@music163/tango-boot': {
version: '0.2.1',
library: 'TangoBoot',
type: 'baseDependency',
resources: ['https://unpkg.com/@music163/tango-boot@{{version}}/dist/boot.js'],
resources: ['https://cdn.jsdelivr.net/npm/@music163/tango-boot@{{version}}/dist/boot.js'],
// resources: ['http://localhost:9001/boot.js'],
description: '云音乐低代码运行时框架',
},
@@ -56,27 +62,30 @@ const tangoConfigJson = {
library: 'TangoAntd',
type: 'baseDependency',
resources: [
'https://unpkg.com/@music163/antd@{{version}}/dist/index.js',
'https://unpkg.com/antd@4.24.13/dist/antd.css',
'https://cdn.jsdelivr.net/npm/@music163/antd@{{version}}/dist/index.js',
'https://cdn.jsdelivr.net/npm/antd@4.24.13/dist/antd.css',
],
description: '云音乐低代码中后台应用基础物料',
designerResources: [
'https://unpkg.com/@music163/antd@{{version}}/dist/designer.js',
'https://cdn.jsdelivr.net/npm/@music163/antd@{{version}}/dist/designer.js',
// 'http://localhost:9002/designer.js',
'https://unpkg.com/antd@4.24.13/dist/antd.css',
'https://cdn.jsdelivr.net/npm/antd@4.24.13/dist/antd.css',
],
},
},
};
const appJson: any = {
pages: [
{
path: '/',
name: '首页',
},
],
};
const helperCode = `
export function registerComponentPrototype(proto) {
if (!proto) return;
if (!window.localTangoComponentPrototypes) {
window.localTangoComponentPrototypes = {};
}
if (proto.name) {
window.localTangoComponentPrototypes[proto.name] = proto;
}
}
`;
const routesCode = `
import Index from "./pages/list";
@@ -145,6 +154,9 @@ import {
Input,
FormilyForm,
} from "@music163/antd";
import { Space } from '@music163/antd';
import { LocalButton } from '../components';
class App extends React.Component {
render() {
return (
@@ -152,16 +164,63 @@ class App extends React.Component {
<Section title="Section Title">
</Section>
<Section>
<Space>
<LocalButton />
<Button>button</Button>
<Input />
</Space>
</Section>
</Page>
);
}
}
export default definePage(App);
`;
const componentsButtonCode = `
import React from 'react';
import { registerComponentPrototype } from '../utils';
export default function MyButton(props) {
return <button {...props}>my button</button>
}
registerComponentPrototype({
name: 'LocalButton',
title: 'Local Button',
exportType: 'namedExport',
package: '/src/components',
props: [
{ name: 'background', title: '背景色', setter: 'colorSetter' },
],
});
`;
const componentsInputCode = `
import React from 'react';
import { registerComponentPrototype } from '../utils';
export default function MyInput(props) {
return <input {...props} />;
}
registerComponentPrototype({
name: 'LocalInput',
title: 'Local Input',
exportType: 'namedExport',
package: '/src/components',
props: [
{ name: 'color', title: '文本色', setter: 'colorSetter' },
],
});
`;
const componentsEntryCode = `
export { default as LocalButton } from './button';
export { default as LocalInput } from './input';
`;
const storeApp = `
import { defineStore } from '@music163/tango-boot';
@@ -170,6 +229,35 @@ export default defineStore({
title: 'Page Title',
array: [1, 2, 3],
test: function() {
console.log('test');
console.log('test');
console.log('test');
console.log('test');
console.log('test');
console.log('test');
console.log('test');
console.log('test');
console.log('test');
console.log('test');
console.log('test');
console.log('test');
console.log('test');
console.log('test');
console.log('test');
console.log('test');
console.log('test');
console.log('test');
console.log('test');
console.log('test');
console.log('test');
console.log('test');
console.log('test');
console.log('test');
console.log('test');
console.log('test');
}
}, 'app');
`;
@@ -178,6 +266,13 @@ import { defineServices } from '@music163/tango-boot';
import './sub';
export default defineServices({
longLongLongLongLongLongLongLongGet: {
url: 'https://nei.hz.netease.com/api/apimock-v2/cc974ffbaa7a85c77f30e4ce67deb67f/api/getUserProfile',
formatter: res => res.data,
headers: {
'Content-Type': 'application/json',
}
},
get: {
url: 'https://nei.hz.netease.com/api/apimock-v2/cc974ffbaa7a85c77f30e4ce67deb67f/api/getUserProfile',
formatter: res => res.data,
@@ -233,20 +328,22 @@ p {
export const sampleFiles = [
{ filename: '/package.json', code: JSON.stringify(packageJson) },
{ filename: '/appJson.json', code: JSON.stringify(appJson) },
{ filename: '/tango.config.json', code: JSON.stringify(tangoConfigJson) },
{ filename: '/README.md', code: '# readme' },
{ filename: '/src/index.less', code: lessCode },
{ filename: '/src/style.css', code: cssCode },
{ filename: '/src/index.js', code: entryCode },
{ filename: '/src/pages/list.js', code: viewHomeCode },
{ filename: '/src/components/button.js', code: componentsButtonCode },
{ filename: '/src/components/input.js', code: componentsInputCode },
{ filename: '/src/components/index.js', code: componentsEntryCode },
{ filename: '/src/routes.js', code: routesCode },
{ filename: '/src/stores/index.js', code: storeIndexCode },
{ filename: '/src/stores/app.js', code: storeApp },
{ filename: '/src/stores/counter.js', code: storeCounter },
{ filename: '/src/services/index.js', code: serviceCode },
{ filename: '/src/services/sub.js', code: subServiceCode },
{ filename: '/src/utils/index.js', code: `export function foo() {}` },
{ filename: '/src/utils/index.js', code: helperCode },
];
export const genDefaultPage = (index: number) => ({

View File

@@ -2,4 +2,5 @@
@import '~antd/dist/antd.less'; // 引入官方提供的 less 样式入口文件
@primary-color: #2f54eb;
@border-radius-base: 2px;
@border-radius-base: 2px;

View File

@@ -0,0 +1,11 @@
import React from 'react';
import { Outlet } from 'umi';
import './index.less';
export default function Layout() {
return (
<div>
<Outlet />
</div>
);
}

View File

@@ -0,0 +1,9 @@
const DocsPage = () => {
return (
<div>
<p>This is umi docs.</p>
</div>
);
};
export default DocsPage;

View File

@@ -1,21 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Tango Playground</title>
</head>
<body>
<div id="root"></div>
<script src="https://unpkg.com/react@16.14.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16.14.0/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/react-is@16.13.1/umd/react-is.production.min.js"></script>
<script src="https://unpkg.com/moment/min/moment-with-locales.js"></script>
<script src="https://unpkg.com/styled-components@5.3.11/dist/styled-components.js"></script>
<script src="https://unpkg.com/@babel/standalone@7.15.6/babel.js"></script>
<script src="https://unpkg.com/prettier@2.6.0/standalone.js"></script>
<script src="https://unpkg.com/prettier@2.6.0/parser-babel.js"></script>
<script src="https://unpkg.com/antd@4.24.13/dist/antd-with-locales.min.js"></script>
</body>
</html>

View File

@@ -12,11 +12,10 @@ import {
CodeEditor,
Sandbox,
DndQuery,
themeLight,
} from '@music163/tango-designer';
import { createEngine, Workspace } from '@music163/tango-core';
import { Logo, ProjectDetail, bootHelperVariables } from './share';
import { sampleFiles } from '../mock/project';
import './index.less';
import { Logo, ProjectDetail, bootHelperVariables, sampleFiles } from '../helpers';
import {
ApiOutlined,
AppstoreAddOutlined,
@@ -29,15 +28,11 @@ import {
const workspace = new Workspace({
entry: '/src/index.js',
files: sampleFiles,
onFilesChange: (filenames) => {
console.log('onFilesChange', filenames, workspace.files);
},
});
// 2. 引擎初始化
const engine = createEngine({
workspace,
defaultActiveSidebarPanel: 'outline',
});
// @ts-ignore
@@ -55,6 +50,7 @@ export default function App() {
const [menuData, setMenuData] = useState(false);
return (
<Designer
theme={themeLight}
engine={engine}
config={{
customActionVariables: bootHelperVariables,
@@ -69,6 +65,7 @@ export default function App() {
<Box px="l">
<Toolbar>
<Toolbar.Item key="routeSwitch" placement="left" />
<Toolbar.Item key="history" placement="left" />
<Toolbar.Item key="preview" placement="left" />
<Toolbar.Item key="modeSwitch" placement="right" />
<Toolbar.Item key="togglePanel" placement="right" />
@@ -83,7 +80,6 @@ export default function App() {
}
>
<Sidebar>
<Sidebar.Item key="outline" label="结构" icon={<BuildOutlined />} />
<Sidebar.Item
key="components"
label="组件"
@@ -93,6 +89,7 @@ export default function App() {
loading: menuLoading,
}}
/>
<Sidebar.Item key="outline" label="结构" icon={<BuildOutlined />} />
<Sidebar.Item
key="variables"
label="变量"
@@ -123,6 +120,9 @@ export default function App() {
workspace.setComponentPrototypes(sandboxWindow.TangoAntd.prototypes);
}
}
if (sandboxWindow.localTangoComponentPrototypes) {
workspace.setComponentPrototypes(sandboxWindow.localTangoComponentPrototypes);
}
setMenuLoading(false);
}
}}

View File

@@ -1,3 +1,3 @@
{
"extends": "../../tsconfig.json"
"extends": "./src/.umi/tsconfig.json"
}

1
apps/playground/typings.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
import 'umi/typings';

View File

@@ -16,8 +16,8 @@
"dependencies": {
"@music163/tango-setting-form": "*",
"@music163/tango-ui": "*",
"mobx": "6.9.0",
"mobx-react-lite": "4.0.2"
"mobx": "6.12.0",
"mobx-react-lite": "4.0.5"
},
"devDependencies": {
"@storybook/addon-actions": "^6.5.14",

View File

@@ -0,0 +1,70 @@
import React from 'react';
import { Box } from 'coral-system';
import { CodeSandbox } from '@music163/tango-sandbox';
import { JsonView } from '@music163/tango-ui';
export default {
title: 'CodeSandbox',
};
const entryFilename = '/index.js';
const packageJson = {
name: 'demo',
private: true,
main: entryFilename,
dependencies: {
react: '^17.0.1',
'react-dom': '^17.0.1',
},
};
const entryFile = `
import React from 'react';
import ReactDOM from "react-dom";
import App from './src';
const rootElement = document.getElementById("root");
ReactDOM.render(
<App />,
rootElement
);
`;
const srcAppFile = `
import React from 'react';
export default function App() {
return <div style={{ background: 'yellow', color: 'red' }}>hello world</div>;
}
`;
const files = {
'/package.json': {
code: JSON.stringify(packageJson),
},
[entryFilename]: {
code: entryFile,
},
'/src/index.js': { code: srcAppFile },
};
export function Basic() {
return (
<Box display="flex" height="100vh">
<CodeSandbox
template="create-react-app"
bundlerURL="https://codesandbox.fn.netease.com"
files={files}
entry={entryFilename}
style={{
flex: 1,
}}
/>
<Box width="30%">
<JsonView src={files} />
</Box>
</Box>
);
}

View File

@@ -1,7 +1,6 @@
import React, { useState } from 'react';
import { EditableVariableTree, FormModel, SettingForm } from '@music163/tango-setting-form';
import { ComponentPrototypeType, getValue } from '@music163/tango-helpers';
import { createServices } from '@music163/request';
import React from 'react';
import { FormModel, SettingForm } from '@music163/tango-setting-form';
import { ComponentPrototypeType } from '@music163/tango-helpers';
import { Box } from 'coral-system';
import { JsonView } from '@music163/tango-ui';
import { toJS } from 'mobx';
@@ -9,51 +8,9 @@ import { observer } from 'mobx-react-lite';
import { Card } from 'antd';
export default {
title: 'SettingForm/ Setters',
title: 'SettingForm',
};
const modelVariables = [
{
title: 'stores',
key: 'stores',
selectable: false,
children: [
{
title: 'app',
key: 'stores.app',
selectable: false,
children: [
{ title: 'app.title', key: 'stores.app.title', raw: '"hello"' },
{ title: 'app.age', key: 'stores.app.age', raw: '20' },
{ title: 'app.detail', key: 'stores.app.detail', raw: '{ foo: "foo" }' },
{ title: 'app.list', key: 'stores.app.list', type: 'function', raw: '() => {}' },
],
},
{
title: 'user',
key: 'stores.user',
selectable: false,
children: [
{ title: 'name', key: 'stores.user.name' },
{ title: 'age', key: 'stores.user.age' },
],
},
],
},
{
title: 'services',
key: 'services',
selectable: false,
children: [
{
title: 'listUsers',
key: 'services.listUsers',
type: 'function',
},
],
},
];
const prototype: ComponentPrototypeType = {
name: 'Test',
exportType: 'namedExport',
@@ -257,47 +214,6 @@ const prototype: ComponentPrototypeType = {
],
};
const deerService = createServices(
{
listMy: {
url: '/my/upload/list',
},
listFav: {
url: '/my/star/list',
},
listPub: {
url: '/list',
},
},
{
baseURL: 'https://febase-openapi.fn.netease.com/deer/api/deer/pic',
withCredentials: false, // 解决跨域时必须非*问题
headers: {
'Febase-Auth': 'dskPVkIRnQ2dEn1DbyxURmUj4rl4BsCh53xFAsJnvVs=',
},
},
);
const context = {
stores: {
app: {
title: 'Sample App',
age: 20,
detail: {
foo: 'foo',
bar: 'bar',
biz: {
x: 'xxx',
},
},
newKey: 'xxx',
},
foo: {
test: 'test string',
},
},
};
/**
* 表单值预览
*/
@@ -310,9 +226,7 @@ export function Basic() {
const model = new FormModel(
{
router: 'www.163.com',
expression: {
foo: 'foo',
},
expression: `{ foo: 'foo' }`,
object: {
name: 'Alice',
},
@@ -324,19 +238,7 @@ export function Basic() {
return (
<Box display="flex">
<SettingForm
model={model}
remoteServices={{
ImageService: deerService as any,
}}
prototype={prototype}
evaluateContext={{
tango: context,
__UNSAFE_TANGO_CURRENT_PAGE_RENDER_RUNTIME__: { routeData: { params: {}, query: {} } },
}}
modelVariables={modelVariables}
expressionVariables={modelVariables}
/>
<SettingForm model={model} prototype={prototype} />
<Box position="relative">
<Card title="表单状态预览" style={{ position: 'sticky', top: 0 }}>
<FormValuePreview model={model} />
@@ -346,6 +248,14 @@ export function Basic() {
);
}
export function Lite() {
return (
<Box width={320} border="solid">
<SettingForm showSearch={false} showGroups={false} prototype={prototype} />
</Box>
);
}
export function NoExpressionSwitch() {
const model = new FormModel({});
return (
@@ -354,30 +264,3 @@ export function NoExpressionSwitch() {
</Box>
);
}
export function Binding() {
const [data, setData] = useState<any>({});
return (
<Box>
<Box as="code" bg="highlight" color="white" fontSize="24px">
selected: {data?.key}
</Box>
<EditableVariableTree
dataSource={modelVariables as any}
getPreviewValue={(node) => {
if (!node || !node.key) {
return;
}
if (node.type === 'function') {
return node.raw;
}
const keyPath = node.key.replaceAll('?', '');
return getValue(context, keyPath);
}}
onSelect={setData}
onAddVariable={console.log}
onSave={console.log}
/>
</Box>
);
}

View File

@@ -28,6 +28,41 @@ export function Basic() {
);
}
const code = `
function foo() {
console.log("test");
console.log("test");
console.log("test");
console.log("test");
console.log("test");
console.log("test");
console.log("test");
console.log("test");
console.log("test");
console.log("test");
console.log("test");
console.log("test");
console.log("test");
console.log("test");
console.log("test");
console.log("test");
console.log("test");
console.log("test");
console.log("test");
console.log("test");
console.log("test");
console.log("test");
console.log("test");
console.log("test");
console.log("test");
console.log("test");
}
`;
export function Readonly() {
return <InputCode shape="inset" readOnly value={code} />;
}
export function Inset() {
return <InputCode shape="inset" />;
}

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { ToggleButton, IconFont } from '@music163/tango-ui';
import { Box, Flex } from 'coral-system';
import { ToggleButton, UndoOutlined, RedoOutlined } from '@music163/tango-ui';
import { Box, Group, Flex } from 'coral-system';
export default {
title: 'UI/ToggleButton',
@@ -9,28 +9,73 @@ export default {
export function Basic() {
return (
<Box>
<Flex gap="l" bg="#efefef" p="l">
<Box display="flex" columnGap="l" p="l">
<ToggleButton>
<IconFont type="icon-undo" />
<UndoOutlined />
</ToggleButton>
<ToggleButton selected>
<IconFont type="icon-redo" />
<RedoOutlined />
</ToggleButton>
<ToggleButton disabled>
<IconFont type="icon-redo" />
<RedoOutlined />
</ToggleButton>
</Box>
<Box display="flex" columnGap="l" p="l">
<ToggleButton type="primary">
<UndoOutlined />
</ToggleButton>
<ToggleButton type="primary" selected>
<RedoOutlined />
</ToggleButton>
<ToggleButton type="primary" disabled>
<RedoOutlined />
</ToggleButton>
</Box>
</Box>
);
}
export function DarkMode() {
return (
<Box>
<Flex gap="l" bg="#222" p="l">
<ToggleButton shape="ghost">
<UndoOutlined />
</ToggleButton>
<ToggleButton shape="ghost" selected>
<RedoOutlined />
</ToggleButton>
<ToggleButton shape="ghost" disabled>
<UndoOutlined />
</ToggleButton>
</Flex>
<Flex gap="l" bg="#222" p="l">
<ToggleButton shape="ghost">
<IconFont type="icon-undo" />
<ToggleButton shape="text">
<UndoOutlined />
</ToggleButton>
<ToggleButton shape="ghost" selected>
<IconFont type="icon-redo" />
<ToggleButton shape="text" selected>
<RedoOutlined />
</ToggleButton>
<ToggleButton shape="ghost" disabled>
<IconFont type="icon-undo" />
<ToggleButton shape="text" disabled>
<RedoOutlined />
</ToggleButton>
</Flex>
</Box>
);
}
export function ButtonGroup() {
return (
<Group attached>
<ToggleButton>
<UndoOutlined />
</ToggleButton>
<ToggleButton selected>
<RedoOutlined />
</ToggleButton>
<ToggleButton disabled>
<RedoOutlined />
</ToggleButton>
</Group>
);
}

View File

@@ -0,0 +1,72 @@
import React from 'react';
import { VariableTree } from '@music163/tango-designer';
import { Box } from 'coral-system';
export default {
title: 'Designer/VariableTree',
};
const data = [
{
title: 'Stores',
key: 'stores',
selectable: false,
children: [
{
title: 'user',
key: 'stores.user',
children: [
{
title: 'name',
key: 'stores.user.name',
raw: '"Tom"',
},
{
title: 'age',
key: 'stores.user.age',
raw: '18',
},
],
},
{
title: 'book',
key: 'stores.book',
children: [
{
title: 'name',
key: 'stores.book.name',
raw: '"JavaScript 高级程序设计"',
},
{
title: 'price',
key: 'stores.book.price',
raw: '99.99',
},
],
},
],
},
{
title: 'Services',
key: 'services',
selectable: false,
children: [
{
title: 'add',
key: 'services.add',
},
{
title: 'multiply',
key: 'services.multiply',
},
],
},
];
export function Basic() {
return (
<Box width={600} border="solid">
<VariableTree dataSource={data} />
</Box>
);
}

View File

@@ -62,13 +62,13 @@ const config = {
({
// Replace with your project's social card
image: 'img/social-card.png',
announcementBar: {
id: 'notion_alert',
content: '🏗 当前版本为测试版请暂时不要用于生产环境正式版将于2023年Q4发布',
backgroundColor: 'var(--ifm-color-primary-contrast-background)',
textColor: 'var(--ifm-color-primary-contrast-foreground)',
isCloseable: false,
},
// announcementBar: {
// id: 'notion_alert',
// content: '🏗 当前版本为测试版请暂时不要用于生产环境正式版将于2023年Q4发布',
// backgroundColor: 'var(--ifm-color-primary-contrast-background)',
// textColor: 'var(--ifm-color-primary-contrast-foreground)',
// isCloseable: false,
// },
navbar: {
title: '',
logo: {
@@ -137,7 +137,7 @@ const config = {
],
},
{
title: '更多',
title: '相关资源',
items: [
{
label: '博客',
@@ -149,6 +149,19 @@ const config = {
},
],
},
{
title: '更多产品',
items: [
{
label: '海豹 D2C - Figma 插件',
to: 'https://www.figma.com/community/plugin/1174548852019950797/seal-figma-to-code-d2c',
},
{
label: '海豹 D2C - MasterGo 插件',
to: 'https://mastergo.com/community/plugin/98956774428196/',
},
],
},
],
logo: {
alt: 'NetEase Cloud Music',

View File

@@ -24,7 +24,7 @@ const timelines = [
{
icon: null,
title: '1.0 RC',
date: '2023.10.30',
date: '2023.11.30',
description: translate({
id: 'homepage.timeline.rc',
message: '核心 API 基本稳定,不再发生 BR发布 1.0 RC 版本。',
@@ -33,7 +33,7 @@ const timelines = [
{
icon: null,
title: '1.0',
date: 'Before 2023.12.25',
date: 'Before 2023.12.29',
description: translate({
id: 'homepage.timeline.stable',
message: 'API 完全稳定,提供良好的社区支持,可用于生产环境。',

View File

@@ -154,7 +154,7 @@ const config = {
// testLocationInResults: false,
// The glob patterns Jest uses to detect test files
testMatch: ['**/__tests__/**/*.[jt]s?(x)', '**/?(*.)+(spec|test).[tj]s?(x)'],
testMatch: ['**/tests/**/*.[jt]s?(x)', '**/?(*.)+(spec|test).[tj]s?(x)'],
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
// testPathIgnorePatterns: [

View File

@@ -1,6 +1,5 @@
{
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
"useWorkspaces": true,
"version": "independent",
"packages": ["packages/*"],
"npmClient": "yarn",

View File

@@ -6,8 +6,8 @@
"apps/*"
],
"scripts": {
"start": "yarn workspace playground start",
"start:story": "yarn workspace storybook storybook",
"start": "yarn workspace playground dev",
"start:docs": "yarn workspace storybook storybook",
"start:site": "yarn workspace website start",
"deploy:site": "yarn workspace website deploy",
"build": "lerna run build",
@@ -18,18 +18,19 @@
"test:watch": "jest --watch",
"eslint": "eslint packages/**/src/*.{ts,tsx} --cache",
"publish": "lerna publish",
"ver": "lerna version --no-private --conventional-commits",
"release": "yarn eslint && yarn build && yarn run ver && lerna publish from-git",
"ver": "lerna version --no-private",
"release": "yarn eslint && yarn build && lerna version --no-private --conventional-commits && lerna publish from-git",
"release:npm": "yarn eslint && yarn build && yarn run ver && lerna publish from-git",
"release:beta": "yarn eslint && yarn build && yarn run ver && lerna publish from-package --dist-tag beta",
"up": "yarn upgrade-interactive --latest",
"prepare": "husky install"
},
"devDependencies": {
"@babel/preset-env": "^7.23.2",
"@babel/preset-react": "^7.22.15",
"@babel/preset-typescript": "^7.23.2",
"@commitlint/cli": "^17.5.0",
"@commitlint/config-conventional": "^17.3.0",
"@babel/preset-env": "^7.23.6",
"@babel/preset-react": "^7.23.3",
"@babel/preset-typescript": "^7.23.3",
"@commitlint/cli": "^18.4.3",
"@commitlint/config-conventional": "^18.4.3",
"@types/jest": "^29.5.7",
"@types/lodash-es": "^4.17.10",
"@types/lodash.get": "^4.4.8",
@@ -38,13 +39,13 @@
"@types/react": "^18.2.34",
"@types/react-dom": "^18.2.14",
"@types/styled-components": "^5.1.29",
"@typescript-eslint/eslint-plugin": "^6.9.1",
"@typescript-eslint/parser": "^6.9.1",
"conventional-changelog-cli": "^2.2.2",
"@typescript-eslint/eslint-plugin": "^6.13.2",
"@typescript-eslint/parser": "^6.13.2",
"conventional-changelog-cli": "^4.1.0",
"copyfiles": "^2.4.1",
"eslint": "^8.52.0",
"eslint": "^8.55.0",
"eslint-config-ali": "^14.0.2",
"eslint-config-prettier": "^9.0.0",
"eslint-config-prettier": "^9.1.0",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-import": "^2.29.0",
"eslint-plugin-react": "^7.33.2",
@@ -53,14 +54,17 @@
"husky": "^8.0.3",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"lerna": "^6.6.1",
"lint-staged": "^14.0.1",
"prettier": "^2.8.7",
"lerna": "^8.0.0",
"lint-staged": "^15.2.0",
"prettier": "^3.1.0",
"react": "^17.0.0",
"react-dom": "^17.0.0",
"styled-components": "^5.3.6",
"typedoc": "^0.25.3",
"typescript": "^5.2.2"
"typedoc": "^0.25.4",
"typescript": "^5.3.3"
},
"engines": {
"node": ">=18"
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": [

View File

@@ -3,6 +3,60 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.0.0-alpha.6](https://github.com/netease/tango/compare/@music163/tango-context@1.0.0-alpha.5...@music163/tango-context@1.0.0-alpha.6) (2024-01-16)
**Note:** Version bump only for package @music163/tango-context
# [1.0.0-alpha.5](https://github.com/netease/tango/compare/@music163/tango-context@1.0.0-alpha.4...@music163/tango-context@1.0.0-alpha.5) (2023-12-29)
### Bug Fixes
- refactor VariableTree & Workspace ([#83](https://github.com/netease/tango/issues/83)) ([8c07821](https://github.com/netease/tango/commit/8c07821d93cea4dfc43f81ca948b845176821184))
# [1.0.0-alpha.4](https://github.com/netease/tango/compare/@music163/tango-context@1.0.0-alpha.3...@music163/tango-context@1.0.0-alpha.4) (2023-12-26)
**Note:** Version bump only for package @music163/tango-context
# [1.0.0-alpha.3](https://github.com/netease/tango/compare/@music163/tango-context@1.0.0-alpha.2...@music163/tango-context@1.0.0-alpha.3) (2023-12-25)
**Note:** Version bump only for package @music163/tango-context
# [1.0.0-alpha.1](https://github.com/netease/tango/compare/@music163/tango-context@0.2.12...@music163/tango-context@1.0.0-alpha.1) (2023-12-12)
**Note:** Version bump only for package @music163/tango-context
## [0.2.12](https://github.com/netease/tango/compare/@music163/tango-context@0.2.11...@music163/tango-context@0.2.12) (2023-12-04)
**Note:** Version bump only for package @music163/tango-context
## [0.2.11](https://github.com/netease/tango/compare/@music163/tango-context@0.2.10...@music163/tango-context@0.2.11) (2023-12-04)
**Note:** Version bump only for package @music163/tango-context
## [0.2.10](https://github.com/netease/tango/compare/@music163/tango-context@0.2.9...@music163/tango-context@0.2.10) (2023-11-30)
**Note:** Version bump only for package @music163/tango-context
## [0.2.9](https://github.com/netease/tango/compare/@music163/tango-context@0.2.8...@music163/tango-context@0.2.9) (2023-11-29)
**Note:** Version bump only for package @music163/tango-context
## [0.2.8](https://github.com/netease/tango/compare/@music163/tango-context@0.2.7...@music163/tango-context@0.2.8) (2023-11-27)
**Note:** Version bump only for package @music163/tango-context
## [0.2.7](https://github.com/netease/tango/compare/@music163/tango-context@0.2.6...@music163/tango-context@0.2.7) (2023-11-23)
**Note:** Version bump only for package @music163/tango-context
## [0.2.6](https://github.com/netease/tango/compare/@music163/tango-context@0.2.5...@music163/tango-context@0.2.6) (2023-11-20)
**Note:** Version bump only for package @music163/tango-context
## [0.2.5](https://github.com/netease/tango/compare/@music163/tango-context@0.2.4...@music163/tango-context@0.2.5) (2023-11-16)
**Note:** Version bump only for package @music163/tango-context
## [0.2.4](https://github.com/netease/tango/compare/@music163/tango-context@0.2.3...@music163/tango-context@0.2.4) (2023-11-02)
### Bug Fixes

View File

@@ -1,6 +1,6 @@
{
"name": "@music163/tango-context",
"version": "0.2.4",
"version": "1.0.0-alpha.6",
"description": "react context for tango-apps",
"keywords": [
"react",
@@ -31,9 +31,9 @@
"react": ">= 16.8"
},
"dependencies": {
"@music163/tango-core": "^0.2.3",
"@music163/tango-helpers": "^0.1.5",
"mobx-react-lite": "4.0.2"
"@music163/tango-core": "^1.0.0-alpha.6",
"@music163/tango-helpers": "^1.0.0-alpha.2",
"mobx-react-lite": "4.0.5"
},
"publishConfig": {
"access": "public",

View File

@@ -1,12 +1,5 @@
import type { Engine } from '@music163/tango-core';
import { createContext } from '@music163/tango-helpers';
interface CustomVariableData {
key: string;
title: string;
children?: CustomVariableData[];
[key: string]: any;
}
import { IVariableTreeNode, createContext } from '@music163/tango-helpers';
export interface ITangoEngineContext {
/**
@@ -17,8 +10,8 @@ export interface ITangoEngineContext {
* 自定义配置数据
*/
config?: {
customActionVariables?: CustomVariableData[];
customExpressionVariables?: CustomVariableData[];
customActionVariables?: IVariableTreeNode[];
customExpressionVariables?: IVariableTreeNode[];
};
}
@@ -39,23 +32,25 @@ export const useDesigner = () => {
export const useWorkspaceData = () => {
const ctx = useTangoEngine();
const workspace = useWorkspace();
const modelVariables: any[] = []; // 绑定变量列表
const storeActionVariables: any[] = []; // 模型中的所有 actions
const storeVariables: any[] = []; // 模型中的所有变量
const serviceVariables: any[] = []; // 服务中的所有变量
const modelVariables: IVariableTreeNode[] = []; // 绑定变量列表
const storeActionVariables: IVariableTreeNode[] = []; // 模型中的所有 actions
const storeVariables: IVariableTreeNode[] = []; // 模型中的所有变量
const serviceVariables: IVariableTreeNode[] = []; // 服务中的所有变量
Object.values(workspace.storeModules).forEach((file) => {
const prefix = `stores.${file.name}`;
const states = file.states.map((item) => ({
const states: IVariableTreeNode[] = file.states.map((item) => ({
title: item.name,
key: `${prefix}.${item.name}`,
raw: item.code,
showRemoveButton: true,
}));
const actions = file.actions.map((item) => ({
const actions: IVariableTreeNode[] = file.actions.map((item) => ({
title: item.name,
key: `${prefix}.${item.name}`,
type: 'function',
raw: item.code,
showRemoveButton: true,
}));
modelVariables.push({
@@ -63,6 +58,7 @@ export const useWorkspaceData = () => {
key: prefix,
selectable: false,
children: states,
showAddButton: true,
});
storeActionVariables.push({
@@ -77,8 +73,7 @@ export const useWorkspaceData = () => {
key: prefix,
selectable: false,
children: [...states, ...actions],
showAddChildIcon: true,
showRemoveIcon: true,
showAddButton: true,
});
});
@@ -88,10 +83,12 @@ export const useWorkspaceData = () => {
title: file.name,
key: prefix,
selectable: false,
showAddButton: true,
children: Object.keys(file.serviceFunctions || {}).map((key) => ({
title: key,
key: [prefix, key].join('.'),
type: 'function',
showRemoveButton: true,
})),
});
});
@@ -102,7 +99,7 @@ export const useWorkspaceData = () => {
value: item.path,
}));
let actionVariables: CustomVariableData[] = [
let actionVariables: IVariableTreeNode[] = [
buildVariableOptions('数据模型', '$stores', storeActionVariables),
buildVariableOptions('服务函数', '$services', serviceVariables),
];
@@ -111,7 +108,7 @@ export const useWorkspaceData = () => {
actionVariables = actionVariables.concat(ctx.config?.customActionVariables);
}
let expressionVariables: CustomVariableData[] = [
let expressionVariables: IVariableTreeNode[] = [
buildVariableOptions('数据模型', '$stores', storeVariables),
buildVariableOptions('服务函数', '$services', serviceVariables),
];
@@ -129,7 +126,7 @@ export const useWorkspaceData = () => {
};
};
function buildVariableOptions(title: string, key: string, children: any[]) {
function buildVariableOptions(title: string, key: string, children: IVariableTreeNode[]) {
return {
key,
title,

View File

@@ -3,6 +3,88 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.0.0-alpha.6](https://github.com/netease/tango/compare/@music163/tango-core@1.0.0-alpha.5...@music163/tango-core@1.0.0-alpha.6) (2024-01-16)
### Bug Fixes
- 优化变量树实现 ([#90](https://github.com/netease/tango/issues/90)) ([62d403f](https://github.com/netease/tango/commit/62d403f80a5ad08c216bc0c035ffc12c2cf329d2))
# [1.0.0-alpha.5](https://github.com/netease/tango/compare/@music163/tango-core@1.0.0-alpha.4...@music163/tango-core@1.0.0-alpha.5) (2023-12-29)
### Bug Fixes
- check if store value is valid ([765ea07](https://github.com/netease/tango/commit/765ea07cd5be6528e3b326e4ecb163647774d9e5))
- refactor VariableTree & Workspace ([#83](https://github.com/netease/tango/issues/83)) ([8c07821](https://github.com/netease/tango/commit/8c07821d93cea4dfc43f81ca948b845176821184))
# [1.0.0-alpha.4](https://github.com/netease/tango/compare/@music163/tango-core@1.0.0-alpha.3...@music163/tango-core@1.0.0-alpha.4) (2023-12-26)
### Bug Fixes
- list overflow in expSetter ([4d6b67e](https://github.com/netease/tango/commit/4d6b67eecd02b31f01d1bc896c6eeb83f6b34b35))
# [1.0.0-alpha.3](https://github.com/netease/tango/compare/@music163/tango-core@1.0.0-alpha.2...@music163/tango-core@1.0.0-alpha.3) (2023-12-25)
### Bug Fixes
- export componentEntry module ([149002d](https://github.com/netease/tango/commit/149002ddddaee859333469f65c054d811bde2f7f))
# [1.0.0-alpha.1](https://github.com/netease/tango/compare/@music163/tango-core@0.2.11...@music163/tango-core@1.0.0-alpha.1) (2023-12-12)
**Note:** Version bump only for package @music163/tango-core
## [0.2.11](https://github.com/netease/tango/compare/@music163/tango-core@0.2.10...@music163/tango-core@0.2.11) (2023-12-04)
### Bug Fixes
- update FileType ([c884712](https://github.com/netease/tango/commit/c8847123312bc47ea06575f4e714dfd596893e39))
## [0.2.10](https://github.com/netease/tango/compare/@music163/tango-core@0.2.9...@music163/tango-core@0.2.10) (2023-12-04)
### Bug Fixes
- add filesFormatter to sandbox & fix update props with imports ([#71](https://github.com/netease/tango/issues/71)) ([138cbe9](https://github.com/netease/tango/commit/138cbe9b203b370aff42c1ae8086d69edacf35e9))
## [0.2.9](https://github.com/netease/tango/compare/@music163/tango-core@0.2.8...@music163/tango-core@0.2.9) (2023-11-30)
### Bug Fixes
- fix render components tree ([76a5a2c](https://github.com/netease/tango/commit/76a5a2c65920bc42b019cd1f32a3cacd0d888638))
## [0.2.8](https://github.com/netease/tango/compare/@music163/tango-core@0.2.7...@music163/tango-core@0.2.8) (2023-11-29)
### Bug Fixes
- workspace onFilesChange ([#70](https://github.com/netease/tango/issues/70)) ([accd826](https://github.com/netease/tango/commit/accd8263764c811ea8175a9cd341fc6fa6c75967))
## [0.2.7](https://github.com/netease/tango/compare/@music163/tango-core@0.2.6...@music163/tango-core@0.2.7) (2023-11-27)
### Bug Fixes
- list service functions ([91b53da](https://github.com/netease/tango/commit/91b53da38e362ae320457429037dc347adf90bd3))
- parse tango variables in view file ([d917633](https://github.com/netease/tango/commit/d91763379c4ccf9d826b717aa80a07abdaf2e3a2))
## [0.2.6](https://github.com/netease/tango/compare/@music163/tango-core@0.2.5...@music163/tango-core@0.2.6) (2023-11-23)
### Bug Fixes
- prototype to import declaration ([#67](https://github.com/netease/tango/issues/67)) ([8a1234e](https://github.com/netease/tango/commit/8a1234e565489c92f041356b7a06339eaeee48df))
- refactor update import specifiers ([#68](https://github.com/netease/tango/issues/68)) ([558f2cd](https://github.com/netease/tango/commit/558f2cd0a692c6bbc866d08250d25e2619af183f))
## [0.2.5](https://github.com/netease/tango/compare/@music163/tango-core@0.2.4...@music163/tango-core@0.2.5) (2023-11-20)
### Bug Fixes
- refactor parse expression ([#61](https://github.com/netease/tango/issues/61)) ([dbbd1dd](https://github.com/netease/tango/commit/dbbd1dddc75c532b7c9710ab0941c8680100f093))
## [0.2.4](https://github.com/netease/tango/compare/@music163/tango-core@0.2.3...@music163/tango-core@0.2.4) (2023-11-16)
### Bug Fixes
- allow override existing file via addFile ([6cf673e](https://github.com/netease/tango/commit/6cf673e7f6f330613e1d6331256a59900e8cbc68))
- export isJSXElementById from traverse ([#54](https://github.com/netease/tango/issues/54)) ([208e275](https://github.com/netease/tango/commit/208e275bb1cd98ed21d816c26179f9ebc11df223))
- parse variables from view ([089a482](https://github.com/netease/tango/commit/089a482f750e9d9a7743a6641d6e39989347d318))
- update types ([653386d](https://github.com/netease/tango/commit/653386dcc0b064b41915548129952eeabe53019f))
## [0.2.3](https://github.com/netease/tango/compare/@music163/tango-core@0.2.2...@music163/tango-core@0.2.3) (2023-11-02)
### Bug Fixes

View File

@@ -1,6 +1,6 @@
{
"name": "@music163/tango-core",
"version": "0.2.3",
"version": "1.0.0-alpha.6",
"description": "tango core",
"author": "wwsun <ww.sun@outlook.com>",
"homepage": "",
@@ -24,14 +24,14 @@
"prepublishOnly": "yarn build"
},
"dependencies": {
"@babel/generator": "^7.23.0",
"@babel/parser": "^7.23.0",
"@babel/traverse": "^7.23.2",
"@babel/types": "^7.23.0",
"@music163/tango-helpers": "^0.1.5",
"@types/babel__generator": "^7.6.6",
"@types/babel__traverse": "^7.20.3",
"mobx": "6.9.0",
"@babel/generator": "^7.23.5",
"@babel/parser": "^7.23.5",
"@babel/traverse": "^7.23.5",
"@babel/types": "^7.23.5",
"@music163/tango-helpers": "^1.0.0-alpha.2",
"@types/babel__generator": "^7.6.7",
"@types/babel__traverse": "^7.20.4",
"mobx": "6.12.0",
"path-browserify": "^1.0.1"
},
"peerDependencies": {

View File

@@ -1,3 +1,5 @@
import { isValidExpressionCode } from './ast';
const defineServiceHandlerNames = ['defineServices', 'createServices'];
const sfHandlerPattern = new RegExp(`^(${defineServiceHandlerNames.join('|')})$`);
@@ -20,3 +22,28 @@ const defineStoreHandlerName = 'defineStore';
export function isDefineStore(name: string) {
return defineStoreHandlerName === name;
}
/**
* 是否是 tango 的变量引用
* @example tango.stores.app.name
* @example tango.stores?.app?.name
*
* @param name
* @returns
*/
export function isTangoVariable(name: string) {
return /^tango\??\.(stores|services)\??\./.test(name) && name.split('.').length > 2;
}
const templatePattern = /^{(.+)}$/s;
/**
* 判断给定字符串是否被表达式容器`{expCode}`包裹
* @param code
*/
export function isWrappedByExpressionContainer(code: string, isStrict = true) {
if (isStrict && isValidExpressionCode(code)) {
return false;
}
return templatePattern.test(code);
}

View File

@@ -6,10 +6,10 @@ import * as t from '@babel/types';
import {
logger,
isValidObjectString,
isVariableString,
getVariableContent,
isPlainObject,
} from '@music163/tango-helpers';
import { isWrappedByExpressionContainer } from '../assert';
// @see https://babeljs.io/docs/en/babel-parser#pluginss
const babelParserConfig: ParserOptions = {
@@ -22,7 +22,6 @@ const babelParserConfig: ParserOptions = {
'classProperties',
'asyncGenerators',
'functionBind',
'functionSent',
'dynamicImport',
'optionalChaining',
],
@@ -44,6 +43,12 @@ export function isValidCode(code: string) {
/**
* 检测代码是否是合法的表达式代码
* 表达式是一组代码的集合,它返回一个值;每一个合法的表达式都能计算成某个值
* 只要你输入这段代码,可以形成一个值,就算是表达式
* @example x = 1; // 1
* @example 1=1
* @example 'hello'
* @example { foo: 'bar' }
* @param code
* @returns
*/
@@ -91,8 +96,8 @@ export function code2expression(code: string) {
try {
expNode = t.cloneNode(parseExpression(code, babelParserConfig), false, true);
} catch (err) {
console.error('invalid code', err);
expNode = t.identifier('undefined');
logger.error('invalid code', err);
// expNode = t.identifier('undefined');
}
return expNode;
}
@@ -103,7 +108,7 @@ export function code2expression(code: string) {
* @returns File
*/
export function expressionCode2ast(code: string) {
if (isVariableString(code)) {
if (isWrappedByExpressionContainer(code)) {
code = getVariableContent(code);
}
const node = code2expression(code);
@@ -130,7 +135,7 @@ export function value2node(
ret = t.numericLiteral(value);
break;
case 'string':
if (isVariableString(value)) {
if (isWrappedByExpressionContainer(value)) {
// 再检查是否是表达式容器,例如 {this.foo}, {1}
const innerString = getVariableContent(value);
ret = code2expression(innerString);
@@ -201,7 +206,7 @@ export function value2jsxAttributeValueNode(value: any) {
if (isValidObjectString(value)) {
// 先检查是否是对象字符串
ret = t.jsxExpressionContainer(code2expression(value));
} else if (isVariableString(value)) {
} else if (isWrappedByExpressionContainer(value)) {
// 再检查是否是表达式容器,例如 {this.foo}, {1}
const innerString = getVariableContent(value);
ret = t.jsxExpressionContainer(code2expression(innerString));
@@ -221,7 +226,7 @@ export function value2jsxChildrenValueNode(value: any) {
let ret: t.JSXElement | t.JSXFragment | t.JSXExpressionContainer | t.JSXSpreadChild | t.JSXText;
switch (typeof value) {
case 'string':
if (isVariableString(value)) {
if (isWrappedByExpressionContainer(value)) {
const innerString = getVariableContent(value);
ret = t.jsxExpressionContainer(code2expression(innerString));
} else {

View File

@@ -22,16 +22,16 @@ import {
code2expression,
object2node,
} from './parse';
import { isValidComponentName } from '../string';
import { isDefineService, isDefineStore } from '../assert';
import { getFullPath, isValidComponentName } from '../string';
import { isDefineService, isDefineStore, isTangoVariable } from '../assert';
import type {
IRouteData,
IStorePropertyData,
IClassPropertyNodeData,
ITangoViewNodeData,
IImportDeclarationPayload,
IServiceFunctionPayload,
InsertChildPositionType,
IImportSpecifierData,
IExportSpecifierData,
} from '../../types';
import { IdGenerator } from '../id-generator';
@@ -194,7 +194,7 @@ export function queryXFormItemFields(ast: t.File | t.JSXElement) {
* @param node
* @param id
*/
function isJSXElementById(node: t.JSXElement, jsxElementNodeId: string) {
export function isJSXElementById(node: t.JSXElement, jsxElementNodeId: string) {
let isTargetJSXElement = false;
visitJSXElementAttributes(node, (name, value) => {
if (name === SLOT.dnd && value === jsxElementNodeId) {
@@ -448,11 +448,12 @@ function getLastExportDeclarationIndex(node: t.Program) {
/**
* 生成新的 ImportDeclaration
* @deprecated 使用 makeImportDeclaration2 代替
* @param importedModule
* @param importedSourcePath
* @returns
*/
export function makeImportDeclaration(importedModule: IImportDeclarationPayload) {
export function makeImportDeclarationLegacy(importedModule: IImportDeclarationPayload) {
const specifierNodes =
importedModule.specifiers?.map((localName) =>
t.importSpecifier(t.identifier(localName), t.identifier(localName)),
@@ -467,6 +468,36 @@ export function makeImportDeclaration(importedModule: IImportDeclarationPayload)
return t.importDeclaration(newSpecifierNodes, t.stringLiteral(importedModule.sourcePath));
}
function specifierData2node(data: IImportSpecifierData) {
switch (data.type) {
case 'ImportDefaultSpecifier':
return t.importDefaultSpecifier(t.identifier(data.localName));
case 'ImportSpecifier':
return t.importSpecifier(
t.identifier(data.importedName || data.localName),
t.identifier(data.localName),
);
case 'ImportNamespaceSpecifier':
return t.importNamespaceSpecifier(t.identifier(data.localName));
default:
return;
}
}
function specifierDataList2nodes(specifiers: IImportSpecifierData[]) {
return specifiers.map((item) => specifierData2node(item)).filter((item) => !!item);
}
export function makeImportDeclaration(source: string, specifiers: IImportSpecifierData[]) {
const specifierNodes = specifierDataList2nodes(specifiers);
return t.importDeclaration(specifierNodes, t.stringLiteral(source));
}
/**
* @deprecated 使用 parseImportDeclaration 代替
* @param node
* @returns
*/
function getImportDeclarationData(node: t.ImportDeclaration): IImportDeclarationPayload {
const sourcePath = node2value(node.source);
let defaultSpecifier;
@@ -487,16 +518,33 @@ function getImportDeclarationData(node: t.ImportDeclaration): IImportDeclaration
/**
* 添加新的导入语句
* @deprecated 使用 addImportDeclaration2 代替
* @param ast
* @param importedModule
* @param importedSourcePath
* @returns
*/
export function addImportDeclaration(ast: t.File, importedModule: IImportDeclarationPayload) {
export function addImportDeclarationLegacy(ast: t.File, importedModule: IImportDeclarationPayload) {
traverse(ast, {
Program(path) {
const lastIndex = getLastImportDeclarationIndex(path.node);
const newImportDeclaration = makeImportDeclaration(importedModule);
const newImportDeclaration = makeImportDeclarationLegacy(importedModule);
path.node.body.splice(lastIndex, 0, newImportDeclaration);
path.stop();
},
});
return ast;
}
export function addImportDeclaration(
ast: t.File,
source: string,
specifiers: IImportSpecifierData[],
) {
traverse(ast, {
Program(path) {
const lastIndex = getLastImportDeclarationIndex(path.node);
const newImportDeclaration = makeImportDeclaration(source, specifiers);
path.node.body.splice(lastIndex, 0, newImportDeclaration);
path.stop();
},
@@ -506,17 +554,21 @@ export function addImportDeclaration(ast: t.File, importedModule: IImportDeclara
/**
* 更新已有的导入语句
* @deprecated 使用 updateImportDeclaration2 代替
* @param ast
* @param importedModule
* @param importedSourcePath
* @returns
*/
export function updateImportDeclaration(ast: t.File, importedModule: IImportDeclarationPayload) {
export function updateImportDeclarationLegacy(
ast: t.File,
importedModule: IImportDeclarationPayload,
) {
traverse(ast, {
ImportDeclaration(path) {
const currentSourcePath = node2value(path.node.source);
if (currentSourcePath === importedModule.sourcePath) {
const newImportDeclaration = makeImportDeclaration(importedModule);
const newImportDeclaration = makeImportDeclarationLegacy(importedModule);
path.replaceWith(newImportDeclaration);
path.stop(); // 只修改匹配到的第一条
}
@@ -525,6 +577,50 @@ export function updateImportDeclaration(ast: t.File, importedModule: IImportDecl
return ast;
}
export function updateImportDeclaration(
ast: t.File,
source: string,
specifiers: IImportSpecifierData[],
) {
traverse(ast, {
ImportDeclaration(path) {
const currentSourcePath = node2value(path.node.source);
if (currentSourcePath === source) {
const newImportDeclaration = makeImportDeclaration(source, specifiers);
path.replaceWith(newImportDeclaration);
path.stop(); // 只修改匹配到的第一条
}
},
});
return ast;
}
/**
* 再已有的导入语句中添加新的导入符号
* @param ast
* @param source
* @param newSpecifiers
* @returns
*/
export function insertImportSpecifiers(
ast: t.File,
source: string,
newSpecifiers: IImportSpecifierData[],
) {
traverse(ast, {
ImportDeclaration(path) {
const currentSourcePath = node2value(path.node.source);
if (currentSourcePath === source) {
const nodes = specifierDataList2nodes(newSpecifiers);
path.node.specifiers.push(...nodes);
// 只在匹配到的第一个导入声明语句添加即可,不再重复执行
path.stop();
}
},
});
return ast;
}
/**
* 新增类属性
* @param ast
@@ -592,7 +688,6 @@ export function addRouteToRouteFile(ast: t.File, routePath: string, importFilePa
const newNode = t.objectExpression([
t.objectProperty(t.identifier('path'), t.stringLiteral(routePath)),
t.objectProperty(t.identifier('component'), t.identifier(component)),
// FIXME: tango/issues#144 https://g.hz.netease.com/tango/issues/-/issues/144
t.objectProperty(t.identifier('exact'), t.booleanLiteral(true)),
]);
path.node.elements.push(newNode);
@@ -902,7 +997,7 @@ export function deleteServiceConfigFromServiceFile(ast: t.File, serviceFunctionN
* @param payload
* @returns
*/
export function serviceConfig2Node(payload: IServiceFunctionPayload) {
export function serviceConfig2Node(payload: object) {
return object2node(payload, (value, key) => {
if (key === 'formatter' && value) {
return code2expression(value);
@@ -911,10 +1006,7 @@ export function serviceConfig2Node(payload: IServiceFunctionPayload) {
});
}
export function updateServiceConfigToServiceFile(
ast: t.File,
config: { [key: string]: IServiceFunctionPayload },
) {
export function updateServiceConfigToServiceFile(ast: t.File, config: Dict<object>) {
traverse(ast, {
CallExpression(path) {
const calleeName = keyNode2value(path.node.callee) as string;
@@ -986,7 +1078,12 @@ export function traverseServiceFile(ast: t.File) {
const baseConfig: any = {
encryptFetch: false,
};
const imports: Record<string, IImportSpecifierData[]> = {};
traverse(ast, {
ImportDeclaration(path) {
const { source, specifiers } = parseImportDeclaration(path.node);
imports[source] = specifiers;
},
CallExpression(path) {
const calleeName = keyNode2value(path.node.callee) as string;
if (isDefineService(calleeName)) {
@@ -1003,6 +1100,7 @@ export function traverseServiceFile(ast: t.File) {
},
});
return {
imports,
services,
baseConfig,
};
@@ -1107,6 +1205,7 @@ export function cloneJSXElementWithoutTrackingData(node: t.JSXElement) {
}
export function traverseViewFile(ast: t.File, idGenerator: IdGenerator) {
const imports: Record<string, IImportSpecifierData[]> = {};
const importedModules: Dict<IImportDeclarationPayload | IImportDeclarationPayload[]> = {};
const nodes: Array<ITangoViewNodeData<t.JSXElement>> = [];
const cloneAst = t.cloneNode(ast, true, true);
@@ -1115,12 +1214,43 @@ export function traverseViewFile(ast: t.File, idGenerator: IdGenerator) {
traverse(ast, {
ImportDeclaration(path) {
const declarationData = getImportDeclarationData(path.node);
const exist = importedModules[declarationData.sourcePath];
if (!exist) {
importedModules[declarationData.sourcePath] = declarationData;
const { source, specifiers } = parseImportDeclaration(path.node);
if (imports[source]) {
// 存在重复的导入语句,合并导入符号列表
imports[source] = imports[source].concat(specifiers);
} else {
importedModules[declarationData.sourcePath] = Array.isArray(exist)
imports[source] = specifiers;
}
// FIXME: 下面的逻辑兼容旧的逻辑,后续需要移除掉
const declarationData = specifiers.reduce(
(prev, cur) => {
switch (cur.type) {
case 'ImportDefaultSpecifier':
prev.defaultSpecifier = cur.localName;
break;
case 'ImportSpecifier':
prev.specifiers.push(cur.localName);
break;
case 'ImportNamespaceSpecifier':
prev.specifiers.push(cur.localName);
break;
default:
break;
}
return prev;
},
{
defaultSpecifier: undefined,
specifiers: [],
sourcePath: source,
},
);
const exist = importedModules[source];
if (!exist) {
importedModules[source] = declarationData;
} else {
importedModules[source] = Array.isArray(exist)
? exist.concat([declarationData])
: [exist, declarationData];
}
@@ -1128,15 +1258,23 @@ export function traverseViewFile(ast: t.File, idGenerator: IdGenerator) {
MemberExpression(path) {
const variable = node2value(path.node, false);
const parentNode = path.parentPath.node;
if (
/^tango\.(stores|services)\./.test(variable) &&
variable.split('.').length === 3 &&
!t.isMemberExpression(parentNode) &&
isTangoVariable(variable) &&
!variables.includes(variable)
) {
variables.push(variable);
}
},
OptionalMemberExpression(path) {
const variable = node2value(path.node, false);
if (isTangoVariable(variable) && !variables.includes(variable)) {
variables.push(variable);
}
},
JSXElement(path) {
const attributes = getJSXElementAttributes(path.node);
@@ -1183,7 +1321,77 @@ export function traverseViewFile(ast: t.File, idGenerator: IdGenerator) {
ast,
cleanAst,
nodes,
imports,
importedModules,
variables,
};
}
export function traverseComponentsEntryFile(ast: t.File, baseDir?: string) {
const exportMap: Record<string, IExportSpecifierData> = {};
traverse(ast, {
ExportNamedDeclaration(path) {
const node = path.node;
let source = node2value(node.source);
if (baseDir) {
// fix relative source path
source = getFullPath(baseDir, source);
}
node.specifiers.forEach((specifier) => {
if (t.isExportSpecifier(specifier)) {
const name = keyNode2value(specifier.exported) as string;
if (name) {
exportMap[name] = {
source,
exportedName: name,
};
}
}
});
},
});
return { ast, exportMap };
}
/**
* 解析导入语句
*/
function parseImportDeclaration(node: t.ImportDeclaration) {
const source = node2value(node.source) as string;
const specifiers: IImportSpecifierData[] = [];
node.specifiers.forEach((specifierNode) => {
const data: IImportSpecifierData = {
localName: keyNode2value(specifierNode.local) as string,
type: specifierNode.type,
};
if (specifierNode.type === 'ImportSpecifier') {
data.importedName = keyNode2value(specifierNode.imported) as string;
}
specifiers.push(data);
});
return {
source,
specifiers,
};
}
/**
* 基本的文件解析过程
* @param ast
*/
export function traverseFile(ast: t.File) {
const imports: Record<string, IImportSpecifierData[]> = {};
traverse(ast, {
ImportDeclaration(path) {
const { source, specifiers } = parseImportDeclaration(path.node);
imports[source] = specifiers;
},
});
return {
imports,
};
}

View File

@@ -1,5 +1,6 @@
import { getVariableContent, isVariableString } from '@music163/tango-helpers';
import { value2node, expression2code } from './ast';
import { getVariableContent } from '@music163/tango-helpers';
import { value2node, expression2code, isValidExpressionCode } from './ast';
import { isWrappedByExpressionContainer } from './assert';
/**
* 将 js value 转换为代码字符串
@@ -36,7 +37,9 @@ export function value2expressionCode(val: any) {
switch (typeof val) {
case 'string': {
if (isVariableString(val)) {
if (isValidExpressionCode(val)) {
ret = val;
} else if (isWrappedByExpressionContainer(val, false)) {
ret = getVariableContent(val);
} else if (isStringCode(val)) {
ret = val;

View File

@@ -1,5 +1,5 @@
import { Dict } from '@music163/tango-helpers';
import { IImportDeclarationPayload, IImportSpecifierData } from '../types';
import { IImportDeclarationPayload, IImportSpecifierSourceData } from '../types';
/**
* 导入列表解析为导入声明对象
@@ -7,7 +7,10 @@ import { IImportDeclarationPayload, IImportSpecifierData } from '../types';
* @param nameMap
* @returns
*/
export function namesToImportDeclarations(names: string[], nameMap: Dict<IImportSpecifierData>) {
export function namesToImportDeclarations(
names: string[],
nameMap: Dict<IImportSpecifierSourceData>,
) {
const map = {};
names.forEach((name) => {
const mod = nameMap[name];

View File

@@ -3,16 +3,51 @@ import {
ComponentPropType,
ComponentPrototypeType,
isNil,
isVariableString,
logger,
uuid,
} from '@music163/tango-helpers';
import { getRelativePath, isFilepath } from './string';
import type { IImportDeclarationPayload } from '../types';
import type { IImportDeclarationPayload, IImportSpecifierData } from '../types';
import { code2expression } from './ast';
import { isWrappedByExpressionContainer } from './assert';
export function prototype2importDeclarationData(
prototype: ComponentPrototypeType,
relativeFilepath?: string,
): { source: string; specifiers: IImportSpecifierData[] } {
let source = prototype.package;
if (relativeFilepath && isFilepath(source)) {
source = getRelativePath(relativeFilepath, source);
}
if (source.endsWith('.js')) {
source = source.slice(0, -3);
}
const specifiers: IImportSpecifierData[] = [];
if (prototype.exportType === 'defaultExport') {
specifiers.push({
localName: prototype.name,
type: 'ImportDefaultSpecifier',
});
} else {
[prototype.name, ...(prototype.relatedImports || [])].forEach((item) => {
specifiers.push({
localName: item,
type: 'ImportSpecifier',
});
});
}
return {
source,
specifiers,
};
}
/**
* 根据组件的 prototype 生成 ImportDeclarationPayload
* @deprecated
*/
export function getImportDeclarationPayloadByPrototype(
prototype: ComponentPrototypeType,
@@ -35,6 +70,9 @@ export function getImportDeclarationPayloadByPrototype(
if (relativeFilepath && isFilepath(sourcePath)) {
sourcePath = getRelativePath(relativeFilepath, sourcePath);
}
if (sourcePath.endsWith('.js')) {
sourcePath = sourcePath.slice(0, -3);
}
return {
defaultSpecifier,
@@ -82,7 +120,7 @@ function getPropKeyValuePair(item: ComponentPropType, generateValue: (...args: a
break;
}
case 'string': {
if (!isVariableString(value)) {
if (!isWrappedByExpressionContainer(value)) {
// 不是变量字符串
value = `"${value}"`;
} else {

View File

@@ -33,6 +33,10 @@ export function inferFileType(filename: string): FileType {
return FileType.JsonViewModule;
}
if (/\/(blocks|components)\/index\.js/.test(filename)) {
return FileType.ComponentsEntryModule;
}
if (/\/services\/.+\.js$/.test(filename)) {
return FileType.ServiceModule;
}
@@ -49,10 +53,6 @@ export function inferFileType(filename: string): FileType {
return FileType.StoreModule;
}
if (/\/blocks\/[\w-]+\/index\.js/.test(filename)) {
return FileType.BlockEntryModule;
}
if (/\.jsx?$/.test(filename)) {
return FileType.Module;
}
@@ -170,24 +170,35 @@ export function getBlockNameByFilename(filename: string) {
}
/**
* FIXME: 有问题,需要优化下
* 基于 from 文件的地址计算 to 文件的相对引用路径
* @param from
* @param to
* 合并两个路径
* @param root
* @param filename
* @returns
*/
export function getRelativePath(from: string, to: string) {
const fromFolder = path.dirname(from);
return path.relative(fromFolder, to);
export function getFullPath(root: string, filename: string) {
return path.join(root, filename);
}
/**
* 计算 targetFile 在 sourceFile 中的相对引用路径
* @param sourceFile
* @param targetFile
* @returns
*/
export function getRelativePath(sourceFile: string, targetFile: string) {
sourceFile = path.dirname(sourceFile);
return path.relative(sourceFile, targetFile);
}
/**
* 判断给定字符串是否是文件路径
* @example ./pages/index.js -- yes
* @example ../pages/index.js -- yes
* @example ../components -- yes
* @example /src/pages/index.js -- yes
* @example @music163/tango-designer -- no
* @param str
*/
export function isFilepath(str: string) {
return /^(\.\.?\/|\/).*\.[a-z]+$/.test(str);
return /^(\.\.?\/|\/).*(\.[a-z]+)?$/.test(str);
}

View File

@@ -1,4 +1,8 @@
export * from './models';
export * from './factory';
export * from './helpers';
export * from './types';
export * from './types';
export { default as generator } from '@babel/generator';
export { default as traverse } from '@babel/traverse';
export * from '@babel/parser';

View File

@@ -0,0 +1,40 @@
import path from 'path';
import { action, computed, makeObservable, observable } from 'mobx';
import { TangoModule } from './module';
import { IWorkspace } from './interfaces';
import { IExportSpecifierData, IFileConfig } from '../types';
import { traverseComponentsEntryFile } from '../helpers';
/**
* 本地组件目录的入口文件,例如 '/components/index.js' 或 `/blocks/index.js`
*/
export class TangoComponentsEntryModule extends TangoModule {
exportList: Record<string, IExportSpecifierData>;
constructor(workspace: IWorkspace, props: IFileConfig) {
super(workspace, props, false);
this.update(props.code, false, false);
makeObservable(this, {
_code: observable,
_cleanCode: observable,
exportList: observable,
code: computed,
cleanCode: computed,
update: action,
});
}
_analysisAst() {
const baseDir = path.dirname(this.filename);
const { exportMap } = traverseComponentsEntryFile(this.ast, baseDir);
this.exportList = exportMap;
Object.keys(this.exportList).forEach((key) => {
this.workspace.componentPrototypes.set(key, {
name: key,
exportType: 'namedExport',
package: baseDir,
type: 'element',
});
});
}
}

View File

@@ -56,6 +56,7 @@ export class TangoFile {
this._code = code;
this._cleanCode = code;
}
this.workspace.onFilesChange([this.filename]);
}
}
@@ -134,6 +135,7 @@ export class TangoJsonFile extends TangoFile {
logger.error(err);
}
}
this.workspace.onFilesChange([this.filename]);
}
/**

View File

@@ -11,5 +11,6 @@ export * from './route-module';
export * from './service-module';
export * from './store-module';
export * from './view-module';
export * from './component-module';
export * from './node';
export * from './interfaces';

View File

@@ -5,11 +5,10 @@ import { DragSource } from './drag-source';
import {
IFileConfig,
FileType,
IImportDeclarationPayload,
InsertChildPositionType,
ITangoConfigPackages,
IPageConfigData,
IServiceFunctionPayload,
IImportSpecifierSourceData,
IImportSpecifierData,
} from '../types';
import { TangoFile, TangoJsonFile } from './file';
@@ -25,7 +24,7 @@ export interface IViewFile {
/**
* 通过导入组件名查找组件来自的包
*/
importMap?: Dict<IImportSpecifierData>;
importMap?: Dict<IImportSpecifierSourceData>;
listImportSources?: () => string[];
listModals?: () => Array<{ label: string; value: string }>;
@@ -33,6 +32,14 @@ export interface IViewFile {
update: (code?: string, isFormatCode?: boolean, refreshWorkspace?: boolean) => void;
/**
* 添加新的导入符号
* @param source 导入来源
* @param newSpecifiers 新导入符号列表
* @returns this
*/
addImportSpecifiers: (source: string, newSpecifiers: IImportSpecifierData[]) => IViewFile;
getNode: (targetNodeId: string) => IViewNode;
removeNode: (targetNodeId: string) => IViewFile;
@@ -41,31 +48,15 @@ export interface IViewFile {
targetNodeId: string,
newNode: any,
position?: InsertChildPositionType,
sourcePrototype?: string | ComponentPrototypeType,
) => IViewFile;
insertAfter: (
targetNodeId: string,
newNode: any,
sourcePrototype?: string | ComponentPrototypeType,
) => IViewFile;
insertAfter: (targetNodeId: string, newNode: any) => IViewFile;
insertBefore: (
targetNodeId: string,
newNode: any,
sourcePrototype?: string | ComponentPrototypeType,
) => IViewFile;
insertBefore: (targetNodeId: string, newNode: any) => IViewFile;
replaceNode: (
targetNodeId: string,
newNode: any,
sourcePrototype?: string | ComponentPrototypeType,
) => IViewFile;
replaceNode: (targetNodeId: string, newNode: any) => IViewFile;
replaceViewChildren: (
rawNodes: any[],
importDeclarations?: IImportDeclarationPayload[],
) => IViewFile;
replaceViewChildren: (rawNodes: any[]) => IViewFile;
updateNodeAttribute: (
nodeId: string,
@@ -148,8 +139,8 @@ export interface IWorkspace {
storeModules?: Record<string, TangoStoreModule>;
serviceModules?: Record<string, TangoServiceModule>;
refresh: (names: string[]) => void;
ready: () => void;
refresh: (names: string[]) => void;
setActiveRoute: (path: string) => void;
setActiveFile: (filename: string) => void;
@@ -157,24 +148,31 @@ export interface IWorkspace {
setComponentPrototypes: (prototypes: Record<string, ComponentPrototypeType>) => void;
getPrototype: (name: string | ComponentPrototypeType) => ComponentPrototypeType;
onFilesChange?: (filenames: string[]) => void;
getFile: (filename: string) => TangoFile;
listFiles: () => Record<string, string>;
addFile: (filename: string, code: string, fileType?: FileType) => void;
// ----------------- 文件操作 -----------------
addFiles: (files: IFileConfig[]) => void;
updateFile: (filename: string, code: string, shouldFormatCode?: boolean) => void;
addFile: (filename: string, code: string, fileType?: FileType) => void;
addServiceFile: (serviceName: string, code: string) => void;
addStoreFile: (storeName: string, code: string) => void;
addViewFile: (viewName: string, code: string) => void;
removeFile: (filename: string) => void;
renameFile: (oldFilename: string, newFilename: string) => void;
renameFolder: (oldFoldername: string, newFoldername: string) => void;
updateFile: (filename: string, code: string, shouldFormatCode?: boolean) => void;
listFiles: () => Record<string, string>;
getFile: (filename: string) => TangoFile;
/**
* 查询节点
* @param id 节点 ID
* @param module 节点所在的模块名
* @returns 返回节点对象
* 文件变化回调
* @param filenames 文件名列表
*/
getNode: (id: string, module?: string) => IViewNode;
onFilesChange: (filenames: string[]) => void;
// ----------------- 节点操作 -----------------
removeSelectedNode: () => void;
cloneSelectedNode: () => void;
@@ -195,31 +193,48 @@ export interface IWorkspace {
relatedImports?: string[],
) => void;
getServiceFunction?: (serviceKey: string) => object;
listServiceFunctions?: () => Record<string, object>;
removeServiceFunction?: (serviceName: string, modName?: string) => void;
addServiceFunction?: (
payload: IServiceFunctionPayload | IServiceFunctionPayload[],
modName?: string,
) => void;
updateServiceFunction?: (payload: IServiceFunctionPayload, modName?: string) => void;
updateServiceBaseConfig?: (IServiceFunctionPayload: object, modName?: string) => void;
/**
* 查询节点
* @param id 节点 ID
* @param module 节点所在的模块名
* @returns 返回节点对象
*/
getNode: (id: string, module?: string) => IViewNode;
addStoreModule?: (storeName: string, code: string) => void;
removeStoreModule?: (storeName: string) => void;
// ----------------- 服务函数文件操作 -----------------
getServiceFunction?: (serviceKey: string) => {
name: string;
moduleName: string;
config: Dict<object>;
};
listServiceFunctions?: () => Dict<object>;
removeServiceFunction?: (serviceKey: string) => void;
addServiceFunction?: (serviceName: string, config: Dict, modName?: string) => void;
addServiceFunctions?: (configs: Dict<object>, modName?: string) => void;
updateServiceFunction?: (serviceName: string, payload: Dict, modName?: string) => void;
updateServiceBaseConfig?: (config: Dict, modName?: string) => void;
// ----------------- 状态管理文件操作 -----------------
addStoreState?: (storeName: string, stateName: string, initValue: string) => void;
removeStoreState?: (storeName: string, stateName: string) => void;
removeStoreModule?: (storeName: string) => void;
removeStoreVariable?: (variablePath: string) => void;
updateStoreVariable?: (variablePath: string, code: string) => void;
updateModuleCodeByVariablePath?: (variablePath: string, code: string) => void;
// ----------------- 视图文件操作 -----------------
// TODO: 是否需要,和 addFile 有啥区别 ???
addViewPage: (name: string, code: string) => void;
removeViewModule: (routePath: string) => void;
copyViewPage: (sourceRoutePath: string, targetPageData: IPageConfigData) => void;
// ----------------- 路由文件操作 -----------------
updateRoute: (sourceRoutePath: string, targetPageData: IPageConfigData) => void;
// ----------------- 依赖包操作 -----------------
addDependency?: (data: any) => void;
listDependencies?: () => any;
getDependency?: (pkgName: string) => object;
updateDependency?: (
name: string,
@@ -243,9 +258,11 @@ export interface IWorkspace {
removeBizComp?: (name: string) => void;
// ----------------- getter -----------------
get activeViewModule(): IViewFile;
get pages(): any[];
get bizComps(): string[];
get baseComps(): string[];
get blocks(): any[];
get localComps(): string[];
}

View File

@@ -1,9 +1,16 @@
import * as t from '@babel/types';
import { action, computed, makeObservable, observable } from 'mobx';
import { isNil } from '@music163/tango-helpers';
import { code2ast, ast2code, formatCode } from '../helpers';
import {
code2ast,
ast2code,
formatCode,
traverseFile,
addImportDeclaration,
updateImportDeclaration,
} from '../helpers';
import { TangoFile } from './file';
import { IFileConfig } from '../types';
import { IFileConfig, IImportSpecifierData, ImportDeclarationDataType } from '../types';
import { IWorkspace } from './interfaces';
/**
@@ -14,6 +21,11 @@ import { IWorkspace } from './interfaces';
export class TangoModule extends TangoFile {
ast: t.File;
/**
* 导入的依赖列表
*/
importList: ImportDeclarationDataType;
constructor(workspace: IWorkspace, props: IFileConfig, isSyncCode = true) {
super(workspace, props, isSyncCode);
}
@@ -34,11 +46,23 @@ export class TangoModule extends TangoFile {
this._analysisAst();
this.workspace.onFilesChange([this.filename]);
if (refreshWorkspace) {
this.workspace.refresh([this.filename]);
}
}
addImportDeclaration(source: string, specifiers: IImportSpecifierData[]) {
this.ast = addImportDeclaration(this.ast, source, specifiers);
return this;
}
updateImportDeclaration(source: string, specifiers: IImportSpecifierData[]) {
this.ast = updateImportDeclaration(this.ast, source, specifiers);
return this;
}
/**
* 基于最新的 ast 进行源码同步
*/
@@ -69,7 +93,10 @@ export class TangoModule extends TangoFile {
this.ast = code2ast(code);
}
_analysisAst() {}
_analysisAst() {
const { imports } = traverseFile(this.ast);
this.importList = imports;
}
}
/**

View File

@@ -7,7 +7,7 @@ import {
deleteServiceConfigFromServiceFile,
updateBaseConfigToServiceFile,
} from '../helpers';
import { IFileConfig, IServiceFunctionPayload } from '../types';
import { IFileConfig } from '../types';
import { IWorkspace } from './interfaces';
import { TangoModule } from './module';
@@ -58,7 +58,8 @@ export class TangoServiceModule extends TangoModule {
}
_analysisAst() {
const { services, baseConfig } = traverseServiceFile(this.ast);
const { imports, services, baseConfig } = traverseServiceFile(this.ast);
this.importList = imports;
this._serviceFunctions = services;
this._baseConfig = baseConfig;
if (baseConfig.namespace) {
@@ -66,25 +67,23 @@ export class TangoServiceModule extends TangoModule {
}
}
addServiceFunction(payload: IServiceFunctionPayload) {
const { name, ...rest } = payload;
this.ast = updateServiceConfigToServiceFile(this.ast, { [name]: clone(rest, false) });
addServiceFunction(name: string, config: object) {
this.ast = updateServiceConfigToServiceFile(this.ast, { [name]: clone(config, false) });
return this;
}
addServiceFunctions(payloads: IServiceFunctionPayload[]) {
const config = payloads.reduce((acc, cur) => {
const { name, ...rest } = cur;
acc[name] = clone(rest, false);
return acc;
}, {});
this.ast = updateServiceConfigToServiceFile(this.ast, config);
/**
* 批量添加服务函数
* @param configs { [name: string]: object }
* @returns
*/
addServiceFunctions(configs: Dict<object>) {
this.ast = updateServiceConfigToServiceFile(this.ast, configs);
return this;
}
updateServiceFunction(payload: IServiceFunctionPayload) {
const { name, ...rest } = payload;
this.ast = updateServiceConfigToServiceFile(this.ast, { [name]: clone(rest, false) });
updateServiceFunction(name: string, payload: object) {
this.ast = updateServiceConfigToServiceFile(this.ast, { [name]: clone(payload, false) });
return this;
}

View File

@@ -1,6 +1,6 @@
import * as t from '@babel/types';
import { action, computed, makeObservable, observable, toJS } from 'mobx';
import { ComponentPrototypeType, Dict } from '@music163/tango-helpers';
import { Dict } from '@music163/tango-helpers';
import {
ast2code,
traverseViewFile,
@@ -8,15 +8,17 @@ import {
insertSiblingAfterJSXElement,
appendChildToJSXElement,
addImportDeclaration,
updateImportDeclaration,
replaceJSXElement,
getImportDeclarationPayloadByPrototype,
removeUnusedImportSpecifiers,
insertSiblingBeforeJSXElement,
replaceRootJSXElementChildren,
IdGenerator,
updateJSXAttributes,
queryXFormItemFields,
prototype2importDeclarationData,
insertImportSpecifiers,
addImportDeclarationLegacy,
updateImportDeclarationLegacy,
} from '../helpers';
import { TangoNode } from './node';
import {
@@ -24,11 +26,59 @@ import {
ITangoViewNodeData,
IImportDeclarationPayload,
InsertChildPositionType,
IImportSpecifierSourceData,
ImportDeclarationDataType,
IImportSpecifierData,
} from '../types';
import { IViewFile, IWorkspace } from './interfaces';
import { TangoModule } from './module';
/**
* 导入信息转为 变量名->来源 的 map 结构
* @param importedModules
* @returns
*/
function buildImportMap(importedModules: ImportDeclarationDataType) {
const map: Dict<IImportSpecifierSourceData> = {};
Object.keys(importedModules).forEach((source) => {
const specifiers = importedModules[source];
specifiers?.forEach((specifier) => {
map[specifier.localName] = {
source,
isDefault: specifier.type === 'ImportDefaultSpecifier',
};
});
});
return map;
}
/**
* 将节点列表转换为 tree data 嵌套数组
* @param list
*/
function nodeListToTreeData(list: ITangoViewNodeData[]) {
const map: Record<string, ITangoViewNodeData> = {};
list.forEach((item) => {
// 如果不存在,则初始化
if (!map[item.id]) {
map[item.id] = {
...item,
children: [],
};
}
// 是否找到父节点,找到则塞进去
if (item.parentId && map[item.parentId]) {
map[item.parentId].children.push(map[item.id]);
}
});
// 保留根节点
const ret = Object.values(map).filter((item) => !item.parentId);
return ret;
}
/**
* 视图模块
*/
@@ -38,7 +88,7 @@ export class TangoViewModule extends TangoModule implements IViewFile {
/**
* 通过导入组件名查找组件来自的包
*/
importMap: Dict<IImportSpecifierData>;
importMap: Dict<IImportSpecifierSourceData>;
/**
* 视图中依赖的 tango 变量,仅 stores 和 services
@@ -51,6 +101,7 @@ export class TangoViewModule extends TangoModule implements IViewFile {
private _nodes: Map<string, TangoNode>;
/**
* 导入的模块
* @deprecated
*/
private _importedModules: Dict<IImportDeclarationPayload | IImportDeclarationPayload[]>;
@@ -98,6 +149,7 @@ export class TangoViewModule extends TangoModule implements IViewFile {
ast: newAst,
cleanAst,
nodes,
imports,
importedModules,
variables,
} = traverseViewFile(this.ast, this._idGenerator);
@@ -107,7 +159,8 @@ export class TangoViewModule extends TangoModule implements IViewFile {
this._cleanCode = ast2code(cleanAst);
this._importedModules = importedModules;
this.importMap = this.buildImportMap(importedModules);
this.importList = imports;
this.importMap = buildImportMap(imports);
this.variables = variables;
this._nodes.clear();
@@ -127,7 +180,7 @@ export class TangoViewModule extends TangoModule implements IViewFile {
* 依赖列表
*/
listImportSources() {
return Object.keys(this._importedModules);
return Object.keys(this.importList);
}
/**
@@ -168,23 +221,30 @@ export class TangoViewModule extends TangoModule implements IViewFile {
}
/**
* 基于组件的 prototype 信息更新导入信息
* @param prototype
* @param shouldUpdateCode
* 添加导入符号
* @param source
* @param newSpecifiers
* @returns
*/
updateImportSpecifiersByPrototype(sourcePrototype: string | ComponentPrototypeType) {
const prototype = this.workspace.getPrototype(sourcePrototype);
if (prototype) {
const importDeclaration = getImportDeclarationPayloadByPrototype(prototype, this.filename);
this.updateImportSpecifiers(importDeclaration);
addImportSpecifiers(source: string, newSpecifiers: IImportSpecifierData[]) {
const existSpecifiers = this.importList[source];
if (existSpecifiers) {
const insertedSpecifiers = newSpecifiers.filter((item) => {
return !existSpecifiers.find((existItem) => existItem.localName === item.localName);
});
this.ast = insertImportSpecifiers(this.ast, source, insertedSpecifiers);
} else {
// 不存在导入来源,直接添加新的导入语句
this.ast = addImportDeclaration(this.ast, source, newSpecifiers);
}
return this;
}
/**
* 更新导入的变量(新版)
* 更新导入的变量
* @deprecated 使用 updateImportDeclaration 代替
*/
updateImportSpecifiers(importDeclaration: IImportDeclarationPayload) {
updateImportSpecifiersLegacy(importDeclaration: IImportDeclarationPayload) {
const mods = this._importedModules[importDeclaration.sourcePath];
let ast;
// 如果模块已存在,需要去重
@@ -199,12 +259,12 @@ export class TangoViewModule extends TangoModule implements IViewFile {
(name) => !specifiers.includes(name),
);
ast = updateImportDeclaration(this.ast, {
ast = updateImportDeclarationLegacy(this.ast, {
...importDeclaration,
specifiers: newSpecifiers.concat(targetMod.specifiers),
});
} else {
ast = addImportDeclaration(this.ast, importDeclaration);
ast = addImportDeclarationLegacy(this.ast, importDeclaration);
}
this.ast = ast;
return this;
@@ -247,9 +307,24 @@ export class TangoViewModule extends TangoModule implements IViewFile {
updateNodeAttributes(nodeId: string, config: Record<string, any>, relatedImports?: string[]) {
if (relatedImports && relatedImports.length) {
// 导入依赖的组件
relatedImports.forEach((name: string) => {
const newImportData = relatedImports.reduce((prev, name) => {
const proto = this.workspace.getPrototype(name);
this.updateImportSpecifiersByPrototype(proto);
const { source, specifiers } = prototype2importDeclarationData(proto, this.filename);
const existSpecifiers: IImportSpecifierData[] = prev[source];
if (existSpecifiers) {
// merge specifiers
specifiers.forEach((item) => {
if (!existSpecifiers.find((existItem) => existItem.localName === item.localName)) {
existSpecifiers.push(item);
}
});
} else {
prev[source] = specifiers;
}
return prev;
}, {});
Object.keys(newImportData).forEach((source) => {
this.addImportSpecifiers(source, newImportData[source]);
});
}
this.ast = updateJSXAttributes(this.ast, nodeId, config);
@@ -260,43 +335,25 @@ export class TangoViewModule extends TangoModule implements IViewFile {
* 插入子节点的最后面
* @param targetNodeId
* @param newNode
* @param sourceName
* @param position
* @returns
*/
insertChild(
targetNodeId: string,
newNode: t.JSXElement,
position: InsertChildPositionType = 'last',
sourceName: string | ComponentPrototypeType,
) {
this.ast = appendChildToJSXElement(this.ast, targetNodeId, newNode, position);
if (sourceName) {
this.updateImportSpecifiersByPrototype(sourceName);
}
return this;
}
insertAfter(
targetNodeId: string,
newNode: t.JSXElement,
sourceName?: string | ComponentPrototypeType,
) {
insertAfter(targetNodeId: string, newNode: t.JSXElement) {
this.ast = insertSiblingAfterJSXElement(this.ast, targetNodeId, newNode);
if (sourceName) {
this.updateImportSpecifiersByPrototype(sourceName);
}
return this;
}
insertBefore(
targetNodeId: string,
newNode: t.JSXElement,
sourceName?: string | ComponentPrototypeType,
) {
insertBefore(targetNodeId: string, newNode: t.JSXElement) {
this.ast = insertSiblingBeforeJSXElement(this.ast, targetNodeId, newNode);
if (sourceName) {
this.updateImportSpecifiersByPrototype(sourceName);
}
return this;
}
@@ -304,22 +361,15 @@ export class TangoViewModule extends TangoModule implements IViewFile {
* 替换目标节点为新节点
* @param targetNodeId
* @param newNode
* @param sourcePrototype
*/
replaceNode(
targetNodeId: string,
newNode: t.JSXElement,
sourceName?: string | ComponentPrototypeType,
) {
replaceNode(targetNodeId: string, newNode: t.JSXElement) {
this.ast = replaceJSXElement(this.ast, targetNodeId, newNode);
if (sourceName) {
this.updateImportSpecifiersByPrototype(sourceName);
}
return this;
}
/**
* 替换 jsx 跟结点的子元素
* @deprecated 不推荐使用
*/
replaceViewChildren(
childrenNodes: t.JSXElement[],
@@ -331,60 +381,10 @@ export class TangoViewModule extends TangoModule implements IViewFile {
if (importDeclarations?.length) {
importDeclarations.forEach((item) => {
this.updateImportSpecifiers(item);
this.updateImportSpecifiersLegacy(item);
});
}
return this;
}
private buildImportMap(
importedModules: Dict<IImportDeclarationPayload | IImportDeclarationPayload[]>,
) {
const map: Dict<IImportSpecifierData> = {};
Object.keys(importedModules).forEach((modName) => {
const mod = importedModules[modName];
(Array.isArray(mod) ? mod : [mod]).forEach((item) => {
if (item.defaultSpecifier) {
map[item.defaultSpecifier] = {
source: modName,
isDefault: true,
};
}
if (item.specifiers.length) {
item.specifiers.forEach((spe) => {
map[spe] = { source: modName };
});
}
});
});
return map;
}
}
/**
* 将节点列表转换为 tree data 嵌套数组
* @param list
*/
function nodeListToTreeData(list: ITangoViewNodeData[]) {
const map: Record<string, ITangoViewNodeData> = {};
list.forEach((item) => {
// 如果不存在,则初始化
if (!map[item.id]) {
map[item.id] = {
...item,
children: [],
};
}
// 是否找到父节点,找到则塞进去
if (item.parentId && map[item.parentId]) {
map[item.parentId].children.push(map[item.id]);
}
});
// 保留根节点
const ret = Object.values(map).filter((item) => !item.parentId);
return ret;
}

View File

@@ -1,11 +1,14 @@
import { action, computed, makeObservable, observable } from 'mobx';
import { JSXElement } from '@babel/types';
import {
array2object,
ComponentPrototypeType,
Dict,
hasFileExtension,
isStoreVariablePath,
isString,
logger,
parseServiceVariablePath,
parseStoreVariablePath,
uniq,
} from '@music163/tango-helpers';
import {
@@ -15,8 +18,7 @@ import {
isPathnameMatchRoute,
getJSXElementChildrenNames,
namesToImportDeclarations,
getBlockNameByFilename,
getPrivilegeCode,
prototype2importDeclarationData,
} from '../helpers';
import { DropMethod } from './drop-target';
import { HistoryMessage, TangoHistory } from './history';
@@ -24,19 +26,14 @@ import { TangoNode } from './node';
import { TangoJsModule } from './module';
import { TangoFile, TangoJsonFile, TangoLessFile } from './file';
import { IWorkspace } from './interfaces';
import {
IFileConfig,
FileType,
ITangoConfigPackages,
IPageConfigData,
IServiceFunctionPayload,
} from '../types';
import { IFileConfig, FileType, ITangoConfigPackages, IPageConfigData } from '../types';
import { SelectSource } from './select-source';
import { DragSource } from './drag-source';
import { TangoRouteModule } from './route-module';
import { TangoStoreEntryModule, TangoStoreModule } from './store-module';
import { TangoServiceModule } from './service-module';
import { TangoViewModule } from './view-module';
import { TangoComponentsEntryModule } from './component-module';
export interface IWorkspaceOptions {
/**
@@ -58,7 +55,7 @@ export interface IWorkspaceOptions {
/**
* 工作区文件变更事件
*/
onFilesChange?: (filenames: string[]) => void;
onFilesChange?: IWorkspace['onFilesChange'];
}
/**
@@ -74,8 +71,14 @@ export class Workspace extends EventTarget implements IWorkspace {
*/
files: Map<string, TangoFile>;
/**
* 组件配置
*/
componentPrototypes: Map<string, ComponentPrototypeType>;
/**
* 入口文件
*/
entry: string;
/**
@@ -93,12 +96,6 @@ export class Workspace extends EventTarget implements IWorkspace {
*/
activeViewFile: string;
/**
* 本地区块 { [blockName]: filePath }
* FIXME: 修正类型 Record<string, TangoBlockModule>
*/
localBlocks: Record<string, string>;
/**
* 路由配置模块
*/
@@ -113,6 +110,8 @@ export class Workspace extends EventTarget implements IWorkspace {
serviceModules: Record<string, TangoServiceModule> = {};
componentsEntryModule: TangoComponentsEntryModule;
/**
* package.json 文件
*/
@@ -123,17 +122,6 @@ export class Workspace extends EventTarget implements IWorkspace {
*/
tangoConfigJson: TangoJsonFile;
/**
* appJson.json 文件
* FIXME: 是否保留 ???
*/
appJson: TangoJsonFile;
/**
* 代码变更回调
*/
onFilesChange?: (filenames: string[]) => void;
/**
* 绑定事件
* TODO: 是否需要自己来管理 listeners并及时进行 gc
@@ -169,15 +157,12 @@ export class Workspace extends EventTarget implements IWorkspace {
* 获取页面列表
*/
get pages() {
const appJsonPages = this.appJson?.getValue('pages');
const pagesMap = array2object(appJsonPages || [], (item) => item.path);
const ret: IPageConfigData[] = [];
this.routeModule?.routes.forEach((item) => {
const data = pagesMap[item.path];
if (item.path !== '*') {
ret.push({
...data,
path: item.path,
name: item.component,
});
}
});
@@ -212,8 +197,8 @@ export class Workspace extends EventTarget implements IWorkspace {
return list;
}
get blocks() {
return Object.keys(this.localBlocks);
get localComps(): string[] {
return Object.keys(this.componentsEntryModule?.exportList || {});
}
constructor(options?: IWorkspaceOptions) {
@@ -222,14 +207,17 @@ export class Workspace extends EventTarget implements IWorkspace {
this.selectSource = new SelectSource(this);
this.dragSource = new DragSource(this);
this.componentPrototypes = new Map();
this.localBlocks = {};
this.entry = options?.entry;
this.activeRoute = options?.defaultActiveRoute || '/';
this.activeFile = options?.entry;
this.activeViewFile = '';
this.files = new Map();
this.isReady = false;
this.onFilesChange = options?.onFilesChange;
if (options?.onFilesChange) {
// 使用用户提供的 onFilesChange
this.onFilesChange = options.onFilesChange;
}
if (options?.files) {
this.addFiles(options.files);
@@ -244,8 +232,6 @@ export class Workspace extends EventTarget implements IWorkspace {
activeRoute: observable,
activeFile: observable,
activeViewFile: observable,
localBlocks: observable,
blocks: computed,
pages: computed,
bizComps: computed,
setActiveRoute: action,
@@ -312,7 +298,7 @@ export class Workspace extends EventTarget implements IWorkspace {
}
/**
* 添加一组文件到工作区
* 添加一组文件到工作区,如果文件同名,后面的文件会覆盖前面的文件
* @param files
*/
addFiles(files: IFileConfig[] = []) {
@@ -328,81 +314,100 @@ export class Workspace extends EventTarget implements IWorkspace {
* @param fileType 模块类型
*/
addFile(filename: string, code: string, fileType?: FileType) {
if (!this.files.has(filename)) {
const moduleType = fileType || inferFileType(filename);
const props = {
filename,
code,
type: moduleType,
};
const moduleType = fileType || inferFileType(filename);
const props = {
filename,
code,
type: moduleType,
};
let module;
switch (moduleType) {
case FileType.StoreEntryModule:
module = new TangoStoreEntryModule(this, props);
this.storeEntryModule = module;
break;
case FileType.RouteModule: {
module = new TangoRouteModule(this, props);
this.routeModule = module;
// check if activeRoute exists
const route = module.routes.find((item) => item.path === this.activeRoute);
if (!route) {
this.setActiveRoute(module.routes[0]?.path);
}
break;
let module;
switch (moduleType) {
case FileType.StoreEntryModule:
module = new TangoStoreEntryModule(this, props);
this.storeEntryModule = module;
break;
case FileType.ComponentsEntryModule:
module = new TangoComponentsEntryModule(this, props);
this.componentsEntryModule = module;
break;
case FileType.RouteModule: {
module = new TangoRouteModule(this, props);
this.routeModule = module;
// check if activeRoute exists
const route = module.routes.find((item) => item.path === this.activeRoute);
if (!route) {
this.setActiveRoute(module.routes[0]?.path);
}
case FileType.JsxViewModule:
module = new TangoViewModule(this, props);
break;
case FileType.ServiceModule:
module = new TangoServiceModule(this, props);
this.serviceModules[module.name] = module;
break;
case FileType.StoreModule:
module = new TangoStoreModule(this, props);
this.storeModules[module.name] = module;
break;
case FileType.BlockEntryModule: {
const blockName = getBlockNameByFilename(props.filename);
const prototype: ComponentPrototypeType = {
name: blockName,
exportType: 'defaultExport',
package: props.filename,
type: 'block',
};
this.localBlocks[blockName] = props.filename;
this.componentPrototypes.set(blockName, prototype);
module = new TangoViewModule(this, props);
break;
}
case FileType.Module:
module = new TangoJsModule(this, props);
break;
case FileType.Less:
module = new TangoLessFile(this, props);
break;
case FileType.PackageJson:
module = new TangoJsonFile(this, props);
this.packageJson = module;
break;
case FileType.TangoConfigJson:
module = new TangoJsonFile(this, props);
this.tangoConfigJson = module;
break;
case FileType.AppJson:
module = new TangoJsonFile(this, props);
this.appJson = module;
break;
case FileType.Json:
module = new TangoJsonFile(this, props);
break;
default:
module = new TangoFile(this, props);
break;
}
this.files.set(filename, module);
case FileType.JsxViewModule:
module = new TangoViewModule(this, props);
break;
case FileType.ServiceModule:
module = new TangoServiceModule(this, props);
this.serviceModules[module.name] = module;
break;
case FileType.StoreModule:
module = new TangoStoreModule(this, props);
this.storeModules[module.name] = module;
break;
case FileType.Module:
module = new TangoJsModule(this, props);
break;
case FileType.Less:
module = new TangoLessFile(this, props);
break;
case FileType.PackageJson:
module = new TangoJsonFile(this, props);
this.packageJson = module;
break;
case FileType.TangoConfigJson:
module = new TangoJsonFile(this, props);
this.tangoConfigJson = module;
break;
case FileType.Json:
module = new TangoJsonFile(this, props);
break;
default:
module = new TangoFile(this, props);
}
this.files.set(filename, module);
}
addServiceFile(serviceName: string, code: string) {
const filename = `/src/services/${serviceName}.js`;
this.addFile(filename, code, FileType.ServiceModule);
const indexServiceModule = this.serviceModules.index;
indexServiceModule?.addImportDeclaration(`./${serviceName}`, []).update();
}
addStoreFile(storeName: string, code: string) {
const filename = `/src/stores/${storeName}.js`;
this.addFile(filename, code);
if (!this.storeEntryModule) {
this.addFile('/src/stores/index.js', '');
}
this.storeEntryModule.addStore(storeName).update();
}
/**
* 添加视图文件
* @param viewName 文件名
* @param code 代码
*/
addViewFile(viewName: string, code: string) {
const viewRoute = viewName.startsWith('/') ? viewName : `/${viewName}`;
const filename = `/src/pages/${viewName}.js`;
this.addFile(filename, code);
this.addRoute(
{
name: viewName,
path: viewRoute,
},
filename,
);
}
updateFile(filename: string, code: string, shouldFormatCode = false) {
@@ -498,50 +503,14 @@ export class Workspace extends EventTarget implements IWorkspace {
this.routeModule.removeRoute(routePath).update();
this.setActiveRoute(this.routeModule.routes[0]?.path || '/');
}
// remove appJson page
this.appJson
?.setValue('pages', (pages) => {
return (pages as IPageConfigData[]).filter((page) => page.path !== routePath);
})
.update();
this.removeFile(filename);
}
/**
* 添加新的视图文件
* @deprecated
* @param route 视图名
* @param code 视图代码
*/
addViewPage(routeConfig: string | IPageConfigData, code: string) {
const config =
typeof routeConfig === 'string'
? {
name: routeConfig,
path: routeConfig.startsWith('/') ? routeConfig : `/${routeConfig}`,
}
: routeConfig;
const filename = getFilepath(config.path, '/src/pages', '.js');
this.addFile(filename, code);
this.addRoute(config, filename);
}
/**
* 添加新的路由
*/
addRoute(routeData: IPageConfigData, importFilePath: string) {
this.routeModule?.addRoute(routeData.path, importFilePath).update();
this.appJson
?.setValue('pages', (pages) => {
(pages as IPageConfigData[]).push({
name: routeData.name || routeData.path,
path: routeData.path,
privilegeCode: getPrivilegeCode(this.packageJson?.getValue('name'), routeData.path),
});
return pages;
})
.update();
}
/**
@@ -553,18 +522,6 @@ export class Workspace extends EventTarget implements IWorkspace {
if (sourceRoutePath !== targetPageData.path) {
this.routeModule?.updateRoute(sourceRoutePath, targetPageData.path).update();
}
this.appJson
?.setValue('pages', (pages) => {
for (const page of pages as IPageConfigData[]) {
if (page.path === sourceRoutePath) {
page.path = targetPageData.path;
page.name = targetPageData.name;
break;
}
}
return pages;
})
.update();
}
/**
@@ -604,16 +561,10 @@ export class Workspace extends EventTarget implements IWorkspace {
/**
* 添加新的模型文件
* @param name 模型名
* @param code 模型代码
* @deprecated 使用 addStoreFile 代替
*/
addStoreModule(name: string, code: string) {
const filename = `/src/stores/${name}.js`;
this.addFile(filename, code);
if (!this.storeEntryModule) {
this.addFile('/src/stores/index.js', '');
}
this.storeEntryModule.addStore(name).update();
this.addStoreFile(name, code);
}
/**
@@ -646,15 +597,23 @@ export class Workspace extends EventTarget implements IWorkspace {
}
/**
* 根据变量路径更新模块内容
* TODO: 改名,不直观
* 根据变量路径删除状态变量
* @param variablePath
*/
removeStoreVariable(variablePath: string) {
const { storeName, variableName } = parseStoreVariablePath(variablePath);
this.removeStoreState(storeName, variableName);
}
/**
* 根据变量路径更新状态变量的值
* @param variablePath 变量路径
* @param code 变量代码
*/
updateModuleCodeByVariablePath(variablePath: string, code: string) {
if (/^stores\.\w+\.\w+$/.test(variablePath)) {
const [, storeName, stateName] = variablePath.split('.');
this.storeModules[storeName]?.updateState(stateName, code).update();
updateStoreVariable(variablePath: string, code: string) {
if (isStoreVariablePath(variablePath)) {
const { storeName, variableName } = parseStoreVariablePath(variablePath);
this.storeModules[storeName]?.updateState(variableName, code).update();
}
}
@@ -665,7 +624,7 @@ export class Workspace extends EventTarget implements IWorkspace {
* @returns
*/
getServiceFunction(serviceKey: string) {
const { name, moduleName } = this.parseServiceKey(serviceKey);
const { name, moduleName } = parseServiceVariablePath(serviceKey);
if (!name) {
return;
}
@@ -679,15 +638,14 @@ export class Workspace extends EventTarget implements IWorkspace {
/**
* 获取服务函数的列表
* @returns 返回服务函数的列表 { [serviceKey: string]: Dict }
*/
listServiceFunctions() {
const ret: Record<string, object> = {};
Object.values(this.serviceModules).forEach((module) => {
const ret: Record<string, Dict> = {};
Object.keys(this.serviceModules).forEach((moduleName) => {
const module = this.serviceModules[moduleName];
Object.keys(module.serviceFunctions).forEach((name) => {
const serviceKey =
module.baseConfig.namespace !== 'index'
? name
: [module.baseConfig.namespace, name].join('.');
const serviceKey = moduleName === 'index' ? name : [moduleName, name].join('.');
ret[serviceKey] = module.serviceFunctions[name];
});
});
@@ -697,47 +655,60 @@ export class Workspace extends EventTarget implements IWorkspace {
/**
* 更新服务函数
*/
updateServiceFunction(payload: IServiceFunctionPayload, moduleName = 'index') {
this.serviceModules[moduleName].updateServiceFunction(payload).update();
updateServiceFunction(serviceName: string, payload: Dict, moduleName = 'index') {
this.serviceModules[moduleName].updateServiceFunction(serviceName, payload).update();
}
/**
* 新增服务函数,支持批量添加
*/
addServiceFunction(
payload: IServiceFunctionPayload | IServiceFunctionPayload[],
moduleName = 'index',
) {
if (Array.isArray(payload)) {
this.serviceModules[moduleName]?.addServiceFunctions(payload).update();
} else {
this.serviceModules[moduleName]?.addServiceFunction(payload).update();
}
addServiceFunction(name: string, config: Dict, moduleName = 'index') {
this.serviceModules[moduleName]?.addServiceFunction(name, config).update();
}
addServiceFunctions(configs: Dict<Dict>, modName = 'index') {
this.serviceModules[modName]?.addServiceFunctions(configs).update();
}
/**
* 删除服务函数
* @param name
*/
removeServiceFunction(name: string, moduleName = 'index') {
removeServiceFunction(serviceKey: string) {
const { moduleName, name } = parseServiceVariablePath(serviceKey);
this.serviceModules[moduleName]?.deleteServiceFunction(name).update();
}
/**
* 更新服务的基础配置
*/
updateServiceBaseConfig(config: object, moduleName = 'index') {
updateServiceBaseConfig(config: Dict, moduleName = 'index') {
this.serviceModules[moduleName]?.updateBaseConfig(config).update();
}
addDependency(data: any) {
// TODO: implement it to replace addBizComp & addServiceComp
}
/**
* 获取 package.json 中的依赖列表
* @returns
* TODO: fix this logic to merge dependencies from package.json and tango.config.json
*/
listDependencies() {
return this.packageJson?.getValue('dependencies');
}
getDependency(pkgName: string) {
const packages = this.tangoConfigJson?.getValue('packages');
const dependencies = this.packageJson?.getValue('dependencies'); // 兼容老版本
const detail = {
version: dependencies?.[pkgName],
...(packages?.[pkgName] || {}),
};
return detail;
}
/**
* 更新依赖,没有就添加
* @param name
@@ -922,7 +893,7 @@ export class Workspace extends EventTarget implements IWorkspace {
);
importDeclarations.forEach((importDeclaration) => {
this.activeViewModule.updateImportSpecifiers(importDeclaration);
this.activeViewModule.updateImportSpecifiersLegacy(importDeclaration);
});
this.copyTempNodes.forEach((node) => {
@@ -973,7 +944,11 @@ export class Workspace extends EventTarget implements IWorkspace {
const sourcePrototype = this.getPrototype(sourceName);
const newNode = prototype2jsxElement(sourcePrototype);
const file = this.getNode(targetNodeId).file;
file.insertChild(targetNodeId, newNode, 'last', sourcePrototype).update();
const { source, specifiers } = prototype2importDeclarationData(sourcePrototype, file.filename);
file
.insertChild(targetNodeId, newNode, 'last')
.addImportSpecifiers(source, specifiers)
.update();
this.history.push({
message: HistoryMessage.InsertNode,
data: {
@@ -993,7 +968,8 @@ export class Workspace extends EventTarget implements IWorkspace {
const sourcePrototype = this.getPrototype(sourceName);
const newNode = prototype2jsxElement(sourcePrototype);
const file = this.getNode(targetNodeId).file;
file.replaceNode(targetNodeId, newNode, sourcePrototype).update();
const { source, specifiers } = prototype2importDeclarationData(sourcePrototype, file.filename);
file.replaceNode(targetNodeId, newNode).addImportSpecifiers(source, specifiers).update();
this.history.push({
message: HistoryMessage.ReplaceNode,
data: {
@@ -1011,7 +987,14 @@ export class Workspace extends EventTarget implements IWorkspace {
if (insertedPrototype) {
const newNode = prototype2jsxElement(insertedPrototype);
const file = this.selectSource.file;
file.insertChild(this.selectSource.first.id, newNode, 'last', insertedPrototype).update();
const { source, specifiers } = prototype2importDeclarationData(
insertedPrototype,
file.filename,
);
file
.insertChild(this.selectSource.first.id, newNode, 'last')
.addImportSpecifiers(source, specifiers)
.update();
this.history.push({
message: HistoryMessage.InsertNode,
data: {
@@ -1067,30 +1050,40 @@ export class Workspace extends EventTarget implements IWorkspace {
const targetFile = dropTarget.node?.file;
const sourceFile = dragSource.node?.file;
// dragSourcePrototype to importDeclarations
const { source, specifiers } = prototype2importDeclarationData(
dragSourcePrototype,
targetFile.filename,
);
let isValidOperation = true;
switch (dropTarget.method) {
// 直接往目标节点的 children 里添加一个节点
case DropMethod.InsertChild: {
targetFile.insertChild(dropTarget.id, newNode, 'last', dragSourcePrototype);
targetFile
.insertChild(dropTarget.id, newNode, 'last')
.addImportSpecifiers(source, specifiers);
break;
}
case DropMethod.InsertFirstChild: {
targetFile.insertChild(dropTarget.id, newNode, 'first', dragSourcePrototype);
targetFile
.insertChild(dropTarget.id, newNode, 'first')
.addImportSpecifiers(source, specifiers);
break;
}
// 往目标节点的后边插入一个节点
case DropMethod.InsertAfter: {
targetFile.insertAfter(dropTarget.id, newNode, dragSourcePrototype);
targetFile.insertAfter(dropTarget.id, newNode).addImportSpecifiers(source, specifiers);
break;
}
// 往目标节点的前方插入一个节点
case DropMethod.InsertBefore: {
targetFile.insertBefore(dropTarget.id, newNode, dragSourcePrototype);
targetFile.insertBefore(dropTarget.id, newNode).addImportSpecifiers(source, specifiers);
break;
}
// 替换目标节点
case DropMethod.ReplaceNode: {
targetFile.replaceNode(dropTarget.id, newNode, dragSourcePrototype);
targetFile.replaceNode(dropTarget.id, newNode).addImportSpecifiers(source, specifiers);
break;
}
default:
@@ -1126,6 +1119,10 @@ export class Workspace extends EventTarget implements IWorkspace {
}
}
onFilesChange(filenams: string[]) {
// do nothing
}
/**
* 刷新目标文件
* @param filenames
@@ -1139,7 +1136,6 @@ export class Workspace extends EventTarget implements IWorkspace {
},
}),
);
this.onFilesChange?.(filenames);
}
/**
@@ -1218,40 +1214,4 @@ export class Workspace extends EventTarget implements IWorkspace {
logger.error('copyFiles failed, source: %s, target: %s', sourceFilePath, targetFilePath);
}
}
/**
* 解析 serviceKey
* @param serviceKey
* @returns
*
* @example services.list => { moduleName: 'index', name: 'list' }
* @example services.sub.list => { moduleName: 'sub', name: 'list' }
* @example foo => undefined
*/
private parseServiceKey(serviceKey: string) {
const parts = serviceKey.split('.');
if (parts[0] !== 'services') {
return {};
}
let moduleName = 'index';
let name = '';
switch (parts.length) {
case 2: {
name = parts[1];
break;
}
case 3: {
moduleName = parts[1];
name = parts[2];
break;
}
default:
break;
}
return {
moduleName,
name,
};
}
}

View File

@@ -10,11 +10,25 @@ export enum FileType {
Module = 'module',
StoreEntryModule = 'storeEntryModule',
RouteModule = 'routeModule',
BlockEntryModule = 'blockEntryModule',
ServiceModule = 'serviceModule',
StoreModule = 'storeModule',
// 组件配置文件
ComponentPrototypeModule = 'componentPrototypeModule',
// 组件运行调试入口文件,一般为 `/app.js`
ComponentDemoEntryModule = 'componentDemoEntryModule',
/**
* 本地组件目录的入口文件
*/
ComponentsEntryModule = 'componentsEntryModule',
/**
* @deprecated 已废弃
*/
BlockEntryModule = 'blockEntryModule',
// jsx 类型视图文件
JsxViewModule = 'jsxViewModule',
// json 类型视图文件
JsonViewModule = 'jsonViewModule',
// 非 js 文件
@@ -73,15 +87,57 @@ export interface ITangoViewNodeData<T = JSXElement> {
}
/**
* 导变量的解析数据
* 导变量数据
*/
export interface IImportSpecifierData {
export interface IExportSpecifierData {
/**
* 来源
*/
source: string;
/**
* exported name
*/
exportedName: string;
/**
* local name
*/
localName?: string;
}
/**
* 导入变量的来源
*/
export interface IImportSpecifierSourceData {
source: string;
isDefault?: boolean;
}
/**
* 解析的导入变量数据
*/
export interface IImportSpecifierData {
/**
* 导入名
*/
importedName?: string;
/**
* 本地名
*/
localName: string;
/**
* 类型
*/
type: 'ImportDefaultSpecifier' | 'ImportSpecifier' | 'ImportNamespaceSpecifier';
}
/**
* 解析的导入语句数据
*/
export type ImportDeclarationDataType = Record<string, IImportSpecifierData[]>;
/**
* 模块导入的参数类型
* @deprecated 使用 IImportSpecifierData 代替
*/
export interface IImportDeclarationPayload {
defaultSpecifier?: string;
@@ -89,23 +145,6 @@ export interface IImportDeclarationPayload {
sourcePath: string;
}
export interface IClassPropertyNodeData {
reference: string;
propertyName: string;
propertyBody: string;
}
/**
* 服务函数参数类型
*/
export interface IServiceFunctionPayload {
[key: string]: any;
/**
* 服务函数名
*/
name: string;
}
/**
* Store 属性类型
*/
@@ -174,7 +213,7 @@ export interface IPageConfigData {
*/
name?: string;
/**
* PMS 权限码
* 权限码
*/
privilegeCode?: string;
}

View File

@@ -0,0 +1,25 @@
import { isTangoVariable, isWrappedByExpressionContainer } from '../src/helpers';
describe('assert', () => {
it('isTangoVariable', () => {
expect(isTangoVariable('tango.stores.app.name')).toBeTruthy();
expect(isTangoVariable('tango.stores?.app?.name')).toBeTruthy();
expect(isTangoVariable('tango.stores.app?.name')).toBeTruthy();
// expect(isTangoVariable('tango.copyToClipboard')).toBeTruthy();
});
it('isWrappedByExpressionContainer', () => {
expect(isWrappedByExpressionContainer('{this.foo}')).toBeTruthy();
expect(isWrappedByExpressionContainer('{!false}')).toBeTruthy();
expect(isWrappedByExpressionContainer('{[]}')).toBeTruthy();
expect(isWrappedByExpressionContainer('{{ foo: "bar" }}')).toBeTruthy();
expect(isWrappedByExpressionContainer('{[{ foo: "bar" }]}')).toBeTruthy();
expect(isWrappedByExpressionContainer('{123}')).toBeTruthy();
expect(isWrappedByExpressionContainer('{"hello"}')).toBeTruthy();
expect(isWrappedByExpressionContainer('{ foo: "bar" }')).toBeFalsy();
expect(isWrappedByExpressionContainer('{ type: tango.stores?.homePage?.tabKey }')).toBeFalsy();
expect(isWrappedByExpressionContainer('{ type: tango.stores.homePage.tabKey }')).toBeFalsy();
expect(isWrappedByExpressionContainer('{ foo: "bar" }')).toBeFalsy();
expect(isWrappedByExpressionContainer('{ color: tango.stores.app.color }')).toBeFalsy();
});
});

View File

@@ -3,26 +3,37 @@ import {
serviceConfig2Node,
isValidCode,
isValidExpressionCode,
code2expression,
} from '../src/helpers';
describe('helpers', () => {
test('isValidCode', () => {
describe('ast helpers', () => {
it('isValidCode', () => {
expect(isValidCode('() => { hello world }')).toBeFalsy();
expect(isValidCode('function() {}')).toBeFalsy();
});
test('isValidExpression', () => {
it('isValidExpression', () => {
expect(isValidExpressionCode('() => { }')).toBeTruthy();
expect(isValidExpressionCode('1')).toBeTruthy();
expect(isValidExpressionCode('a = 1')).toBeTruthy();
expect(isValidExpressionCode('1 + 1')).toBeTruthy();
expect(isValidExpressionCode('"hello"')).toBeTruthy();
expect(isValidExpressionCode('false')).toBeTruthy();
expect(isValidExpressionCode('{ bizId: "vip", type: "category" }')).toBeTruthy();
expect(isValidExpressionCode('[1,2,3]')).toBeTruthy();
expect(isValidExpressionCode('<div>hello</div>')).toBeTruthy();
expect(isValidExpressionCode('<div>hello</div>')).toBeTruthy();
expect(isValidExpressionCode('{1}')).toBeFalsy();
expect(isValidExpressionCode('{"1"}')).toBeFalsy();
expect(isValidExpressionCode('{ 1+1 }')).toBeFalsy();
expect(isValidExpressionCode('{<div>aaa</div>}')).toBeFalsy();
expect(isValidExpressionCode('{() => {}}')).toBeFalsy();
expect(isValidExpressionCode('{[1,2,3]}')).toBeFalsy();
expect(isValidExpressionCode('{tango.stores.app.title}')).toBeFalsy();
});
test('object2node', () => {
it('object2node', () => {
const node = object2node({
url: '/api/backend/clientversion/appmarket/list',
method: 'POST',
@@ -40,4 +51,22 @@ describe('helpers', () => {
} as any);
expect(node.type).toEqual('ObjectExpression');
});
it('code2expression', () => {
expect(code2expression('')).toBeUndefined();
expect(code2expression('{tango.stores.app}')).toBeUndefined();
expect(code2expression('{ type: window.bar }').type).toEqual('ObjectExpression');
expect(code2expression('() => {};').type).toEqual('ArrowFunctionExpression');
expect(code2expression('<Button>hello</Button>').type).toBe('JSXElement');
expect(code2expression('<BreadcrumbItem children="节点名称" />').type).toBe('JSXElement');
const arrayCode = `
[
{ label: 'foo', value: 'foo' },
{ label: 'bar', value: 'bar' },
]
`;
expect(code2expression(arrayCode).type).toEqual('ArrayExpression');
});
});

View File

@@ -25,40 +25,6 @@ describe('helpers', () => {
expect(code2ast('function App() {}').type).toEqual('File');
});
it('code2expression: null', () => {
expect(code2expression('')).toBeUndefined();
});
it('code2expression: object', () => {
const code = `
{
foo: 'bar',
}
`;
const node = code2expression(code);
expect(node.type).toEqual('ObjectExpression');
});
it('code2expression: arrow function', () => {
expect(code2expression('() => {};').type).toEqual('ArrowFunctionExpression');
});
it('code2expression: list', () => {
const code = `
[
{ label: 'foo', value: 'foo' },
{ label: 'bar', value: 'bar' },
]
`;
const node = code2expression(code);
expect(node.type).toEqual('ArrayExpression');
});
it('code2expression: jsxElement', () => {
const node = code2expression('<Button>hello</Button>');
expect(node.type).toEqual('JSXElement');
});
it('parse jsxElement attributes', () => {
const node = code2expression(
"<XColumn dataIndex='col' enumMap={{ 1: '已解决', 2: '未解决' }} />",
@@ -73,11 +39,6 @@ describe('helpers', () => {
});
});
it('code2expression: closed jsxElement', () => {
const node = code2expression('<BreadcrumbItem children="节点名称" />');
expect(node.type).toEqual('JSXElement');
});
it('value2node: number', () => {
expect(value2node(1).type).toEqual('NumericLiteral');
});
@@ -126,7 +87,6 @@ describe('helpers', () => {
});
describe('string helpers', () => {
it('value2code: empty array', () => {
expect(value2code([])).toEqual('[]');
});
@@ -151,9 +111,9 @@ describe('string helpers', () => {
it('namesToImportDeclarations', () => {
expect(
namesToImportDeclarations(['Button', 'Box', 'React'], {
Button: { package: '@music163/antd' },
Box: { package: '@music163/antd' },
React: { package: 'react', isDefault: true },
Button: { source: '@music163/antd' },
Box: { source: '@music163/antd' },
React: { source: 'react', isDefault: true },
}),
).toEqual([
{ sourcePath: '@music163/antd', specifiers: ['Button', 'Box'] },
@@ -169,8 +129,10 @@ describe('string helpers', () => {
expect(getRelativePath('/src/pages/index.js', '/src/blocks/sample-block/index.js')).toEqual(
'../blocks/sample-block/index.js',
);
// TODO: fix me
// expect(getRelativePath('/src/pages/', '/src/pages/index.js')).toEqual('./index.js');
expect(getRelativePath('/src/pages/index.js', '/src/components')).toEqual('../components');
expect(getRelativePath('/src/pages/index.js', '/src/components/input.js')).toEqual(
'../components/input.js',
);
});
it('getFilepath', () => {
@@ -183,7 +145,9 @@ describe('string helpers', () => {
expect(isFilepath('./pages/index.js')).toBeTruthy();
expect(isFilepath('../pages/index.js')).toBeTruthy();
expect(isFilepath('./pages/index.css')).toBeTruthy();
expect(isFilepath('/src/pages/index.js')).toBeTruthy();
expect(isFilepath('./src/pages/index.js')).toBeTruthy();
expect(isFilepath('./src/components')).toBeTruthy();
expect(isFilepath('../src/components')).toBeTruthy();
expect(isFilepath('path')).toBeFalsy();
expect(isFilepath('path-browserify')).toBeFalsy();
expect(isFilepath('@music/one')).toBeFalsy();

View File

@@ -3,6 +3,108 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.0.0-alpha.7](https://github.com/netease/tango/compare/@music163/tango-designer@1.0.0-alpha.6...@music163/tango-designer@1.0.0-alpha.7) (2024-01-16)
### Bug Fixes
- 优化变量树实现 ([#90](https://github.com/netease/tango/issues/90)) ([62d403f](https://github.com/netease/tango/commit/62d403f80a5ad08c216bc0c035ffc12c2cf329d2))
# [1.0.0-alpha.6](https://github.com/netease/tango/compare/@music163/tango-designer@1.0.0-alpha.5...@music163/tango-designer@1.0.0-alpha.6) (2023-12-29)
### Bug Fixes
- check if store value is valid ([765ea07](https://github.com/netease/tango/commit/765ea07cd5be6528e3b326e4ecb163647774d9e5))
- refactor VariableTree & Workspace ([#83](https://github.com/netease/tango/issues/83)) ([8c07821](https://github.com/netease/tango/commit/8c07821d93cea4dfc43f81ca948b845176821184))
# [1.0.0-alpha.5](https://github.com/netease/tango/compare/@music163/tango-designer@1.0.0-alpha.4...@music163/tango-designer@1.0.0-alpha.5) (2023-12-26)
### Bug Fixes
- list overflow in expSetter ([4d6b67e](https://github.com/netease/tango/commit/4d6b67eecd02b31f01d1bc896c6eeb83f6b34b35))
# [1.0.0-alpha.4](https://github.com/netease/tango/compare/@music163/tango-designer@1.0.0-alpha.3...@music163/tango-designer@1.0.0-alpha.4) (2023-12-25)
**Note:** Version bump only for package @music163/tango-designer
# [1.0.0-alpha.2](https://github.com/netease/tango/compare/@music163/tango-designer@1.0.0-alpha.1...@music163/tango-designer@1.0.0-alpha.2) (2023-12-13)
### Bug Fixes
- update variable tree with help messages and styling & add tangoConfig schema ([5e7e285](https://github.com/netease/tango/commit/5e7e285452b46888b447991b0e8548b6392acb3a))
# [1.0.0-alpha.1](https://github.com/netease/tango/compare/@music163/tango-designer@0.5.14...@music163/tango-designer@1.0.0-alpha.1) (2023-12-12)
### Bug Fixes
- add showSearch and showGroups to SettingForm & update internal icons ([#73](https://github.com/netease/tango/issues/73)) ([a8357f4](https://github.com/netease/tango/commit/a8357f45b8b0c1f2b91a856918f6bdecdc65aa73))
- render form item without setter & enhance tools ([#76](https://github.com/netease/tango/issues/76)) ([1acfa68](https://github.com/netease/tango/commit/1acfa6864b1faad1a441facc426d8d94b6b090b5))
## [0.5.14](https://github.com/netease/tango/compare/@music163/tango-designer@0.5.13...@music163/tango-designer@0.5.14) (2023-12-04)
**Note:** Version bump only for package @music163/tango-designer
## [0.5.13](https://github.com/netease/tango/compare/@music163/tango-designer@0.5.12...@music163/tango-designer@0.5.13) (2023-12-04)
### Bug Fixes
- add filesFormatter to sandbox & fix update props with imports ([#71](https://github.com/netease/tango/issues/71)) ([138cbe9](https://github.com/netease/tango/commit/138cbe9b203b370aff42c1ae8086d69edacf35e9))
## [0.5.12](https://github.com/netease/tango/compare/@music163/tango-designer@0.5.11...@music163/tango-designer@0.5.12) (2023-11-30)
### Bug Fixes
- fix render components tree ([76a5a2c](https://github.com/netease/tango/commit/76a5a2c65920bc42b019cd1f32a3cacd0d888638))
## [0.5.11](https://github.com/netease/tango/compare/@music163/tango-designer@0.5.10...@music163/tango-designer@0.5.11) (2023-11-29)
**Note:** Version bump only for package @music163/tango-designer
## [0.5.10](https://github.com/netease/tango/compare/@music163/tango-designer@0.5.9...@music163/tango-designer@0.5.10) (2023-11-28)
### Bug Fixes
- update default theme ([14cd4c5](https://github.com/netease/tango/commit/14cd4c54a1e5eed834d01921e186bdba9bbe7922))
## [0.5.9](https://github.com/netease/tango/compare/@music163/tango-designer@0.5.8...@music163/tango-designer@0.5.9) (2023-11-28)
### Bug Fixes
- update theme ([2550411](https://github.com/netease/tango/commit/2550411c1c93037931d44aa9f7822ffe8caa900b))
## [0.5.8](https://github.com/netease/tango/compare/@music163/tango-designer@0.5.7...@music163/tango-designer@0.5.8) (2023-11-27)
### Bug Fixes
- event setter ([588861f](https://github.com/netease/tango/commit/588861f6492b45954e29ceed950567781b822492))
## [0.5.7](https://github.com/netease/tango/compare/@music163/tango-designer@0.5.6...@music163/tango-designer@0.5.7) (2023-11-23)
### Bug Fixes
- update varTreeNode style ([#69](https://github.com/netease/tango/issues/69)) ([f604996](https://github.com/netease/tango/commit/f604996e56c9e4d58ff7f80cd98c3753986f081f))
## [0.5.6](https://github.com/netease/tango/compare/@music163/tango-designer@0.5.5...@music163/tango-designer@0.5.6) (2023-11-23)
**Note:** Version bump only for package @music163/tango-designer
## [0.5.5](https://github.com/netease/tango/compare/@music163/tango-designer@0.5.4...@music163/tango-designer@0.5.5) (2023-11-20)
### Bug Fixes
- pass down sandbox props ([d6ffb50](https://github.com/netease/tango/commit/d6ffb50e85573cb17ee52a797b84de5e6cedb044))
## [0.5.4](https://github.com/netease/tango/compare/@music163/tango-designer@0.5.3...@music163/tango-designer@0.5.4) (2023-11-20)
### Bug Fixes
- refactor parse expression ([#61](https://github.com/netease/tango/issues/61)) ([dbbd1dd](https://github.com/netease/tango/commit/dbbd1dddc75c532b7c9710ab0941c8680100f093))
## [0.5.3](https://github.com/netease/tango/compare/@music163/tango-designer@0.5.2...@music163/tango-designer@0.5.3) (2023-11-16)
### Bug Fixes
- allow override existing file via addFile ([6cf673e](https://github.com/netease/tango/commit/6cf673e7f6f330613e1d6331256a59900e8cbc68))
## [0.5.2](https://github.com/netease/tango/compare/@music163/tango-designer@0.5.1...@music163/tango-designer@0.5.2) (2023-11-02)
### Bug Fixes

View File

@@ -1,6 +1,6 @@
{
"name": "@music163/tango-designer",
"version": "0.5.2",
"version": "1.0.0-alpha.7",
"description": "lowcode designer",
"keywords": [
"react"
@@ -33,12 +33,12 @@
"dependencies": {
"@ant-design/icons": "^4.8.0",
"@music163/request": "^0.1.2",
"@music163/tango-context": "^0.2.4",
"@music163/tango-core": "^0.2.3",
"@music163/tango-helpers": "^0.1.5",
"@music163/tango-sandbox": "^0.1.8",
"@music163/tango-setting-form": "^0.2.7",
"@music163/tango-ui": "^0.1.8",
"@music163/tango-context": "^1.0.0-alpha.6",
"@music163/tango-core": "^1.0.0-alpha.6",
"@music163/tango-helpers": "^1.0.0-alpha.2",
"@music163/tango-sandbox": "^1.0.0-alpha.6",
"@music163/tango-setting-form": "^1.0.0-alpha.6",
"@music163/tango-ui": "^1.0.0-alpha.4",
"antd": "^4.24.2",
"cash-dom": "^8.1.2",
"classnames": "^2.3.2",

View File

@@ -1,3 +1,4 @@
export * from './drag-box';
export * from './input-kv';
export * from './variable-tree';
export * from './variable-tree-modal';

View File

@@ -0,0 +1,45 @@
import React, { useState } from 'react';
import { ModalProps, Modal } from 'antd';
import { Box } from 'coral-system';
import { IVariableTreeNode, noop, useBoolean } from '@music163/tango-helpers';
import { VariableTree, VariableTreeProps } from './variable-tree';
interface VariableTreeModalProps extends VariableTreeProps {
trigger?: React.ReactElement;
title?: ModalProps['title'];
modalProps?: ModalProps;
}
export function VariableTreeModal({
trigger,
title,
modalProps,
onSelect = noop,
...rest
}: VariableTreeModalProps) {
const [node, setNode] = useState<IVariableTreeNode>();
const [visible, { on, off }] = useBoolean(false);
return (
<Box>
{React.cloneElement(trigger, { onClick: on })}
<Modal
title={title}
open={visible}
onCancel={off}
okButtonProps={{
disabled: !node,
}}
onOk={() => {
if (node) {
onSelect(node);
off();
}
}}
width="60%"
{...modalProps}
>
<VariableTree height={400} onSelect={setNode} {...rest} />
</Modal>
</Box>
);
}

View File

@@ -1,631 +0,0 @@
import React, { useMemo, useState } from 'react';
import {
Tree,
Button,
Form,
Input,
Space,
ModalProps,
Modal,
Radio,
Empty,
Popconfirm,
Tooltip,
} from 'antd';
import { Box, Text, css } from 'coral-system';
import {
PlusOutlined,
FunctionOutlined,
DeleteOutlined,
CopyOutlined,
EyeOutlined,
} from '@ant-design/icons';
import {
filterTreeData,
isFunction,
isNil,
isString,
noop,
useBoolean,
} from '@music163/tango-helpers';
import {
Panel,
InputCode,
Search,
JsonView,
JsonViewProps,
CopyClipboard,
} from '@music163/tango-ui';
import { isValidExpressionCode } from '@music163/tango-core';
export interface IVariableTreeNode {
/**
* 唯一标识符
*/
key: string;
/**
* 标题
*/
title?: string;
/**
* 是否可选中
*/
selectable?: boolean;
/**
* 展示添加子节点的图标
*/
showAddChildIcon?: boolean;
/**
* 展示删除删除图标
*/
showDeleteIcon?: boolean;
/**
* 结点类型,用来展示图标
*/
type?: 'function' | 'property';
/**
* 定义的原始值
*/
raw?: any;
/**
* 子结点
*/
children?: IVariableTreeNode[];
[key: string]: any;
}
type VariableModelType = 'preview' | 'define' | 'add';
export interface EditableVariableTreeProps {
/**
* 允许的变量面板
*/
modes?: VariableModelType[];
/**
* 默认的变量面板
*/
defaultMode?: VariableModelType;
/**
* 数据源
*/
dataSource?: VariableTreeProps['dataSource'];
/**
* 点击添加变量的回调
*/
onAddVariable?: AddNodeFormProps['onSubmit'];
/**
* 预览值取值函数
*/
getPreviewValue?: (node: IVariableTreeNode) => unknown;
/**
* 选择列表项时的回调
*/
onSelect?: (data: IVariableTreeNode) => void;
/**
* 保存结点定义的回调
*/
onSave?: NodeDefineProps['onSave'];
/**
* 删除结点定义的回调
*/
onDeleteVariable?: (storeName: string, stateName: string) => void;
/**
* 删除模型时的回调
*/
onDeleteStore?: (storeName: string) => void;
/**
* 高度
*/
height?: number | string;
/**
* 搜索框的样式
*/
searchStyle?: React.CSSProperties;
/**
* 搜索框的后置结点
*/
searchAddonAfter?: React.ReactNode;
}
const previewOptions = [
{ label: '运行时', value: 'preview' },
{ label: '定义', value: 'define' },
];
export function EditableVariableTree({
height,
searchAddonAfter,
searchStyle,
dataSource,
onAddVariable = noop,
onDeleteVariable = noop,
onDeleteStore = noop,
getPreviewValue = noop,
onSelect = noop,
onSave = noop,
modes = ['add', 'preview', 'define'],
defaultMode = 'preview',
}: EditableVariableTreeProps) {
const [keyword, setKeyword] = useState('');
const [node, setNode] = useState<IVariableTreeNode>();
const [mode, setMode] = useState<EditableVariableTreeProps['defaultMode']>(defaultMode);
const hasPanelSwitch = modes.includes('define') && modes.includes('preview');
const treeData = useMemo(() => {
const pattern = new RegExp(keyword, 'ig');
return keyword
? filterTreeData(dataSource, (leaf) => pattern.test(leaf.title), 'children', true)
: dataSource;
}, [keyword, dataSource]);
return (
<Box
className="EditableVariableTree"
display="flex"
overflow="auto"
height={height}
position="relative"
>
<Box p="l" width="40%" borderRight="solid" borderColor="line.normal">
<Box mb="m" position="sticky" top="0" bg="white" zIndex={2}>
<Input.Group compact>
<Search
placeholder="请输入变量名"
onChange={(val) => setKeyword(val?.trim())}
style={searchStyle}
/>
{searchAddonAfter}
</Input.Group>
</Box>
<VariableTree
dataSource={treeData}
showViewIcon
onSelect={(item) => {
setNode(item);
mode === 'add' && setMode(defaultMode);
onSelect(item);
}}
onView={(item) => {
setNode(item);
mode === 'add' && setMode(defaultMode);
}}
onAdd={(item) => {
setNode(item);
setMode('add');
}}
onRemove={(item) => {
const [type, storeName] = item.key.split('.');
setNode(null);
onDeleteStore(storeName);
}}
/>
</Box>
{node ? (
<Panel
title={
{
preview: '变量值预览',
add: '添加变量',
define: '变量定义',
}[mode]
}
extra={
hasPanelSwitch && mode !== 'add' ? (
<Radio.Group
optionType="button"
buttonStyle="solid"
size="small"
value={mode}
onChange={(e) => setMode(e.target.value)}
options={previewOptions}
/>
) : null
}
width="60%"
borderRadius="m"
position="sticky"
top="0"
bodyProps={{ px: 'm' }}
>
{mode === 'preview' && (
<ValuePreview
value={getPreviewValue(node)}
onSelect={(valuePath) => {
return ['tango', node.key.replaceAll('.', '?.'), valuePath].join('.');
}}
/>
)}
{mode === 'define' && (
<NodeDefineForm
node={node}
onSave={onSave}
onDelete={(item) => {
const [type, storeName, stateName] = item.key.split('.');
onDeleteVariable(storeName, stateName);
setNode(null);
}}
/>
)}
{mode === 'add' && (
<AddNodeForm
parentNode={node}
onCancel={() => {
setMode(defaultMode);
setNode(null);
}}
onSubmit={(storeName, data) => {
onAddVariable(storeName, data);
setMode(defaultMode);
setNode(null);
}}
/>
)}
</Panel>
) : (
<Panel title="提示" flex="1" position="sticky" top="0">
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="请从左侧列表中选择一个变量" />
</Panel>
)}
</Box>
);
}
interface EditableVariableTreeModalProps extends EditableVariableTreeProps {
trigger?: React.ReactElement;
title?: ModalProps['title'];
modalProps?: ModalProps;
}
export function EditableVariableTreeModal({
trigger,
title,
modalProps,
onSelect = noop,
...rest
}: EditableVariableTreeModalProps) {
const [node, setNode] = useState<IVariableTreeNode>();
const [visible, { on, off }] = useBoolean(false);
return (
<Box>
{React.cloneElement(trigger, { onClick: on })}
<Modal
title={title}
open={visible}
onCancel={off}
okButtonProps={{
disabled: !node,
}}
onOk={() => {
if (node) {
onSelect(node);
off();
}
}}
width="60%"
{...modalProps}
>
<EditableVariableTree height={480} onSelect={setNode} {...rest} />
</Modal>
</Box>
);
}
const varTreeStyle = css`
overflow: auto;
.ant-tree {
font-family: Consolas, Menlo, Courier, monospace;
}
.ant-tree .ant-tree-treenode {
padding: 0;
}
.ant-tree.ant-tree-directory .ant-tree-treenode::before {
bottom: 0;
}
.ant-tree-indent-unit {
width: 12px;
}
.anticon-function {
margin-left: 4px;
color: var(--tango-colors-text2);
}
`;
interface VariableTreeProps {
dataSource?: IVariableTreeNode[];
onSelect?: (data: IVariableTreeNode) => void;
onAdd?: (data: IVariableTreeNode) => void;
onRemove?: (data: IVariableTreeNode) => void;
onCopy?: (data: IVariableTreeNode) => void;
onView?: (data: IVariableTreeNode) => void;
showDeleteIcon?: boolean;
showViewIcon?: boolean;
}
export function VariableTree({
dataSource = [],
onSelect = noop,
onAdd = noop,
onRemove = noop,
onCopy = noop,
onView = noop,
showDeleteIcon = false,
showViewIcon = false,
}: VariableTreeProps) {
return (
<Box className="VariableTree" css={varTreeStyle}>
<Tree
blockNode
showIcon={false}
defaultExpandAll
treeData={dataSource}
onSelect={(keys, detail) => {
onSelect(detail.node);
}}
titleRender={(node) => {
const isLeaf = !node.children;
if (isLeaf) {
const isDeletable = node.showDeleteIcon ?? showDeleteIcon;
return (
<Box display="flex" justifyContent="space-between" alignItems="center">
<Text>
{node.title}
{node.type === 'function' && <FunctionOutlined />}
</Text>
<Box>
{isDeletable && (
<Popconfirm
title="确认删除吗?该操作会导致引用此模型的代码报错,请谨慎操作!"
onConfirm={() => onRemove(node)}
>
<Button
type="text"
size="small"
icon={<DeleteOutlined />}
onClick={(e) => {
e.stopPropagation();
}}
/>
</Popconfirm>
)}
<CopyClipboard text={`tango.${node.key.replaceAll('.', '?.')}`}>
{({ copied, onClick }) => {
const label = copied ? '已复制' : '复制变量路径';
return (
<Tooltip title={label}>
<Button
type="text"
size="small"
icon={<CopyOutlined />}
onClick={(e) => {
e.stopPropagation();
onClick();
onCopy(node);
}}
/>
</Tooltip>
);
}}
</CopyClipboard>
{showViewIcon && (
<Tooltip title="查看变量详情">
<Button
type="text"
size="small"
icon={<EyeOutlined />}
onClick={(e) => {
e.stopPropagation();
onView(node);
}}
/>
</Tooltip>
)}
</Box>
</Box>
);
}
return (
<Box display="flex" alignItems="center" justifyContent="space-between">
<Text mr="m">{node.title}</Text>
<Box>
{node.showAddChildIcon && (
<Tooltip title={`${node.title} 中添加变量`}>
<Button
type="text"
size="small"
icon={<PlusOutlined />}
onClick={(e) => {
e.stopPropagation();
onAdd(node);
}}
/>
</Tooltip>
)}
</Box>
</Box>
);
}}
/>
</Box>
);
}
interface NodePreviewProps {
value?: unknown;
/**
* 选择预览结点的回调
*/
onSelect?: JsonViewProps['onCopy'];
}
export function ValuePreview({ value, onSelect }: NodePreviewProps) {
if (isNil(value)) {
return <span></span>;
}
if (isFunction(value)) {
return <span></span>;
}
if (typeof value === 'object') {
return <JsonView src={value} enableCopy onCopy={onSelect} />;
}
return (
<InputCode
shape="inset"
value={isString(value) ? `"${value}"` : String(value)}
editable={false}
/>
);
}
interface NodeDefineProps {
node: IVariableTreeNode;
onSave?: (code: string, node: IVariableTreeNode) => void;
onDelete?: (node: IVariableTreeNode) => void;
}
/**
* 变量值定义面板
*/
function NodeDefineForm({ node, onSave = noop, onDelete = noop }: NodeDefineProps) {
const [value, setValue] = useState('');
const [error, setError] = useState('');
const [editable, { on, off }] = useBoolean();
if (isNil(node.raw)) {
return <Empty description="缺失定义数据" />;
}
return (
<Box>
<InputCode
shape="inset"
value={node.raw}
onChange={(nextValue) => setValue(nextValue)}
editable={editable}
onBlur={() => {
if (!isValidExpressionCode(value)) {
setError('代码格式错误,请检查代码语法!');
} else {
setError('');
}
}}
/>
<Box color="red" my="m">
{error}
</Box>
<Box mt="l">
{editable ? (
<Space>
<Button
onClick={() => {
off();
setError('');
}}
>
</Button>
<Button
type="primary"
onClick={() => {
if (error) {
return;
}
onSave(value, node);
off();
}}
>
</Button>
</Space>
) : (
<Space>
<Button onClick={on}></Button>
<Popconfirm
title="确认删除此变量吗?该操作会导致引用该变量的代码报错!"
onConfirm={() => {
onDelete(node);
}}
>
<Button danger></Button>
</Popconfirm>
</Space>
)}
</Box>
</Box>
);
}
interface AddNodeFormProps {
parentNode: IVariableTreeNode;
onCancel?: React.MouseEventHandler<HTMLButtonElement>;
onSubmit?: (storeName: string, data: { name: string; initialValue: string }) => void;
}
function AddNodeForm({ parentNode, onCancel, onSubmit }: AddNodeFormProps) {
return (
<Form
layout="vertical"
autoComplete="off"
validateTrigger="onBlur"
onFinish={(values) => {
if (onSubmit && parentNode) {
const [type, storeName] = parentNode.key.split('.');
onSubmit(storeName, values);
}
}}
>
<Form.Item label="所属模型">
<Input value={parentNode?.key} disabled />
</Form.Item>
<Form.Item
label="变量名"
name="name"
rules={[
{ required: true },
{ pattern: /^\w+$/, message: '非法的变量标识符' },
{
validator(_, value) {
const found = parentNode.children.find((item) => item.title === value);
return found
? Promise.reject(new Error('和已有变量名冲突,请换一个名字!'))
: Promise.resolve();
},
},
]}
>
<Input placeholder="请输入变量名" />
</Form.Item>
<Form.Item
label="初值"
name="initialValue"
rules={[
{ required: true },
{
validator(_, value) {
return isValidExpressionCode(value)
? Promise.resolve()
: Promise.reject(new Error('代码存在语法错误,请输入合法的代码片段!'));
},
},
]}
>
<InputCode shape="inset" placeholder="请输入初值" />
</Form.Item>
<Form.Item>
<Space>
<Button onClick={onCancel}></Button>
<Button type="primary" htmlType="submit">
</Button>
</Space>
</Form.Item>
</Form>
);
}

View File

@@ -0,0 +1,182 @@
import { isValidFunctionCode, url2serviceName } from '@music163/tango-helpers';
import { InputCode } from '@music163/tango-ui';
import { Button, Dropdown, Form, FormProps, Input, Select, Space } from 'antd';
import { Box } from 'coral-system';
import React, { useState } from 'react';
interface AddServiceFormProps extends FormProps {
initialValues?: object;
onSubmit: (values: object) => void;
onCancel: () => void;
serviceModules: object[];
serviceNames: string[];
}
export function AddServiceForm({
initialValues,
onSubmit,
onCancel,
serviceModules,
serviceNames,
...formProps
}: AddServiceFormProps) {
const isModifyMode = !!initialValues?.['name']; // 是否为更新模式
const [disabled, setDisabled] = useState(isModifyMode);
const [form] = Form.useForm();
return (
<Form
form={form}
labelCol={{ span: 6 }}
wrapperCol={{ span: 17 }}
colon={false}
layout="horizontal"
initialValues={initialValues}
onFinish={(values) => {
onSubmit(values);
}}
{...formProps}
>
<Form.Item
label="命名空间"
name="moduleName"
rules={[
{
required: true,
},
]}
>
<Select options={serviceModules} disabled={disabled || isModifyMode} />
</Form.Item>
<Form.Item
label="方法名"
name="name"
rules={[
{ required: true },
{ pattern: /^[a-z]\w+$/, message: '请输入合法的方法名称' },
!isModifyMode && {
validator(_, value) {
const isExist = serviceNames.includes(value);
return isExist
? Promise.reject(new Error('重复的方法名,请换一个!'))
: Promise.resolve();
},
},
]}
extra={
!disabled && (
<Box>
<Button
type="link"
size="small"
onClick={() => {
const url = form.getFieldValue('url');
if (!url) {
return;
}
const funcName = url2serviceName(url);
form.setFieldValue('name', funcName);
}}
>
</Button>
</Box>
)
}
>
<Input placeholder="请输入数据服务调用名称" disabled={disabled || isModifyMode} />
</Form.Item>
<Form.Item label="路径" name="url" rules={[{ required: true, type: 'url' }]}>
<Input placeholder="请输入数据服务调用名称" disabled={disabled || isModifyMode} />
</Form.Item>
<Form.Item label="方法" name="method">
<Select
options={[
{ label: 'GET', value: 'get' },
{ label: 'POST', value: 'post' },
]}
placeholder="请选择 HTTP 请求方法"
disabled={disabled}
/>
</Form.Item>
<Form.Item
label="格式化响应"
name="formatter"
tooltip="提供格式化函数,对结果进行格式化"
validateTrigger="onBlur"
rules={[
{
validator(_, value) {
if (!value) {
return Promise.resolve();
}
return isValidFunctionCode(value)
? Promise.resolve()
: Promise.reject(new Error('请提供合法的函数代码!'));
},
},
]}
extra={
!disabled && (
<Box>
<Dropdown
menu={{
items: [
{ label: '直接返回(默认)', key: 'res => res' },
{ label: '返回数据部分(云音乐标准规范)', key: 'res => res.data' },
],
onClick: ({ key }) => {
form.setFieldValue('formatter', key);
},
}}
>
<Button type="link" size="small">
使
</Button>
</Dropdown>
</Box>
)
}
>
<InputCode showLineNumbers placeholder="res => res" editable={!disabled} />
</Form.Item>
<Form.Item wrapperCol={{ offset: 6, span: 18 }}>
{isModifyMode ? (
<Space>
{!disabled ? (
<>
<Button
onClick={() => {
setDisabled(true);
}}
>
</Button>
<Button type="primary" htmlType="submit">
</Button>
</>
) : (
<Button
onClick={() => {
setDisabled(false);
}}
>
</Button>
)}
</Space>
) : (
<Space>
<Button onClick={onCancel}></Button>
<Button type="primary" htmlType="submit">
</Button>
</Space>
)}
</Form.Item>
</Form>
);
}

View File

@@ -0,0 +1,124 @@
import React from 'react';
import { Button, Form, Input, Space } from 'antd';
import { isValidExpressionCode } from '@music163/tango-core';
import { InputCode, Panel } from '@music163/tango-ui';
import { IVariableTreeNode } from '@music163/tango-helpers';
export interface AddStoreVariableFormProps {
parentNode: IVariableTreeNode;
onCancel?: React.MouseEventHandler<HTMLButtonElement>;
onSubmit?: (storeName: string, data: { name: string; initialValue: string }) => void;
}
export function AddStoreVariableForm({
parentNode,
onCancel,
onSubmit,
}: AddStoreVariableFormProps) {
return (
<Form
layout="vertical"
autoComplete="off"
validateTrigger="onBlur"
onFinish={(values) => {
if (onSubmit && parentNode) {
const [type, storeName] = parentNode.key.split('.');
onSubmit(storeName, values);
}
}}
>
<Form.Item label="所属模型">
<Input value={parentNode?.key} disabled />
</Form.Item>
<Form.Item
label="变量名"
name="name"
rules={[
{ required: true },
{ pattern: /^\w+$/, message: '非法的变量标识符' },
{
validator(_, value) {
const found = parentNode.children.find((item) => item.title === value);
return found
? Promise.reject(new Error('和已有变量名冲突,请换一个名字!'))
: Promise.resolve();
},
},
]}
>
<Input placeholder="请输入变量名" />
</Form.Item>
<Form.Item
label="初值"
name="initialValue"
rules={[
{ required: true },
{
validator(_, value) {
return value && isValidExpressionCode(value)
? Promise.resolve()
: Promise.reject(new Error('代码存在语法错误,请输入合法的代码片段!'));
},
},
]}
>
<InputCode shape="inset" placeholder="请输入初值" />
</Form.Item>
<Form.Item>
<Space>
<Button onClick={onCancel}></Button>
<Button type="primary" htmlType="submit">
</Button>
</Space>
</Form.Item>
</Form>
);
}
export interface AddStoreFormProps {
storeNames?: string[];
onCancel?: () => void;
onSubmit?: (data: { name: string }) => void;
}
export function AddStoreForm({ storeNames = [], onCancel, onSubmit }: AddStoreFormProps) {
return (
<Panel title="添加模型" subTitle="模型可以用来组织一组变量" bodyProps={{ p: 'l' }}>
<Form
autoComplete="off"
colon={false}
onFinish={(values) => {
onSubmit && onSubmit(values);
}}
>
<Form.Item
label="模型名称"
name="name"
rules={[
{ required: true },
{ pattern: /^[a-z]\w+$/, message: '非法的变量标识符' },
{
validator(_, value) {
const found = storeNames.includes(value);
return found
? Promise.reject(new Error('已存在该模型名,请换一个!'))
: Promise.resolve();
},
},
]}
>
<Input placeholder="请输入模型名称" />
</Form.Item>
<Form.Item label=" ">
<Space>
<Button onClick={onCancel}></Button>
<Button type="primary" htmlType="submit">
</Button>
</Space>
</Form.Item>
</Form>
</Panel>
);
}

View File

@@ -0,0 +1,419 @@
import React, { useCallback, useMemo, useState } from 'react';
import {
IVariableTreeNode,
filterTreeData,
isServiceVariablePath,
isStoreVariablePath,
noop,
parseServiceVariablePath,
} from '@music163/tango-helpers';
import { css, Box, Text } from 'coral-system';
import { Button, Popconfirm, Tooltip, Tree } from 'antd';
import {
CopyOutlined,
DeleteOutlined,
EyeOutlined,
FunctionOutlined,
PlusOutlined,
} from '@ant-design/icons';
import { CopyClipboard, Panel, Search } from '@music163/tango-ui';
import { AddStoreForm, AddStoreVariableForm } from './add-store';
import { AddServiceForm } from './add-service';
import {
NodeCommonDetail,
ValueDefine,
ValueDefineProps,
ValueDetail,
ValueDetailProps,
} from './value-detail';
import { ValuePreview } from './value-preview';
import { ServicePreview } from './service-preview';
const varTreeStyle = css`
overflow: auto;
position: relative;
.ant-tree {
font-family: Consolas, Menlo, Courier, monospace;
}
.ant-tree-node-content-wrapper.ant-tree-node-content-wrapper-normal {
width: calc(100% - 50px);
}
.ant-tree .ant-tree-treenode {
padding: 0;
}
.ant-tree.ant-tree-directory .ant-tree-treenode::before {
bottom: 0;
}
.ant-tree-indent-unit {
width: 12px;
}
`;
type SelectNodeCallback = (data: IVariableTreeNode) => void;
type DetailModeType =
| 'detail'
| 'storeVariableDetail'
| 'serviceDetail'
| 'addVariable'
| 'addStore'
| 'addService';
interface IVariableTreeRenderDetailState {
mode: DetailModeType;
setMode: (mode: DetailModeType) => void;
activeNode: IVariableTreeNode;
clear: () => void;
}
export interface VariableTreeProps {
defaultValueDetailMode?: ValueDetailProps['defaultMode'];
dataSource: IVariableTreeNode[];
appContext?: object;
serviceModules?: any[];
getPreviewValue?: (node: IVariableTreeNode) => unknown;
getServiceData?: (serviceKey: string) => object;
getServiceNames?: (moduleName: string) => string[];
getStoreNames?: () => string[];
onSelect?: SelectNodeCallback;
onAddStoreVariable?: (storeName: string, data: any) => void;
onUpdateStoreVariable?: ValueDefineProps['onSave'];
onRemoveStoreVariable?: (variableKey: string) => void;
onAddStore?: (newStoreName: string) => void;
onAddService?: (data: object) => void;
onUpdateService?: (data: object) => void;
onRemoveService?: (variableKey: string) => void;
onCopy?: (data: IVariableTreeNode) => void;
onView?: SelectNodeCallback;
height?: number | string;
showViewButton?: boolean;
/**
* 自定义头部
*/
renderHeaderExtra?: (
props: VariableTreeProps,
state: IVariableTreeRenderDetailState,
) => React.ReactNode;
/**
* 自定义变量详情渲染
* @param props
* @param state
* @returns
*/
renderDetail?: (
props: VariableTreeProps,
state: IVariableTreeRenderDetailState,
) => React.ReactNode;
}
export function VariableTree(props: VariableTreeProps) {
const {
dataSource = [],
serviceModules = [],
appContext = {},
defaultValueDetailMode,
onSelect = noop,
onAddStoreVariable = noop,
onAddStore = noop,
onAddService = noop,
onUpdateStoreVariable = noop,
onUpdateService = noop,
onRemoveStoreVariable,
onRemoveService,
onCopy = noop,
onView = noop,
getServiceData,
getServiceNames,
getStoreNames,
getPreviewValue = noop,
showViewButton,
renderHeaderExtra,
renderDetail: renderDetailProp,
...rest
} = props;
const [keyword, setKeyword] = useState('');
const [activeNode, setActiveNode] = useState<IVariableTreeNode>();
const [mode, setMode] = useState<DetailModeType>();
const clear = useCallback(() => {
setActiveNode(null);
setMode(null);
}, []);
const selectNode = useCallback((node: IVariableTreeNode, callback?: SelectNodeCallback) => {
if (isStoreVariablePath(node.key)) {
setMode('storeVariableDetail');
} else if (isServiceVariablePath(node.key)) {
setMode('serviceDetail');
} else {
setMode('detail');
}
setActiveNode(node);
callback?.(node);
}, []);
const treeData = useMemo(() => {
const pattern = new RegExp(keyword, 'ig');
return keyword
? filterTreeData(dataSource, (leaf) => pattern.test(leaf.title), 'children', true)
: dataSource;
}, [keyword, dataSource]);
const state = { activeNode, mode, setMode, clear };
return (
<Box display="flex" columnGap="l" className="VariableTree" css={varTreeStyle} {...rest}>
<Box className="VariableList" width="40%">
{renderHeaderExtra?.(props, state)}
<Box mb="m" position="sticky" top="0" bg="white" zIndex={2}>
<Search placeholder="请输入变量名" onChange={(val) => setKeyword(val?.trim())} />
</Box>
<Tree
blockNode
showIcon={false}
defaultExpandAll
treeData={treeData}
onSelect={(keys, detail) => {
selectNode(detail.node, onSelect);
}}
titleRender={(node) => {
const isLeaf = !node.children;
if (isLeaf) {
const showView = node.showViewButton ?? showViewButton;
return (
<Box display="flex" justifyContent="space-between" alignItems="center">
<Text flex="1" truncated>
{node.title}
{node.type === 'function' && (
<Text color="text3" ml="m">
<FunctionOutlined />
</Text>
)}
</Text>
<Box flex="0 0 72px" textAlign="right">
{node.showRemoveButton && (
<Popconfirm
title={`确认删除吗 ${node.title}?该操作会导致引用此模型的代码报错,请谨慎操作!`}
onConfirm={() => {
if (isStoreVariablePath(node.key)) {
onRemoveStoreVariable(node.key);
} else {
onRemoveService(node.key);
}
}}
>
<Button
type="text"
size="small"
icon={<DeleteOutlined />}
onClick={(e) => {
e.stopPropagation();
}}
/>
</Popconfirm>
)}
<CopyClipboard text={`tango.${node.key.replaceAll('.', '?.')}`}>
{({ copied, onClick }) => {
const label = copied ? '已复制' : '复制变量路径';
return (
<Tooltip title={label}>
<Button
type="text"
size="small"
icon={<CopyOutlined />}
onClick={(e) => {
e.stopPropagation();
onClick();
onCopy(node);
}}
/>
</Tooltip>
);
}}
</CopyClipboard>
{showView && (
<Tooltip title="查看变量详情">
<Button
type="text"
size="small"
icon={<EyeOutlined />}
onClick={(e) => {
e.stopPropagation();
selectNode(node, onView);
}}
/>
</Tooltip>
)}
</Box>
</Box>
);
}
return (
<Box display="flex" alignItems="center" justifyContent="space-between">
<Text mr="m">{node.title}</Text>
<Box>
{/^\$?stores$/.test(node.key) && (
<Tooltip title="新建数据模型">
<Button
type="text"
size="small"
icon={<PlusOutlined />}
onClick={(e) => {
e.stopPropagation();
setMode('addStore');
}}
/>
</Tooltip>
)}
{node.showAddButton && /^stores\.[a-zA-Z0-9]+$/.test(node.key) && (
<Tooltip title={`${node.title} 中添加变量`}>
<Button
type="text"
size="small"
icon={<PlusOutlined />}
onClick={(e) => {
e.stopPropagation();
setActiveNode(node);
setMode('addVariable');
}}
/>
</Tooltip>
)}
{/^services(\.[a-zA-Z0-9]+)?$/.test(node.key) && (
<Tooltip title={`${node.title} 中添加服务函数`}>
<Button
type="text"
size="small"
icon={<PlusOutlined />}
onClick={(e) => {
e.stopPropagation();
setActiveNode(node);
setMode('addService');
}}
/>
</Tooltip>
)}
</Box>
</Box>
);
}}
/>
</Box>
<Box className="VariableDetail" flex="1" position="sticky" top="0" overflow="auto">
{(function renderDetail() {
const customRenderer = renderDetailProp?.(props, state);
if (customRenderer) {
return customRenderer;
}
if (mode === 'detail') {
return <NodeCommonDetail data={activeNode} />;
}
if (mode === 'storeVariableDetail') {
return (
<ValueDetail key={activeNode.key} defaultMode={defaultValueDetailMode}>
{(previewMode) =>
previewMode === 'runtime' ? (
<ValuePreview
value={getPreviewValue(activeNode)}
onCopy={(valuePath) => {
return ['tango', activeNode.key.replaceAll('.', '?.'), valuePath].join('.');
}}
/>
) : (
<ValueDefine data={activeNode} onSave={onUpdateStoreVariable} />
)
}
</ValueDetail>
);
}
if (mode === 'serviceDetail') {
return (
<>
<Panel shape="solid" title="服务函数配置">
<AddServiceForm
key={activeNode.key}
serviceModules={serviceModules}
serviceNames={(function () {
const { moduleName } = parseServiceVariablePath(activeNode.key);
return getServiceNames?.(moduleName) || [];
})()}
initialValues={{
...getServiceData?.(activeNode.key),
}}
onCancel={clear}
onSubmit={(values) => {
onUpdateService(values);
clear();
}}
/>
</Panel>
<Panel shape="solid" title="服务函数预览" mt="l">
<ServicePreview
key={activeNode.key}
appContext={appContext}
functionKey={activeNode.key}
/>
</Panel>
</>
);
}
if (mode === 'addVariable') {
return (
<AddStoreVariableForm
parentNode={activeNode}
onSubmit={(storeName, data) => {
onAddStoreVariable(storeName, data);
clear();
}}
onCancel={() => {
clear();
}}
/>
);
}
if (mode === 'addStore') {
return (
<AddStoreForm
storeNames={getStoreNames?.() || []}
onSubmit={({ name }) => {
onAddStore(name);
clear();
}}
onCancel={() => {
clear();
}}
/>
);
}
if (mode === 'addService') {
return (
<Panel shape="solid" title="创建服务函数">
<AddServiceForm
key={activeNode.key}
serviceModules={serviceModules}
serviceNames={activeNode.children?.map((item) => item.title) || []}
initialValues={{
moduleName: activeNode.title,
}}
onCancel={() => {
clear();
}}
onSubmit={(values) => {
onAddService(values);
clear();
}}
/>
</Panel>
);
}
})()}
</Box>
</Box>
);
}

View File

@@ -0,0 +1,93 @@
import React, { useState } from 'react';
import { Box } from 'coral-system';
import { Button, Empty } from 'antd';
import { PlayCircleOutlined } from '@ant-design/icons';
import { InputCode, Panel, JsonView } from '@music163/tango-ui';
import { isNil, logger, code2object, getValue } from '@music163/tango-helpers';
export interface ServicePreviewProps {
appContext?: any;
functionKey?: string;
}
export function ServicePreview({ appContext, functionKey }: ServicePreviewProps) {
const [payload, setPayload] = useState({});
const [result, setResult] = useState<any>();
const [error, setError] = useState('');
return (
<Box>
<Panel title="请求参数" bodyProps={{ px: 'l' }}>
<InputCode
placeholder={'添加请求参数,对象格式,如 { key: value }'}
editable
showLineNumbers
onChange={(value: string) => {
const obj = code2object(value);
setPayload(obj);
}}
/>
<Button
block
type="primary"
style={{ margin: '8px 0' }}
disabled={!appContext}
onClick={() => {
if (!appContext) {
setError('执行上下文未准备好,请关闭面板重试');
return;
}
try {
const fn = getValue(appContext, functionKey);
fn(payload).then((data: any) => {
setError('');
setResult(data);
});
} catch (err) {
setError('接口调用失败,请检查参数是否正确');
logger.error(err);
}
}}
icon={<PlayCircleOutlined />}
>
</Button>
</Panel>
<Panel title="请求响应" bodyProps={{ px: 'l' }}>
{error || (result ? <ResponseDataPreview data={result} /> : '点击预览按钮测试接口返回值')}
</Panel>
</Box>
);
}
interface ResponseDataPreviewProps {
data?: object;
}
function ResponseDataPreview({ data }: ResponseDataPreviewProps) {
let ret: React.ReactNode;
if (!isNil(data)) {
switch (typeof data) {
case 'object':
ret = <JsonView src={data as object} />;
break;
default:
ret = String(data);
break;
}
}
const initialRet = <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />;
return (
<Box
className="ResponseDataPreview"
overflow="auto"
height="auto"
minHeight={260}
marginTop={10}
>
{ret || initialRet}
</Box>
);
}

View File

@@ -0,0 +1,122 @@
import React, { useState } from 'react';
import { IVariableTreeNode, isNil, noop, useBoolean } from '@music163/tango-helpers';
import { Empty, Space, Button, Radio, Alert } from 'antd';
import { Box } from 'coral-system';
import { InputCode, Panel } from '@music163/tango-ui';
import { isValidExpressionCode } from '@music163/tango-core';
const previewOptions = [
{ label: '运行时', value: 'runtime' },
{ label: '定义', value: 'define' },
];
export type ValueDetailModeType = 'runtime' | 'define';
export interface ValueDetailProps {
defaultMode?: ValueDetailModeType;
children: (mode: ValueDetailModeType) => React.ReactNode;
}
export function ValueDetail({ defaultMode = 'runtime', children }: ValueDetailProps) {
const [mode, setMode] = useState<ValueDetailModeType>(defaultMode);
return (
<Panel
title="变量详情"
shape="solid"
extra={
<Radio.Group
optionType="button"
buttonStyle="solid"
size="small"
value={mode}
onChange={(e) => {
setMode(e.target.value);
}}
options={previewOptions}
/>
}
>
{children(mode)}
</Panel>
);
}
export interface ValueDefineProps {
data: IVariableTreeNode;
onSave?: (variableKey: string, code: string) => void;
}
/**
* 变量值定义面板
*/
export function ValueDefine({ data, onSave = noop }: ValueDefineProps) {
const [value, setValue] = useState(data.raw);
const [error, setError] = useState('');
const [editable, { on, off }] = useBoolean();
if (isNil(data.raw)) {
return <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="缺失定义数据" />;
}
return (
<Box>
<InputCode
shape="inset"
value={value}
onChange={(nextValue) => setValue(nextValue)}
readOnly={!editable}
onBlur={() => {
if (!value) {
setError('输入内容不可为空!');
} else if (value && !isValidExpressionCode(`foo = ${value}`)) {
// 这里先保证输入是一个合法的表达式
setError('代码格式错误,请检查代码语法!');
} else {
setError('');
}
}}
/>
<Box color="red" my="m">
{error}
</Box>
<Box mt="l">
{editable ? (
<Space>
<Button
onClick={() => {
off();
setError('');
}}
>
</Button>
<Button
type="primary"
onClick={() => {
if (error) {
return;
}
onSave(data.key, value);
off();
}}
>
</Button>
</Space>
) : (
<Button onClick={on}></Button>
)}
</Box>
</Box>
);
}
export interface NodeCommonDetailProps {
data: IVariableTreeNode;
}
export function NodeCommonDetail({ data }: NodeCommonDetailProps) {
if (!data?.help) {
return <div />;
}
return <Alert type="info" message={`使用说明:${data.help}`} closable />;
}

View File

@@ -0,0 +1,34 @@
import React from 'react';
import { isFunction, isNil, isString } from '@music163/tango-helpers';
import { InputCode, JsonView, JsonViewProps } from '@music163/tango-ui';
import { Empty } from 'antd';
interface ValuePreviewProps {
value?: unknown;
/**
* 选择预览结点的回调
*/
onCopy?: JsonViewProps['onCopy'];
}
export function ValuePreview({ value, onCopy }: ValuePreviewProps) {
if (isNil(value)) {
return <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂时没有可预览的数据" />;
}
if (isFunction(value)) {
return <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂不支持预览函数" />;
}
if (typeof value === 'object') {
return <JsonView src={value} enableCopy onCopy={onCopy} />;
}
return (
<InputCode
shape="inset"
value={isString(value) ? `"${value}"` : String(value)}
editable={false}
/>
);
}

View File

@@ -36,6 +36,8 @@ export function DesignerPanel(props: DesignerPanelProps) {
height="48px"
bg="colors.custom.topNavBg"
color="colors.custom.topNavColor"
borderBottom="solid"
borderColor="colors.custom.topNavBorderColor"
className="DesignerPanelHeader"
>
<Box display="flex" alignItems="center">

View File

@@ -3,9 +3,9 @@ import { SystemProvider, extendTheme } from 'coral-system';
import { ConfigProvider } from 'antd';
import { TangoEngineProvider, ITangoEngineContext } from '@music163/tango-context';
import { createFromIconfontCN } from '@ant-design/icons';
import { DesignerProvider, IDesignerContext } from './context';
import { theme as baseTheme } from './theme';
import zhCN from 'antd/lib/locale/zh_CN';
import { DesignerProvider, IDesignerContext } from './context';
import defaultTheme from './themes/default';
export interface DesignerProps extends IDesignerContext, ITangoEngineContext {
/**
@@ -34,7 +34,7 @@ export function Designer(props: DesignerProps) {
iconfontScriptUrl = '//at.alicdn.com/t/c/font_2891794_lzc7rtwuzf.js',
children,
} = props;
const theme = useMemo(() => extendTheme(themeProp, baseTheme), [themeProp]);
const theme = useMemo(() => extendTheme(themeProp, defaultTheme), [themeProp]);
useEffect(() => {
createFromIconfontCN({
scriptUrl: iconfontScriptUrl,

View File

@@ -72,7 +72,6 @@ export function getElementData(
const bounding = getElementBoundingData(element, relativeContainer);
const data = getElementDataProps(element);
const dnd = parseDndId(data.dnd);
// FIXME: 是否改为按需执行,仅 dropTarget 需要 display 信息,且目前 display 信息并不准确
const display = getElementCSSDisplay(element);
return {
id: dnd.id,

View File

@@ -1 +1,2 @@
export * from './dom';
export * from './template';

View File

@@ -0,0 +1,9 @@
const newStoreTemplate = `
import { defineStore } from '@music163/tango-boot';
export default defineStore({
});
`;
export const CODE_TEMPLATES = {
newStoreTemplate,
};

View File

@@ -12,6 +12,8 @@ export * from './sidebar';
export * from './toolbar';
export * from './selection-menu';
export * from './widgets';
export * from './themes';
export * from './components';
export { register as registerSetter } from '@music163/tango-setting-form';

View File

@@ -17,6 +17,12 @@ const navigatorStyle = css`
.navigatorInput {
flex: 1;
border-color: transparent;
background-color: var(--tango-colors-fill2);
&:focus {
background-color: #fff;
}
}
`;
@@ -68,7 +74,7 @@ export class Navigator extends React.Component<NavigatorProps, NavigatorState> {
className="navigator"
bg="white"
px="l"
py="4px"
py="m"
borderBottom="solid"
borderBottomColor="line.normal"
css={navigatorStyle}

View File

@@ -23,6 +23,12 @@ export type SandboxProps = Omit<CodeSandboxProps, 'files' | 'eventHandlers' | 'o
* tangoConfigJson 处理器
*/
configFormatter?: IMergeTangoConfigJsonConfig['formatter'];
/**
* 文件格式化器
* @param files 沙箱文件列表
* @returns 修改后的文件列表
*/
filesFormatter?: (files: CodeSandboxProps['files']) => CodeSandboxProps['files'];
sandboxType?: 'design' | 'preview';
mode?: 'single' | 'combined';
injectScript?: string;
@@ -41,6 +47,7 @@ const LANDING_PAGE_PATH = '/__background_landing_page__';
function useSandbox({
isPreview: isPreviewProp,
configFormatter,
filesFormatter,
onViewChange,
onMessage: onMessageProp,
onLoad: onLoadProp,
@@ -79,7 +86,12 @@ function useSandbox({
prev[filename] = { code };
return prev;
}, {});
files = fixSandboxFiles(files, workspace.entry);
if (filesFormatter) {
// 支持用户对传入的文件进行修改
files = filesFormatter(files);
}
const onMessage = (data: any) => onMessageProp && onMessageProp(data, getSandboxConfig());
const onLoad = () => onLoadProp && onLoadProp(getSandboxConfig());
@@ -119,6 +131,7 @@ const PreviewSandbox = observer(
startRoute,
injectScript,
sandboxType: 'preview',
...rest,
});
return (
@@ -166,6 +179,7 @@ const DesignSandbox = observer(
startRoute,
injectScript,
sandboxType: 'design',
...rest,
});
return (
@@ -411,6 +425,7 @@ function fixSandboxFiles(files: Record<string, { code: string }>, entry = '/src/
`,
};
}
return files;
}

View File

@@ -4,8 +4,7 @@ import { AutoComplete } from 'antd';
import { ActionSelect } from '@music163/tango-ui';
import { FormItemComponentProps } from '@music163/tango-setting-form';
import { useWorkspace, useWorkspaceData } from '@music163/tango-context';
import { wrapCode } from '@music163/tango-helpers';
import { ExpressionModal } from './expression-setter';
import { ExpressionModal, getWrappedExpressionCode } from './expression-setter';
enum EventAction {
NoAction = 'noAction',
@@ -48,8 +47,9 @@ export function EventSetter(props: EventSetterProps) {
const handleChange = useCallback<FormItemComponentProps['onChange']>(
(nextValue: any, ...args) => {
if (nextValue !== value) {
onChange(nextValue, ...args);
const ret = getWrappedExpressionCode(nextValue);
if (ret !== value) {
onChange(ret, ...args);
}
},
[onChange, value],
@@ -84,7 +84,7 @@ export function EventSetter(props: EventSetterProps) {
onCancel={() => setExpModalVisible(false)}
onOk={(nextValue) => {
setExpModalVisible(false);
handleChange(wrapCode(nextValue));
handleChange(nextValue);
}}
dataSource={actionVariables}
/>

View File

@@ -1,24 +1,29 @@
import React, { useState, useEffect, useCallback } from 'react';
import { Box, Text, css } from 'coral-system';
import { Modal } from 'antd';
import { isValidExpressionCode, value2expressionCode } from '@music163/tango-core';
import {
isVariableString,
isValidExpressionCode,
isWrappedByExpressionContainer,
value2expressionCode,
} from '@music163/tango-core';
import {
getVariableContent,
noop,
useBoolean,
getValue,
wrapCode,
IVariableTreeNode,
} from '@music163/tango-helpers';
import { CloseCircleFilled, ExpandAltOutlined } from '@ant-design/icons';
import { IconButton, Panel, InputCode } from '@music163/tango-ui';
import { FormItemComponentProps } from '@music163/tango-setting-form';
import { useWorkspace, useWorkspaceData } from '@music163/tango-context';
import { EditableVariableTree, IVariableTreeNode } from '../components';
import { VariableTree } from '../components';
import { useSandboxQuery } from '../context';
import { CODE_TEMPLATES } from '../helpers';
import { shapeServiceValues } from '../sidebar/datasource-panel/interface-config';
export const expressionValueValidate = (value: string) => {
if (isVariableString(value)) {
if (isWrappedByExpressionContainer(value)) {
const exp = getVariableContent(value);
if (!isValidExpressionCode(exp)) {
return '表达式存在语法错误!';
@@ -27,7 +32,7 @@ export const expressionValueValidate = (value: string) => {
};
export const jsonValueValidate = (value: string) => {
if (isVariableString(value)) {
if (isWrappedByExpressionContainer(value)) {
const jsonStr = getVariableContent(value);
try {
JSON.parse(jsonStr);
@@ -37,6 +42,23 @@ export const jsonValueValidate = (value: string) => {
}
};
/**
* 返回 `{}` 包裹后的表达式代码
* @param code 原始代码
* @returns
*/
export function getWrappedExpressionCode(code: string) {
let ret;
if (!code) {
// do nothing
} else if (isWrappedByExpressionContainer(code)) {
ret = code;
} else {
ret = `{${code}}`;
}
return ret;
}
const suffixStyle = css`
.anticon-close-circle {
color: rgba(0, 0, 0, 0.25);
@@ -63,6 +85,7 @@ export function ExpressionSetter(props: ExpressionSetterProps) {
value: valueProp,
status,
allowClear = true,
newStoreTemplate,
} = props;
const [visible, { on, off }] = useBoolean();
const [inputValue, setInputValue] = useState(() => {
@@ -78,15 +101,17 @@ export function ExpressionSetter(props: ExpressionSetterProps) {
const change = useCallback(
(code: string) => {
if (!code) {
onChange(undefined);
return;
}
if (getVariableContent(code) === value2expressionCode(valueProp)) {
if (code === valueProp) {
return;
}
onChange(wrapCode(code));
const ret = getWrappedExpressionCode(code);
if (ret === valueProp) {
return;
}
onChange(ret);
},
[valueProp, onChange],
);
@@ -127,11 +152,12 @@ export function ExpressionSetter(props: ExpressionSetterProps) {
subTitle={modalTip}
placeholder={placeholder}
autoCompleteOptions={autoCompleteOptions}
newStoreTemplate={newStoreTemplate}
visible={visible}
value={inputValue}
onCancel={() => off()}
onOk={(value) => {
onChange(value);
change(value);
off();
}}
/>
@@ -150,6 +176,10 @@ export interface ExpressionModalProps {
onOk?: (value: string) => void;
dataSource?: IVariableTreeNode[];
autoCompleteOptions?: string[];
/**
* 新建 store 的模板代码
*/
newStoreTemplate?: string;
}
export function ExpressionModal({
@@ -163,18 +193,19 @@ export function ExpressionModal({
value,
dataSource,
autoCompleteOptions,
newStoreTemplate = CODE_TEMPLATES.newStoreTemplate,
}: ExpressionModalProps) {
const [exp, setExp] = useState(value ?? defaultValue);
const [error, setError] = useState('');
const workspace = useWorkspace();
const onAction = useCallback(
(action: string, args: unknown[]) => {
workspace[action]?.(...args);
},
[workspace],
);
const sandbox = useSandboxQuery();
const { expressionVariables } = useWorkspaceData();
const serviceModules = Object.keys(workspace.serviceModules).map((key) => ({
label: key === 'index' ? '默认模块' : key,
value: key,
}));
const sandbox = useSandboxQuery();
const evaluateContext = sandbox.window;
const handleExpInputChange = (val: string) => {
@@ -189,15 +220,12 @@ export function ExpressionModal({
<Modal
closable={false}
destroyOnClose
width="70%"
width="60%"
open={visible}
onCancel={onCancel}
onOk={() => {
onOk(exp);
}}
okButtonProps={{
disabled: !!error,
}}
bodyStyle={{
padding: 0,
}}
@@ -218,10 +246,29 @@ export function ExpressionModal({
/>
{error ? <Text color="red"></Text> : null}
</Panel>
<Panel title="从变量列表中选中" shape="solid" borderTop="0">
<EditableVariableTree
<Panel
title="从变量列表中选中"
shape="solid"
borderTop="0"
overflow="hidden"
bodyProps={{ overflow: 'hidden' }}
>
<VariableTree
height={380}
showViewButton
dataSource={dataSource || expressionVariables}
appContext={sandbox?.window['tango']}
getStoreNames={() => Object.keys(workspace.storeModules)}
serviceModules={serviceModules}
getServiceData={(serviceKey) => {
const data = workspace.getServiceFunction(serviceKey);
return {
name: data.name,
moduleName: data.moduleName,
method: 'get',
...data.config,
};
}}
onSelect={(node) => {
if (!node.key) {
return;
@@ -237,17 +284,28 @@ export function ExpressionModal({
}
setExp(str);
}}
onAddVariable={(storeName, data) => {
onAction('addStoreState', [storeName, data.name, data.initialValue]);
onAddStoreVariable={(storeName, data) => {
workspace.addStoreState(storeName, data.name, data.initialValue);
}}
onDeleteVariable={(storeName, stateName) => {
onAction('removeStoreState', [storeName, stateName]);
onUpdateStoreVariable={(variableKey, code) => {
workspace.updateStoreVariable(variableKey, code);
}}
onDeleteStore={(storeName) => {
onAction('removeStoreModule', [storeName]);
onAddStore={(storeName) => {
workspace.addStoreFile(storeName, newStoreTemplate);
}}
onSave={(code, node) => {
onAction('updateModuleCodeByVariablePath', [node.key, code]);
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) {
@@ -255,7 +313,7 @@ export function ExpressionModal({
}
if (node.type === 'function') {
return node.raw;
return;
}
return getValue(evaluateContext['tango'], node.key);

View File

@@ -168,4 +168,6 @@ const setters: IFormItemCreateOptions[] = [
{ name: 'borderSetter', component: BorderSetter },
];
setters.forEach(register);
export function registerBuiltinSetters() {
setters.forEach(register);
}

View File

@@ -5,8 +5,9 @@ import { getValue, isFunction } from '@music163/tango-helpers';
import { FormItemComponentProps } from '@music163/tango-setting-form';
import { MenuOutlined } from '@ant-design/icons';
import { useWorkspace, useWorkspaceData } from '@music163/tango-context';
import { EditableVariableTreeModal } from '../components';
import { VariableTreeModal } from '../components';
import { useSandboxQuery } from '../context';
import { CODE_TEMPLATES } from '../helpers';
function object2treeData(
val: any,
@@ -52,43 +53,40 @@ function traverseTreeData(val: any, callback: (val: any) => void) {
}
}
export function ModelSetter({ value, onChange }: FormItemComponentProps) {
export function ModelSetter({
value,
onChange,
newStoreTemplate = CODE_TEMPLATES.newStoreTemplate,
}: FormItemComponentProps) {
const [inputValue, setInputValue] = useState(value);
const { modelVariables } = useWorkspaceData();
const evaluateContext = useSandboxQuery().window || {};
const workspace = useWorkspace();
const onAction = useCallback(
(action: string, args: unknown[]) => {
workspace[action]?.(...args);
},
[workspace],
);
const definedVariables = useMemo(() => {
const map = new Map();
const list: string[] = [];
traverseTreeData(modelVariables, (node) => {
if (node.key.split('.').length > 1) {
map.set(node.key, node);
list.push(node.key);
}
});
return map;
return list;
}, [modelVariables]);
const variables = evaluateContext['tango']?.stores
? [
object2treeData(evaluateContext['tango']?.stores, 'stores', 0, 1, (keyPath, val) => {
if (keyPath.split('.').length === 2 && definedVariables.has(keyPath)) {
return {
showAddChildIcon: true,
showRemoveIcon: true,
};
}
const ret: any = {};
if (isFunction(val)) {
// 不可以同步给函数类型变量
return {
disabled: true,
type: 'function',
};
ret.disabled = true;
ret.type = 'function';
}
if (!definedVariables.includes(keyPath)) {
ret.showAddButton = false;
ret.showRemoveButton = false;
}
return ret;
}),
]
: modelVariables;
@@ -122,31 +120,34 @@ export function ModelSetter({ value, onChange }: FormItemComponentProps) {
onChange={onInputChange}
onBlur={onInputBlur}
suffix={
<EditableVariableTreeModal
<VariableTreeModal
title="同步到的变量"
trigger={
<Tooltip title="从模型列表选择" placement="topRight">
<MenuOutlined />
</Tooltip>
}
modes={['preview']}
dataSource={variables as any[]}
onSelect={(node) => {
const modelPath = node.key.split('.').slice(1).join('.');
onChange(modelPath);
}}
onAddVariable={(storeName, data) => {
onAction('addStoreState', [storeName, data.name, data.initialValue]);
onAddStoreVariable={(storeName, data) => {
workspace.addStoreState(storeName, data.name, data.initialValue);
}}
onDeleteVariable={(storeName, stateName) => {
onAction('removeStoreState', [storeName, stateName]);
onUpdateStoreVariable={(variableKey, code) => {
workspace.updateStoreVariable(variableKey, code);
}}
onDeleteStore={(storeName) => {
onAction('removeStoreModule', [storeName]);
onAddStore={(storeName) => {
workspace.addStoreFile(storeName, newStoreTemplate);
}}
onSave={(code, node) => {
onAction('updateModuleCodeByVariablePath', [node.key, code]);
onRemoveStoreVariable={(variablePath) => {
workspace.removeStoreVariable(variablePath);
}}
onRemoveService={(serviceKey) => {
workspace.removeServiceFunction(serviceKey);
}}
getStoreNames={() => Object.keys(workspace.storeModules)}
getPreviewValue={(node) => {
if (!node || !node.key) {
return;

View File

@@ -4,7 +4,9 @@ import { SettingForm, FormModel, SettingFormProps } from '@music163/tango-settin
import { Panel } from '@music163/tango-ui';
import { clone } from '@music163/tango-helpers';
import { observer, useDesigner, useWorkspace } from '@music163/tango-context';
import './setters';
import { registerBuiltinSetters } from './setters';
registerBuiltinSetters();
export interface SettingPanelProps extends SettingFormProps {
title?: React.ReactNode;

View File

@@ -13,7 +13,7 @@ import { QuestionCircleOutlined } from '@ant-design/icons';
import { Button, Empty, Spin, Popover } from 'antd';
import { getDragGhostElement } from '../helpers';
type MenuKeyType = 'common' | 'atom' | 'snippet' | 'block' | 'bizComp';
type MenuKeyType = 'common' | 'atom' | 'snippet' | 'bizComp' | 'localComp';
type MenuValueType = Array<{ title: string; items: string[] }>;
export type MenuDataType = PartialRecord<MenuKeyType, MenuValueType>;
@@ -26,10 +26,6 @@ export interface ComponentsPanelProps {
* 展示业务组件分类
*/
showBizComps?: boolean;
/**
* 展示基础组件分类
*/
showBlocks?: boolean;
/**
* 动态加载物料 loading避免未加载完成用户点击空列表
*/
@@ -47,7 +43,7 @@ const localeMap = {
atom: '原子组件',
snippet: '组合',
bizComp: '业务组件',
block: '区块',
localComp: '本地组件',
};
const emptyMenuData: MenuDataType = {
@@ -78,19 +74,18 @@ export const ComponentsPanel = observer(
({
menuData = emptyMenuData,
showBizComps = true,
showBlocks = true,
getBizCompName = upperCamelCase,
loading = false,
}: ComponentsPanelProps) => {
const [keyword, setKeyword] = useState<string>('');
const allList = useFlatMenuData<MenuDataType>(menuData);
const workspace = useWorkspace();
const [bizCompData, blockData] = useMemo(
const [bizCompData, localCompData] = useMemo(
() => [
[{ title: '已安装业务组件', items: workspace.bizComps.map(getBizCompName) }],
[{ title: '项目区块', items: workspace.blocks }],
[{ title: '本地组件', items: workspace.localComps }],
],
[workspace.bizComps, workspace.blocks, getBizCompName],
[workspace.bizComps, workspace.localComps, getBizCompName],
);
const tabs = Object.keys(menuData).map((key) => ({
@@ -106,11 +101,12 @@ export const ComponentsPanel = observer(
children: <MaterialList type="bizComp" data={bizCompData} />,
});
}
if (showBlocks) {
if (localCompData.length) {
tabs.push({
key: 'block',
label: localeMap.block,
children: <MaterialList type="block" data={blockData} />,
key: 'localComps',
label: localeMap.localComp,
children: <MaterialList data={localCompData} />,
});
}
const contentNode =
@@ -145,7 +141,7 @@ interface MaterialListProps {
/**
* 物料类型
*/
type?: 'common' | 'bizComp' | 'block';
type?: 'common' | 'bizComp' | 'localComp';
}
function MaterialList({ data, filterKeyword, type = 'common' }: MaterialListProps) {
@@ -175,7 +171,7 @@ function MaterialList({ data, filterKeyword, type = 'common' }: MaterialListProp
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="没有匹配到任何组件" />
)}
<Grid
columns={type === 'block' ? 1 : 2}
columns={type === 'localComp' ? 1 : 2}
spacing="1px"
bg="background.normal"
padding="0"
@@ -185,9 +181,7 @@ function MaterialList({ data, filterKeyword, type = 'common' }: MaterialListProp
if (!prototype) {
logger.log(`<${item}> prototype not found!`);
}
return prototype ? (
<MaterialGrid key={item} data={prototype} type={type} />
) : null;
return prototype ? <MaterialGrid key={item} data={prototype} /> : null;
})}
{addBlock && <Box bg="white" />}
</Grid>
@@ -201,7 +195,6 @@ function MaterialList({ data, filterKeyword, type = 'common' }: MaterialListProp
interface MaterialProps {
data: ComponentPrototypeType;
type: 'common' | 'atom' | 'snippet' | 'bizComp' | 'block';
}
const StyledCommonGridItem = styled.div`
@@ -219,11 +212,6 @@ const StyledCommonGridItem = styled.div`
text-overflow: ellipsis;
white-space: nowrap;
&[class~='block-item'] {
flex-direction: row;
justify-content: flex-start;
}
.material-icon {
font-size: 40px;
}
@@ -260,7 +248,7 @@ const StyledCommonGridItem = styled.div`
}
`;
function MaterialGrid({ data, type }: MaterialProps) {
function MaterialGrid({ data }: MaterialProps) {
const workspace = useWorkspace();
const handleDragStart = (e: React.DragEvent) => {
@@ -286,7 +274,6 @@ function MaterialGrid({ data, type }: MaterialProps) {
data-name={data.name}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
className={type === 'block' ? 'block-item' : null}
>
{icon.startsWith('icon-') ? (
<IconFont className="material-icon" type={data.icon || 'icon-placeholder'} />
@@ -296,7 +283,7 @@ function MaterialGrid({ data, type }: MaterialProps) {
<Text fontSize="12px" lineHeight="1.5">
{data.title}
</Text>
<Text fontSize="12px" color={type === 'block' ? 'inherit' : 'gray.50'}>
<Text fontSize="12px" color="gray.50">
{data.name}
</Text>
{data.docs || data.help ? (

View File

@@ -4,8 +4,6 @@ import { Tabs } from '@music163/tango-ui';
import InterfaceConfig from './interface-config';
import ProxyConfig from './proxy-config';
export * from './interface-config';
export function DataSourcePanel(props: Record<string, any>) {
return (
<Box className="DataSourceView" height="100%" overflowY="auto">

View File

@@ -1,535 +1,60 @@
import React, { useState, useEffect, useMemo } from 'react';
import React from 'react';
import { observer, useWorkspace, useWorkspaceData } from '@music163/tango-context';
import { Box, css } from 'coral-system';
import { Button, FormProps, Empty, Space, Dropdown } from 'antd';
import { PlayCircleOutlined } from '@ant-design/icons';
import { Form, InputCode, Panel, JsonView, Search } from '@music163/tango-ui';
import {
isNil,
isVariableString,
getVariableContent,
isValidFunctionCode,
logger,
code2object,
filterTreeData,
} from '@music163/tango-helpers';
import { useSandboxQuery } from '../../context';
import { Box } from 'coral-system';
import { getVariableContent } from '@music163/tango-helpers';
import { isWrappedByExpressionContainer } from '@music163/tango-core';
import { VariableTree } from '../../components';
import { useSandboxQuery } from '../../context';
/*
* 服务函数的操作类型
*/
enum ServiceFunctionOperationModeType {
ADD = 'add',
UPDATE = 'update',
DELETE = 'delete',
}
/**
* 服务函数 HTTP Type
* 云音乐网关只支持 get 和 post
*/
enum ServiceFunctionMethodType {
GET = 'GET',
// PUT = 'PUT',
POST = 'POST',
// PATCH = 'PATCH',
// DELETE = 'DELETE',
}
/**
* requestType
* headers: Content-Type
*/
enum DataServiceRequestType {
'application/json' = 'json',
'application/x-www-form-urlencoded' = 'x-www-form-urlencoded',
}
/**
* 将 api 路径转换为默认的驼峰方法名
*/
export const getApiDefaultName = (url: string) =>
url
// 去除 api + 模块名前缀
// - 云音乐 api 规范为 /api/模块名/
// - 后端公技基本使用 /模块名/api/
// - 中台类服务似乎常用 /api/middle/模块名/
// TODO: 是否需要去除模块名?一般同一个应用是同一个模块名,去掉可以缩减方法名长度,但确实有模块名不一样且后面的路径完全一致的接口
// 目前的实现是去除了模块名,只干掉 /api/middle/ 和 /api/backend/ 这种常用前缀
.replace(/^\/[^/]+?\/api\/|^\/api\/middle\/|^\/api\/backend\/|^\/api\//, '')
// 去除路由参数
.replace(/\/\{.*?\}/, '')
// 忽略下划线与减号,将后面的字符转成大驼峰
.replace(/[-/_]+\w/g, (str) => str.replace(/[-/_]+/, '').toUpperCase())
// 首字母转小写
.replace(/^./, (str) => str.toLowerCase())
// 方法名以数字开头,添加 api 前缀
.replace(/^\d/, (str) => `api${str}`);
interface DataServiceViewProps {
onAdd?: (values: Record<string, string> | Array<Record<string, string>>) => void;
onUpdate?: (values: Record<string, string>) => void;
onDelete?: (values: Record<string, string>) => void;
}
// http 方法类型
const httpMethods = Object.keys(ServiceFunctionMethodType).map((key) => ({
label: key,
value: key,
}));
// requestType 类型
const requestTypes = Object.keys(DataServiceRequestType).map((key) => ({
label: key,
value: DataServiceRequestType[key],
}));
const detailFormStyle = css`
.ant-form-item {
margin-bottom: 12px;
// 移除掉不必要的属性
export function shapeServiceValues(val: any) {
const shapeValues = { ...val };
delete shapeValues.type;
// 兼容旧版,如果 formatter 包裹了 {} 则删掉首尾
if (shapeValues.formatter && isWrappedByExpressionContainer(shapeValues.formatter)) {
shapeValues.formatter = getVariableContent(shapeValues.formatter);
}
`;
return shapeValues;
}
const DataSourceView = observer(({ onAdd, onUpdate, onDelete }: DataServiceViewProps) => {
const [serviceData, setServiceData] = useState<any>();
const [keyword, setKeyword] = useState<string>();
const workspace = useWorkspace();
const DataSourceView = observer(() => {
const sandbox = useSandboxQuery();
const workspace = useWorkspace();
const { serviceVariables } = useWorkspaceData();
const serviceModules = Object.keys(workspace.serviceModules).map((key) => ({
label: key === 'index' ? '默认模块' : key,
value: key,
}));
const serviceFunctions = serviceVariables.reduce((acc, cur) => {
acc = acc.concat(cur.children.map((item: any) => item.key));
return acc;
}, []);
const dataSource = useMemo(() => {
if (!keyword) {
return serviceVariables;
}
return filterTreeData(
serviceVariables,
(leaf) => leaf.title.includes(keyword),
'children',
true,
);
}, [serviceVariables, keyword]);
const isAddMode = serviceData && !serviceData.name;
return (
<Box className="ServiceFunctionList" display="flex" borderTopColor="line.normal">
<Box width="35%" overflow="auto" borderRight="solid" borderColor="line.normal">
<Panel
height="100%"
bodyProps={{
flex: 1,
}}
>
<Box
p="m"
display="flex"
flexDirection="column"
justifyContent="center"
alignItems="center"
rowGap="m"
>
<Button
block
onClick={() => {
setServiceData({});
}}
>
</Button>
<Search placeholder="搜索服务函数" onChange={setKeyword} />
</Box>
<VariableTree
dataSource={dataSource}
showDeleteIcon
onSelect={(item) => {
setServiceData({ key: item.key, ...workspace.getServiceFunction(item.key) });
}}
onRemove={(item) => {
workspace.removeServiceFunction(item.key);
onDelete?.(item);
}}
/>
</Panel>
</Box>
<Box width="65%" overflow="auto" css={detailFormStyle}>
{serviceData && (
<>
<Panel title={isAddMode ? '新建服务函数' : '服务函数详情'}>
<ServiceDetailForm
key={serviceData.key || 'createSF'}
serviceModules={serviceModules}
serviceKeys={serviceFunctions}
initialValues={
isAddMode
? {
moduleName: 'index',
}
: {
name: serviceData.name,
moduleName: serviceData.moduleName,
method: 'get',
...serviceData.config,
}
}
onCancel={() => {
setServiceData(undefined);
}}
onSubmit={(values, mode: ServiceFunctionOperationModeType) => {
function shapeServiceValues(val: any) {
const shapeValues = { ...val };
delete shapeValues.type;
// 兼容旧版,如果 formatter 包裹了 {} 则删掉首尾
if (shapeValues.formatter && isVariableString(shapeValues.formatter)) {
shapeValues.formatter = getVariableContent(shapeValues.formatter);
}
return shapeValues;
}
// 移除掉不必要的属性
const { moduleName, ...data } = shapeServiceValues(values);
if (mode === ServiceFunctionOperationModeType.ADD) {
workspace.addServiceFunction(data, moduleName);
onAdd && onAdd(values);
} else if (mode === ServiceFunctionOperationModeType.UPDATE) {
workspace.updateServiceFunction(data, moduleName);
onUpdate && onUpdate(values);
}
}}
/>
</Panel>
{serviceData?.key ? (
<ServiceFunctionPreview
key={serviceData.key}
appContext={sandbox?.window['tango']}
functionName={serviceData.key}
/>
) : null}
</>
)}
{!serviceData && (
<Box py="xxl">
<Empty description="请从左侧选择数据服务函数或新建数据服务函数" />
</Box>
)}
</Box>
<Box className="ServiceFunctionList" p="m">
<VariableTree
dataSource={serviceVariables}
appContext={sandbox?.window['tango']}
serviceModules={serviceModules}
onRemoveService={(variableKey) => {
workspace.removeServiceFunction(variableKey);
}}
onAddService={(data) => {
const { name, moduleName, ...payload } = shapeServiceValues(data);
workspace.addServiceFunction(name, payload, moduleName);
}}
onUpdateService={(data) => {
const { name, moduleName, ...payload } = shapeServiceValues(data);
workspace.updateServiceFunction(name, payload, moduleName);
}}
getServiceData={(serviceKey) => {
const data = workspace.getServiceFunction(serviceKey);
return {
name: data.name,
moduleName: data.moduleName,
method: 'get',
...data.config,
};
}}
/>
</Box>
);
});
interface ServiceDetailFormProps extends FormProps {
serviceKeys?: string[];
serviceModules?: any[];
onCancel?: () => void;
onSubmit?: (values: any, mode: ServiceFunctionOperationModeType) => void;
}
function ServiceDetailForm({
serviceKeys = [],
serviceModules = [],
onCancel,
onSubmit,
initialValues,
...formProps
}: ServiceDetailFormProps) {
const isModifyMode = !!initialValues?.name; // 是否为更新模式
const [disabled, setDisabled] = useState(isModifyMode);
const [form] = Form.useForm();
// 监听服务类型变化,默认是自定义
const formType = Form.useWatch('type', form);
// 新建的时候,类型变换重置部分字段
useEffect(() => {
if (!formType) return;
!isModifyMode &&
form.setFieldsValue({
url: undefined,
method: undefined,
formatter: undefined,
});
}, [formType, form, isModifyMode]);
return (
<Form
form={form}
labelCol={6}
wrapperCol={17}
colon={false}
layout="horizontal"
initialValues={initialValues}
onFinish={(values) => {
onSubmit(
values,
isModifyMode
? ServiceFunctionOperationModeType.UPDATE
: ServiceFunctionOperationModeType.ADD,
);
setDisabled(true);
}}
{...formProps}
>
<Form.Item
label="所属模块"
name="moduleName"
rules={[
{
required: true,
},
]}
component="select"
componentProps={{
options: serviceModules,
disabled: disabled || isModifyMode,
}}
/>
<Form.Item
label="方法名"
name="name"
rules={[
{ required: true },
{ pattern: /^[a-z]\w+$/, message: '请输入合法的方法名称' },
!isModifyMode && {
validator(_, value) {
const isExist = serviceKeys.includes(
['services', form.getFieldValue('moduleName'), value].join('.'),
);
return isExist
? Promise.reject(new Error('重复的方法名,请换一个!'))
: Promise.resolve();
},
},
]}
component="input"
componentProps={{
placeholder: '请输入数据服务调用名称',
disabled: disabled || isModifyMode,
}}
extra={
!disabled &&
!isModifyMode && (
<Box>
<Button
type="link"
size="small"
disabled={!form.getFieldValue('url')}
onClick={() => {
const funcName = getApiDefaultName(form.getFieldValue('url'));
form.setFieldValue('name', funcName);
}}
>
使
</Button>
</Box>
)
}
/>
<Form.Item
label="路径"
name="url"
rules={[{ required: true, type: 'url' }]}
component="input"
componentProps={{
placeholder: '请输入数据服务调用名称',
disabled: disabled || isModifyMode,
}}
/>
<Form.Item
label="方法"
name="method"
component="select"
componentProps={{
options: httpMethods,
placeholder: '请选择 HTTP 请求方法',
disabled,
}}
/>
<Form.Item
label="编码类型"
name="requestType"
tooltip="设置请求头的 Content-Type若不设置则默认变为为 JSON"
component="select"
componentProps={{
options: requestTypes,
placeholder: '请求头Content-Type默认为JSON',
disabled,
}}
/>
<Form.Item
label="格式化响应"
name="formatter"
tooltip="提供格式化函数,对结果进行格式化"
validateTrigger="onBlur"
rules={[
{
validator(_, value) {
if (!value) {
return Promise.resolve();
}
return isValidFunctionCode(value)
? Promise.resolve()
: Promise.reject(new Error('请提供合法的函数代码!'));
},
},
]}
extra={
!disabled && (
<Box>
<Dropdown
menu={{
items: [
{ label: '直接返回(默认)', key: 'res => res' },
{ label: '返回数据部分(云音乐标准规范)', key: 'res => res.data' },
],
onClick: ({ key }) => {
form.setFieldValue('formatter', key);
},
}}
>
<Button type="link" size="small">
使
</Button>
</Dropdown>
</Box>
)
}
>
<InputCode editable={!disabled} showLineNumbers placeholder="res => res" />
</Form.Item>
<Form.Item wrapperCol={{ offset: 6, span: 18 }}>
{isModifyMode ? (
<Space>
{!disabled ? (
<>
<Button
onClick={() => {
setDisabled(true);
}}
>
</Button>
<Button type="primary" htmlType="submit">
</Button>
</>
) : (
<Button
onClick={() => {
setDisabled(false);
}}
>
</Button>
)}
</Space>
) : (
<Space>
<Button onClick={onCancel}></Button>
<Button type="primary" htmlType="submit">
</Button>
</Space>
)}
</Form.Item>
</Form>
);
}
interface ServicePreviewProps {
data?: object;
}
function ServicePreview({ data }: ServicePreviewProps) {
let ret: React.ReactNode;
if (!isNil(data)) {
switch (typeof data) {
case 'object':
ret = <JsonView src={data as object} />;
break;
default:
ret = String(data);
break;
}
}
const initialRet = <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />;
return (
<Box
className="ServicePreviewPanel"
overflow="auto"
height="auto"
minHeight={260}
marginTop={10}
>
{ret || initialRet}
</Box>
);
}
interface ServiceFunctionPreviewProps {
appContext?: any;
functionName?: string;
}
function ServiceFunctionPreview({ appContext, functionName }: ServiceFunctionPreviewProps) {
const [payload, setPayload] = useState({});
const [result, setResult] = useState<any>();
const [error, setError] = useState('');
return (
<Panel
title="接口测试"
extra={
<Space>
<Button
disabled={!appContext}
size="small"
onClick={() => {
if (!appContext) {
setError('执行上下文未准备好,请关闭面板重试');
return;
}
try {
appContext.services[functionName](payload).then((data: any) => {
setResult(data);
});
} catch (err) {
setError('接口调用失败,请检查参数是否正确');
logger.error(err);
}
}}
icon={<PlayCircleOutlined />}
>
</Button>
</Space>
}
borderTop="solid"
borderTopColor="line.normal"
>
<Panel title="请求参数" bodyProps={{ px: 'l' }}>
<InputCode
placeholder={'添加请求参数,对象格式,如 { key: value }'}
editable
showLineNumbers
onChange={(value: string) => {
const obj = code2object(value);
setPayload(obj);
}}
/>
</Panel>
<Panel title="请求响应" bodyProps={{ px: 'l' }}>
{error || (result ? <ServicePreview data={result} /> : '点击预览按钮测试接口返回值')}
</Panel>
</Panel>
);
}
export default DataSourceView;

View File

@@ -366,19 +366,19 @@ function RenderItem({
</Popconfirm>,
]
: baseNeedUpgrade
? [
<Popconfirm
title={`确认升级 ${record.name}${basePackage.version} 吗?`}
onConfirm={() => onDepUpgrade({ version: basePackage.version })}
>
<a key="upgrade"></a>
</Popconfirm>,
]
: [
<Text key="latest" color={'rgba(0, 0, 0, 0.45)'}>
{!isUndefined(baseNeedUpgrade) && '已是最新'}
</Text>,
]
? [
<Popconfirm
title={`确认升级 ${record.name}${basePackage.version} 吗?`}
onConfirm={() => onDepUpgrade({ version: basePackage.version })}
>
<a key="upgrade"></a>
</Popconfirm>,
]
: [
<Text key="latest" color={'rgba(0, 0, 0, 0.45)'}>
{!isUndefined(baseNeedUpgrade) && '已是最新'}
</Text>,
]
}
>
<List.Item.Meta
@@ -453,7 +453,7 @@ function AddDependencyModal({
const onFinishCallback = (name?: string) => {
name && message.success(`${name} 添加成功`);
// FIXME: 修复沙箱添加组件后 HMR 失效的 bug添加完刷新
// TIP: 修复沙箱添加组件后 HMR 失效的 bug添加完刷新
sandbox.reload();
off();
};

View File

@@ -148,6 +148,12 @@ export const ComponentsTree: React.FC<ComponentsTreeProps> = observer(
setSelectedKeys(workspace.selectSource.selected.map((item) => item.id));
}, [workspace.selectSource.selected]);
useEffect(() => {
setExpandedKeys(getNodeKeys(nodesTree));
// nodeTree 更新后重新计算expandKeys
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [nodesTree?.[0]?.id]);
if (!nodesTree.length) {
return (
<Box>
@@ -156,12 +162,6 @@ export const ComponentsTree: React.FC<ComponentsTreeProps> = observer(
);
}
useEffect(() => {
setExpandedKeys(getNodeKeys(nodesTree));
// nodeTree 更新后重新计算expandKeys
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [nodesTree?.[0]?.id]);
return (
<Box css={outlineStyle}>
<Tree
@@ -230,4 +230,4 @@ export const ComponentsTree: React.FC<ComponentsTreeProps> = observer(
</Box>
);
},
);
);

View File

@@ -3,11 +3,14 @@ import cx from 'classnames';
import { Box, HTMLCoralProps, css } from 'coral-system';
import { Resizable, ResizeHandle } from 'react-resizable';
export interface ResizableBoxProps extends HTMLCoralProps<'div'> {
export interface ResizableBoxProps {
width?: number;
height?: number;
resizeHandlePosition?: 'left' | 'right' | 'top' | 'bottom';
axis?: 'x' | 'y';
children?: React.ReactNode;
className?: string;
style?: React.CSSProperties;
}
const resizeHandleStyle = (axis: ResizableBoxProps['axis'], barStyle: HTMLCoralProps<'div'>) => css`

View File

@@ -75,6 +75,8 @@ export interface SidebarPanelItemProps
}
function BaseSidebarPanel({ panelWidth: defaultPanelWidth = 280, footer, children }: SidebarProps) {
const designer = useDesigner();
const items = useMemo(() => {
const ret: Record<string, SidebarPanelItemProps> = {};
React.Children.forEach(children, (child) => {
@@ -90,20 +92,6 @@ function BaseSidebarPanel({ panelWidth: defaultPanelWidth = 280, footer, childre
return ret;
}, [children]);
const designer = useDesigner();
const panel = items[designer.activeSidebarPanel];
const floatPanelStyle: any = panel?.isFloat
? {
position: 'absolute',
left: '50px',
top: 0,
zIndex: 1000,
height: '100%',
boxShadow: 'var(--tango-shadows-lowRight)',
}
: {};
const panelWidth = typeof panel?.width === 'number' ? panel?.width : defaultPanelWidth;
return (
<Box display="flex" flexShrink={0} css={sidebarStyle} className="SidebarPanel">
<Box className="SidebarPanelBar" display="flex" flexDirection="column" position="relative">
@@ -142,19 +130,41 @@ function BaseSidebarPanel({ panelWidth: defaultPanelWidth = 280, footer, childre
{footer}
</Box>
</Box>
{panel ? (
<ResizableBox key={panel.label} width={panelWidth} style={floatPanelStyle}>
<SidebarPanelExpandedContent
isFloat={panel.isFloat}
closeable={panel.isFloat}
onClose={() => {
designer.closeSidebarPanel();
{Object.keys(items).map((key) => {
const panel = items[key];
const floatPanelStyle: any = panel?.isFloat
? {
position: 'absolute',
left: '50px',
top: 0,
zIndex: 1000,
height: '100%',
boxShadow: 'var(--tango-shadows-lowRight)',
}
: {};
const panelWidth = typeof panel?.width === 'number' ? panel?.width : defaultPanelWidth;
return (
<ResizableBox
key={panel.label}
width={panelWidth}
style={{
display: designer.activeSidebarPanel === key ? 'block' : 'none',
...floatPanelStyle,
}}
>
{panel.children}
</SidebarPanelExpandedContent>
</ResizableBox>
) : null}
<SidebarPanelExpandedContent
isFloat={panel.isFloat}
closeable={panel.isFloat}
onClose={() => {
designer.closeSidebarPanel();
}}
>
{panel.children}
</SidebarPanelExpandedContent>
</ResizableBox>
);
})}
</Box>
);
}

View File

@@ -1,132 +1,61 @@
import React from 'react';
import { Button, Space } from 'antd';
import { Button } from 'antd';
import { FileAddOutlined } from '@ant-design/icons';
import { useBoolean } from '@music163/tango-helpers';
import { Panel, Form, IconButton } from '@music163/tango-ui';
import { FileAddOutlined, QuestionCircleOutlined } from '@ant-design/icons';
import { Panel } from '@music163/tango-ui';
import { observer, useWorkspace, useWorkspaceData } from '@music163/tango-context';
import { EditableVariableTree, EditableVariableTreeProps } from '../components';
import { VariableTree } from '../components';
import { CODE_TEMPLATES } from '../helpers';
import { AddStoreForm } from '../components/variable-tree/add-store';
const defaultStoreTemplate = `
import { defineStore } from '@music163/tango-boot';
export default defineStore({
});
`;
export interface VariablePanelProps extends EditableVariableTreeProps {
wrapperHeight?: number | string;
export interface VariablePanelProps {
newStoreTemplate?: string;
}
export const VariablePanel = observer(
({
wrapperHeight = '100%',
newStoreTemplate = defaultStoreTemplate,
...restProps
}: VariablePanelProps) => {
({ newStoreTemplate = CODE_TEMPLATES.newStoreTemplate }: VariablePanelProps) => {
const [isAdd, { on, off }] = useBoolean();
const workspace = useWorkspace();
const { storeVariables } = useWorkspaceData();
const storeNames = storeVariables.map((item) => item.title);
const storeNames = storeVariables.map((item) => item.title) as string[];
return (
<Panel
shape="solid"
className="ModelView"
height={wrapperHeight}
height="100%"
title="视图模型与变量管理"
subTitle={
<IconButton
tooltip="如何使用"
icon={<QuestionCircleOutlined />}
href="https://music-doc.st.netease.com/st/tango-docs/docs/guide/basic/model"
/>
}
extra={
<Button size="small" type="text" icon={<FileAddOutlined />} onClick={on}>
<Button size="small" type="primary" icon={<FileAddOutlined />} onClick={on}>
</Button>
}
bodyProps={{ p: 0 }}
bodyProps={{ p: 'm' }}
>
{isAdd ? (
<AddStoreForm
existNames={storeNames}
storeNames={storeNames}
onCancel={off}
onSubmit={(values) => {
workspace.addStoreModule(values.name, newStoreTemplate);
workspace.addStoreFile(values.name, newStoreTemplate);
off();
}}
/>
) : (
<EditableVariableTree
modes={['add', 'define']}
defaultMode="define"
<VariableTree
defaultValueDetailMode="define"
dataSource={storeVariables}
onAddVariable={(storeName, data) => {
onAddStoreVariable={(storeName, data) => {
workspace.addStoreState(storeName, data.name, data.initialValue);
}}
onDeleteVariable={(storeName, stateName) => {
workspace.removeStoreState(storeName, stateName);
onRemoveStoreVariable={(variableKey) => {
workspace.removeStoreVariable(variableKey);
}}
onDeleteStore={(storeName) => {
workspace.removeStoreModule(storeName);
onUpdateStoreVariable={(variableKey, code) => {
workspace.updateStoreVariable(variableKey, code);
}}
onSave={(code, node) => {
workspace.updateModuleCodeByVariablePath(node.key, code);
}}
height="100%"
{...restProps}
/>
)}
</Panel>
);
},
);
interface AddStoreFormProps {
existNames?: string[];
onCancel?: () => void;
onSubmit?: (data: { name: string }) => void;
}
function AddStoreForm({ existNames = [], onCancel, onSubmit }: AddStoreFormProps) {
return (
<Panel title="添加模型" subTitle="模型可以用来组织一组变量">
<Form
autoComplete="off"
onFinish={(values) => {
onSubmit && onSubmit(values);
}}
>
<Form.Item
label="模型名称"
name="name"
rules={[
{ required: true },
{ pattern: /^[a-z]\w+$/, message: '非法的变量标识符' },
{
validator(_, value) {
const found = existNames.includes(value);
return found
? Promise.reject(new Error('已存在该模型名,请换一个!'))
: Promise.resolve();
},
},
]}
component="input"
componentProps={{
placeholder: '请输入模型名称',
}}
/>
<Form.Item label=" ">
<Space>
<Button onClick={onCancel}></Button>
<Button type="primary" htmlType="submit">
</Button>
</Space>
</Form.Item>
</Form>
</Panel>
);
}

View File

@@ -1,6 +1,6 @@
import { extendTheme } from 'coral-system';
export const theme = extendTheme({
export default extendTheme({
colors: {
primary: {
10: '#e8f3ff',
@@ -27,11 +27,16 @@ export const theme = extendTheme({
custom: {
topNavBg: '#222',
topNavColor: '#fff',
topNavBorderColor: '#222',
toolbarDividerColor: 'gray.60',
toolbarButtonBg: 'rgba(223, 223, 223, 0.08)',
toolbarButtonBgHover: '#4080ff',
toolbarButtonTextColor: '#f5f5f5',
toolbarButtonBgDisabled: 'rgba(223,223,223, 0.08)',
toolbarButtonBgActive: '#4080ff',
toolbarButtonTextColor: '#FFF',
toolbarButtonTextColorHover: '#FFF',
toolbarButtonTextColorDisabled: '#bfbfbf',
toolbarButtonTextColorDisabled: 'hsla(0,0%,100%,0.3)',
toolbarButtonTextColorActive: '#fff',
sidebarBg: '#fff',
sidebarExpandBg: '#fff',
sidebarItemActiveBg: '#f2f3f5',

View File

@@ -0,0 +1,2 @@
export { default as themeDefault } from './default';
export { default as themeLight } from './light';

View File

@@ -0,0 +1,25 @@
import { extendTheme } from 'coral-system';
export default extendTheme({
colors: {
custom: {
topNavBg: '#FFF',
topNavColor: '#333',
topNavBorderColor: 'rgb(229,230,235)',
toolbarDividerColor: 'rgb(229,230,235)',
toolbarButtonBg: 'rgb(242,243,245)',
toolbarButtonBgHover: 'rgb(229,230,235)',
toolbarButtonBgDisabled: 'rgb(247,248,250)',
toolbarButtonBgActive: 'colors.brand',
toolbarButtonTextColor: 'colors.text2',
toolbarButtonTextColorHover: 'colors.text2',
toolbarButtonTextColorDisabled: 'colors.text4',
toolbarButtonTextColorActive: '#FFF',
sidebarBg: '#fff',
sidebarExpandBg: '#fff',
sidebarItemActiveBg: '#f2f3f5',
sidebarItemHoverBg: '#f2f3f5',
viewportBg: '#f0f2f5',
},
},
});

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { Box } from 'coral-system';
import { ToggleButton, IconFont } from '@music163/tango-ui';
import { ToggleButton, RedoOutlined, UndoOutlined } from '@music163/tango-ui';
import { observer, useWorkspace } from '@music163/tango-context';
export const HistoryTool = observer(() => {
@@ -10,24 +10,24 @@ export const HistoryTool = observer(() => {
<ToggleButton
shape="ghost"
disabled={!workspace.history.couldBack || workspace.history.index === 0}
tip="撤销"
tooltip="撤销"
onClick={() => {
workspace.history.back();
workspace.selectSource.clear();
}}
>
<IconFont type="icon-undo" />
<UndoOutlined />
</ToggleButton>
<ToggleButton
shape="ghost"
disabled={!workspace.history.couldForward}
tip="重做"
tooltip="重做"
onClick={() => {
workspace.history.forward();
workspace.selectSource.clear();
}}
>
<IconFont type="icon-redo" />
<RedoOutlined />
</ToggleButton>
</Box>
);

View File

@@ -1,9 +1,12 @@
import React from 'react';
import { Group } from 'coral-system';
import { ToggleButton } from '@music163/tango-ui';
import { observer, useDesigner } from '@music163/tango-context';
import { Modal } from 'antd';
import { ToggleButton, CodeOutlined } from '@music163/tango-ui';
import { observer, useDesigner, useWorkspace } from '@music163/tango-context';
import { BorderOutlined } from '@ant-design/icons';
export const ModeSwitchTool = observer(() => {
const workspace = useWorkspace();
const designer = useDesigner();
return (
<Group attached>
@@ -13,9 +16,9 @@ export const ModeSwitchTool = observer(() => {
onClick={() => {
designer.setActiveView('design');
}}
tip="设计视图"
tooltip="设计视图"
>
<BorderOutlined />
</ToggleButton>
<ToggleButton
shape="ghost"
@@ -23,10 +26,18 @@ export const ModeSwitchTool = observer(() => {
onClick={() => {
designer.setActiveView('code'); // 切换到源码视图
designer.setActiveSidebarPanel(''); // 关闭左侧面板
if (workspace.activeFile !== workspace.activeViewFile) {
Modal.confirm({
title: '当前打开的文件与视图不匹配,是否切换到当前视图对应的文件?',
onOk: () => {
workspace.setActiveFile(workspace.activeViewFile);
},
});
}
}}
tip="源码视图"
tooltip="源码视图"
>
<CodeOutlined />
</ToggleButton>
</Group>
);

View File

@@ -17,7 +17,7 @@ export const PreviewTool = observer(() => {
designer.setActiveView('design');
}
}}
tip="预览"
tooltip="预览"
>
<EyeOutlined />
</ToggleButton>

View File

@@ -1,10 +1,10 @@
import React, { useMemo, useState } from 'react';
import { Button, Popconfirm, Modal, ModalProps } from 'antd';
import { Button, Popconfirm, Modal, ModalProps, Form, Input } from 'antd';
import { Box, css, Group, Text } from 'coral-system';
import { FileOutlined, DeleteOutlined, CopyOutlined, SettingOutlined } from '@ant-design/icons';
import { array2object, callAll } from '@music163/tango-helpers';
import { IPageConfigData } from '@music163/tango-core';
import { Form, ToggleButton } from '@music163/tango-ui';
import { ToggleButton } from '@music163/tango-ui';
import { observer, useWorkspace } from '@music163/tango-context';
export const RouteSwitchTool = observer(() => {
@@ -71,6 +71,7 @@ function PageSelect({ value, onSelect, onRemove, onUpdate, onCopy, options }: Pa
}}
/>
}
icon={<FileOutlined />}
>
{current?.name || current?.label || value}
</ToggleButton>
@@ -249,17 +250,13 @@ function CopyViewModalForm({
}}
>
<Form
wrapperCol={20}
wrapperCol={{ span: 20 }}
form={form}
initialValues={{ name: current?.name, path: current?.path }}
>
<Form.Item
label="页面标题"
name="name"
required
component="input"
componentProps={{ placeholder: '请输入标题' }}
/>
<Form.Item label="页面标题" name="name" required>
<Input placeholder="请输入标题" />
</Form.Item>
<Form.Item
label="页面路由"
name="path"
@@ -280,11 +277,9 @@ function CopyViewModalForm({
message: '非法的路由地址',
},
]}
component="input"
componentProps={{
placeholder: '输入页面路由',
}}
/>
>
<Input placeholder="请输入页面路由" />
</Form.Item>
</Form>
</Modal>
);

View File

@@ -1,7 +1,13 @@
import React from 'react';
import { observer, useDesigner } from '@music163/tango-context';
import { Box } from 'coral-system';
import { IconFont, ToggleButton } from '@music163/tango-ui';
import {
OpenPanelFilledLeftOutlined,
OpenPanelFilledRightOutlined,
OpenPanelLeftOutlined,
OpenPanelRightOutlined,
ToggleButton,
} from '@music163/tango-ui';
export const TogglePanelTool = observer(() => {
const designer = useDesigner();
@@ -14,20 +20,15 @@ export const TogglePanelTool = observer(() => {
designer.setActiveSidebarPanel(designer.activeSidebarPanel ? '' : 'outline');
}}
>
<IconFont
type={
designer.activeSidebarPanel ? 'icon-open-panel-filled-left' : 'icon-open-panel-left'
}
/>
{designer.activeSidebarPanel ? <OpenPanelFilledLeftOutlined /> : <OpenPanelLeftOutlined />}
</ToggleButton>
<ToggleButton
tooltip="隐藏/显示右侧面板"
shape="ghost"
onClick={() => designer.toggleRightPanel()}
disabled={designer.activeView === 'code'}
>
<IconFont
type={designer.showRightPanel ? 'icon-open-panel-filled-right' : 'icon-open-panel-right'}
/>
{designer.showRightPanel ? <OpenPanelFilledRightOutlined /> : <OpenPanelRightOutlined />}
</ToggleButton>
</Box>
);

View File

@@ -81,7 +81,7 @@ function ToolbarItem({ placement, widgetProps, children }: ToolbarItemProps) {
}
function Separator() {
return <Box className="Separator" width={1} height={16} bg="gray.60" />;
return <Box className="Separator" width={1} height={16} bg="colors.custom.toolbarDividerColor" />;
}
Toolbar.Item = ToolbarItem;

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