Compare commits

...

39 Commits

Author SHA1 Message Date
wwsun 640859d36e chore(release): publish
- @music163/tango-context@0.2.8
 - @music163/tango-core@0.2.7
 - @music163/tango-designer@0.5.8
 - @music163/tango-sandbox@0.1.12
 - @music163/tango-setting-form@0.2.11
2023-11-27 14:02:30 +08:00
wwsun d91763379c fix: parse tango variables in view file 2023-11-27 13:55:53 +08:00
wwsun 588861f649 fix: event setter 2023-11-27 13:46:45 +08:00
wwsun 91b53da38e fix: list service functions 2023-11-24 09:50:14 +08:00
wwsun 9b0ada94a0 chore(release): publish
- @music163/tango-designer@0.5.7
2023-11-23 18:50:57 +08:00
Wells f604996e56 fix: update varTreeNode style (#69)
Co-authored-by: wwsun <ww.sww@outlook.com>
2023-11-23 18:46:56 +08:00
wwsun fb26540f27 chore(release): publish
- @music163/tango-context@0.2.7
 - @music163/tango-core@0.2.6
 - @music163/tango-designer@0.5.6
 - @music163/tango-helpers@0.1.8
 - @music163/tango-sandbox@0.1.11
 - @music163/tango-setting-form@0.2.10
 - @music163/tango-ui@0.1.11
2023-11-23 15:56:48 +08:00
Wells 558f2cd0a6 fix: refactor update import specifiers (#68)
* fix: refactor update import declaration

* fix: rename module methods

* fix: update types

* fix: update

---------

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

* test: update

* fix: test if code has expression wrapper

* fix: parse code to expression node

* fix: update long text style in variableTree

---------

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

* fix: refactor workspace

* fix: refactor viewModule

* fix: update interfaces

* fix: always switch to the view matched code file

* fix: keep sidebar state

* test: update

---------

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

* feat: collpase-panel add showBottomBorder prop

* feat: outline-panel add props

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

* fix: rename onFileChange to onFilesChange

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

* revert: remove defaultAddComponentType

* revert: remove addComponentType ts def
2023-10-31 16:49:14 +08:00
BoBoooooo fd2fa943b0 fix: resizable-box support horizontal and vertical dragging (#48) 2023-10-31 15:44:03 +08:00
wwsun 70fc6f8030 fix: optional context config 2023-10-25 11:24:18 +08:00
Jiyu Shao 4f7233bd6c chore: fix tsconfig paths (#44) 2023-10-25 11:22:01 +08:00
69 changed files with 5713 additions and 5129 deletions
+2 -1
View File
@@ -2,11 +2,12 @@
"extends": ["eslint-config-ali/typescript/react", "prettier"], "extends": ["eslint-config-ali/typescript/react", "prettier"],
"ignorePatterns": ["**/dist/**/*", "**/lib/**/*", "**/node_modules/**/*", "scripts/**/*"], "ignorePatterns": ["**/dist/**/*", "**/lib/**/*", "**/node_modules/**/*", "scripts/**/*"],
"rules": { "rules": {
"@typescript-eslint/consistent-type-definitions": "off",
"@typescript-eslint/dot-notation": "off", "@typescript-eslint/dot-notation": "off",
"@typescript-eslint/no-unused-vars": "warn", "@typescript-eslint/no-unused-vars": "warn",
"import/no-cycle": "off", "import/no-cycle": "off",
"no-nested-ternary": "off", "no-nested-ternary": "off",
"no-useless-return":"off", "no-useless-return": "off",
"no-param-reassign": "off", "no-param-reassign": "off",
"prefer-destructuring": "off" "prefer-destructuring": "off"
} }
+14 -6
View File
@@ -5,7 +5,7 @@
<h1 align="center">Tango LowCode Builder</h1> <h1 align="center">Tango LowCode Builder</h1>
<div align="center"> <div align="center">
A source code based low-code builder, developed by NetEase Cloud Music Develop Team. A source code based low-code builder.
[![license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/NetEase/tango/blob/main/LICENSE) [![license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/NetEase/tango/blob/main/LICENSE)
[![NPM version](https://img.shields.io/npm/v/@music163/tango-designer.svg?style=flat-square)](http://npmjs.org/package/@music163/tango-designer) [![NPM version](https://img.shields.io/npm/v/@music163/tango-designer.svg?style=flat-square)](http://npmjs.org/package/@music163/tango-designer)
@@ -79,6 +79,7 @@ yarn start
Join NetEase Tango Community to share your ideas, suggestions, or questions and connect with other users and contributors. Join NetEase Tango Community to share your ideas, suggestions, or questions and connect with other users and contributors.
- Discord: <https://discord.gg/B6hkGTe4Rz> - Discord: <https://discord.gg/B6hkGTe4Rz>
- [Usage Trends](https://npm-compare.com/@music163/tango-helpers,@music163/tango-context,@music163/tango-core,@music163/tango-setting-form,@music163/tango-sandbox,@music163/tango-ui,@music163/tango-designer)
## 🤝 Contributing ## 🤝 Contributing
@@ -89,16 +90,23 @@ Please read the [github contribution guide](https://docs.github.com/en/get-start
- Commit and push your code - Commit and push your code
- Open a Pull Request - Open a Pull Request
## 📈 Usage Trends
[Usage Trends of Tango Packages](https://npm-compare.com/@music163/tango-helpers,@music163/tango-context,@music163/tango-core,@music163/tango-setting-form,@music163/tango-sandbox,@music163/tango-ui,@music163/tango-designer)
## 💗 Acknowledgments ## 💗 Acknowledgments
Thanks to the NetEase Cloud Music Front-end team, Public Technology team, Live Broadcasting Technology team, and all the colleagues who participated in the Tango project. Thanks to the NetEase Cloud Music Front-end team, Public Technology team, Live Broadcasting Technology team, and all the developers who participated in the Tango project.
Thank you to CodeSandbox for providing the [Sandpack](https://sandpack.codesandbox.io/) project, which provides powerful online code execution capabilities for Tango. Thank you to CodeSandbox for providing the [Sandpack](https://sandpack.codesandbox.io/) project, which provides powerful online code execution capabilities for Tango.
## 📣 Product Promotion
![](https://p5.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/31629770956/da9e/3a74/4e00/7c69cf46a713f1b008bd1243b5b1ab1c.png)
Don't waste your time restoring the UI, try the NetEase Cloud Music "Seal D2C" development tool! Easily turn your design into code, support React, RN, Vue, WeChat apps and other multi-end scenarios, what you see is what you get!
Experience "Seal D2C" now:
- I'm a Figma user: <https://www.figma.com/community/plugin/1174548852019950797/seal-figma-to-code-d2c/>
- I'm a MasterGo user: <https://mastergo.com/community/plugin/98956774428196/>
## 📄 License ## 📄 License
This project is licensed under the terms of the [MIT license](./LICENSE) This project is licensed under the terms of the [MIT license](./LICENSE)
+14 -6
View File
@@ -5,7 +5,7 @@
<h1 align="center">Tango 低代码设计器</h1> <h1 align="center">Tango 低代码设计器</h1>
<div align="center"> <div align="center">
一个源码驱动的低代码设计器,来自网易云音乐技术团队 一个源码驱动的低代码设计器框架
[![license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/NetEase/tango/blob/main/LICENSE) [![license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/NetEase/tango/blob/main/LICENSE)
[![NPM version](https://img.shields.io/npm/v/@music163/tango-designer.svg?style=flat-square)](http://npmjs.org/package/@music163/tango-designer) [![NPM version](https://img.shields.io/npm/v/@music163/tango-designer.svg?style=flat-square)](http://npmjs.org/package/@music163/tango-designer)
@@ -79,6 +79,7 @@ yarn start
参与 NetEase Tango 的社区,以分享您的想法、建议或问题,并与其他用户和贡献者建立联系。 参与 NetEase Tango 的社区,以分享您的想法、建议或问题,并与其他用户和贡献者建立联系。
- Discord: <https://discord.gg/B6hkGTe4Rz> - Discord: <https://discord.gg/B6hkGTe4Rz>
- [使用趋势](https://npm-compare.com/@music163/tango-helpers,@music163/tango-context,@music163/tango-core,@music163/tango-setting-form,@music163/tango-sandbox,@music163/tango-ui,@music163/tango-designer)
## 🤝 参与共建 ## 🤝 参与共建
@@ -90,16 +91,23 @@ yarn start
- 合并修改 git rebase master - 合并修改 git rebase master
- 发起 Pull Request - 发起 Pull Request
## 📈 使用量趋势
[Tango 使用量趋势](https://npm-compare.com/@music163/tango-helpers,@music163/tango-context,@music163/tango-core,@music163/tango-setting-form,@music163/tango-sandbox,@music163/tango-ui,@music163/tango-designer)
## 💗 致谢 ## 💗 致谢
感谢网易云音乐公共技术团队,大前端团队,直播技术团队,以及所有参与过 Tango 项目的同学们 感谢网易云音乐公共技术团队,大前端团队,直播技术团队,以及所有参与过 Tango 项目的开发者
感谢 CodeSandbox 提供的 [Sandpack](https://sandpack.codesandbox.io/) 项目,为 Tango 提供了强大的基于浏览器的代码构建与执行能力。 感谢 CodeSandbox 提供的 [Sandpack](https://sandpack.codesandbox.io/) 项目,为 Tango 提供了强大的基于浏览器的代码构建与执行能力。
## 📣 产品推广
![](https://p5.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/31629770956/da9e/3a74/4e00/7c69cf46a713f1b008bd1243b5b1ab1c.png)
不要再浪费时间还原 UI 啦,快来试试网易云音乐「海豹 D2C」研发工具吧!轻松将设计稿转为代码,支持 React、RN、Vue、微信小程序等多端场景,所见即所得!
立即体验「海豹 D2C」:
- 我是 Figma 用户:<https://www.figma.com/community/plugin/1174548852019950797/seal-figma-to-code-d2c/>
- 我是 MasterGo 用户:<https://mastergo.com/community/plugin/98956774428196/>
## 📄 开源协议 ## 📄 开源协议
此项目遵循 [MIT 开源协议](./LICENSE) 此项目遵循 [MIT 开源协议](./LICENSE)
+26 -10
View File
@@ -17,37 +17,43 @@ const tangoConfigJson = {
version: '17.0.2', version: '17.0.2',
library: 'React', library: 'React',
type: 'dependency', type: 'dependency',
resources: ['https://unpkg.com/react@{{version}}/umd/react.development.js'], resources: ['https://cdn.jsdelivr.net/npm/react@{{version}}/umd/react.development.js'],
}, },
'react-dom': { 'react-dom': {
version: '17.0.2', version: '17.0.2',
library: 'ReactDOM', library: 'ReactDOM',
type: 'dependency', type: 'dependency',
resources: ['https://unpkg.com/react-dom@{{version}}/umd/react-dom.development.js'], resources: [
'https://cdn.jsdelivr.net/npm/react-dom@{{version}}/umd/react-dom.development.js',
],
}, },
'react-is': { 'react-is': {
version: '16.13.1', version: '16.13.1',
library: 'ReactIs', library: 'ReactIs',
type: 'dependency', type: 'dependency',
resources: ['https://unpkg.com/react-is@{{version}}/umd/react-is.production.min.js'], resources: [
'https://cdn.jsdelivr.net/npm/react-is@{{version}}/umd/react-is.production.min.js',
],
}, },
'styled-components': { 'styled-components': {
version: '5.3.5', version: '5.3.5',
library: 'styled', library: 'styled',
type: 'dependency', type: 'dependency',
resources: ['https://unpkg.com/styled-components@{{version}}/dist/styled-components.min.js'], resources: [
'https://cdn.jsdelivr.net/npm/styled-components@{{version}}/dist/styled-components.min.js',
],
}, },
moment: { moment: {
version: '2.29.4', version: '2.29.4',
library: 'moment', library: 'moment',
type: 'dependency', type: 'dependency',
resources: ['https://unpkg.com/moment@{{version}}/moment.js'], resources: ['https://cdn.jsdelivr.net/npm/moment@{{version}}/moment.js'],
}, },
'@music163/tango-boot': { '@music163/tango-boot': {
version: '0.2.1', version: '0.2.1',
library: 'TangoBoot', library: 'TangoBoot',
type: 'baseDependency', type: 'baseDependency',
resources: ['https://unpkg.com/@music163/tango-boot@{{version}}/dist/boot.js'], resources: ['https://cdn.jsdelivr.net/npm/@music163/tango-boot@{{version}}/dist/boot.js'],
// resources: ['http://localhost:9001/boot.js'], // resources: ['http://localhost:9001/boot.js'],
description: '云音乐低代码运行时框架', description: '云音乐低代码运行时框架',
}, },
@@ -56,14 +62,14 @@ const tangoConfigJson = {
library: 'TangoAntd', library: 'TangoAntd',
type: 'baseDependency', type: 'baseDependency',
resources: [ resources: [
'https://unpkg.com/@music163/antd@{{version}}/dist/index.js', 'https://cdn.jsdelivr.net/npm/@music163/antd@{{version}}/dist/index.js',
'https://unpkg.com/antd@4.24.13/dist/antd.css', 'https://cdn.jsdelivr.net/npm/antd@4.24.13/dist/antd.css',
], ],
description: '云音乐低代码中后台应用基础物料', description: '云音乐低代码中后台应用基础物料',
designerResources: [ designerResources: [
'https://unpkg.com/@music163/antd@{{version}}/dist/designer.js', 'https://cdn.jsdelivr.net/npm/@music163/antd@{{version}}/dist/designer.js',
// 'http://localhost:9002/designer.js', // 'http://localhost:9002/designer.js',
'https://unpkg.com/antd@4.24.13/dist/antd.css', 'https://cdn.jsdelivr.net/npm/antd@4.24.13/dist/antd.css',
], ],
}, },
}, },
@@ -145,6 +151,7 @@ import {
Input, Input,
FormilyForm, FormilyForm,
} from "@music163/antd"; } from "@music163/antd";
import { Space } from '@music163/antd';
class App extends React.Component { class App extends React.Component {
render() { render() {
return ( return (
@@ -152,8 +159,10 @@ class App extends React.Component {
<Section title="Section Title"> <Section title="Section Title">
</Section> </Section>
<Section> <Section>
<Space>
<Button>button</Button> <Button>button</Button>
<Input /> <Input />
</Space>
</Section> </Section>
</Page> </Page>
); );
@@ -178,6 +187,13 @@ import { defineServices } from '@music163/tango-boot';
import './sub'; import './sub';
export default defineServices({ 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: { get: {
url: 'https://nei.hz.netease.com/api/apimock-v2/cc974ffbaa7a85c77f30e4ce67deb67f/api/getUserProfile', url: 'https://nei.hz.netease.com/api/apimock-v2/cc974ffbaa7a85c77f30e4ce67deb67f/api/getUserProfile',
formatter: res => res.data, formatter: res => res.data,
+9 -9
View File
@@ -8,14 +8,14 @@
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
<script src="https://unpkg.com/react@16.14.0/umd/react.development.js"></script> <script src="https://cdn.jsdelivr.net/npm/react@16.14.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16.14.0/umd/react-dom.development.js"></script> <script src="https://cdn.jsdelivr.net/npm/react-dom@16.14.0/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/react-is@16.13.1/umd/react-is.production.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/react-is@16.13.1/umd/react-is.production.min.js"></script>
<script src="https://unpkg.com/moment/min/moment-with-locales.js"></script> <script src="https://cdn.jsdelivr.net/npm/moment/min/moment-with-locales.js"></script>
<script src="https://unpkg.com/styled-components@5.3.11/dist/styled-components.js"></script> <script src="https://cdn.jsdelivr.net/npm/styled-components@5.3.11/dist/styled-components.js"></script>
<script src="https://unpkg.com/@babel/standalone@7.15.6/babel.js"></script> <script src="https://cdn.jsdelivr.net/npm/@babel/standalone@7.15.6/babel.js"></script>
<script src="https://unpkg.com/prettier@2.6.0/standalone.js"></script> <script src="https://cdn.jsdelivr.net/npm/prettier@2.6.0/standalone.js"></script>
<script src="https://unpkg.com/prettier@2.6.0/parser-babel.js"></script> <script src="https://cdn.jsdelivr.net/npm/prettier@2.6.0/parser-babel.js"></script>
<script src="https://unpkg.com/antd@4.24.13/dist/antd-with-locales.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/antd@4.24.13/dist/antd-with-locales.min.js"></script>
</body> </body>
</html> </html>
+1
View File
@@ -34,6 +34,7 @@ const workspace = new Workspace({
// 2. 引擎初始化 // 2. 引擎初始化
const engine = createEngine({ const engine = createEngine({
workspace, workspace,
defaultActiveSidebarPanel: 'outline',
}); });
// @ts-ignore // @ts-ignore
+70
View File
@@ -0,0 +1,70 @@
import React from 'react';
import { Box } from 'coral-system';
import { CodeSandbox } from '@music163/tango-sandbox';
import { JsonView } from '@music163/tango-ui';
export default {
title: 'CodeSandbox',
};
const entryFilename = '/index.js';
const packageJson = {
name: 'demo',
private: true,
main: entryFilename,
dependencies: {
react: '^17.0.1',
'react-dom': '^17.0.1',
},
};
const entryFile = `
import React from 'react';
import ReactDOM from "react-dom";
import App from './src';
const rootElement = document.getElementById("root");
ReactDOM.render(
<App />,
rootElement
);
`;
const srcAppFile = `
import React from 'react';
export default function App() {
return <div style={{ background: 'yellow', color: 'red' }}>hello world</div>;
}
`;
const files = {
'/package.json': {
code: JSON.stringify(packageJson),
},
[entryFilename]: {
code: entryFile,
},
'/src/index.js': { code: srcAppFile },
};
export function Basic() {
return (
<Box display="flex" height="100vh">
<CodeSandbox
template="create-react-app"
bundlerURL="https://codesandbox.fn.netease.com"
files={files}
entry={entryFilename}
style={{
flex: 1,
}}
/>
<Box width="30%">
<JsonView src={files} />
</Box>
</Box>
);
}
+14 -1
View File
@@ -137,7 +137,7 @@ const config = {
], ],
}, },
{ {
title: '更多', title: '相关资源',
items: [ items: [
{ {
label: '博客', label: '博客',
@@ -149,6 +149,19 @@ const config = {
}, },
], ],
}, },
{
title: '更多产品',
items: [
{
label: '海豹 D2C - Figma 插件',
to: 'https://www.figma.com/community/plugin/1174548852019950797/seal-figma-to-code-d2c',
},
{
label: '海豹 D2C - MasterGo 插件',
to: 'https://mastergo.com/community/plugin/98956774428196/',
},
],
},
], ],
logo: { logo: {
alt: 'NetEase Cloud Music', alt: 'NetEase Cloud Music',
+2 -2
View File
@@ -24,7 +24,7 @@ const timelines = [
{ {
icon: null, icon: null,
title: '1.0 RC', title: '1.0 RC',
date: '2023.10.30', date: '2023.11.30',
description: translate({ description: translate({
id: 'homepage.timeline.rc', id: 'homepage.timeline.rc',
message: '核心 API 基本稳定,不再发生 BR,发布 1.0 RC 版本。', message: '核心 API 基本稳定,不再发生 BR,发布 1.0 RC 版本。',
@@ -33,7 +33,7 @@ const timelines = [
{ {
icon: null, icon: null,
title: '1.0', title: '1.0',
date: 'Before 2023.12.25', date: 'Before 2023.12.29',
description: translate({ description: translate({
id: 'homepage.timeline.stable', id: 'homepage.timeline.stable',
message: 'API 完全稳定,提供良好的社区支持,可用于生产环境。', message: 'API 完全稳定,提供良好的社区支持,可用于生产环境。',
+1 -1
View File
@@ -154,7 +154,7 @@ const config = {
// testLocationInResults: false, // testLocationInResults: false,
// The glob patterns Jest uses to detect test files // The glob patterns Jest uses to detect test files
testMatch: ['**/__tests__/**/*.[jt]s?(x)', '**/?(*.)+(spec|test).[tj]s?(x)'], testMatch: ['**/tests/**/*.[jt]s?(x)', '**/?(*.)+(spec|test).[tj]s?(x)'],
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
// testPathIgnorePatterns: [ // testPathIgnorePatterns: [
+21 -24
View File
@@ -7,7 +7,7 @@
], ],
"scripts": { "scripts": {
"start": "yarn workspace playground start", "start": "yarn workspace playground start",
"start:story": "yarn workspace storybook storybook", "start:docs": "yarn workspace storybook storybook",
"start:site": "yarn workspace website start", "start:site": "yarn workspace website start",
"deploy:site": "yarn workspace website deploy", "deploy:site": "yarn workspace website deploy",
"build": "lerna run build", "build": "lerna run build",
@@ -25,45 +25,42 @@
"prepare": "husky install" "prepare": "husky install"
}, },
"devDependencies": { "devDependencies": {
"@babel/preset-env": "^7.19.3", "@babel/preset-env": "^7.23.2",
"@babel/preset-react": "^7.18.6", "@babel/preset-react": "^7.22.15",
"@babel/preset-typescript": "^7.18.6", "@babel/preset-typescript": "^7.23.2",
"@commitlint/cli": "^17.5.0", "@commitlint/cli": "^17.5.0",
"@commitlint/config-conventional": "^17.3.0", "@commitlint/config-conventional": "^17.3.0",
"@types/jest": "^29.2.4", "@types/jest": "^29.5.7",
"@types/lodash-es": "^4.17.8", "@types/lodash-es": "^4.17.10",
"@types/lodash.get": "^4.4.7", "@types/lodash.get": "^4.4.8",
"@types/lodash.isequal": "^4.5.6", "@types/lodash.isequal": "^4.5.7",
"@types/lodash.set": "^4.3.7", "@types/lodash.set": "^4.3.8",
"@types/react": "^18.2.7", "@types/react": "^18.2.34",
"@types/react-dom": "^18.2.4", "@types/react-dom": "^18.2.14",
"@types/styled-components": "^5.1.25", "@types/styled-components": "^5.1.29",
"@typescript-eslint/eslint-plugin": "^6.4.1", "@typescript-eslint/eslint-plugin": "^6.9.1",
"@typescript-eslint/parser": "^6.4.1", "@typescript-eslint/parser": "^6.9.1",
"conventional-changelog-cli": "^2.2.2", "conventional-changelog-cli": "^2.2.2",
"copyfiles": "^2.4.1", "copyfiles": "^2.4.1",
"eslint": "^8.40.0", "eslint": "^8.52.0",
"eslint-config-ali": "^14.0.2", "eslint-config-ali": "^14.0.2",
"eslint-config-prettier": "^9.0.0", "eslint-config-prettier": "^9.0.0",
"eslint-import-resolver-typescript": "^3.6.0", "eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-import": "^2.28.1", "eslint-plugin-import": "^2.29.0",
"eslint-plugin-react": "^7.33.2", "eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.0",
"http-server": "^14.1.0", "http-server": "^14.1.0",
"husky": "^8.0.3", "husky": "^8.0.3",
"jest": "^29.1.1", "jest": "^29.7.0",
"jest-environment-jsdom": "^29.1.1", "jest-environment-jsdom": "^29.7.0",
"lerna": "^6.6.1", "lerna": "^6.6.1",
"lint-staged": "^14.0.1", "lint-staged": "^14.0.1",
"prettier": "^2.8.7", "prettier": "^2.8.7",
"react": "^17.0.0", "react": "^17.0.0",
"react-dom": "^17.0.0", "react-dom": "^17.0.0",
"styled-components": "^5.3.6", "styled-components": "^5.3.6",
"typedoc": "^0.25.0", "typedoc": "^0.25.3",
"typescript": "^5.0.4" "typescript": "^5.2.2"
},
"resolutions": {
"@yarnpkg/parsers": "3.0.0-rc.48.1"
}, },
"lint-staged": { "lint-staged": {
"*.{js,jsx,ts,tsx}": [ "*.{js,jsx,ts,tsx}": [
+22
View File
@@ -3,6 +3,28 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.2.8](https://github.com/netease/tango/compare/@music163/tango-context@0.2.7...@music163/tango-context@0.2.8) (2023-11-27)
**Note:** Version bump only for package @music163/tango-context
## [0.2.7](https://github.com/netease/tango/compare/@music163/tango-context@0.2.6...@music163/tango-context@0.2.7) (2023-11-23)
**Note:** Version bump only for package @music163/tango-context
## [0.2.6](https://github.com/netease/tango/compare/@music163/tango-context@0.2.5...@music163/tango-context@0.2.6) (2023-11-20)
**Note:** Version bump only for package @music163/tango-context
## [0.2.5](https://github.com/netease/tango/compare/@music163/tango-context@0.2.4...@music163/tango-context@0.2.5) (2023-11-16)
**Note:** Version bump only for package @music163/tango-context
## [0.2.4](https://github.com/netease/tango/compare/@music163/tango-context@0.2.3...@music163/tango-context@0.2.4) (2023-11-02)
### Bug Fixes
- optional context config ([70fc6f8](https://github.com/netease/tango/commit/70fc6f8030202f5d340281c0424752044ed0fb83))
## [0.2.3](https://github.com/netease/tango/compare/@music163/tango-context@0.2.2...@music163/tango-context@0.2.3) (2023-10-23) ## [0.2.3](https://github.com/netease/tango/compare/@music163/tango-context@0.2.2...@music163/tango-context@0.2.3) (2023-10-23)
**Note:** Version bump only for package @music163/tango-context **Note:** Version bump only for package @music163/tango-context
+3 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "@music163/tango-context", "name": "@music163/tango-context",
"version": "0.2.3", "version": "0.2.8",
"description": "react context for tango-apps", "description": "react context for tango-apps",
"keywords": [ "keywords": [
"react", "react",
@@ -31,8 +31,8 @@
"react": ">= 16.8" "react": ">= 16.8"
}, },
"dependencies": { "dependencies": {
"@music163/tango-core": "^0.2.2", "@music163/tango-core": "^0.2.7",
"@music163/tango-helpers": "^0.1.4", "@music163/tango-helpers": "^0.1.8",
"mobx-react-lite": "4.0.2" "mobx-react-lite": "4.0.2"
}, },
"publishConfig": { "publishConfig": {
+3 -3
View File
@@ -16,9 +16,9 @@ export interface ITangoEngineContext {
/** /**
* 自定义配置数据 * 自定义配置数据
*/ */
config: { config?: {
customActionVariables: CustomVariableData[]; customActionVariables?: CustomVariableData[];
customExpressionVariables: CustomVariableData[]; customExpressionVariables?: CustomVariableData[];
}; };
} }
+40
View File
@@ -3,6 +3,46 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.2.7](https://github.com/netease/tango/compare/@music163/tango-core@0.2.6...@music163/tango-core@0.2.7) (2023-11-27)
### Bug Fixes
- list service functions ([91b53da](https://github.com/netease/tango/commit/91b53da38e362ae320457429037dc347adf90bd3))
- parse tango variables in view file ([d917633](https://github.com/netease/tango/commit/d91763379c4ccf9d826b717aa80a07abdaf2e3a2))
## [0.2.6](https://github.com/netease/tango/compare/@music163/tango-core@0.2.5...@music163/tango-core@0.2.6) (2023-11-23)
### Bug Fixes
- prototype to import declaration ([#67](https://github.com/netease/tango/issues/67)) ([8a1234e](https://github.com/netease/tango/commit/8a1234e565489c92f041356b7a06339eaeee48df))
- refactor update import specifiers ([#68](https://github.com/netease/tango/issues/68)) ([558f2cd](https://github.com/netease/tango/commit/558f2cd0a692c6bbc866d08250d25e2619af183f))
## [0.2.5](https://github.com/netease/tango/compare/@music163/tango-core@0.2.4...@music163/tango-core@0.2.5) (2023-11-20)
### Bug Fixes
- refactor parse expression ([#61](https://github.com/netease/tango/issues/61)) ([dbbd1dd](https://github.com/netease/tango/commit/dbbd1dddc75c532b7c9710ab0941c8680100f093))
## [0.2.4](https://github.com/netease/tango/compare/@music163/tango-core@0.2.3...@music163/tango-core@0.2.4) (2023-11-16)
### Bug Fixes
- allow override existing file via addFile ([6cf673e](https://github.com/netease/tango/commit/6cf673e7f6f330613e1d6331256a59900e8cbc68))
- export isJSXElementById from traverse ([#54](https://github.com/netease/tango/issues/54)) ([208e275](https://github.com/netease/tango/commit/208e275bb1cd98ed21d816c26179f9ebc11df223))
- parse variables from view ([089a482](https://github.com/netease/tango/commit/089a482f750e9d9a7743a6641d6e39989347d318))
- update types ([653386d](https://github.com/netease/tango/commit/653386dcc0b064b41915548129952eeabe53019f))
## [0.2.3](https://github.com/netease/tango/compare/@music163/tango-core@0.2.2...@music163/tango-core@0.2.3) (2023-11-02)
### Bug Fixes
- add onFilesChange to workspace ([#50](https://github.com/netease/tango/issues/50)) ([d775a50](https://github.com/netease/tango/commit/d775a5003ab1a14f801d3b38f7187ea15ef7d74d))
- **core:** create store without entry file ([ae806ae](https://github.com/netease/tango/commit/ae806aee3583ef5f957836e15d715035c4556867))
- **core:** logging drop action ([963bfe1](https://github.com/netease/tango/commit/963bfe1ab80fa4114637963534b2891a27538120))
- designer model add defaultActiveSidebarPanel ([#51](https://github.com/netease/tango/issues/51)) ([9dd11f7](https://github.com/netease/tango/commit/9dd11f7a6d5807c80a63c84c1ad0df5bc1ce9558))
- refactor workspace onFilesChange ([f53ce94](https://github.com/netease/tango/commit/f53ce94c6ed777d45ba6137188bfbc4566e03942))
- update types ([46d5f59](https://github.com/netease/tango/commit/46d5f59b4cd4fa71ed247f5e470e66c8c9f6d4a0))
## [0.2.2](https://github.com/netease/tango/compare/@music163/tango-core@0.2.1...@music163/tango-core@0.2.2) (2023-10-23) ## [0.2.2](https://github.com/netease/tango/compare/@music163/tango-core@0.2.1...@music163/tango-core@0.2.2) (2023-10-23)
### Bug Fixes ### Bug Fixes
+8 -8
View File
@@ -1,6 +1,6 @@
{ {
"name": "@music163/tango-core", "name": "@music163/tango-core",
"version": "0.2.2", "version": "0.2.7",
"description": "tango core", "description": "tango core",
"author": "wwsun <ww.sun@outlook.com>", "author": "wwsun <ww.sun@outlook.com>",
"homepage": "", "homepage": "",
@@ -24,13 +24,13 @@
"prepublishOnly": "yarn build" "prepublishOnly": "yarn build"
}, },
"dependencies": { "dependencies": {
"@babel/generator": "^7.22.15", "@babel/generator": "^7.23.0",
"@babel/parser": "^7.22.15", "@babel/parser": "^7.23.0",
"@babel/traverse": "^7.22.15", "@babel/traverse": "^7.23.2",
"@babel/types": "^7.22.15", "@babel/types": "^7.23.0",
"@music163/tango-helpers": "^0.1.4", "@music163/tango-helpers": "^0.1.8",
"@types/babel__generator": "^7.6.4", "@types/babel__generator": "^7.6.6",
"@types/babel__traverse": "^7.18.3", "@types/babel__traverse": "^7.20.3",
"mobx": "6.9.0", "mobx": "6.9.0",
"path-browserify": "^1.0.1" "path-browserify": "^1.0.1"
}, },
+6
View File
@@ -10,6 +10,10 @@ interface ICreateEngineOptions {
* 默认的模拟器模式 * 默认的模拟器模式
*/ */
defaultSimulatorMode?: SimulatorNameType; defaultSimulatorMode?: SimulatorNameType;
/**
* 默认激活的侧边栏
*/
defaultActiveSidebarPanel?: string;
} }
/** /**
@@ -20,12 +24,14 @@ interface ICreateEngineOptions {
export function createEngine({ export function createEngine({
workspace, workspace,
defaultSimulatorMode = 'desktop', defaultSimulatorMode = 'desktop',
defaultActiveSidebarPanel = '',
}: ICreateEngineOptions) { }: ICreateEngineOptions) {
const engine = new Engine({ const engine = new Engine({
workspace, workspace,
designer: new Designer({ designer: new Designer({
workspace, workspace,
simulator: defaultSimulatorMode, simulator: defaultSimulatorMode,
activeSidebarPanel: defaultActiveSidebarPanel,
}), }),
}); });
+27
View File
@@ -1,3 +1,5 @@
import { isValidExpressionCode } from './ast';
const defineServiceHandlerNames = ['defineServices', 'createServices']; const defineServiceHandlerNames = ['defineServices', 'createServices'];
const sfHandlerPattern = new RegExp(`^(${defineServiceHandlerNames.join('|')})$`); const sfHandlerPattern = new RegExp(`^(${defineServiceHandlerNames.join('|')})$`);
@@ -20,3 +22,28 @@ const defineStoreHandlerName = 'defineStore';
export function isDefineStore(name: string) { export function isDefineStore(name: string) {
return defineStoreHandlerName === name; return defineStoreHandlerName === name;
} }
/**
* 是否是 tango 的变量引用
* @example tango.stores.app.name
* @example tango.stores?.app?.name
*
* @param name
* @returns
*/
export function isTangoVariable(name: string) {
return /^tango\??\.(stores|services)\??\./.test(name) && name.split('.').length > 2;
}
const templatePattern = /^{(.+)}$/s;
/**
* 判断给定字符串是否被表达式容器`{expCode}`包裹
* @param code
*/
export function isWrappedByExpressionContainer(code: string, isStrict = true) {
if (isStrict && isValidExpressionCode(code)) {
return false;
}
return templatePattern.test(code);
}
+8 -8
View File
@@ -6,10 +6,10 @@ import * as t from '@babel/types';
import { import {
logger, logger,
isValidObjectString, isValidObjectString,
isVariableString,
getVariableContent, getVariableContent,
isPlainObject, isPlainObject,
} from '@music163/tango-helpers'; } from '@music163/tango-helpers';
import { isWrappedByExpressionContainer } from '../assert';
// @see https://babeljs.io/docs/en/babel-parser#pluginss // @see https://babeljs.io/docs/en/babel-parser#pluginss
const babelParserConfig: ParserOptions = { const babelParserConfig: ParserOptions = {
@@ -22,7 +22,6 @@ const babelParserConfig: ParserOptions = {
'classProperties', 'classProperties',
'asyncGenerators', 'asyncGenerators',
'functionBind', 'functionBind',
'functionSent',
'dynamicImport', 'dynamicImport',
'optionalChaining', 'optionalChaining',
], ],
@@ -44,6 +43,7 @@ export function isValidCode(code: string) {
/** /**
* 检测代码是否是合法的表达式代码 * 检测代码是否是合法的表达式代码
* 表达式是一组代码的集合,它返回一个值;每一个合法的表达式都能计算成某个值
* @param code * @param code
* @returns * @returns
*/ */
@@ -91,8 +91,8 @@ export function code2expression(code: string) {
try { try {
expNode = t.cloneNode(parseExpression(code, babelParserConfig), false, true); expNode = t.cloneNode(parseExpression(code, babelParserConfig), false, true);
} catch (err) { } catch (err) {
console.error('invalid code', err); logger.error('invalid code', err);
expNode = t.identifier('undefined'); // expNode = t.identifier('undefined');
} }
return expNode; return expNode;
} }
@@ -103,7 +103,7 @@ export function code2expression(code: string) {
* @returns File * @returns File
*/ */
export function expressionCode2ast(code: string) { export function expressionCode2ast(code: string) {
if (isVariableString(code)) { if (isWrappedByExpressionContainer(code)) {
code = getVariableContent(code); code = getVariableContent(code);
} }
const node = code2expression(code); const node = code2expression(code);
@@ -130,7 +130,7 @@ export function value2node(
ret = t.numericLiteral(value); ret = t.numericLiteral(value);
break; break;
case 'string': case 'string':
if (isVariableString(value)) { if (isWrappedByExpressionContainer(value)) {
// 再检查是否是表达式容器,例如 {this.foo}, {1} // 再检查是否是表达式容器,例如 {this.foo}, {1}
const innerString = getVariableContent(value); const innerString = getVariableContent(value);
ret = code2expression(innerString); ret = code2expression(innerString);
@@ -201,7 +201,7 @@ export function value2jsxAttributeValueNode(value: any) {
if (isValidObjectString(value)) { if (isValidObjectString(value)) {
// 先检查是否是对象字符串 // 先检查是否是对象字符串
ret = t.jsxExpressionContainer(code2expression(value)); ret = t.jsxExpressionContainer(code2expression(value));
} else if (isVariableString(value)) { } else if (isWrappedByExpressionContainer(value)) {
// 再检查是否是表达式容器,例如 {this.foo}, {1} // 再检查是否是表达式容器,例如 {this.foo}, {1}
const innerString = getVariableContent(value); const innerString = getVariableContent(value);
ret = t.jsxExpressionContainer(code2expression(innerString)); ret = t.jsxExpressionContainer(code2expression(innerString));
@@ -221,7 +221,7 @@ export function value2jsxChildrenValueNode(value: any) {
let ret: t.JSXElement | t.JSXFragment | t.JSXExpressionContainer | t.JSXSpreadChild | t.JSXText; let ret: t.JSXElement | t.JSXFragment | t.JSXExpressionContainer | t.JSXSpreadChild | t.JSXText;
switch (typeof value) { switch (typeof value) {
case 'string': case 'string':
if (isVariableString(value)) { if (isWrappedByExpressionContainer(value)) {
const innerString = getVariableContent(value); const innerString = getVariableContent(value);
ret = t.jsxExpressionContainer(code2expression(innerString)); ret = t.jsxExpressionContainer(code2expression(innerString));
} else { } else {
+200 -15
View File
@@ -23,15 +23,15 @@ import {
object2node, object2node,
} from './parse'; } from './parse';
import { isValidComponentName } from '../string'; import { isValidComponentName } from '../string';
import { isDefineService, isDefineStore } from '../assert'; import { isDefineService, isDefineStore, isTangoVariable } from '../assert';
import type { import type {
IRouteData, IRouteData,
IStorePropertyData, IStorePropertyData,
IClassPropertyNodeData,
ITangoViewNodeData, ITangoViewNodeData,
IImportDeclarationPayload, IImportDeclarationPayload,
IServiceFunctionPayload, IServiceFunctionPayload,
InsertChildPositionType, InsertChildPositionType,
IImportSpecifierData,
} from '../../types'; } from '../../types';
import { IdGenerator } from '../id-generator'; import { IdGenerator } from '../id-generator';
@@ -194,7 +194,7 @@ export function queryXFormItemFields(ast: t.File | t.JSXElement) {
* @param node * @param node
* @param id * @param id
*/ */
function isJSXElementById(node: t.JSXElement, jsxElementNodeId: string) { export function isJSXElementById(node: t.JSXElement, jsxElementNodeId: string) {
let isTargetJSXElement = false; let isTargetJSXElement = false;
visitJSXElementAttributes(node, (name, value) => { visitJSXElementAttributes(node, (name, value) => {
if (name === SLOT.dnd && value === jsxElementNodeId) { if (name === SLOT.dnd && value === jsxElementNodeId) {
@@ -448,11 +448,12 @@ function getLastExportDeclarationIndex(node: t.Program) {
/** /**
* 生成新的 ImportDeclaration * 生成新的 ImportDeclaration
* @deprecated 使用 makeImportDeclaration2 代替
* @param importedModule * @param importedModule
* @param importedSourcePath * @param importedSourcePath
* @returns * @returns
*/ */
export function makeImportDeclaration(importedModule: IImportDeclarationPayload) { export function makeImportDeclarationLegacy(importedModule: IImportDeclarationPayload) {
const specifierNodes = const specifierNodes =
importedModule.specifiers?.map((localName) => importedModule.specifiers?.map((localName) =>
t.importSpecifier(t.identifier(localName), t.identifier(localName)), t.importSpecifier(t.identifier(localName), t.identifier(localName)),
@@ -467,6 +468,36 @@ export function makeImportDeclaration(importedModule: IImportDeclarationPayload)
return t.importDeclaration(newSpecifierNodes, t.stringLiteral(importedModule.sourcePath)); 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 { function getImportDeclarationData(node: t.ImportDeclaration): IImportDeclarationPayload {
const sourcePath = node2value(node.source); const sourcePath = node2value(node.source);
let defaultSpecifier; let defaultSpecifier;
@@ -487,16 +518,33 @@ function getImportDeclarationData(node: t.ImportDeclaration): IImportDeclaration
/** /**
* 添加新的导入语句 * 添加新的导入语句
* @deprecated 使用 addImportDeclaration2 代替
* @param ast * @param ast
* @param importedModule * @param importedModule
* @param importedSourcePath * @param importedSourcePath
* @returns * @returns
*/ */
export function addImportDeclaration(ast: t.File, importedModule: IImportDeclarationPayload) { export function addImportDeclarationLegacy(ast: t.File, importedModule: IImportDeclarationPayload) {
traverse(ast, { traverse(ast, {
Program(path) { Program(path) {
const lastIndex = getLastImportDeclarationIndex(path.node); const lastIndex = getLastImportDeclarationIndex(path.node);
const newImportDeclaration = makeImportDeclaration(importedModule); const newImportDeclaration = makeImportDeclarationLegacy(importedModule);
path.node.body.splice(lastIndex, 0, newImportDeclaration);
path.stop();
},
});
return ast;
}
export function addImportDeclaration(
ast: t.File,
source: string,
specifiers: IImportSpecifierData[],
) {
traverse(ast, {
Program(path) {
const lastIndex = getLastImportDeclarationIndex(path.node);
const newImportDeclaration = makeImportDeclaration(source, specifiers);
path.node.body.splice(lastIndex, 0, newImportDeclaration); path.node.body.splice(lastIndex, 0, newImportDeclaration);
path.stop(); path.stop();
}, },
@@ -506,17 +554,21 @@ export function addImportDeclaration(ast: t.File, importedModule: IImportDeclara
/** /**
* 更新已有的导入语句 * 更新已有的导入语句
* @deprecated 使用 updateImportDeclaration2 代替
* @param ast * @param ast
* @param importedModule * @param importedModule
* @param importedSourcePath * @param importedSourcePath
* @returns * @returns
*/ */
export function updateImportDeclaration(ast: t.File, importedModule: IImportDeclarationPayload) { export function updateImportDeclarationLegacy(
ast: t.File,
importedModule: IImportDeclarationPayload,
) {
traverse(ast, { traverse(ast, {
ImportDeclaration(path) { ImportDeclaration(path) {
const currentSourcePath = node2value(path.node.source); const currentSourcePath = node2value(path.node.source);
if (currentSourcePath === importedModule.sourcePath) { if (currentSourcePath === importedModule.sourcePath) {
const newImportDeclaration = makeImportDeclaration(importedModule); const newImportDeclaration = makeImportDeclarationLegacy(importedModule);
path.replaceWith(newImportDeclaration); path.replaceWith(newImportDeclaration);
path.stop(); // 只修改匹配到的第一条 path.stop(); // 只修改匹配到的第一条
} }
@@ -525,6 +577,50 @@ export function updateImportDeclaration(ast: t.File, importedModule: IImportDecl
return ast; return ast;
} }
export function updateImportDeclaration(
ast: t.File,
source: string,
specifiers: IImportSpecifierData[],
) {
traverse(ast, {
ImportDeclaration(path) {
const currentSourcePath = node2value(path.node.source);
if (currentSourcePath === source) {
const newImportDeclaration = makeImportDeclaration(source, specifiers);
path.replaceWith(newImportDeclaration);
path.stop(); // 只修改匹配到的第一条
}
},
});
return ast;
}
/**
* 再已有的导入语句中添加新的导入符号
* @param ast
* @param source
* @param newSpecifiers
* @returns
*/
export function insertImportSpecifiers(
ast: t.File,
source: string,
newSpecifiers: IImportSpecifierData[],
) {
traverse(ast, {
ImportDeclaration(path) {
const currentSourcePath = node2value(path.node.source);
if (currentSourcePath === source) {
const nodes = specifierDataList2nodes(newSpecifiers);
path.node.specifiers.push(...nodes);
// 只在匹配到的第一个导入声明语句添加即可,不再重复执行
path.stop();
}
},
});
return ast;
}
/** /**
* 新增类属性 * 新增类属性
* @param ast * @param ast
@@ -986,7 +1082,12 @@ export function traverseServiceFile(ast: t.File) {
const baseConfig: any = { const baseConfig: any = {
encryptFetch: false, encryptFetch: false,
}; };
const imports: Record<string, IImportSpecifierData[]> = {};
traverse(ast, { traverse(ast, {
ImportDeclaration(path) {
const { source, specifiers } = parseImportDeclaration(path.node);
imports[source] = specifiers;
},
CallExpression(path) { CallExpression(path) {
const calleeName = keyNode2value(path.node.callee) as string; const calleeName = keyNode2value(path.node.callee) as string;
if (isDefineService(calleeName)) { if (isDefineService(calleeName)) {
@@ -1003,6 +1104,7 @@ export function traverseServiceFile(ast: t.File) {
}, },
}); });
return { return {
imports,
services, services,
baseConfig, baseConfig,
}; };
@@ -1107,6 +1209,7 @@ export function cloneJSXElementWithoutTrackingData(node: t.JSXElement) {
} }
export function traverseViewFile(ast: t.File, idGenerator: IdGenerator) { export function traverseViewFile(ast: t.File, idGenerator: IdGenerator) {
const imports: Record<string, IImportSpecifierData[]> = {};
const importedModules: Dict<IImportDeclarationPayload | IImportDeclarationPayload[]> = {}; const importedModules: Dict<IImportDeclarationPayload | IImportDeclarationPayload[]> = {};
const nodes: Array<ITangoViewNodeData<t.JSXElement>> = []; const nodes: Array<ITangoViewNodeData<t.JSXElement>> = [];
const cloneAst = t.cloneNode(ast, true, true); const cloneAst = t.cloneNode(ast, true, true);
@@ -1115,12 +1218,43 @@ export function traverseViewFile(ast: t.File, idGenerator: IdGenerator) {
traverse(ast, { traverse(ast, {
ImportDeclaration(path) { ImportDeclaration(path) {
const declarationData = getImportDeclarationData(path.node); const { source, specifiers } = parseImportDeclaration(path.node);
const exist = importedModules[declarationData.sourcePath]; if (imports[source]) {
if (!exist) { // 存在重复的导入语句,合并导入符号列表
importedModules[declarationData.sourcePath] = declarationData; imports[source] = imports[source].concat(specifiers);
} else { } else {
importedModules[declarationData.sourcePath] = Array.isArray(exist) imports[source] = specifiers;
}
// FIXME: 下面的逻辑兼容旧的逻辑,后续需要移除掉
const declarationData = specifiers.reduce(
(prev, cur) => {
switch (cur.type) {
case 'ImportDefaultSpecifier':
prev.defaultSpecifier = cur.localName;
break;
case 'ImportSpecifier':
prev.specifiers.push(cur.localName);
break;
case 'ImportNamespaceSpecifier':
prev.specifiers.push(cur.localName);
break;
default:
break;
}
return prev;
},
{
defaultSpecifier: undefined,
specifiers: [],
sourcePath: source,
},
);
const exist = importedModules[source];
if (!exist) {
importedModules[source] = declarationData;
} else {
importedModules[source] = Array.isArray(exist)
? exist.concat([declarationData]) ? exist.concat([declarationData])
: [exist, declarationData]; : [exist, declarationData];
} }
@@ -1128,15 +1262,23 @@ export function traverseViewFile(ast: t.File, idGenerator: IdGenerator) {
MemberExpression(path) { MemberExpression(path) {
const variable = node2value(path.node, false); const variable = node2value(path.node, false);
const parentNode = path.parentPath.node;
if ( if (
/^tango\.(stores|services)\./.test(variable) && !t.isMemberExpression(parentNode) &&
variable.split('.').length === 3 && isTangoVariable(variable) &&
!variables.includes(variable) !variables.includes(variable)
) { ) {
variables.push(variable); variables.push(variable);
} }
}, },
OptionalMemberExpression(path) {
const variable = node2value(path.node, false);
if (isTangoVariable(variable) && !variables.includes(variable)) {
variables.push(variable);
}
},
JSXElement(path) { JSXElement(path) {
const attributes = getJSXElementAttributes(path.node); const attributes = getJSXElementAttributes(path.node);
@@ -1183,7 +1325,50 @@ export function traverseViewFile(ast: t.File, idGenerator: IdGenerator) {
ast, ast,
cleanAst, cleanAst,
nodes, nodes,
imports,
importedModules, importedModules,
variables, variables,
}; };
} }
/**
* 解析导入语句
*/
function parseImportDeclaration(node: t.ImportDeclaration) {
const source = node2value(node.source) as string;
const specifiers: IImportSpecifierData[] = [];
node.specifiers.forEach((specifierNode) => {
const data: IImportSpecifierData = {
localName: keyNode2value(specifierNode.local) as string,
type: specifierNode.type,
};
if (specifierNode.type === 'ImportSpecifier') {
data.importedName = keyNode2value(specifierNode.imported) as string;
}
specifiers.push(data);
});
return {
source,
specifiers,
};
}
/**
* 基本的文件解析过程
* @param ast
*/
export function traverseFile(ast: t.File) {
const imports: Record<string, IImportSpecifierData[]> = {};
traverse(ast, {
ImportDeclaration(path) {
const { source, specifiers } = parseImportDeclaration(path.node);
imports[source] = specifiers;
},
});
return {
imports,
};
}
+6 -3
View File
@@ -1,5 +1,6 @@
import { getVariableContent, isVariableString } from '@music163/tango-helpers'; import { getVariableContent } from '@music163/tango-helpers';
import { value2node, expression2code } from './ast'; import { value2node, expression2code, isValidExpressionCode } from './ast';
import { isWrappedByExpressionContainer } from './assert';
/** /**
* 将 js value 转换为代码字符串 * 将 js value 转换为代码字符串
@@ -36,7 +37,9 @@ export function value2expressionCode(val: any) {
switch (typeof val) { switch (typeof val) {
case 'string': { case 'string': {
if (isVariableString(val)) { if (isValidExpressionCode(val)) {
ret = val;
} else if (isWrappedByExpressionContainer(val, false)) {
ret = getVariableContent(val); ret = getVariableContent(val);
} else if (isStringCode(val)) { } else if (isStringCode(val)) {
ret = val; ret = val;
+5 -2
View File
@@ -1,5 +1,5 @@
import { Dict } from '@music163/tango-helpers'; 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 * @param nameMap
* @returns * @returns
*/ */
export function namesToImportDeclarations(names: string[], nameMap: Dict<IImportSpecifierData>) { export function namesToImportDeclarations(
names: string[],
nameMap: Dict<IImportSpecifierSourceData>,
) {
const map = {}; const map = {};
names.forEach((name) => { names.forEach((name) => {
const mod = nameMap[name]; const mod = nameMap[name];
+41 -3
View File
@@ -3,16 +3,51 @@ import {
ComponentPropType, ComponentPropType,
ComponentPrototypeType, ComponentPrototypeType,
isNil, isNil,
isVariableString,
logger, logger,
uuid, uuid,
} from '@music163/tango-helpers'; } from '@music163/tango-helpers';
import { getRelativePath, isFilepath } from './string'; import { getRelativePath, isFilepath } from './string';
import type { IImportDeclarationPayload } from '../types'; import type { IImportDeclarationPayload, IImportSpecifierData } from '../types';
import { code2expression } from './ast'; import { code2expression } from './ast';
import { isWrappedByExpressionContainer } from './assert';
export function prototype2importDeclarationData(
prototype: ComponentPrototypeType,
relativeFilepath?: string,
): { source: string; specifiers: IImportSpecifierData[] } {
let source = prototype.package;
if (relativeFilepath && isFilepath(source)) {
source = getRelativePath(relativeFilepath, source);
}
if (source.endsWith('.js')) {
source = source.slice(0, -3);
}
const specifiers: IImportSpecifierData[] = [];
if (prototype.exportType === 'defaultExport') {
specifiers.push({
localName: prototype.name,
type: 'ImportDefaultSpecifier',
});
} else {
[prototype.name, ...(prototype.relatedImports || [])].forEach((item) => {
specifiers.push({
localName: item,
type: 'ImportSpecifier',
});
});
}
return {
source,
specifiers,
};
}
/** /**
* 根据组件的 prototype 生成 ImportDeclarationPayload * 根据组件的 prototype 生成 ImportDeclarationPayload
* @deprecated
*/ */
export function getImportDeclarationPayloadByPrototype( export function getImportDeclarationPayloadByPrototype(
prototype: ComponentPrototypeType, prototype: ComponentPrototypeType,
@@ -35,6 +70,9 @@ export function getImportDeclarationPayloadByPrototype(
if (relativeFilepath && isFilepath(sourcePath)) { if (relativeFilepath && isFilepath(sourcePath)) {
sourcePath = getRelativePath(relativeFilepath, sourcePath); sourcePath = getRelativePath(relativeFilepath, sourcePath);
} }
if (sourcePath.endsWith('.js')) {
sourcePath = sourcePath.slice(0, -3);
}
return { return {
defaultSpecifier, defaultSpecifier,
@@ -82,7 +120,7 @@ function getPropKeyValuePair(item: ComponentPropType, generateValue: (...args: a
break; break;
} }
case 'string': { case 'string': {
if (!isVariableString(value)) { if (!isWrappedByExpressionContainer(value)) {
// 不是变量字符串 // 不是变量字符串
value = `"${value}"`; value = `"${value}"`;
} else { } else {
-1
View File
@@ -33,7 +33,6 @@ export function inferFileType(filename: string): FileType {
return FileType.JsonViewModule; return FileType.JsonViewModule;
} }
// FIXME: 暂时写死,后续要支持多 services 文件解析
if (/\/services\/.+\.js$/.test(filename)) { if (/\/services\/.+\.js$/.test(filename)) {
return FileType.ServiceModule; return FileType.ServiceModule;
} }
+4
View File
@@ -2,3 +2,7 @@ export * from './models';
export * from './factory'; export * from './factory';
export * from './helpers'; export * from './helpers';
export * from './types'; export * from './types';
export { default as generator } from '@babel/generator';
export { default as traverse } from '@babel/types';
export * from '@babel/parser';
+36 -18
View File
@@ -3,22 +3,26 @@ import { IWorkspace } from './interfaces';
export type SimulatorNameType = 'desktop' | 'phone'; export type SimulatorNameType = 'desktop' | 'phone';
type SimulatorType = { export type DesignerViewType = 'design' | 'code';
interface ISimulatorType {
name: SimulatorNameType; name: SimulatorNameType;
width: number; width: number;
height: number; height: number;
}; }
type ViewportBoundingType = { interface IViewportBounding {
width: number; width: number;
height: number; height: number;
}; }
export type DesignerViewType = 'design' | 'code'; interface IDesignerOptions {
workspace: IWorkspace;
simulator?: SimulatorNameType | ISimulatorType;
activeSidebarPanel?: string;
}
type DesignerOptionsType = { workspace: IWorkspace; simulator?: SimulatorNameType | SimulatorType }; const ISimulatorTypes: Record<string, ISimulatorType> = {
const simulatorTypes: Record<string, SimulatorType> = {
desktop: { desktop: {
name: 'desktop', name: 'desktop',
width: 1366, width: 1366,
@@ -35,12 +39,12 @@ export class Designer {
/** /**
* 当前的沙箱模拟器类型 * 当前的沙箱模拟器类型
*/ */
_simulator: SimulatorType = simulatorTypes.desktop; _simulator: ISimulatorType = ISimulatorTypes.desktop;
/** /**
* 当前的视图尺寸 * 当前的视图尺寸
*/ */
_viewport: ViewportBoundingType = { _viewport: IViewportBounding = {
width: 1366, width: 1366,
height: 800, height: 800,
}; };
@@ -70,9 +74,14 @@ export class Designer {
*/ */
_isPreview = false; _isPreview = false;
/**
* 默认展开的侧边栏
*/
defaultActiveSidebarPanel?: string;
private readonly workspace: IWorkspace; private readonly workspace: IWorkspace;
get simulator(): SimulatorType { get simulator(): ISimulatorType {
return toJS(this._simulator); return toJS(this._simulator);
} }
@@ -100,11 +109,21 @@ export class Designer {
return this._showRightPanel; return this._showRightPanel;
} }
constructor(options: DesignerOptionsType) { constructor(options: IDesignerOptions) {
this.workspace = options.workspace; this.workspace = options.workspace;
if (options.simulator) {
this.setSimulator(options.simulator); const { simulator, activeSidebarPanel: defaultActiveSidebarPanel } = options;
// 默认设计器模式
if (simulator) {
this.setSimulator(simulator);
} }
// 默认展开的侧边栏
if (defaultActiveSidebarPanel) {
this.setActiveSidebarPanel(defaultActiveSidebarPanel);
}
makeObservable(this, { makeObservable(this, {
_simulator: observable, _simulator: observable,
_viewport: observable, _viewport: observable,
@@ -131,15 +150,15 @@ export class Designer {
}); });
} }
setSimulator(value: SimulatorType | SimulatorNameType) { setSimulator(value: ISimulatorType | SimulatorNameType) {
if (typeof value === 'string') { if (typeof value === 'string') {
this._simulator = simulatorTypes[value]; this._simulator = ISimulatorTypes[value];
} else { } else {
this._simulator = value; this._simulator = value;
} }
} }
setViewport(value: ViewportBoundingType) { setViewport(value: IViewportBounding) {
this._viewport = value; this._viewport = value;
} }
@@ -154,7 +173,6 @@ export class Designer {
this._activeSidebarPanel = ''; this._activeSidebarPanel = '';
} }
} }
closeSidebarPanel() { closeSidebarPanel() {
this._activeSidebarPanel = ''; this._activeSidebarPanel = '';
} }
+56 -39
View File
@@ -5,11 +5,11 @@ import { DragSource } from './drag-source';
import { import {
IFileConfig, IFileConfig,
FileType, FileType,
IImportDeclarationPayload,
InsertChildPositionType, InsertChildPositionType,
ITangoConfigPackages, ITangoConfigPackages,
IPageConfigData, IPageConfigData,
IServiceFunctionPayload, IServiceFunctionPayload,
IImportSpecifierSourceData,
IImportSpecifierData, IImportSpecifierData,
} from '../types'; } from '../types';
import { TangoFile, TangoJsonFile } from './file'; import { TangoFile, TangoJsonFile } from './file';
@@ -25,7 +25,7 @@ export interface IViewFile {
/** /**
* 通过导入组件名查找组件来自的包 * 通过导入组件名查找组件来自的包
*/ */
importMap?: Dict<IImportSpecifierData>; importMap?: Dict<IImportSpecifierSourceData>;
listImportSources?: () => string[]; listImportSources?: () => string[];
listModals?: () => Array<{ label: string; value: string }>; listModals?: () => Array<{ label: string; value: string }>;
@@ -33,6 +33,14 @@ export interface IViewFile {
update: (code?: string, isFormatCode?: boolean, refreshWorkspace?: boolean) => void; update: (code?: string, isFormatCode?: boolean, refreshWorkspace?: boolean) => void;
/**
* 添加新的导入符号
* @param source 导入来源
* @param newSpecifiers 新导入符号列表
* @returns this
*/
addImportSpecifiers: (source: string, newSpecifiers: IImportSpecifierData[]) => IViewFile;
getNode: (targetNodeId: string) => IViewNode; getNode: (targetNodeId: string) => IViewNode;
removeNode: (targetNodeId: string) => IViewFile; removeNode: (targetNodeId: string) => IViewFile;
@@ -41,31 +49,15 @@ export interface IViewFile {
targetNodeId: string, targetNodeId: string,
newNode: any, newNode: any,
position?: InsertChildPositionType, position?: InsertChildPositionType,
sourcePrototype?: string | ComponentPrototypeType,
) => IViewFile; ) => IViewFile;
insertAfter: ( insertAfter: (targetNodeId: string, newNode: any) => IViewFile;
targetNodeId: string,
newNode: any,
sourcePrototype?: string | ComponentPrototypeType,
) => IViewFile;
insertBefore: ( insertBefore: (targetNodeId: string, newNode: any) => IViewFile;
targetNodeId: string,
newNode: any,
sourcePrototype?: string | ComponentPrototypeType,
) => IViewFile;
replaceNode: ( replaceNode: (targetNodeId: string, newNode: any) => IViewFile;
targetNodeId: string,
newNode: any,
sourcePrototype?: string | ComponentPrototypeType,
) => IViewFile;
replaceViewChildren: ( replaceViewChildren: (rawNodes: any[]) => IViewFile;
rawNodes: any[],
importDeclarations?: IImportDeclarationPayload[],
) => IViewFile;
updateNodeAttribute: ( updateNodeAttribute: (
nodeId: string, nodeId: string,
@@ -148,8 +140,8 @@ export interface IWorkspace {
storeModules?: Record<string, TangoStoreModule>; storeModules?: Record<string, TangoStoreModule>;
serviceModules?: Record<string, TangoServiceModule>; serviceModules?: Record<string, TangoServiceModule>;
refresh: (names: string[]) => void;
ready: () => void; ready: () => void;
refresh: (names: string[]) => void;
setActiveRoute: (path: string) => void; setActiveRoute: (path: string) => void;
setActiveFile: (filename: string) => void; setActiveFile: (filename: string) => void;
@@ -157,23 +149,28 @@ export interface IWorkspace {
setComponentPrototypes: (prototypes: Record<string, ComponentPrototypeType>) => void; setComponentPrototypes: (prototypes: Record<string, ComponentPrototypeType>) => void;
getPrototype: (name: string | ComponentPrototypeType) => ComponentPrototypeType; getPrototype: (name: string | ComponentPrototypeType) => ComponentPrototypeType;
/** onFilesChange?: (filenames: string[]) => void;
* 查询节点
* @param id 节点 ID
* @param module 节点所在的模块名
* @returns 返回节点对象
*/
getNode: (id: string, module?: string) => IViewNode;
getFile: (filename: string) => TangoFile; // ----------------- 文件操作 -----------------
listFiles: () => Record<string, string>;
addFile: (filename: string, code: string, fileType?: FileType) => void;
addFiles: (files: IFileConfig[]) => void; 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; removeFile: (filename: string) => void;
renameFile: (oldFilename: string, newFilename: string) => void; renameFile: (oldFilename: string, newFilename: string) => void;
renameFolder: (oldFoldername: string, newFoldername: string) => void; renameFolder: (oldFoldername: string, newFoldername: string) => void;
updateFile: (filename: string, code: string, shouldFormatCode?: boolean) => void;
listFiles: () => Record<string, string>;
getFile: (filename: string) => TangoFile;
// ----------------- 节点操作 -----------------
removeSelectedNode: () => void; removeSelectedNode: () => void;
cloneSelectedNode: () => void; cloneSelectedNode: () => void;
copySelectedNode: () => void; copySelectedNode: () => void;
@@ -193,6 +190,16 @@ export interface IWorkspace {
relatedImports?: string[], relatedImports?: string[],
) => void; ) => void;
/**
* 查询节点
* @param id 节点 ID
* @param module 节点所在的模块名
* @returns 返回节点对象
*/
getNode: (id: string, module?: string) => IViewNode;
// ----------------- 服务函数文件操作 -----------------
getServiceFunction?: (serviceKey: string) => object; getServiceFunction?: (serviceKey: string) => object;
listServiceFunctions?: () => Record<string, object>; listServiceFunctions?: () => Record<string, object>;
removeServiceFunction?: (serviceName: string, modName?: string) => void; removeServiceFunction?: (serviceName: string, modName?: string) => void;
@@ -203,21 +210,25 @@ export interface IWorkspace {
updateServiceFunction?: (payload: IServiceFunctionPayload, modName?: string) => void; updateServiceFunction?: (payload: IServiceFunctionPayload, modName?: string) => void;
updateServiceBaseConfig?: (IServiceFunctionPayload: object, modName?: string) => void; updateServiceBaseConfig?: (IServiceFunctionPayload: object, modName?: string) => void;
addStoreModule?: (storeName: string, code: string) => void; // ----------------- 状态管理文件操作 -----------------
removeStoreModule?: (storeName: string) => void;
addStoreState?: (storeName: string, stateName: string, initValue: string) => void; addStoreState?: (storeName: string, stateName: string, initValue: string) => void;
removeStoreState?: (storeName: string, stateName: string) => void; removeStoreState?: (storeName: string, stateName: string) => void;
removeStoreModule?: (storeName: string) => void;
updateModuleCodeByVariablePath?: (variablePath: string, code: string) => void; // ----------------- 视图文件操作 -----------------
// TODO: 是否需要,和 addFile 有啥区别 ???
addViewPage: (name: string, code: string) => void;
removeViewModule: (routePath: string) => void; removeViewModule: (routePath: string) => void;
copyViewPage: (sourceRoutePath: string, targetPageData: IPageConfigData) => void; copyViewPage: (sourceRoutePath: string, targetPageData: IPageConfigData) => void;
// ----------------- 路由文件操作 -----------------
updateRoute: (sourceRoutePath: string, targetPageData: IPageConfigData) => void; updateRoute: (sourceRoutePath: string, targetPageData: IPageConfigData) => void;
// ----------------- 依赖包操作 -----------------
addDependency?: (data: any) => void;
listDependencies?: () => any; listDependencies?: () => any;
getDependency?: (pkgName: string) => object;
updateDependency?: ( updateDependency?: (
name: string, name: string,
@@ -241,6 +252,12 @@ export interface IWorkspace {
removeBizComp?: (name: string) => void; removeBizComp?: (name: string) => void;
// ----------------- 其他操作 -----------------
updateModuleCodeByVariablePath?: (variablePath: string, code: string) => void;
// ----------------- getter -----------------
get activeViewModule(): IViewFile; get activeViewModule(): IViewFile;
get pages(): any[]; get pages(): any[];
get bizComps(): string[]; get bizComps(): string[];
+28 -3
View File
@@ -1,9 +1,16 @@
import * as t from '@babel/types'; import * as t from '@babel/types';
import { action, computed, makeObservable, observable } from 'mobx'; import { action, computed, makeObservable, observable } from 'mobx';
import { isNil } from '@music163/tango-helpers'; 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 { TangoFile } from './file';
import { IFileConfig } from '../types'; import { IFileConfig, IImportSpecifierData, ImportDeclarationDataType } from '../types';
import { IWorkspace } from './interfaces'; import { IWorkspace } from './interfaces';
/** /**
@@ -14,6 +21,11 @@ import { IWorkspace } from './interfaces';
export class TangoModule extends TangoFile { export class TangoModule extends TangoFile {
ast: t.File; ast: t.File;
/**
* 导入语句
*/
imports: ImportDeclarationDataType;
constructor(workspace: IWorkspace, props: IFileConfig, isSyncCode = true) { constructor(workspace: IWorkspace, props: IFileConfig, isSyncCode = true) {
super(workspace, props, isSyncCode); super(workspace, props, isSyncCode);
} }
@@ -39,6 +51,16 @@ export class TangoModule extends TangoFile {
} }
} }
addImportDeclaration(source: string, specifiers: IImportSpecifierData[]) {
this.ast = addImportDeclaration(this.ast, source, specifiers);
return this;
}
updateImportDeclaration(source: string, specifiers: IImportSpecifierData[]) {
this.ast = updateImportDeclaration(this.ast, source, specifiers);
return this;
}
/** /**
* 基于最新的 ast 进行源码同步 * 基于最新的 ast 进行源码同步
*/ */
@@ -69,7 +91,10 @@ export class TangoModule extends TangoFile {
this.ast = code2ast(code); this.ast = code2ast(code);
} }
_analysisAst() {} _analysisAst() {
const { imports } = traverseFile(this.ast);
this.imports = imports;
}
} }
/** /**
+2 -1
View File
@@ -58,7 +58,8 @@ export class TangoServiceModule extends TangoModule {
} }
_analysisAst() { _analysisAst() {
const { services, baseConfig } = traverseServiceFile(this.ast); const { imports, services, baseConfig } = traverseServiceFile(this.ast);
this.imports = imports;
this._serviceFunctions = services; this._serviceFunctions = services;
this._baseConfig = baseConfig; this._baseConfig = baseConfig;
if (baseConfig.namespace) { if (baseConfig.namespace) {
+86 -100
View File
@@ -1,6 +1,6 @@
import * as t from '@babel/types'; import * as t from '@babel/types';
import { action, computed, makeObservable, observable, toJS } from 'mobx'; import { action, computed, makeObservable, observable, toJS } from 'mobx';
import { ComponentPrototypeType, Dict } from '@music163/tango-helpers'; import { Dict } from '@music163/tango-helpers';
import { import {
ast2code, ast2code,
traverseViewFile, traverseViewFile,
@@ -8,15 +8,17 @@ import {
insertSiblingAfterJSXElement, insertSiblingAfterJSXElement,
appendChildToJSXElement, appendChildToJSXElement,
addImportDeclaration, addImportDeclaration,
updateImportDeclaration,
replaceJSXElement, replaceJSXElement,
getImportDeclarationPayloadByPrototype,
removeUnusedImportSpecifiers, removeUnusedImportSpecifiers,
insertSiblingBeforeJSXElement, insertSiblingBeforeJSXElement,
replaceRootJSXElementChildren, replaceRootJSXElementChildren,
IdGenerator, IdGenerator,
updateJSXAttributes, updateJSXAttributes,
queryXFormItemFields, queryXFormItemFields,
prototype2importDeclarationData,
insertImportSpecifiers,
addImportDeclarationLegacy,
updateImportDeclarationLegacy,
} from '../helpers'; } from '../helpers';
import { TangoNode } from './node'; import { TangoNode } from './node';
import { import {
@@ -24,11 +26,59 @@ import {
ITangoViewNodeData, ITangoViewNodeData,
IImportDeclarationPayload, IImportDeclarationPayload,
InsertChildPositionType, InsertChildPositionType,
IImportSpecifierSourceData,
ImportDeclarationDataType,
IImportSpecifierData, IImportSpecifierData,
} from '../types'; } from '../types';
import { IViewFile, IWorkspace } from './interfaces'; import { IViewFile, IWorkspace } from './interfaces';
import { TangoModule } from './module'; 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 * 视图中依赖的 tango 变量,仅 stores 和 services
@@ -51,6 +101,7 @@ export class TangoViewModule extends TangoModule implements IViewFile {
private _nodes: Map<string, TangoNode>; private _nodes: Map<string, TangoNode>;
/** /**
* 导入的模块 * 导入的模块
* @deprecated
*/ */
private _importedModules: Dict<IImportDeclarationPayload | IImportDeclarationPayload[]>; private _importedModules: Dict<IImportDeclarationPayload | IImportDeclarationPayload[]>;
@@ -98,6 +149,7 @@ export class TangoViewModule extends TangoModule implements IViewFile {
ast: newAst, ast: newAst,
cleanAst, cleanAst,
nodes, nodes,
imports,
importedModules, importedModules,
variables, variables,
} = traverseViewFile(this.ast, this._idGenerator); } = traverseViewFile(this.ast, this._idGenerator);
@@ -107,7 +159,8 @@ export class TangoViewModule extends TangoModule implements IViewFile {
this._cleanCode = ast2code(cleanAst); this._cleanCode = ast2code(cleanAst);
this._importedModules = importedModules; this._importedModules = importedModules;
this.importMap = this.buildImportMap(importedModules); this.imports = imports;
this.importMap = buildImportMap(imports);
this.variables = variables; this.variables = variables;
this._nodes.clear(); this._nodes.clear();
@@ -127,7 +180,7 @@ export class TangoViewModule extends TangoModule implements IViewFile {
* 依赖列表 * 依赖列表
*/ */
listImportSources() { listImportSources() {
return Object.keys(this._importedModules); return Object.keys(this.imports);
} }
/** /**
@@ -168,23 +221,30 @@ export class TangoViewModule extends TangoModule implements IViewFile {
} }
/** /**
* 基于组件的 prototype 信息更新导入信息 * 添加导入符号
* @param prototype * @param source
* @param shouldUpdateCode * @param newSpecifiers
* @returns
*/ */
updateImportSpecifiersByPrototype(sourcePrototype: string | ComponentPrototypeType) { addImportSpecifiers(source: string, newSpecifiers: IImportSpecifierData[]) {
const prototype = this.workspace.getPrototype(sourcePrototype); const existSpecifiers = this.imports[source];
if (prototype) { if (existSpecifiers) {
const importDeclaration = getImportDeclarationPayloadByPrototype(prototype, this.filename); const insertedSpecifiers = newSpecifiers.filter((item) => {
this.updateImportSpecifiers(importDeclaration); 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; return this;
} }
/** /**
* 更新导入的变量(新版) * 更新导入的变量
* @deprecated 使用 updateImportDeclaration 代替
*/ */
updateImportSpecifiers(importDeclaration: IImportDeclarationPayload) { updateImportSpecifiersLegacy(importDeclaration: IImportDeclarationPayload) {
const mods = this._importedModules[importDeclaration.sourcePath]; const mods = this._importedModules[importDeclaration.sourcePath];
let ast; let ast;
// 如果模块已存在,需要去重 // 如果模块已存在,需要去重
@@ -199,12 +259,12 @@ export class TangoViewModule extends TangoModule implements IViewFile {
(name) => !specifiers.includes(name), (name) => !specifiers.includes(name),
); );
ast = updateImportDeclaration(this.ast, { ast = updateImportDeclarationLegacy(this.ast, {
...importDeclaration, ...importDeclaration,
specifiers: newSpecifiers.concat(targetMod.specifiers), specifiers: newSpecifiers.concat(targetMod.specifiers),
}); });
} else { } else {
ast = addImportDeclaration(this.ast, importDeclaration); ast = addImportDeclarationLegacy(this.ast, importDeclaration);
} }
this.ast = ast; this.ast = ast;
return this; return this;
@@ -249,7 +309,8 @@ export class TangoViewModule extends TangoModule implements IViewFile {
// 导入依赖的组件 // 导入依赖的组件
relatedImports.forEach((name: string) => { relatedImports.forEach((name: string) => {
const proto = this.workspace.getPrototype(name); const proto = this.workspace.getPrototype(name);
this.updateImportSpecifiersByPrototype(proto); const { source, specifiers } = prototype2importDeclarationData(proto, this.filename);
this.addImportSpecifiers(source, specifiers);
}); });
} }
this.ast = updateJSXAttributes(this.ast, nodeId, config); this.ast = updateJSXAttributes(this.ast, nodeId, config);
@@ -260,43 +321,25 @@ export class TangoViewModule extends TangoModule implements IViewFile {
* 插入子节点的最后面 * 插入子节点的最后面
* @param targetNodeId * @param targetNodeId
* @param newNode * @param newNode
* @param sourceName * @param position
* @returns * @returns
*/ */
insertChild( insertChild(
targetNodeId: string, targetNodeId: string,
newNode: t.JSXElement, newNode: t.JSXElement,
position: InsertChildPositionType = 'last', position: InsertChildPositionType = 'last',
sourceName: string | ComponentPrototypeType,
) { ) {
this.ast = appendChildToJSXElement(this.ast, targetNodeId, newNode, position); this.ast = appendChildToJSXElement(this.ast, targetNodeId, newNode, position);
if (sourceName) {
this.updateImportSpecifiersByPrototype(sourceName);
}
return this; return this;
} }
insertAfter( insertAfter(targetNodeId: string, newNode: t.JSXElement) {
targetNodeId: string,
newNode: t.JSXElement,
sourceName?: string | ComponentPrototypeType,
) {
this.ast = insertSiblingAfterJSXElement(this.ast, targetNodeId, newNode); this.ast = insertSiblingAfterJSXElement(this.ast, targetNodeId, newNode);
if (sourceName) {
this.updateImportSpecifiersByPrototype(sourceName);
}
return this; return this;
} }
insertBefore( insertBefore(targetNodeId: string, newNode: t.JSXElement) {
targetNodeId: string,
newNode: t.JSXElement,
sourceName?: string | ComponentPrototypeType,
) {
this.ast = insertSiblingBeforeJSXElement(this.ast, targetNodeId, newNode); this.ast = insertSiblingBeforeJSXElement(this.ast, targetNodeId, newNode);
if (sourceName) {
this.updateImportSpecifiersByPrototype(sourceName);
}
return this; return this;
} }
@@ -304,22 +347,15 @@ export class TangoViewModule extends TangoModule implements IViewFile {
* 替换目标节点为新节点 * 替换目标节点为新节点
* @param targetNodeId * @param targetNodeId
* @param newNode * @param newNode
* @param sourcePrototype
*/ */
replaceNode( replaceNode(targetNodeId: string, newNode: t.JSXElement) {
targetNodeId: string,
newNode: t.JSXElement,
sourceName?: string | ComponentPrototypeType,
) {
this.ast = replaceJSXElement(this.ast, targetNodeId, newNode); this.ast = replaceJSXElement(this.ast, targetNodeId, newNode);
if (sourceName) {
this.updateImportSpecifiersByPrototype(sourceName);
}
return this; return this;
} }
/** /**
* 替换 jsx 跟结点的子元素 * 替换 jsx 跟结点的子元素
* @deprecated 不推荐使用
*/ */
replaceViewChildren( replaceViewChildren(
childrenNodes: t.JSXElement[], childrenNodes: t.JSXElement[],
@@ -331,60 +367,10 @@ export class TangoViewModule extends TangoModule implements IViewFile {
if (importDeclarations?.length) { if (importDeclarations?.length) {
importDeclarations.forEach((item) => { importDeclarations.forEach((item) => {
this.updateImportSpecifiers(item); this.updateImportSpecifiersLegacy(item);
}); });
} }
return this; 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;
} }
+167 -102
View File
@@ -17,6 +17,7 @@ import {
namesToImportDeclarations, namesToImportDeclarations,
getBlockNameByFilename, getBlockNameByFilename,
getPrivilegeCode, getPrivilegeCode,
prototype2importDeclarationData,
} from '../helpers'; } from '../helpers';
import { DropMethod } from './drop-target'; import { DropMethod } from './drop-target';
import { HistoryMessage, TangoHistory } from './history'; import { HistoryMessage, TangoHistory } from './history';
@@ -55,6 +56,10 @@ export interface IWorkspaceOptions {
* 组件描述列表 * 组件描述列表
*/ */
prototypes?: Record<string, ComponentPrototypeType>; prototypes?: Record<string, ComponentPrototypeType>;
/**
* 工作区文件变更事件
*/
onFilesChange?: (filenames: string[]) => void;
} }
/** /**
@@ -125,6 +130,11 @@ export class Workspace extends EventTarget implements IWorkspace {
*/ */
appJson: TangoJsonFile; appJson: TangoJsonFile;
/**
* 代码变更回调
*/
onFilesChange?: (filenames: string[]) => void;
/** /**
* 绑定事件 * 绑定事件
* TODO: 是否需要自己来管理 listeners,并及时进行 gc * TODO: 是否需要自己来管理 listeners,并及时进行 gc
@@ -220,6 +230,7 @@ export class Workspace extends EventTarget implements IWorkspace {
this.activeViewFile = ''; this.activeViewFile = '';
this.files = new Map(); this.files = new Map();
this.isReady = false; this.isReady = false;
this.onFilesChange = options?.onFilesChange;
if (options?.files) { if (options?.files) {
this.addFiles(options.files); this.addFiles(options.files);
@@ -302,7 +313,7 @@ export class Workspace extends EventTarget implements IWorkspace {
} }
/** /**
* 添加一组文件到工作区 * 添加一组文件到工作区,如果文件同名,后面的文件会覆盖前面的文件
* @param files * @param files
*/ */
addFiles(files: IFileConfig[] = []) { addFiles(files: IFileConfig[] = []) {
@@ -318,81 +329,99 @@ export class Workspace extends EventTarget implements IWorkspace {
* @param fileType 模块类型 * @param fileType 模块类型
*/ */
addFile(filename: string, code: string, fileType?: FileType) { addFile(filename: string, code: string, fileType?: FileType) {
if (!this.files.has(filename)) { const moduleType = fileType || inferFileType(filename);
const moduleType = fileType || inferFileType(filename); const props = {
const props = { filename,
filename, code,
code, type: moduleType,
type: moduleType, };
};
let module; let module;
switch (moduleType) { switch (moduleType) {
case FileType.StoreEntryModule: case FileType.StoreEntryModule:
module = new TangoStoreEntryModule(this, props); module = new TangoStoreEntryModule(this, props);
this.storeEntryModule = module; this.storeEntryModule = module;
break; break;
case FileType.RouteModule: { case FileType.RouteModule: {
module = new TangoRouteModule(this, props); module = new TangoRouteModule(this, props);
this.routeModule = module; this.routeModule = module;
// check if activeRoute exists // check if activeRoute exists
const route = module.routes.find((item) => item.path === this.activeRoute); const route = module.routes.find((item) => item.path === this.activeRoute);
if (!route) { if (!route) {
this.setActiveRoute(module.routes[0]?.path); this.setActiveRoute(module.routes[0]?.path);
}
break;
} }
case FileType.JsxViewModule: break;
module = new TangoViewModule(this, props);
break;
case FileType.ServiceModule:
module = new TangoServiceModule(this, props);
this.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);
} }
case FileType.JsxViewModule:
this.files.set(filename, module); module = new TangoViewModule(this, props);
break;
case FileType.ServiceModule:
module = new TangoServiceModule(this, props);
this.serviceModules[module.name] = module;
break;
case FileType.StoreModule:
module = new TangoStoreModule(this, props);
this.storeModules[module.name] = module;
break;
case FileType.BlockEntryModule: {
const blockName = getBlockNameByFilename(props.filename);
const prototype: ComponentPrototypeType = {
name: blockName,
exportType: 'defaultExport',
package: props.filename,
type: 'block',
};
this.localBlocks[blockName] = props.filename;
this.componentPrototypes.set(blockName, prototype);
module = new TangoViewModule(this, props);
break;
}
case FileType.Module:
module = new TangoJsModule(this, props);
break;
case FileType.Less:
module = new TangoLessFile(this, props);
break;
case FileType.PackageJson:
module = new TangoJsonFile(this, props);
this.packageJson = module;
break;
case FileType.TangoConfigJson:
module = new TangoJsonFile(this, props);
this.tangoConfigJson = module;
break;
case FileType.AppJson:
module = new TangoJsonFile(this, props);
this.appJson = module;
break;
case FileType.Json:
module = new TangoJsonFile(this, props);
break;
default:
module = new TangoFile(this, props);
} }
this.files.set(filename, module);
}
addServiceFile(serviceName: string, code: string) {
const filename = `/src/services/${serviceName}.js`;
this.addFile(filename, code, FileType.ServiceModule);
const indexServiceModule = this.serviceModules.index;
indexServiceModule?.addImportDeclaration(`./${serviceName}`, []).update();
}
addStoreFile(storeName: string, code: string) {
const filename = `/src/stores/${storeName}.js`;
this.addFile(filename, code);
if (!this.storeEntryModule) {
this.addFile('/src/stores/index.js', '');
}
this.storeEntryModule.addStore(storeName).update();
}
addViewFile(viewName: string, code: string) {
// TODO: implement it
} }
updateFile(filename: string, code: string, shouldFormatCode = false) { updateFile(filename: string, code: string, shouldFormatCode = false) {
@@ -499,7 +528,8 @@ export class Workspace extends EventTarget implements IWorkspace {
/** /**
* 添加新的视图文件 * 添加新的视图文件
* @deprecated * @deprecated 使用 addViewFile 代替
* FIXME: 重构这个逻辑
* @param route 视图名 * @param route 视图名
* @param code 视图代码 * @param code 视图代码
*/ */
@@ -594,13 +624,10 @@ export class Workspace extends EventTarget implements IWorkspace {
/** /**
* 添加新的模型文件 * 添加新的模型文件
* @param name 模型名 * @deprecated 使用 addStoreFile 代替
* @param code 模型代码
*/ */
addStoreModule(name: string, code: string) { addStoreModule(name: string, code: string) {
const filename = getFilepath(name, '/src/stores', '.js'); this.addStoreFile(name, code);
this.addFile(filename, code);
this.storeEntryModule?.addStore(name).update();
} }
/** /**
@@ -669,12 +696,10 @@ export class Workspace extends EventTarget implements IWorkspace {
*/ */
listServiceFunctions() { listServiceFunctions() {
const ret: Record<string, object> = {}; const ret: Record<string, object> = {};
Object.values(this.serviceModules).forEach((module) => { Object.keys(this.serviceModules).forEach((moduleName) => {
const module = this.serviceModules[moduleName];
Object.keys(module.serviceFunctions).forEach((name) => { Object.keys(module.serviceFunctions).forEach((name) => {
const serviceKey = const serviceKey = moduleName === 'index' ? name : [moduleName, name].join('.');
module.baseConfig.namespace !== 'index'
? name
: [module.baseConfig.namespace, name].join('.');
ret[serviceKey] = module.serviceFunctions[name]; ret[serviceKey] = module.serviceFunctions[name];
}); });
}); });
@@ -717,14 +742,29 @@ export class Workspace extends EventTarget implements IWorkspace {
this.serviceModules[moduleName]?.updateBaseConfig(config).update(); this.serviceModules[moduleName]?.updateBaseConfig(config).update();
} }
addDependency(data: any) {
// TODO: implement it to replace addBizComp & addServiceComp
}
/** /**
* 获取 package.json 中的依赖列表 * 获取 package.json 中的依赖列表
* @returns * @returns
* TODO: fix this logic to merge dependencies from package.json and tango.config.json
*/ */
listDependencies() { listDependencies() {
return this.packageJson?.getValue('dependencies'); return this.packageJson?.getValue('dependencies');
} }
getDependency(pkgName: string) {
const packages = this.tangoConfigJson?.getValue('packages');
const dependencies = this.packageJson?.getValue('dependencies'); // 兼容老版本
const detail = {
version: dependencies?.[pkgName],
...(packages?.[pkgName] || {}),
};
return detail;
}
/** /**
* 更新依赖,没有就添加 * 更新依赖,没有就添加
* @param name * @param name
@@ -909,7 +949,7 @@ export class Workspace extends EventTarget implements IWorkspace {
); );
importDeclarations.forEach((importDeclaration) => { importDeclarations.forEach((importDeclaration) => {
this.activeViewModule.updateImportSpecifiers(importDeclaration); this.activeViewModule.updateImportSpecifiersLegacy(importDeclaration);
}); });
this.copyTempNodes.forEach((node) => { this.copyTempNodes.forEach((node) => {
@@ -960,7 +1000,11 @@ export class Workspace extends EventTarget implements IWorkspace {
const sourcePrototype = this.getPrototype(sourceName); const sourcePrototype = this.getPrototype(sourceName);
const newNode = prototype2jsxElement(sourcePrototype); const newNode = prototype2jsxElement(sourcePrototype);
const file = this.getNode(targetNodeId).file; const file = this.getNode(targetNodeId).file;
file.insertChild(targetNodeId, newNode, 'last', sourcePrototype).update(); const { source, specifiers } = prototype2importDeclarationData(sourcePrototype, file.filename);
file
.insertChild(targetNodeId, newNode, 'last')
.addImportSpecifiers(source, specifiers)
.update();
this.history.push({ this.history.push({
message: HistoryMessage.InsertNode, message: HistoryMessage.InsertNode,
data: { data: {
@@ -980,7 +1024,8 @@ export class Workspace extends EventTarget implements IWorkspace {
const sourcePrototype = this.getPrototype(sourceName); const sourcePrototype = this.getPrototype(sourceName);
const newNode = prototype2jsxElement(sourcePrototype); const newNode = prototype2jsxElement(sourcePrototype);
const file = this.getNode(targetNodeId).file; const file = this.getNode(targetNodeId).file;
file.replaceNode(targetNodeId, newNode, sourcePrototype).update(); const { source, specifiers } = prototype2importDeclarationData(sourcePrototype, file.filename);
file.replaceNode(targetNodeId, newNode).addImportSpecifiers(source, specifiers).update();
this.history.push({ this.history.push({
message: HistoryMessage.ReplaceNode, message: HistoryMessage.ReplaceNode,
data: { data: {
@@ -998,7 +1043,14 @@ export class Workspace extends EventTarget implements IWorkspace {
if (insertedPrototype) { if (insertedPrototype) {
const newNode = prototype2jsxElement(insertedPrototype); const newNode = prototype2jsxElement(insertedPrototype);
const file = this.selectSource.file; const file = this.selectSource.file;
file.insertChild(this.selectSource.first.id, newNode, 'last', insertedPrototype).update(); const { source, specifiers } = prototype2importDeclarationData(
insertedPrototype,
file.filename,
);
file
.insertChild(this.selectSource.first.id, newNode, 'last')
.addImportSpecifiers(source, specifiers)
.update();
this.history.push({ this.history.push({
message: HistoryMessage.InsertNode, message: HistoryMessage.InsertNode,
data: { data: {
@@ -1054,30 +1106,40 @@ export class Workspace extends EventTarget implements IWorkspace {
const targetFile = dropTarget.node?.file; const targetFile = dropTarget.node?.file;
const sourceFile = dragSource.node?.file; const sourceFile = dragSource.node?.file;
// dragSourcePrototype to importDeclarations
const { source, specifiers } = prototype2importDeclarationData(
dragSourcePrototype,
targetFile.filename,
);
let isValidOperation = true; let isValidOperation = true;
switch (dropTarget.method) { switch (dropTarget.method) {
// 直接往目标节点的 children 里添加一个节点 // 直接往目标节点的 children 里添加一个节点
case DropMethod.InsertChild: { case DropMethod.InsertChild: {
targetFile.insertChild(dropTarget.id, newNode, 'last', dragSourcePrototype); targetFile
.insertChild(dropTarget.id, newNode, 'last')
.addImportSpecifiers(source, specifiers);
break; break;
} }
case DropMethod.InsertFirstChild: { case DropMethod.InsertFirstChild: {
targetFile.insertChild(dropTarget.id, newNode, 'first', dragSourcePrototype); targetFile
.insertChild(dropTarget.id, newNode, 'first')
.addImportSpecifiers(source, specifiers);
break; break;
} }
// 往目标节点的后边插入一个节点 // 往目标节点的后边插入一个节点
case DropMethod.InsertAfter: { case DropMethod.InsertAfter: {
targetFile.insertAfter(dropTarget.id, newNode, dragSourcePrototype); targetFile.insertAfter(dropTarget.id, newNode).addImportSpecifiers(source, specifiers);
break; break;
} }
// 往目标节点的前方插入一个节点 // 往目标节点的前方插入一个节点
case DropMethod.InsertBefore: { case DropMethod.InsertBefore: {
targetFile.insertBefore(dropTarget.id, newNode, dragSourcePrototype); targetFile.insertBefore(dropTarget.id, newNode).addImportSpecifiers(source, specifiers);
break; break;
} }
// 替换目标节点 // 替换目标节点
case DropMethod.ReplaceNode: { case DropMethod.ReplaceNode: {
targetFile.replaceNode(dropTarget.id, newNode, dragSourcePrototype); targetFile.replaceNode(dropTarget.id, newNode).addImportSpecifiers(source, specifiers);
break; break;
} }
default: default:
@@ -1094,13 +1156,6 @@ export class Workspace extends EventTarget implements IWorkspace {
} }
this.selectSource.clear(); this.selectSource.clear();
this.history.push({
message: HistoryMessage.DropNode,
data: {
[targetFile.filename]: targetFile.code,
},
});
} }
targetFile.update(); targetFile.update();
@@ -1109,6 +1164,15 @@ export class Workspace extends EventTarget implements IWorkspace {
} }
dragSource.clear(); dragSource.clear();
if (isValidOperation) {
this.history.push({
message: HistoryMessage.DropNode,
data: {
[targetFile.filename]: targetFile.code,
},
});
}
} }
/** /**
@@ -1124,6 +1188,7 @@ export class Workspace extends EventTarget implements IWorkspace {
}, },
}), }),
); );
this.onFilesChange?.(filenames);
} }
/** /**
@@ -1185,11 +1250,11 @@ export class Workspace extends EventTarget implements IWorkspace {
if (this.files.has(sourceFilePath)) { if (this.files.has(sourceFilePath)) {
// 来源是文件 // 来源是文件
const file = this.files.get(sourceFilePath); const file = this.files.get(sourceFilePath);
this.addFile(targetFilePath + '.js', file.cleanCode, file.type); this.addFile(`${targetFilePath}.js`, file.cleanCode, file.type);
} else if (this.files.has(sourceFilePath + '/index.js')) { } else if (this.files.has(`${sourceFilePath}/index.js`)) {
// 来源是目录 // 来源是目录
Array.from(this.files.keys()).forEach((key) => { Array.from(this.files.keys()).forEach((key) => {
if (key.startsWith(sourceFilePath + '/')) { if (key.startsWith(`${sourceFilePath}/`)) {
const sourceFile = this.getFile(key); const sourceFile = this.getFile(key);
this.addFile( this.addFile(
targetFilePath + key.slice(sourceFilePath.length), targetFilePath + key.slice(sourceFilePath.length),
+27 -8
View File
@@ -73,15 +73,40 @@ export interface ITangoViewNodeData<T = JSXElement> {
} }
/** /**
* 导入变量的解析数据 * 导入变量的来源
*/ */
export interface IImportSpecifierData { export interface IImportSpecifierSourceData {
source: string; source: string;
isDefault?: boolean; isDefault?: boolean;
} }
/**
* 解析的导入变量数据
*/
export interface IImportSpecifierData {
/**
* 导入名
*/
importedName?: string;
/**
* 本地名
*/
localName: string;
/**
* 类型
*/
type: 'ImportDefaultSpecifier' | 'ImportSpecifier' | 'ImportNamespaceSpecifier';
}
/**
* 解析的导入语句数据
* FIXME: 需要考虑到有两行重复的语句的情况
*/
export type ImportDeclarationDataType = Record<string, IImportSpecifierData[]>;
/** /**
* 模块导入的参数类型 * 模块导入的参数类型
* @deprecated 使用 IImportSpecifierData 代替
*/ */
export interface IImportDeclarationPayload { export interface IImportDeclarationPayload {
defaultSpecifier?: string; defaultSpecifier?: string;
@@ -89,12 +114,6 @@ export interface IImportDeclarationPayload {
sourcePath: string; sourcePath: string;
} }
export interface IClassPropertyNodeData {
reference: string;
propertyName: string;
propertyBody: string;
}
/** /**
* 服务函数参数类型 * 服务函数参数类型
*/ */
+25
View File
@@ -0,0 +1,25 @@
import { isTangoVariable, isWrappedByExpressionContainer } from '../src/helpers';
describe('assert', () => {
it('isTangoVariable', () => {
expect(isTangoVariable('tango.stores.app.name')).toBeTruthy();
expect(isTangoVariable('tango.stores?.app?.name')).toBeTruthy();
expect(isTangoVariable('tango.stores.app?.name')).toBeTruthy();
// expect(isTangoVariable('tango.copyToClipboard')).toBeTruthy();
});
it('isWrappedByExpressionContainer', () => {
expect(isWrappedByExpressionContainer('{this.foo}')).toBeTruthy();
expect(isWrappedByExpressionContainer('{!false}')).toBeTruthy();
expect(isWrappedByExpressionContainer('{[]}')).toBeTruthy();
expect(isWrappedByExpressionContainer('{{ foo: "bar" }}')).toBeTruthy();
expect(isWrappedByExpressionContainer('{[{ foo: "bar" }]}')).toBeTruthy();
expect(isWrappedByExpressionContainer('{123}')).toBeTruthy();
expect(isWrappedByExpressionContainer('{"hello"}')).toBeTruthy();
expect(isWrappedByExpressionContainer('{ foo: "bar" }')).toBeFalsy();
expect(isWrappedByExpressionContainer('{ type: tango.stores?.homePage?.tabKey }')).toBeFalsy();
expect(isWrappedByExpressionContainer('{ type: tango.stores.homePage.tabKey }')).toBeFalsy();
expect(isWrappedByExpressionContainer('{ foo: "bar" }')).toBeFalsy();
expect(isWrappedByExpressionContainer('{ color: tango.stores.app.color }')).toBeFalsy();
});
});
+31 -4
View File
@@ -3,15 +3,16 @@ import {
serviceConfig2Node, serviceConfig2Node,
isValidCode, isValidCode,
isValidExpressionCode, isValidExpressionCode,
code2expression,
} from '../src/helpers'; } from '../src/helpers';
describe('helpers', () => { describe('ast helpers', () => {
test('isValidCode', () => { it('isValidCode', () => {
expect(isValidCode('() => { hello world }')).toBeFalsy(); expect(isValidCode('() => { hello world }')).toBeFalsy();
expect(isValidCode('function() {}')).toBeFalsy(); expect(isValidCode('function() {}')).toBeFalsy();
}); });
test('isValidExpression', () => { it('isValidExpression', () => {
expect(isValidExpressionCode('() => { }')).toBeTruthy(); expect(isValidExpressionCode('() => { }')).toBeTruthy();
expect(isValidExpressionCode('1')).toBeTruthy(); expect(isValidExpressionCode('1')).toBeTruthy();
expect(isValidExpressionCode('"hello"')).toBeTruthy(); expect(isValidExpressionCode('"hello"')).toBeTruthy();
@@ -20,9 +21,17 @@ describe('helpers', () => {
expect(isValidExpressionCode('[1,2,3]')).toBeTruthy(); expect(isValidExpressionCode('[1,2,3]')).toBeTruthy();
expect(isValidExpressionCode('<div>hello</div>')).toBeTruthy(); expect(isValidExpressionCode('<div>hello</div>')).toBeTruthy();
expect(isValidExpressionCode('<div>hello</div>')).toBeTruthy(); expect(isValidExpressionCode('<div>hello</div>')).toBeTruthy();
expect(isValidExpressionCode('{1}')).toBeFalsy();
expect(isValidExpressionCode('{"1"}')).toBeFalsy();
expect(isValidExpressionCode('{ 1+1 }')).toBeFalsy();
expect(isValidExpressionCode('{<div>aaa</div>}')).toBeFalsy();
expect(isValidExpressionCode('{() => {}}')).toBeFalsy();
expect(isValidExpressionCode('{[1,2,3]}')).toBeFalsy();
expect(isValidExpressionCode('{tango.stores.app.title}')).toBeFalsy();
}); });
test('object2node', () => { it('object2node', () => {
const node = object2node({ const node = object2node({
url: '/api/backend/clientversion/appmarket/list', url: '/api/backend/clientversion/appmarket/list',
method: 'POST', method: 'POST',
@@ -40,4 +49,22 @@ describe('helpers', () => {
} as any); } as any);
expect(node.type).toEqual('ObjectExpression'); expect(node.type).toEqual('ObjectExpression');
}); });
it('code2expression', () => {
expect(code2expression('')).toBeUndefined();
expect(code2expression('{tango.stores.app}')).toBeUndefined();
expect(code2expression('{ type: window.bar }').type).toEqual('ObjectExpression');
expect(code2expression('() => {};').type).toEqual('ArrowFunctionExpression');
expect(code2expression('<Button>hello</Button>').type).toBe('JSXElement');
expect(code2expression('<BreadcrumbItem children="节点名称" />').type).toBe('JSXElement');
const arrayCode = `
[
{ label: 'foo', value: 'foo' },
{ label: 'bar', value: 'bar' },
]
`;
expect(code2expression(arrayCode).type).toEqual('ArrayExpression');
});
}); });
+3 -43
View File
@@ -25,40 +25,6 @@ describe('helpers', () => {
expect(code2ast('function App() {}').type).toEqual('File'); expect(code2ast('function App() {}').type).toEqual('File');
}); });
it('code2expression: null', () => {
expect(code2expression('')).toBeUndefined();
});
it('code2expression: object', () => {
const code = `
{
foo: 'bar',
}
`;
const node = code2expression(code);
expect(node.type).toEqual('ObjectExpression');
});
it('code2expression: arrow function', () => {
expect(code2expression('() => {};').type).toEqual('ArrowFunctionExpression');
});
it('code2expression: list', () => {
const code = `
[
{ label: 'foo', value: 'foo' },
{ label: 'bar', value: 'bar' },
]
`;
const node = code2expression(code);
expect(node.type).toEqual('ArrayExpression');
});
it('code2expression: jsxElement', () => {
const node = code2expression('<Button>hello</Button>');
expect(node.type).toEqual('JSXElement');
});
it('parse jsxElement attributes', () => { it('parse jsxElement attributes', () => {
const node = code2expression( const node = code2expression(
"<XColumn dataIndex='col' enumMap={{ 1: '已解决', 2: '未解决' }} />", "<XColumn dataIndex='col' enumMap={{ 1: '已解决', 2: '未解决' }} />",
@@ -73,11 +39,6 @@ describe('helpers', () => {
}); });
}); });
it('code2expression: closed jsxElement', () => {
const node = code2expression('<BreadcrumbItem children="节点名称" />');
expect(node.type).toEqual('JSXElement');
});
it('value2node: number', () => { it('value2node: number', () => {
expect(value2node(1).type).toEqual('NumericLiteral'); expect(value2node(1).type).toEqual('NumericLiteral');
}); });
@@ -126,7 +87,6 @@ describe('helpers', () => {
}); });
describe('string helpers', () => { describe('string helpers', () => {
it('value2code: empty array', () => { it('value2code: empty array', () => {
expect(value2code([])).toEqual('[]'); expect(value2code([])).toEqual('[]');
}); });
@@ -151,9 +111,9 @@ describe('string helpers', () => {
it('namesToImportDeclarations', () => { it('namesToImportDeclarations', () => {
expect( expect(
namesToImportDeclarations(['Button', 'Box', 'React'], { namesToImportDeclarations(['Button', 'Box', 'React'], {
Button: { package: '@music163/antd' }, Button: { source: '@music163/antd' },
Box: { package: '@music163/antd' }, Box: { source: '@music163/antd' },
React: { package: 'react', isDefault: true }, React: { source: 'react', isDefault: true },
}), }),
).toEqual([ ).toEqual([
{ sourcePath: '@music163/antd', specifiers: ['Button', 'Box'] }, { sourcePath: '@music163/antd', specifiers: ['Button', 'Box'] },
+43
View File
@@ -3,6 +3,49 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.5.8](https://github.com/netease/tango/compare/@music163/tango-designer@0.5.7...@music163/tango-designer@0.5.8) (2023-11-27)
### Bug Fixes
- event setter ([588861f](https://github.com/netease/tango/commit/588861f6492b45954e29ceed950567781b822492))
## [0.5.7](https://github.com/netease/tango/compare/@music163/tango-designer@0.5.6...@music163/tango-designer@0.5.7) (2023-11-23)
### Bug Fixes
- update varTreeNode style ([#69](https://github.com/netease/tango/issues/69)) ([f604996](https://github.com/netease/tango/commit/f604996e56c9e4d58ff7f80cd98c3753986f081f))
## [0.5.6](https://github.com/netease/tango/compare/@music163/tango-designer@0.5.5...@music163/tango-designer@0.5.6) (2023-11-23)
**Note:** Version bump only for package @music163/tango-designer
## [0.5.5](https://github.com/netease/tango/compare/@music163/tango-designer@0.5.4...@music163/tango-designer@0.5.5) (2023-11-20)
### Bug Fixes
- pass down sandbox props ([d6ffb50](https://github.com/netease/tango/commit/d6ffb50e85573cb17ee52a797b84de5e6cedb044))
## [0.5.4](https://github.com/netease/tango/compare/@music163/tango-designer@0.5.3...@music163/tango-designer@0.5.4) (2023-11-20)
### Bug Fixes
- refactor parse expression ([#61](https://github.com/netease/tango/issues/61)) ([dbbd1dd](https://github.com/netease/tango/commit/dbbd1dddc75c532b7c9710ab0941c8680100f093))
## [0.5.3](https://github.com/netease/tango/compare/@music163/tango-designer@0.5.2...@music163/tango-designer@0.5.3) (2023-11-16)
### Bug Fixes
- allow override existing file via addFile ([6cf673e](https://github.com/netease/tango/commit/6cf673e7f6f330613e1d6331256a59900e8cbc68))
## [0.5.2](https://github.com/netease/tango/compare/@music163/tango-designer@0.5.1...@music163/tango-designer@0.5.2) (2023-11-02)
### Bug Fixes
- limit outline tree node icon ([c5840f2](https://github.com/netease/tango/commit/c5840f29b724966930e239fe00ee13b2fb882503))
- outline-panel tree expand bug & add props ([#52](https://github.com/netease/tango/issues/52)) ([b29e235](https://github.com/netease/tango/commit/b29e23599befbd60845053544648b10976a65824))
- resizable-box support horizontal and vertical dragging ([#48](https://github.com/netease/tango/issues/48)) ([fd2fa94](https://github.com/netease/tango/commit/fd2fa943b05e9f1fa57574e703307725c5aed5d8))
- update style of components panel ([11f166a](https://github.com/netease/tango/commit/11f166a2a67313200922a514a8219f34cfafd8ab))
## [0.5.1](https://github.com/netease/tango/compare/@music163/tango-designer@0.5.0...@music163/tango-designer@0.5.1) (2023-10-23) ## [0.5.1](https://github.com/netease/tango/compare/@music163/tango-designer@0.5.0...@music163/tango-designer@0.5.1) (2023-10-23)
### Bug Fixes ### Bug Fixes
+11 -11
View File
@@ -1,6 +1,6 @@
{ {
"name": "@music163/tango-designer", "name": "@music163/tango-designer",
"version": "0.5.1", "version": "0.5.8",
"description": "lowcode designer", "description": "lowcode designer",
"keywords": [ "keywords": [
"react" "react"
@@ -32,13 +32,13 @@
}, },
"dependencies": { "dependencies": {
"@ant-design/icons": "^4.8.0", "@ant-design/icons": "^4.8.0",
"@music163/request": "^0.1.0", "@music163/request": "^0.1.2",
"@music163/tango-context": "^0.2.3", "@music163/tango-context": "^0.2.8",
"@music163/tango-core": "^0.2.2", "@music163/tango-core": "^0.2.7",
"@music163/tango-helpers": "^0.1.4", "@music163/tango-helpers": "^0.1.8",
"@music163/tango-sandbox": "^0.1.7", "@music163/tango-sandbox": "^0.1.12",
"@music163/tango-setting-form": "^0.2.6", "@music163/tango-setting-form": "^0.2.11",
"@music163/tango-ui": "^0.1.7", "@music163/tango-ui": "^0.1.11",
"antd": "^4.24.2", "antd": "^4.24.2",
"cash-dom": "^8.1.2", "cash-dom": "^8.1.2",
"classnames": "^2.3.2", "classnames": "^2.3.2",
@@ -53,9 +53,9 @@
"semver": "^7.3.8" "semver": "^7.3.8"
}, },
"devDependencies": { "devDependencies": {
"@types/color": "^3.0.4", "@types/color": "^3.0.5",
"@types/react-color": "^3.0.6", "@types/react-color": "^3.0.9",
"@types/react-resizable": "^3.0.4" "@types/react-resizable": "^3.0.6"
}, },
"publishConfig": { "publishConfig": {
"access": "public", "access": "public",
@@ -317,6 +317,10 @@ const varTreeStyle = css`
font-family: Consolas, Menlo, Courier, monospace; 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 { .ant-tree .ant-tree-treenode {
padding: 0; padding: 0;
} }
@@ -372,11 +376,11 @@ export function VariableTree({
const isDeletable = node.showDeleteIcon ?? showDeleteIcon; const isDeletable = node.showDeleteIcon ?? showDeleteIcon;
return ( return (
<Box display="flex" justifyContent="space-between" alignItems="center"> <Box display="flex" justifyContent="space-between" alignItems="center">
<Text> <Text flex="1" truncated>
{node.title} {node.title}
{node.type === 'function' && <FunctionOutlined />} {node.type === 'function' && <FunctionOutlined />}
</Text> </Text>
<Box> <Box flex="0 0 72px" textAlign="right">
{isDeletable && ( {isDeletable && (
<Popconfirm <Popconfirm
title="确认删除吗?该操作会导致引用此模型的代码报错,请谨慎操作!" title="确认删除吗?该操作会导致引用此模型的代码报错,请谨慎操作!"
@@ -119,6 +119,7 @@ const PreviewSandbox = observer(
startRoute, startRoute,
injectScript, injectScript,
sandboxType: 'preview', sandboxType: 'preview',
...rest,
}); });
return ( return (
@@ -166,6 +167,7 @@ const DesignSandbox = observer(
startRoute, startRoute,
injectScript, injectScript,
sandboxType: 'design', sandboxType: 'design',
...rest,
}); });
return ( return (
@@ -4,8 +4,7 @@ import { AutoComplete } from 'antd';
import { ActionSelect } from '@music163/tango-ui'; import { ActionSelect } from '@music163/tango-ui';
import { FormItemComponentProps } from '@music163/tango-setting-form'; import { FormItemComponentProps } from '@music163/tango-setting-form';
import { useWorkspace, useWorkspaceData } from '@music163/tango-context'; import { useWorkspace, useWorkspaceData } from '@music163/tango-context';
import { wrapCode } from '@music163/tango-helpers'; import { ExpressionModal, getWrappedExpressionCode } from './expression-setter';
import { ExpressionModal } from './expression-setter';
enum EventAction { enum EventAction {
NoAction = 'noAction', NoAction = 'noAction',
@@ -48,8 +47,9 @@ export function EventSetter(props: EventSetterProps) {
const handleChange = useCallback<FormItemComponentProps['onChange']>( const handleChange = useCallback<FormItemComponentProps['onChange']>(
(nextValue: any, ...args) => { (nextValue: any, ...args) => {
if (nextValue !== value) { const ret = getWrappedExpressionCode(nextValue);
onChange(nextValue, ...args); if (ret !== value) {
onChange(ret, ...args);
} }
}, },
[onChange, value], [onChange, value],
@@ -84,7 +84,7 @@ export function EventSetter(props: EventSetterProps) {
onCancel={() => setExpModalVisible(false)} onCancel={() => setExpModalVisible(false)}
onOk={(nextValue) => { onOk={(nextValue) => {
setExpModalVisible(false); setExpModalVisible(false);
handleChange(wrapCode(nextValue)); handleChange(nextValue);
}} }}
dataSource={actionVariables} dataSource={actionVariables}
/> />
@@ -1,15 +1,12 @@
import React, { useState, useEffect, useCallback } from 'react'; import React, { useState, useEffect, useCallback } from 'react';
import { Box, Text, css } from 'coral-system'; import { Box, Text, css } from 'coral-system';
import { Modal } from 'antd'; import { Modal } from 'antd';
import { isValidExpressionCode, value2expressionCode } from '@music163/tango-core';
import { import {
isVariableString, isValidExpressionCode,
getVariableContent, isWrappedByExpressionContainer,
noop, value2expressionCode,
useBoolean, } from '@music163/tango-core';
getValue, import { getVariableContent, noop, useBoolean, getValue } from '@music163/tango-helpers';
wrapCode,
} from '@music163/tango-helpers';
import { CloseCircleFilled, ExpandAltOutlined } from '@ant-design/icons'; import { CloseCircleFilled, ExpandAltOutlined } from '@ant-design/icons';
import { IconButton, Panel, InputCode } from '@music163/tango-ui'; import { IconButton, Panel, InputCode } from '@music163/tango-ui';
import { FormItemComponentProps } from '@music163/tango-setting-form'; import { FormItemComponentProps } from '@music163/tango-setting-form';
@@ -18,7 +15,7 @@ import { EditableVariableTree, IVariableTreeNode } from '../components';
import { useSandboxQuery } from '../context'; import { useSandboxQuery } from '../context';
export const expressionValueValidate = (value: string) => { export const expressionValueValidate = (value: string) => {
if (isVariableString(value)) { if (isWrappedByExpressionContainer(value)) {
const exp = getVariableContent(value); const exp = getVariableContent(value);
if (!isValidExpressionCode(exp)) { if (!isValidExpressionCode(exp)) {
return '表达式存在语法错误!'; return '表达式存在语法错误!';
@@ -27,7 +24,7 @@ export const expressionValueValidate = (value: string) => {
}; };
export const jsonValueValidate = (value: string) => { export const jsonValueValidate = (value: string) => {
if (isVariableString(value)) { if (isWrappedByExpressionContainer(value)) {
const jsonStr = getVariableContent(value); const jsonStr = getVariableContent(value);
try { try {
JSON.parse(jsonStr); JSON.parse(jsonStr);
@@ -37,6 +34,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` const suffixStyle = css`
.anticon-close-circle { .anticon-close-circle {
color: rgba(0, 0, 0, 0.25); color: rgba(0, 0, 0, 0.25);
@@ -78,15 +92,17 @@ export function ExpressionSetter(props: ExpressionSetterProps) {
const change = useCallback( const change = useCallback(
(code: string) => { (code: string) => {
if (!code) { if (code === valueProp) {
onChange(undefined);
return;
}
if (getVariableContent(code) === value2expressionCode(valueProp)) {
return; return;
} }
onChange(wrapCode(code)); const ret = getWrappedExpressionCode(code);
if (ret === valueProp) {
return;
}
onChange(ret);
}, },
[valueProp, onChange], [valueProp, onChange],
); );
@@ -131,7 +147,7 @@ export function ExpressionSetter(props: ExpressionSetterProps) {
value={inputValue} value={inputValue}
onCancel={() => off()} onCancel={() => off()}
onOk={(value) => { onOk={(value) => {
onChange(value); change(value);
off(); off();
}} }}
/> />
@@ -189,15 +205,12 @@ export function ExpressionModal({
<Modal <Modal
closable={false} closable={false}
destroyOnClose destroyOnClose
width="70%" width="60%"
open={visible} open={visible}
onCancel={onCancel} onCancel={onCancel}
onOk={() => { onOk={() => {
onOk(exp); onOk(exp);
}} }}
okButtonProps={{
disabled: !!error,
}}
bodyStyle={{ bodyStyle={{
padding: 0, padding: 0,
}} }}
@@ -74,14 +74,6 @@ export function useFlatMenuData<T>(menuData: T) {
]; ];
} }
const StyledSpin = styled(Spin)`
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
margin-left: 25px;
`;
export const ComponentsPanel = observer( export const ComponentsPanel = observer(
({ ({
menuData = emptyMenuData, menuData = emptyMenuData,
@@ -101,8 +93,6 @@ export const ComponentsPanel = observer(
[workspace.bizComps, workspace.blocks, getBizCompName], [workspace.bizComps, workspace.blocks, getBizCompName],
); );
if (loading) return <StyledSpin tip="正在加载物料列表..." spinning />;
const tabs = Object.keys(menuData).map((key) => ({ const tabs = Object.keys(menuData).map((key) => ({
key, key,
label: localeMap[key], label: localeMap[key],
@@ -135,7 +125,9 @@ export const ComponentsPanel = observer(
<Box px="l" py="m" position="sticky" top="0" zIndex={1} bg="white"> <Box px="l" py="m" position="sticky" top="0" zIndex={1} bg="white">
<Search placeholder="搜索物料" onChange={setKeyword} /> <Search placeholder="搜索物料" onChange={setKeyword} />
</Box> </Box>
{!keyword ? contentNode : <MaterialList data={allList} filterKeyword={keyword} />} <Spin spinning={loading} tip="正在加载物料列表...">
{!keyword ? contentNode : <MaterialList data={allList} filterKeyword={keyword} />}
</Spin>
</Box> </Box>
); );
}, },
@@ -6,13 +6,13 @@ import { PlayCircleOutlined } from '@ant-design/icons';
import { Form, InputCode, Panel, JsonView, Search } from '@music163/tango-ui'; import { Form, InputCode, Panel, JsonView, Search } from '@music163/tango-ui';
import { import {
isNil, isNil,
isVariableString,
getVariableContent, getVariableContent,
isValidFunctionCode, isValidFunctionCode,
logger, logger,
code2object, code2object,
filterTreeData, filterTreeData,
} from '@music163/tango-helpers'; } from '@music163/tango-helpers';
import { isWrappedByExpressionContainer } from '@music163/tango-core';
import { useSandboxQuery } from '../../context'; import { useSandboxQuery } from '../../context';
import { VariableTree } from '../../components'; import { VariableTree } from '../../components';
@@ -187,7 +187,10 @@ const DataSourceView = observer(({ onAdd, onUpdate, onDelete }: DataServiceViewP
const shapeValues = { ...val }; const shapeValues = { ...val };
delete shapeValues.type; delete shapeValues.type;
// 兼容旧版,如果 formatter 包裹了 {} 则删掉首尾 // 兼容旧版,如果 formatter 包裹了 {} 则删掉首尾
if (shapeValues.formatter && isVariableString(shapeValues.formatter)) { if (
shapeValues.formatter &&
isWrappedByExpressionContainer(shapeValues.formatter)
) {
shapeValues.formatter = getVariableContent(shapeValues.formatter); shapeValues.formatter = getVariableContent(shapeValues.formatter);
} }
return shapeValues; return shapeValues;
@@ -275,7 +278,7 @@ function ServiceDetailForm({
{...formProps} {...formProps}
> >
<Form.Item <Form.Item
label="所属模块" label="命名空间"
name="moduleName" name="moduleName"
rules={[ rules={[
{ {
+1
View File
@@ -5,3 +5,4 @@ export * from './dependency-panel';
export * from './history-panel'; export * from './history-panel';
export * from './variable-panel'; export * from './variable-panel';
export * from './sidebar'; export * from './sidebar';
export * from './resizable-box';
@@ -1,14 +1,34 @@
import React, { useEffect, useMemo, useState } from 'react'; import React, { useEffect, useMemo, useState } from 'react';
import { Space, Tree } from 'antd'; import { Dropdown, Space, Tree } from 'antd';
import { Box, css } from 'coral-system'; import { Box, css } from 'coral-system';
import { IconFont } from '@music163/tango-ui'; import { IconFont } from '@music163/tango-ui';
import { EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons'; import { EyeOutlined, EyeInvisibleOutlined, EllipsisOutlined } from '@ant-design/icons';
import { observer, useWorkspace } from '@music163/tango-context'; import { observer, useWorkspace } from '@music163/tango-context';
import { DropMethod, ITangoViewNodeData } from '@music163/tango-core'; import { DropMethod, ITangoViewNodeData } from '@music163/tango-core';
import { parseDndId } from '@music163/tango-helpers'; import { noop, parseDndId } from '@music163/tango-helpers';
import { useSandboxQuery } from '../../context'; import { useSandboxQuery } from '../../context';
import { buildQueryBySlotId } from '../../helpers'; import { buildQueryBySlotId } from '../../helpers';
export interface ComponentsTreeProps {
/**
* 显示/隐藏组件按钮,默认显示
*/
showToggleVisibleIcon?: boolean;
/**
* 页面节点选中回调
*/
onSelect?: (nodeKey: string) => void;
/**
* 节点树更多功能菜单
*/
actionItems?: ActionItem[];
}
interface ActionItem {
item: React.ReactNode;
key: string;
}
const outlineStyle = css` const outlineStyle = css`
.ant-tree-title { .ant-tree-title {
color: rgb(85, 85, 85); color: rgb(85, 85, 85);
@@ -32,6 +52,10 @@ const outlineStyle = css`
.ant-tree-switcher-noop { .ant-tree-switcher-noop {
display: none; display: none;
} }
.material-icon {
width: 1em;
height: 1em;
}
`; `;
const filedNames = { const filedNames = {
@@ -51,126 +75,159 @@ const getNodeKeys = (data: ITangoViewNodeData[]) => {
return ids; return ids;
}; };
const OutlineTreeNode: React.FC<any> = observer(({ node }: { node: ITangoViewNodeData }) => { const OutlineTreeNode: React.FC<{ node: ITangoViewNodeData } & ComponentsTreeProps> = observer(
const workspace = useWorkspace(); ({ node, showToggleVisibleIcon, actionItems }) => {
const sandboxQuery = useSandboxQuery(); const workspace = useWorkspace();
const [visible, setVisible] = useState(true); const sandboxQuery = useSandboxQuery();
const nodeLabel = useMemo(() => { const [visible, setVisible] = useState(true);
const { component, index } = parseDndId(node.id); const nodeLabel = useMemo(() => {
return `${component}:${index}`; const { component, index } = parseDndId(node.id);
}, [node.id]); return `${component}:${index}`;
const componentPrototype = workspace.componentPrototypes.get(node.component); }, [node.id]);
const icon = componentPrototype?.icon || 'icon-placeholder'; const componentPrototype = workspace.componentPrototypes.get(node.component);
const icon = componentPrototype?.icon || 'icon-placeholder';
const iconRender = icon.startsWith('icon-') ? ( const iconRender = icon.startsWith('icon-') ? (
<IconFont className="material-icon" type={icon} /> <IconFont className="material-icon" type={icon} />
) : ( ) : (
<img src={icon} alt={componentPrototype.name} /> <img className="material-icon" src={icon} alt={componentPrototype.name} />
); );
const toggleVisible = (nodeId: string) => { const toggleVisible = (nodeId: string) => {
sandboxQuery.getElement(buildQueryBySlotId(nodeId)).style.visibility = visible sandboxQuery.getElement(buildQueryBySlotId(nodeId)).style.visibility = visible
? 'hidden' ? 'hidden'
: 'visible'; : 'visible';
setVisible(!visible); setVisible(!visible);
}; };
const VisibleIcon = visible ? EyeOutlined : EyeInvisibleOutlined; const VisibleIcon = visible ? EyeOutlined : EyeInvisibleOutlined;
return (
<Space>
{iconRender} {nodeLabel}
{/* 切换显示 */}
<VisibleIcon
className="material-icon"
onClick={(e) => {
e.stopPropagation();
toggleVisible(node.id);
}}
/>
</Space>
);
});
export const ComponentsTree = observer(() => {
const workspace = useWorkspace();
const sandboxQuery = useSandboxQuery();
const [selectedKeys, setSelectedKeys] = useState<React.Key[]>(
workspace.selectSource.selected.map((item) => item.id),
);
const file = workspace.activeViewModule;
const nodesTree = file.nodesTree as ITangoViewNodeData[];
useEffect(() => {
setSelectedKeys(workspace.selectSource.selected.map((item) => item.id));
}, [workspace.selectSource.selected]);
if (!nodesTree.length) {
return ( return (
<Box> <Space>
<code>{workspace.activeViewFile}</code> {iconRender} {nodeLabel}
{/* 切换显示 */}
{showToggleVisibleIcon && (
<VisibleIcon
className="material-icon"
onClick={(e) => {
e.stopPropagation();
toggleVisible(node.id);
}}
/>
)}
{/* 更多 ... 功能菜单 */}
{actionItems?.length > 0 && (
<Dropdown
menu={{
items: actionItems.map((_) => ({
key: _.key,
label: _.item,
})),
}}
trigger={['click']}
>
<EllipsisOutlined />
</Dropdown>
)}
</Space>
);
},
);
export const ComponentsTree: React.FC<ComponentsTreeProps> = observer(
({ showToggleVisibleIcon = true, onSelect = noop, actionItems }) => {
const workspace = useWorkspace();
const sandboxQuery = useSandboxQuery();
const [selectedKeys, setSelectedKeys] = useState<React.Key[]>(
workspace.selectSource.selected.map((item) => item.id),
);
const file = workspace.activeViewModule;
const nodesTree = (file?.nodesTree ?? []) as ITangoViewNodeData[];
const [expandedKeys, setExpandedKeys] = useState(getNodeKeys(nodesTree));
useEffect(() => {
setSelectedKeys(workspace.selectSource.selected.map((item) => item.id));
}, [workspace.selectSource.selected]);
if (!nodesTree.length) {
return (
<Box>
<code>{workspace.activeViewFile}</code>
</Box>
);
}
useEffect(() => {
setExpandedKeys(getNodeKeys(nodesTree));
// nodeTree 更新后重新计算expandKeys
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [nodesTree?.[0]?.id]);
return (
<Box css={outlineStyle}>
<Tree
selectedKeys={selectedKeys as string[]}
fieldNames={filedNames}
treeData={nodesTree as any[]}
onSelect={(keys) => {
const slotKey = keys?.[0] as string;
const data = sandboxQuery.getDraggableParentsData(buildQueryBySlotId(slotKey), true);
if (data && data.id) {
workspace.selectSource.select({
id: data.id,
name: data.name,
bounding: data.bounding,
parents: data.parents,
});
}
// export selected
onSelect(slotKey);
setSelectedKeys(keys);
}}
blockNode
expandedKeys={expandedKeys}
onExpand={(keys) => setExpandedKeys(keys as string[])}
draggable
onDragStart={(data) => {
const prototype = workspace.componentPrototypes.get(data.node.component);
if (!prototype) {
return;
}
const { canDrag } = prototype.rules || {};
if (canDrag && !canDrag()) {
return;
}
workspace.dragSource.set({
id: data.node.id,
name: data.node.component,
});
}}
onDrop={(data) => {
const dropKey = data.node.key as string;
let method;
if (data.dropToGap) {
// 插入节点的后面
method = DropMethod.InsertAfter;
} else {
// 作为第一个子节点
method = DropMethod.InsertFirstChild;
}
workspace.dragSource.dropTarget.set(
{
id: dropKey,
},
method,
);
workspace.dropNode();
}}
titleRender={(node) => (
<OutlineTreeNode
actionItems={actionItems}
showToggleVisibleIcon={showToggleVisibleIcon}
node={node}
/>
)}
/>
</Box> </Box>
); );
} },
);
return (
<Box css={outlineStyle}>
<Tree
selectedKeys={selectedKeys}
fieldNames={filedNames}
treeData={nodesTree as any[]}
onSelect={(keys) => {
const data = sandboxQuery.getDraggableParentsData(
buildQueryBySlotId(keys[0] as string),
true,
);
if (data && data.id) {
workspace.selectSource.select({
id: data.id,
name: data.name,
bounding: data.bounding,
parents: data.parents,
});
}
setSelectedKeys(keys);
}}
blockNode
autoExpandParent
expandedKeys={getNodeKeys(nodesTree)}
draggable
onDragStart={(data) => {
const prototype = workspace.componentPrototypes.get(data.node.component);
if (!prototype) {
return;
}
const { canDrag } = prototype.rules || {};
if (canDrag && !canDrag()) {
return;
}
workspace.dragSource.set({
id: data.node.id,
name: data.node.component,
});
}}
onDrop={(data) => {
const dropKey = data.node.key as string;
let method;
if (data.dropToGap) {
// 插入节点的后面
method = DropMethod.InsertAfter;
} else {
// 作为第一个子节点
method = DropMethod.InsertFirstChild;
}
workspace.dragSource.dropTarget.set(
{
id: dropKey,
},
method,
);
workspace.dropNode();
}}
titleRender={(node) => <OutlineTreeNode node={node} />}
/>
</Box>
);
});
@@ -2,20 +2,26 @@ import React from 'react';
import { Box } from 'coral-system'; import { Box } from 'coral-system';
import { CollapsePanel } from '@music163/tango-ui'; import { CollapsePanel } from '@music163/tango-ui';
import { StateTree } from './state-tree'; import { StateTree } from './state-tree';
import { ComponentsTree } from './components-tree'; import { ComponentsTree, ComponentsTreeProps } from './components-tree';
export interface OutlineViewProps { export interface OutlineViewProps extends ComponentsTreeProps {
/** /**
* 展示状态视图 * 展示状态视图
*/ */
showStateView?: boolean; showStateView?: boolean;
} }
export function OutlinePanel({ showStateView = true }: OutlineViewProps) { export function OutlinePanel({ showStateView = true, ...treeProps }: OutlineViewProps) {
return ( return (
<Box height="100%" position="relative"> <Box height="100%" position="relative">
<CollapsePanel title="页面结构" stickyHeader maxHeight="90%" overflowY="auto"> <CollapsePanel
<ComponentsTree /> title="页面结构"
maxHeight={showStateView ? '90%' : '100%'}
collapsed={!showStateView ? false : undefined}
overflowY="auto"
showBottomBorder={showStateView}
>
<ComponentsTree {...treeProps} />
</CollapsePanel> </CollapsePanel>
{showStateView && ( {showStateView && (
<CollapsePanel title="页面状态" stickyHeader maxHeight="90%" overflowY="auto"> <CollapsePanel title="页面状态" stickyHeader maxHeight="90%" overflowY="auto">
+74 -18
View File
@@ -1,45 +1,101 @@
import React, { useState } from 'react'; import React, { useEffect, useState } from 'react';
import cx from 'classnames'; import cx from 'classnames';
import { Box, HTMLCoralProps, css } from 'coral-system'; import { Box, HTMLCoralProps, css } from 'coral-system';
import { Resizable } from 'react-resizable'; import { Resizable, ResizeHandle } from 'react-resizable';
const resizeHandleStyle = css` export interface ResizableBoxProps {
width?: number;
height?: number;
resizeHandlePosition?: 'left' | 'right' | 'top' | 'bottom';
axis?: 'x' | 'y';
children?: React.ReactNode;
className?: string;
style?: React.CSSProperties;
}
const resizeHandleStyle = (axis: ResizableBoxProps['axis'], barStyle: HTMLCoralProps<'div'>) => css`
position: absolute; position: absolute;
top: 0;
z-index: 999; z-index: 999;
width: 4px; cursor: ${axis === 'x' ? 'col-resize' : 'row-resize'};
height: 100%; ${barStyle &&
cursor: col-resize; Object.keys(barStyle)
.map((key) => `${key}: ${barStyle[key]};`)
.join('')}
&:hover, &:hover,
&:active { &:active {
background-color: var(--tango-colors-brand); background-color: var(--tango-colors-brand);
} }
`; `;
export interface ResizableBoxProps extends HTMLCoralProps<'div'> { const getResizeHandles = (
width?: number; resizeHandlePosition: ResizableBoxProps['resizeHandlePosition'],
height?: number; axis: ResizableBoxProps['axis'],
resizeHandlePosition?: 'left' | 'right'; ) => {
} const handles: ResizeHandle[] = [];
switch (axis) {
case 'x':
handles.push(resizeHandlePosition === 'right' ? 'e' : 'w');
break;
case 'y':
handles.push(resizeHandlePosition === 'bottom' ? 's' : 'n');
break;
default:
break;
}
return handles;
};
export function ResizableBox({ export function ResizableBox({
resizeHandlePosition = 'right', resizeHandlePosition = 'right',
width: widthProp, width: widthProp,
height, height: heightProp,
children, children,
className, className,
style, style,
axis = 'x',
}: ResizableBoxProps) { }: ResizableBoxProps) {
const [width, setWidth] = useState(widthProp); const [width, setWidth] = useState(widthProp);
const barStyle = resizeHandlePosition === 'right' ? { right: '-4px' } : { left: '-4px' }; const [height, setHeight] = useState(heightProp);
const barStyle: HTMLCoralProps<'div'> = {
...(axis === 'x'
? {
height: '100%',
width: '4px',
top: '0',
[resizeHandlePosition === 'right' ? 'right' : 'left']: '-4px',
}
: {
height: '4px',
width: '100%',
left: '0',
[resizeHandlePosition === 'bottom' ? 'bottom' : 'top']: '0',
}),
};
useEffect(() => {
setWidth(widthProp);
}, [widthProp]);
useEffect(() => {
setHeight(heightProp);
}, [heightProp]);
return ( return (
<Resizable <Resizable
axis="x" axis={axis}
width={width} width={width}
resizeHandles={getResizeHandles(resizeHandlePosition, axis)}
height={height} height={height}
minConstraints={[150, 150]}
maxConstraints={[window.innerWidth * 0.6, window.innerHeight * 0.8]}
onResize={(e, { size }) => { onResize={(e, { size }) => {
setWidth(size.width); if (axis === 'x') {
setWidth(size.width);
}
if (axis === 'y') {
setHeight(size.height);
}
}} }}
onResizeStart={() => { onResizeStart={() => {
document.body.style.pointerEvents = 'none'; document.body.style.pointerEvents = 'none';
@@ -49,7 +105,7 @@ export function ResizableBox({
document.body.style.pointerEvents = 'auto'; document.body.style.pointerEvents = 'auto';
document.body.style.userSelect = 'auto'; document.body.style.userSelect = 'auto';
}} }}
handle={<Box className="ResizeHandle" css={resizeHandleStyle} {...barStyle} />} handle={<Box className="ResizeHandle" css={resizeHandleStyle(axis, barStyle)} />}
> >
<div <div
className={cx('ResizableBox', className)} className={cx('ResizableBox', className)}
+36 -26
View File
@@ -75,8 +75,10 @@ export interface SidebarPanelItemProps
} }
function BaseSidebarPanel({ panelWidth: defaultPanelWidth = 280, footer, children }: SidebarProps) { function BaseSidebarPanel({ panelWidth: defaultPanelWidth = 280, footer, children }: SidebarProps) {
const designer = useDesigner();
const items = useMemo(() => { const items = useMemo(() => {
const ret: Record<React.Key, SidebarPanelItemProps> = {}; const ret: Record<string, SidebarPanelItemProps> = {};
React.Children.forEach(children, (child) => { React.Children.forEach(children, (child) => {
if (child && React.isValidElement(child)) { if (child && React.isValidElement(child)) {
const widget = getWidget(['sidebar', child.key].join('.')); const widget = getWidget(['sidebar', child.key].join('.'));
@@ -90,20 +92,6 @@ function BaseSidebarPanel({ panelWidth: defaultPanelWidth = 280, footer, childre
return ret; return ret;
}, [children]); }, [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 ( return (
<Box display="flex" flexShrink={0} css={sidebarStyle} className="SidebarPanel"> <Box display="flex" flexShrink={0} css={sidebarStyle} className="SidebarPanel">
<Box className="SidebarPanelBar" display="flex" flexDirection="column" position="relative"> <Box className="SidebarPanelBar" display="flex" flexDirection="column" position="relative">
@@ -142,19 +130,41 @@ function BaseSidebarPanel({ panelWidth: defaultPanelWidth = 280, footer, childre
{footer} {footer}
</Box> </Box>
</Box> </Box>
{panel ? ( {Object.keys(items).map((key) => {
<ResizableBox key={panel.label} width={panelWidth} style={floatPanelStyle}> const panel = items[key];
<SidebarPanelExpandedContent const floatPanelStyle: any = panel?.isFloat
isFloat={panel.isFloat} ? {
closeable={panel.isFloat} position: 'absolute',
onClose={() => { left: '50px',
designer.closeSidebarPanel(); 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
</SidebarPanelExpandedContent> isFloat={panel.isFloat}
</ResizableBox> closeable={panel.isFloat}
) : null} onClose={() => {
designer.closeSidebarPanel();
}}
>
{panel.children}
</SidebarPanelExpandedContent>
</ResizableBox>
);
})}
</Box> </Box>
); );
} }
@@ -53,7 +53,7 @@ export const VariablePanel = observer(
existNames={storeNames} existNames={storeNames}
onCancel={off} onCancel={off}
onSubmit={(values) => { onSubmit={(values) => {
workspace.addStoreModule(values.name, newStoreTemplate); workspace.addStoreFile(values.name, newStoreTemplate);
off(); off();
}} }}
/> />
+13 -3
View File
@@ -1,9 +1,11 @@
import React from 'react'; import React from 'react';
import { Group } from 'coral-system'; import { Group } from 'coral-system';
import { Modal } from 'antd';
import { ToggleButton } from '@music163/tango-ui'; import { ToggleButton } from '@music163/tango-ui';
import { observer, useDesigner } from '@music163/tango-context'; import { observer, useDesigner, useWorkspace } from '@music163/tango-context';
export const ModeSwitchTool = observer(() => { export const ModeSwitchTool = observer(() => {
const workspace = useWorkspace();
const designer = useDesigner(); const designer = useDesigner();
return ( return (
<Group attached> <Group attached>
@@ -13,7 +15,7 @@ export const ModeSwitchTool = observer(() => {
onClick={() => { onClick={() => {
designer.setActiveView('design'); designer.setActiveView('design');
}} }}
tip="设计视图" tooltip="设计视图"
> >
</ToggleButton> </ToggleButton>
@@ -23,8 +25,16 @@ export const ModeSwitchTool = observer(() => {
onClick={() => { onClick={() => {
designer.setActiveView('code'); // 切换到源码视图 designer.setActiveView('code'); // 切换到源码视图
designer.setActiveSidebarPanel(''); // 关闭左侧面板 designer.setActiveSidebarPanel(''); // 关闭左侧面板
if (workspace.activeFile !== workspace.activeViewFile) {
Modal.confirm({
title: '当前打开的文件与视图不匹配,是否切换到当前视图对应的文件?',
onOk: () => {
workspace.setActiveFile(workspace.activeViewFile);
},
});
}
}} }}
tip="源码视图" tooltip="源码视图"
> >
</ToggleButton> </ToggleButton>
+23
View File
@@ -3,6 +3,29 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.1.8](https://github.com/netease/tango/compare/@music163/tango-helpers@0.1.7...@music163/tango-helpers@0.1.8) (2023-11-23)
### Bug Fixes
- refactor update import specifiers ([#68](https://github.com/netease/tango/issues/68)) ([558f2cd](https://github.com/netease/tango/commit/558f2cd0a692c6bbc866d08250d25e2619af183f))
## [0.1.7](https://github.com/netease/tango/compare/@music163/tango-helpers@0.1.6...@music163/tango-helpers@0.1.7) (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.1.6](https://github.com/netease/tango/compare/@music163/tango-helpers@0.1.5...@music163/tango-helpers@0.1.6) (2023-11-16)
### Bug Fixes
- isVariableString ([b9403cf](https://github.com/netease/tango/commit/b9403cfec668b2717a68e92a6f837d5a88096c7c))
- parse variables from view ([089a482](https://github.com/netease/tango/commit/089a482f750e9d9a7743a6641d6e39989347d318))
## [0.1.5](https://github.com/netease/tango/compare/@music163/tango-helpers@0.1.4...@music163/tango-helpers@0.1.5) (2023-11-02)
**Note:** Version bump only for package @music163/tango-helpers
## [0.1.4](https://github.com/netease/tango/compare/@music163/tango-helpers@0.1.3...@music163/tango-helpers@0.1.4) (2023-10-23) ## [0.1.4](https://github.com/netease/tango/compare/@music163/tango-helpers@0.1.3...@music163/tango-helpers@0.1.4) (2023-10-23)
### Bug Fixes ### Bug Fixes
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@music163/tango-helpers", "name": "@music163/tango-helpers",
"version": "0.1.4", "version": "0.1.8",
"description": "Shared types, helpers, and hooks of tango-apps", "description": "Shared types, helpers, and hooks of tango-apps",
"keywords": [ "keywords": [
"shared", "shared",
+19 -5
View File
@@ -182,6 +182,8 @@ const templatePattern = /^{(.+)}$/s;
/** /**
* 判断给定字符串是否为变量字符串 * 判断给定字符串是否为变量字符串
* @deprecated 使用 isWrappedByExpressionContainer 代替
*
* @example {[]} * @example {[]}
* @example {{}} * @example {{}}
* @example {this.foo} * @example {this.foo}
@@ -191,6 +193,7 @@ const templatePattern = /^{(.+)}$/s;
*/ */
export function isVariableString(str: string) { export function isVariableString(str: string) {
// 先检查是否是简单的对象 // 先检查是否是简单的对象
// FIXME: 这里有问题,如果代码中有引用,会被误判
if (code2object(str)) { if (code2object(str)) {
return false; return false;
} }
@@ -205,6 +208,8 @@ export function isVariableString(str: string) {
* @example "hello" => {"hello"} * @example "hello" => {"hello"}
* @example () => {} => {() => {}} * @example () => {} => {() => {}}
* *
* @deprecated 有问题,不要使用
*
* @param code 输入代码 * @param code 输入代码
* @returns 加上花括号后的代码 * @returns 加上花括号后的代码
*/ */
@@ -238,17 +243,26 @@ export function getVariableContent(str: string) {
return str; return str;
} }
// 提供给代码执行环境的全局变量
const patchCode = `
var tango = {
stores: {},
services: {},
config: {},
refs: {},
};
`;
/** /**
* 将代码放到函数体中进行执行 * 将代码放到函数体中进行执行
* @param code * @param code
* @returns 函数执行的结果 * @returns 函数执行的结果
*/ */
export function executeCodeAsFunctionBody(code: string) { export function runCode(code: string) {
let ret; let ret;
try { try {
// eslint-disable-next-line no-new-func // eslint-disable-next-line no-new-func
ret = new Function(code); ret = new Function(`${patchCode}\n return ${code}`)();
ret = ret();
} catch (err) { } catch (err) {
// ignore error // ignore error
} }
@@ -261,14 +275,14 @@ const objectWrapperPattern = /^[{\[].*[}\]]$/s;
/** /**
* 将代码片段转成 js 对象 * 将代码片段转成 js 对象
* @param code 代码文本 * @param code 代码文本
* @param isStrict 是否为严格模式 * @param isStrict 是否为严格模式(是否废弃)
* @returns * @returns
*/ */
export function code2object(code: string, isStrict = true) { export function code2object(code: string, isStrict = true) {
// 非严格模式直接执行 // 非严格模式直接执行
// 严格模式下需检测 code 是一个对象 // 严格模式下需检测 code 是一个对象
if (!isStrict || (isStrict && objectWrapperPattern.test(code))) { if (!isStrict || (isStrict && objectWrapperPattern.test(code))) {
const ret = executeCodeAsFunctionBody(`return ${code}`); const ret = runCode(code);
return typeof ret === 'object' ? ret : undefined; return typeof ret === 'object' ? ret : undefined;
} }
return code; return code;
+1 -1
View File
@@ -72,7 +72,7 @@ export function withDnd(options: IWithDndOptions<any>) {
); );
}); });
hoistNonReactStatic(Component, BaseComponent); hoistNonReactStatic(Component, BaseComponent as any);
Component.displayName = wrapDisplayName(displayName, 'withDnd'); Component.displayName = wrapDisplayName(displayName, 'withDnd');
return Component; return Component;
-143
View File
@@ -33,94 +33,11 @@ export type OptionType = {
relatedImports?: string[]; relatedImports?: string[];
}; };
type TangoSchemaTreeNodePropValueBaseType = {
value: string | number | boolean;
rawCode: string;
};
type TangoSchemaTreeNodePropValueSlotType = {
type: 'Slot';
value: TangoSchemaTreeNodeType;
};
type TangoSchemaTreeNodePropValueType =
| string
| number
| boolean
| object
| TangoSchemaTreeNodePropValueBaseType
| TangoSchemaTreeNodePropValueSlotType;
export type TangoSchemaTreeNodeType = {
/**
* 节点唯一标识
*/
id?: string;
/**
* 节点组件名
*/
component?: string;
/**
* 节点属性集合
*/
props?: Record<string, TangoSchemaTreeNodePropValueType>;
/**
* 节点是否渲染
* TODO: 支持变量表达式
*/
isRender?: boolean;
/**
* 子节点
*/
children?: TangoSchemaTreeNodeType[];
};
/** /**
* 服务函数类型 * 服务函数类型
*/ */
export type ServiceFunctionType = (...args: any[]) => Promise<any>; export type ServiceFunctionType = (...args: any[]) => Promise<any>;
export type TangoRemoteServicesType = {
/**
* 图片服务
*/
ImageService?: {
listMy: ServiceFunctionType;
listFav: ServiceFunctionType;
listPub: ServiceFunctionType;
};
/**
* 数据源服务
*/
OxService?: {
listApp: ServiceFunctionType;
listBranch: ServiceFunctionType;
listApi: ServiceFunctionType;
listApiByPage: ServiceFunctionType;
};
/**
* 依赖面板服务
*/
DependencyService?: {
listBizDependencies: ServiceFunctionType;
listBaseDependencies: ServiceFunctionType;
listPackageVersions: ServiceFunctionType;
};
RiddleService?: {
listBlock: ServiceFunctionType;
postBlock: ServiceFunctionType;
updateBlock: ServiceFunctionType;
};
/**
* AIGC 服务
*/
GptService?: {
codeCompletion?: ServiceFunctionType;
textCompletion?: ServiceFunctionType;
textEmbedding?: ServiceFunctionType;
};
};
export type RelatedDependencyType = { name: string; version: string }; export type RelatedDependencyType = { name: string; version: string };
/** /**
@@ -163,63 +80,3 @@ export type SetterOnChangeDetailType = {
relatedImports?: string[]; relatedImports?: string[];
isRawCode?: boolean; isRawCode?: boolean;
}; };
/**
* 智能向导生成类型
*/
export enum WizardType {
NEW = 'new',
REPLACE = 'replace',
PREVIEW = 'preview',
}
/**
* 智能向导OX模型定义
*/
export interface IOXModel {
key: string; // 唯一标识 (字段名加哈希值)
name: string; // 字段名
description: string; // 字段描述
basicDataModelName: 'List' | 'Integer' | 'int' | 'String' | 'long' | 'boolean' | 'ENUM'; // 字段类型
dataModelFieldDTOList: IOXModel[];
type: 'ARRAY' | 'BASIC' | 'CLASS' | 'GENERIC' | 'ENUM' | 'MAP';
extra: IOXModel;
extras: IOXModel[];
linkedDataModelName: String;
linkedModelDTO: {
type: 'ARRAY' | 'BASIC' | 'CLASS' | 'GENERIC' | 'ENUM' | 'MAP';
fieldDTOList: IOXModel[];
};
paramType: 'RequestParam' | 'JsonRequestParam'; // 请求传参类型
}
/**
* 通用智能向导
*/
type ChildType = Array<Record<string, any>>;
export type IDataProviderFormProps = {
loadData: string;
payload: Record<string, any>;
model: string;
formatter: (res: any) => any;
};
export interface CommonGuideFormValueType {
fatherComponentName: string;
fatherProps: Record<string, any>;
childComponentName: string;
children: ChildType;
dataProviderProps: IDataProviderFormProps;
childPropsConfig: {
// 字段名对应属性名
fieldName: string;
// 字段注释对应属性名
fieldDescriptionName: string;
// 字段值类型对应属性类型 (暂时用不到)
fieldType?: string;
};
}
/**
* end
*/
+2 -2
View File
@@ -15,8 +15,8 @@ export type PartialRecord<K extends keyof any, T> = {
[P in K]?: T; [P in K]?: T;
}; };
export type ReactComponentProps = { export interface ReactComponentProps {
children?: React.ReactNode; children?: React.ReactNode;
style?: React.CSSProperties; style?: React.CSSProperties;
className?: string; className?: string;
}; }
+3 -3
View File
@@ -42,9 +42,9 @@ describe('string', () => {
expect(isVariableString('{value}')).toBeTruthy(); expect(isVariableString('{value}')).toBeTruthy();
expect(isVariableString('{"hello"}')).toBeTruthy(); expect(isVariableString('{"hello"}')).toBeTruthy();
expect(isVariableString('{ foo: "bar" }')).toBeFalsy(); expect(isVariableString('{ foo: "bar" }')).toBeFalsy();
expect(isVariableString('{{ foo: "bar" }}')).toBeTruthy(); expect(isVariableString('{ type: tango.stores?.homePage?.tabKey }')).toBeFalsy();
expect(isVariableString('{() => {}}')).toBeTruthy(); // expect(isVariableString('{ type: tango.stores.homePage.tabKey }')).toBeFalsy(); // FIXME: fix this case
expect(isVariableString('{() => { console.log(); }}')).toBeTruthy(); expect(isVariableString('{ foo: "bar" }')).toBeFalsy();
}); });
it('getVariableContent', () => { it('getVariableContent', () => {
+22
View File
@@ -3,6 +3,28 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.1.12](https://github.com/netease/tango/compare/@music163/tango-sandbox@0.1.11...@music163/tango-sandbox@0.1.12) (2023-11-27)
**Note:** Version bump only for package @music163/tango-sandbox
## [0.1.11](https://github.com/netease/tango/compare/@music163/tango-sandbox@0.1.10...@music163/tango-sandbox@0.1.11) (2023-11-23)
**Note:** Version bump only for package @music163/tango-sandbox
## [0.1.10](https://github.com/netease/tango/compare/@music163/tango-sandbox@0.1.9...@music163/tango-sandbox@0.1.10) (2023-11-20)
**Note:** Version bump only for package @music163/tango-sandbox
## [0.1.9](https://github.com/netease/tango/compare/@music163/tango-sandbox@0.1.8...@music163/tango-sandbox@0.1.9) (2023-11-16)
### Bug Fixes
- fix sandbox props ([afb94d6](https://github.com/netease/tango/commit/afb94d6a304f217df24a5d3d986dec1670cd29a9))
## [0.1.8](https://github.com/netease/tango/compare/@music163/tango-sandbox@0.1.7...@music163/tango-sandbox@0.1.8) (2023-11-02)
**Note:** Version bump only for package @music163/tango-sandbox
## [0.1.7](https://github.com/netease/tango/compare/@music163/tango-sandbox@0.1.6...@music163/tango-sandbox@0.1.7) (2023-10-23) ## [0.1.7](https://github.com/netease/tango/compare/@music163/tango-sandbox@0.1.6...@music163/tango-sandbox@0.1.7) (2023-10-23)
**Note:** Version bump only for package @music163/tango-sandbox **Note:** Version bump only for package @music163/tango-sandbox
+4 -4
View File
@@ -1,6 +1,6 @@
{ {
"name": "@music163/tango-sandbox", "name": "@music163/tango-sandbox",
"version": "0.1.7", "version": "0.1.12",
"description": "sandbox of tango apps", "description": "sandbox of tango apps",
"author": "wwsun <ww.sun@outlook.com>", "author": "wwsun <ww.sun@outlook.com>",
"homepage": "", "homepage": "",
@@ -29,14 +29,14 @@
}, },
"dependencies": { "dependencies": {
"@ant-design/icons": "^4.8.0", "@ant-design/icons": "^4.8.0",
"@music163/tango-core": "^0.2.2", "@music163/tango-core": "^0.2.7",
"@music163/tango-helpers": "^0.1.4", "@music163/tango-helpers": "^0.1.8",
"crypto-js": "^4.1.1", "crypto-js": "^4.1.1",
"lodash.isequal": "4.5.0", "lodash.isequal": "4.5.0",
"react-frame-component": "^5.2.4" "react-frame-component": "^5.2.4"
}, },
"devDependencies": { "devDependencies": {
"@types/crypto-js": "^4.1.1" "@types/crypto-js": "^4.1.3"
}, },
"publishConfig": { "publishConfig": {
"access": "public", "access": "public",
+3 -1
View File
@@ -21,6 +21,7 @@ import { IFiles, CodeSandboxState, CodeSandboxProps, UnsubscribeFunction } from
// }; // };
export class CodeSandbox extends React.Component<CodeSandboxProps, CodeSandboxState> { export class CodeSandbox extends React.Component<CodeSandboxProps, CodeSandboxState> {
static defaultProps = { static defaultProps = {
template: 'create-react-app',
skipEval: false, skipEval: false,
eventHandlers: {}, eventHandlers: {},
externalResources: [] as string[], externalResources: [] as string[],
@@ -181,9 +182,10 @@ export class CodeSandbox extends React.Component<CodeSandboxProps, CodeSandboxSt
}; };
render() { render() {
const { style } = this.props;
const { loading, status, iframeId } = this.state; const { loading, status, iframeId } = this.state;
return ( return (
<div className="CodeSandbox" style={{ position: 'relative', height: '100%' }}> <div className="CodeSandbox" style={{ position: 'relative', height: '100%', ...style }}>
{loading ? <Loading status={status} /> : null} {loading ? <Loading status={status} /> : null}
<iframe <iframe
ref={this.setupFrame} ref={this.setupFrame}
+1
View File
@@ -504,6 +504,7 @@ export interface CodeSandboxProps {
isFile: (path: string) => Promise<boolean>; isFile: (path: string) => Promise<boolean>;
readFile: (path: string) => Promise<string>; readFile: (path: string) => Promise<string>;
}; };
style?: React.CSSProperties;
} }
export interface ViteSandboxProps { export interface ViteSandboxProps {
+22
View File
@@ -3,6 +3,28 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.2.11](https://github.com/netease/tango/compare/@music163/tango-setting-form@0.2.10...@music163/tango-setting-form@0.2.11) (2023-11-27)
**Note:** Version bump only for package @music163/tango-setting-form
## [0.2.10](https://github.com/netease/tango/compare/@music163/tango-setting-form@0.2.9...@music163/tango-setting-form@0.2.10) (2023-11-23)
**Note:** Version bump only for package @music163/tango-setting-form
## [0.2.9](https://github.com/netease/tango/compare/@music163/tango-setting-form@0.2.8...@music163/tango-setting-form@0.2.9) (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.8](https://github.com/netease/tango/compare/@music163/tango-setting-form@0.2.7...@music163/tango-setting-form@0.2.8) (2023-11-16)
**Note:** Version bump only for package @music163/tango-setting-form
## [0.2.7](https://github.com/netease/tango/compare/@music163/tango-setting-form@0.2.6...@music163/tango-setting-form@0.2.7) (2023-11-02)
**Note:** Version bump only for package @music163/tango-setting-form
## [0.2.6](https://github.com/netease/tango/compare/@music163/tango-setting-form@0.2.5...@music163/tango-setting-form@0.2.6) (2023-10-23) ## [0.2.6](https://github.com/netease/tango/compare/@music163/tango-setting-form@0.2.5...@music163/tango-setting-form@0.2.6) (2023-10-23)
### Bug Fixes ### Bug Fixes
+4 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "@music163/tango-setting-form", "name": "@music163/tango-setting-form",
"version": "0.2.6", "version": "0.2.11",
"description": "setting form of tango-apps", "description": "setting form of tango-apps",
"author": "wwsun <ww.sun@outlook.com>", "author": "wwsun <ww.sun@outlook.com>",
"homepage": "", "homepage": "",
@@ -28,8 +28,9 @@
}, },
"dependencies": { "dependencies": {
"@ant-design/icons": "^4.8.0", "@ant-design/icons": "^4.8.0",
"@music163/tango-helpers": "^0.1.4", "@music163/tango-core": "^0.2.7",
"@music163/tango-ui": "^0.1.7", "@music163/tango-helpers": "^0.1.8",
"@music163/tango-ui": "^0.1.11",
"antd": "^4.24.2", "antd": "^4.24.2",
"coral-system": "^1.0.5", "coral-system": "^1.0.5",
"mobx": "6.9.0", "mobx": "6.9.0",
+2 -2
View File
@@ -4,10 +4,10 @@ import { observer } from 'mobx-react-lite';
import { import {
clone, clone,
ComponentPropType, ComponentPropType,
isVariableString,
SetterOnChangeDetailType, SetterOnChangeDetailType,
useBoolean, useBoolean,
} from '@music163/tango-helpers'; } from '@music163/tango-helpers';
import { isWrappedByExpressionContainer } from '@music163/tango-core';
import { IconFont, ToggleButton } from '@music163/tango-ui'; import { IconFont, ToggleButton } from '@music163/tango-ui';
import { QuestionCircleOutlined } from '@ant-design/icons'; import { QuestionCircleOutlined } from '@ant-design/icons';
import { InputProps, Tooltip } from 'antd'; import { InputProps, Tooltip } from 'antd';
@@ -83,7 +83,7 @@ export function createFormItem(options: IFormItemCreateOptions) {
const value = toJS(field.value ?? defaultValue); const value = toJS(field.value ?? defaultValue);
const disableVariableSetter = disableSwitchExpressionSetter ?? disableVariableSetterProp; const disableVariableSetter = disableSwitchExpressionSetter ?? disableVariableSetterProp;
const [isVariable, { toggle: toggleIsVariable }] = useBoolean( const [isVariable, { toggle: toggleIsVariable }] = useBoolean(
() => !disableVariableSetter && isVariableString(value), () => !disableVariableSetter && isWrappedByExpressionContainer(value),
); );
const setterName = isVariable ? 'expressionSetter' : setter; const setterName = isVariable ? 'expressionSetter' : setter;
+18
View File
@@ -3,6 +3,24 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.1.11](https://github.com/netease/tango/compare/@music163/tango-ui@0.1.10...@music163/tango-ui@0.1.11) (2023-11-23)
**Note:** Version bump only for package @music163/tango-ui
## [0.1.10](https://github.com/netease/tango/compare/@music163/tango-ui@0.1.9...@music163/tango-ui@0.1.10) (2023-11-20)
**Note:** Version bump only for package @music163/tango-ui
## [0.1.9](https://github.com/netease/tango/compare/@music163/tango-ui@0.1.8...@music163/tango-ui@0.1.9) (2023-11-16)
**Note:** Version bump only for package @music163/tango-ui
## [0.1.8](https://github.com/netease/tango/compare/@music163/tango-ui@0.1.7...@music163/tango-ui@0.1.8) (2023-11-02)
### Bug Fixes
- outline-panel tree expand bug & add props ([#52](https://github.com/netease/tango/issues/52)) ([b29e235](https://github.com/netease/tango/commit/b29e23599befbd60845053544648b10976a65824))
## [0.1.7](https://github.com/netease/tango/compare/@music163/tango-ui@0.1.6...@music163/tango-ui@0.1.7) (2023-10-23) ## [0.1.7](https://github.com/netease/tango/compare/@music163/tango-ui@0.1.6...@music163/tango-ui@0.1.7) (2023-10-23)
### Bug Fixes ### Bug Fixes
+3 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "@music163/tango-ui", "name": "@music163/tango-ui",
"version": "0.1.7", "version": "0.1.11",
"description": "ui widgets of tango", "description": "ui widgets of tango",
"keywords": [ "keywords": [
"react", "react",
@@ -38,8 +38,8 @@
"@codemirror/lang-javascript": "^6.2.1", "@codemirror/lang-javascript": "^6.2.1",
"@codemirror/lint": "^6.4.2", "@codemirror/lint": "^6.4.2",
"@codemirror/search": "^6.5.4", "@codemirror/search": "^6.5.4",
"@music163/tango-helpers": "^0.1.4", "@music163/tango-helpers": "^0.1.8",
"@uiw/react-codemirror": "^4.19.3", "@uiw/react-codemirror": "^4.21.20",
"antd": "^4.24.2", "antd": "^4.24.2",
"antd-easy-form": "^1.1.3", "antd-easy-form": "^1.1.3",
"coral-system": "^1.0.5", "coral-system": "^1.0.5",
+13 -1
View File
@@ -28,6 +28,10 @@ export interface CollapsePanelProps extends Omit<HTMLCoralProps<'div'>, 'title'>
* 折叠状态变化时的回调 * 折叠状态变化时的回调
*/ */
onCollapse?: (collapse: boolean) => void; onCollapse?: (collapse: boolean) => void;
/**
* 是否显示底部边框线
*/
showBottomBorder?: boolean;
} }
const headerStyle = css` const headerStyle = css`
@@ -49,6 +53,7 @@ export function CollapsePanel(props: CollapsePanelProps) {
defaultCollapsed = false, defaultCollapsed = false,
onCollapse, onCollapse,
stickyHeader, stickyHeader,
showBottomBorder = true,
...rest ...rest
} = props; } = props;
const [collapsed, setCollapsed] = useControllableState({ const [collapsed, setCollapsed] = useControllableState({
@@ -70,8 +75,15 @@ export function CollapsePanel(props: CollapsePanelProps) {
} }
: {}; : {};
const CollapsePanelBorderStyle = showBottomBorder
? {
borderBottom: 'solid',
borderBottomColor: 'line2',
}
: {};
return ( return (
<Box className="CollapsePanel" borderBottom="solid" borderBottomColor="line2" {...rest}> <Box className="CollapsePanel" {...CollapsePanelBorderStyle} {...rest}>
<Box <Box
display="flex" display="flex"
alignItems="center" alignItems="center"
+3 -2
View File
@@ -10,9 +10,10 @@
"@music163/tango-helpers": ["packages/helpers/src/index.ts"], "@music163/tango-helpers": ["packages/helpers/src/index.ts"],
"@music163/tango-core": ["packages/core/src/index.ts"], "@music163/tango-core": ["packages/core/src/index.ts"],
"@music163/tango-context": ["packages/context/src/index.ts"], "@music163/tango-context": ["packages/context/src/index.ts"],
"@music163/tango-setting-form": ["packages/setting-form/src/index.ts"],
"@music163/tango-ui": ["packages/ui/src/index.ts"], "@music163/tango-ui": ["packages/ui/src/index.ts"],
"@music163/tango": ["packages/designer/src/index.ts"] "@music163/tango-sandbox": ["packages/sandbox/src/index.ts"],
"@music163/tango-setting-form": ["packages/setting-form/src/index.ts"],
"@music163/tango-designer": ["packages/designer/src/index.ts"]
} }
} }
} }
+4136 -4306
View File
File diff suppressed because it is too large Load Diff