Compare commits

..

24 Commits

Author SHA1 Message Date
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
54 changed files with 5213 additions and 4787 deletions
+3 -6
View File
@@ -5,7 +5,7 @@
<h1 align="center">Tango LowCode Builder</h1>
<div align="center">
A source code based low-code builder, developed by NetEase Cloud Music Develop Team.
A source code based low-code builder.
[![license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/NetEase/tango/blob/main/LICENSE)
[![NPM version](https://img.shields.io/npm/v/@music163/tango-designer.svg?style=flat-square)](http://npmjs.org/package/@music163/tango-designer)
@@ -79,6 +79,7 @@ yarn start
Join NetEase Tango Community to share your ideas, suggestions, or questions and connect with other users and contributors.
- Discord: <https://discord.gg/B6hkGTe4Rz>
- [Usage Trends](https://npm-compare.com/@music163/tango-helpers,@music163/tango-context,@music163/tango-core,@music163/tango-setting-form,@music163/tango-sandbox,@music163/tango-ui,@music163/tango-designer)
## 🤝 Contributing
@@ -89,13 +90,9 @@ Please read the [github contribution guide](https://docs.github.com/en/get-start
- Commit and push your code
- Open a Pull Request
## 📈 Usage Trends
[Usage Trends of Tango Packages](https://npm-compare.com/@music163/tango-helpers,@music163/tango-context,@music163/tango-core,@music163/tango-setting-form,@music163/tango-sandbox,@music163/tango-ui,@music163/tango-designer)
## 💗 Acknowledgments
Thanks to the NetEase Cloud Music Front-end team, Public Technology team, Live Broadcasting Technology team, and all the colleagues who participated in the Tango project.
Thanks to the NetEase Cloud Music Front-end team, Public Technology team, Live Broadcasting Technology team, and all the developers who participated in the Tango project.
Thank you to CodeSandbox for providing the [Sandpack](https://sandpack.codesandbox.io/) project, which provides powerful online code execution capabilities for Tango.
+3 -6
View File
@@ -5,7 +5,7 @@
<h1 align="center">Tango 低代码设计器</h1>
<div align="center">
一个源码驱动的低代码设计器,来自网易云音乐技术团队
一个源码驱动的低代码设计器框架
[![license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/NetEase/tango/blob/main/LICENSE)
[![NPM version](https://img.shields.io/npm/v/@music163/tango-designer.svg?style=flat-square)](http://npmjs.org/package/@music163/tango-designer)
@@ -79,6 +79,7 @@ yarn start
参与 NetEase Tango 的社区,以分享您的想法、建议或问题,并与其他用户和贡献者建立联系。
- Discord: <https://discord.gg/B6hkGTe4Rz>
- [使用趋势](https://npm-compare.com/@music163/tango-helpers,@music163/tango-context,@music163/tango-core,@music163/tango-setting-form,@music163/tango-sandbox,@music163/tango-ui,@music163/tango-designer)
## 🤝 参与共建
@@ -90,13 +91,9 @@ yarn start
- 合并修改 git rebase master
- 发起 Pull Request
## 📈 使用量趋势
[Tango 使用量趋势](https://npm-compare.com/@music163/tango-helpers,@music163/tango-context,@music163/tango-core,@music163/tango-setting-form,@music163/tango-sandbox,@music163/tango-ui,@music163/tango-designer)
## 💗 致谢
感谢网易云音乐公共技术团队,大前端团队,直播技术团队,以及所有参与过 Tango 项目的同学们
感谢网易云音乐公共技术团队,大前端团队,直播技术团队,以及所有参与过 Tango 项目的开发者
感谢 CodeSandbox 提供的 [Sandpack](https://sandpack.codesandbox.io/) 项目,为 Tango 提供了强大的基于浏览器的代码构建与执行能力。
+16 -10
View File
@@ -17,37 +17,43 @@ const tangoConfigJson = {
version: '17.0.2',
library: 'React',
type: 'dependency',
resources: ['https://unpkg.com/react@{{version}}/umd/react.development.js'],
resources: ['https://cdn.jsdelivr.net/npm/react@{{version}}/umd/react.development.js'],
},
'react-dom': {
version: '17.0.2',
library: 'ReactDOM',
type: 'dependency',
resources: ['https://unpkg.com/react-dom@{{version}}/umd/react-dom.development.js'],
resources: [
'https://cdn.jsdelivr.net/npm/react-dom@{{version}}/umd/react-dom.development.js',
],
},
'react-is': {
version: '16.13.1',
library: 'ReactIs',
type: 'dependency',
resources: ['https://unpkg.com/react-is@{{version}}/umd/react-is.production.min.js'],
resources: [
'https://cdn.jsdelivr.net/npm/react-is@{{version}}/umd/react-is.production.min.js',
],
},
'styled-components': {
version: '5.3.5',
library: 'styled',
type: 'dependency',
resources: ['https://unpkg.com/styled-components@{{version}}/dist/styled-components.min.js'],
resources: [
'https://cdn.jsdelivr.net/npm/styled-components@{{version}}/dist/styled-components.min.js',
],
},
moment: {
version: '2.29.4',
library: 'moment',
type: 'dependency',
resources: ['https://unpkg.com/moment@{{version}}/moment.js'],
resources: ['https://cdn.jsdelivr.net/npm/moment@{{version}}/moment.js'],
},
'@music163/tango-boot': {
version: '0.2.1',
library: 'TangoBoot',
type: 'baseDependency',
resources: ['https://unpkg.com/@music163/tango-boot@{{version}}/dist/boot.js'],
resources: ['https://cdn.jsdelivr.net/npm/@music163/tango-boot@{{version}}/dist/boot.js'],
// resources: ['http://localhost:9001/boot.js'],
description: '云音乐低代码运行时框架',
},
@@ -56,14 +62,14 @@ const tangoConfigJson = {
library: 'TangoAntd',
type: 'baseDependency',
resources: [
'https://unpkg.com/@music163/antd@{{version}}/dist/index.js',
'https://unpkg.com/antd@4.24.13/dist/antd.css',
'https://cdn.jsdelivr.net/npm/@music163/antd@{{version}}/dist/index.js',
'https://cdn.jsdelivr.net/npm/antd@4.24.13/dist/antd.css',
],
description: '云音乐低代码中后台应用基础物料',
designerResources: [
'https://unpkg.com/@music163/antd@{{version}}/dist/designer.js',
'https://cdn.jsdelivr.net/npm/@music163/antd@{{version}}/dist/designer.js',
// 'http://localhost:9002/designer.js',
'https://unpkg.com/antd@4.24.13/dist/antd.css',
'https://cdn.jsdelivr.net/npm/antd@4.24.13/dist/antd.css',
],
},
},
+9 -9
View File
@@ -8,14 +8,14 @@
</head>
<body>
<div id="root"></div>
<script src="https://unpkg.com/react@16.14.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16.14.0/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/react-is@16.13.1/umd/react-is.production.min.js"></script>
<script src="https://unpkg.com/moment/min/moment-with-locales.js"></script>
<script src="https://unpkg.com/styled-components@5.3.11/dist/styled-components.js"></script>
<script src="https://unpkg.com/@babel/standalone@7.15.6/babel.js"></script>
<script src="https://unpkg.com/prettier@2.6.0/standalone.js"></script>
<script src="https://unpkg.com/prettier@2.6.0/parser-babel.js"></script>
<script src="https://unpkg.com/antd@4.24.13/dist/antd-with-locales.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/react@16.14.0/umd/react.development.js"></script>
<script src="https://cdn.jsdelivr.net/npm/react-dom@16.14.0/umd/react-dom.development.js"></script>
<script src="https://cdn.jsdelivr.net/npm/react-is@16.13.1/umd/react-is.production.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/moment/min/moment-with-locales.js"></script>
<script src="https://cdn.jsdelivr.net/npm/styled-components@5.3.11/dist/styled-components.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@babel/standalone@7.15.6/babel.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prettier@2.6.0/standalone.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prettier@2.6.0/parser-babel.js"></script>
<script src="https://cdn.jsdelivr.net/npm/antd@4.24.13/dist/antd-with-locales.min.js"></script>
</body>
</html>
+1
View File
@@ -34,6 +34,7 @@ const workspace = new Workspace({
// 2. 引擎初始化
const engine = createEngine({
workspace,
defaultActiveSidebarPanel: 'outline',
});
// @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>
);
}
+21 -24
View File
@@ -7,7 +7,7 @@
],
"scripts": {
"start": "yarn workspace playground start",
"start:story": "yarn workspace storybook storybook",
"start:docs": "yarn workspace storybook storybook",
"start:site": "yarn workspace website start",
"deploy:site": "yarn workspace website deploy",
"build": "lerna run build",
@@ -25,45 +25,42 @@
"prepare": "husky install"
},
"devDependencies": {
"@babel/preset-env": "^7.19.3",
"@babel/preset-react": "^7.18.6",
"@babel/preset-typescript": "^7.18.6",
"@babel/preset-env": "^7.23.2",
"@babel/preset-react": "^7.22.15",
"@babel/preset-typescript": "^7.23.2",
"@commitlint/cli": "^17.5.0",
"@commitlint/config-conventional": "^17.3.0",
"@types/jest": "^29.2.4",
"@types/lodash-es": "^4.17.8",
"@types/lodash.get": "^4.4.7",
"@types/lodash.isequal": "^4.5.6",
"@types/lodash.set": "^4.3.7",
"@types/react": "^18.2.7",
"@types/react-dom": "^18.2.4",
"@types/styled-components": "^5.1.25",
"@typescript-eslint/eslint-plugin": "^6.4.1",
"@typescript-eslint/parser": "^6.4.1",
"@types/jest": "^29.5.7",
"@types/lodash-es": "^4.17.10",
"@types/lodash.get": "^4.4.8",
"@types/lodash.isequal": "^4.5.7",
"@types/lodash.set": "^4.3.8",
"@types/react": "^18.2.34",
"@types/react-dom": "^18.2.14",
"@types/styled-components": "^5.1.29",
"@typescript-eslint/eslint-plugin": "^6.9.1",
"@typescript-eslint/parser": "^6.9.1",
"conventional-changelog-cli": "^2.2.2",
"copyfiles": "^2.4.1",
"eslint": "^8.40.0",
"eslint": "^8.52.0",
"eslint-config-ali": "^14.0.2",
"eslint-config-prettier": "^9.0.0",
"eslint-import-resolver-typescript": "^3.6.0",
"eslint-plugin-import": "^2.28.1",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-import": "^2.29.0",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"http-server": "^14.1.0",
"husky": "^8.0.3",
"jest": "^29.1.1",
"jest-environment-jsdom": "^29.1.1",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"lerna": "^6.6.1",
"lint-staged": "^14.0.1",
"prettier": "^2.8.7",
"react": "^17.0.0",
"react-dom": "^17.0.0",
"styled-components": "^5.3.6",
"typedoc": "^0.25.0",
"typescript": "^5.0.4"
},
"resolutions": {
"@yarnpkg/parsers": "3.0.0-rc.48.1"
"typedoc": "^0.25.3",
"typescript": "^5.2.2"
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
+10
View File
@@ -3,6 +3,16 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.2.5](https://github.com/netease/tango/compare/@music163/tango-context@0.2.4...@music163/tango-context@0.2.5) (2023-11-16)
**Note:** Version bump only for package @music163/tango-context
## [0.2.4](https://github.com/netease/tango/compare/@music163/tango-context@0.2.3...@music163/tango-context@0.2.4) (2023-11-02)
### Bug Fixes
- optional context config ([70fc6f8](https://github.com/netease/tango/commit/70fc6f8030202f5d340281c0424752044ed0fb83))
## [0.2.3](https://github.com/netease/tango/compare/@music163/tango-context@0.2.2...@music163/tango-context@0.2.3) (2023-10-23)
**Note:** Version bump only for package @music163/tango-context
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@music163/tango-context",
"version": "0.2.3",
"version": "0.2.5",
"description": "react context for tango-apps",
"keywords": [
"react",
@@ -31,8 +31,8 @@
"react": ">= 16.8"
},
"dependencies": {
"@music163/tango-core": "^0.2.2",
"@music163/tango-helpers": "^0.1.4",
"@music163/tango-core": "^0.2.4",
"@music163/tango-helpers": "^0.1.6",
"mobx-react-lite": "4.0.2"
},
"publishConfig": {
+3 -3
View File
@@ -16,9 +16,9 @@ export interface ITangoEngineContext {
/**
* 自定义配置数据
*/
config: {
customActionVariables: CustomVariableData[];
customExpressionVariables: CustomVariableData[];
config?: {
customActionVariables?: CustomVariableData[];
customExpressionVariables?: CustomVariableData[];
};
}
+20
View File
@@ -3,6 +3,26 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.2.4](https://github.com/netease/tango/compare/@music163/tango-core@0.2.3...@music163/tango-core@0.2.4) (2023-11-16)
### Bug Fixes
- allow override existing file via addFile ([6cf673e](https://github.com/netease/tango/commit/6cf673e7f6f330613e1d6331256a59900e8cbc68))
- export isJSXElementById from traverse ([#54](https://github.com/netease/tango/issues/54)) ([208e275](https://github.com/netease/tango/commit/208e275bb1cd98ed21d816c26179f9ebc11df223))
- parse variables from view ([089a482](https://github.com/netease/tango/commit/089a482f750e9d9a7743a6641d6e39989347d318))
- update types ([653386d](https://github.com/netease/tango/commit/653386dcc0b064b41915548129952eeabe53019f))
## [0.2.3](https://github.com/netease/tango/compare/@music163/tango-core@0.2.2...@music163/tango-core@0.2.3) (2023-11-02)
### Bug Fixes
- add onFilesChange to workspace ([#50](https://github.com/netease/tango/issues/50)) ([d775a50](https://github.com/netease/tango/commit/d775a5003ab1a14f801d3b38f7187ea15ef7d74d))
- **core:** create store without entry file ([ae806ae](https://github.com/netease/tango/commit/ae806aee3583ef5f957836e15d715035c4556867))
- **core:** logging drop action ([963bfe1](https://github.com/netease/tango/commit/963bfe1ab80fa4114637963534b2891a27538120))
- designer model add defaultActiveSidebarPanel ([#51](https://github.com/netease/tango/issues/51)) ([9dd11f7](https://github.com/netease/tango/commit/9dd11f7a6d5807c80a63c84c1ad0df5bc1ce9558))
- refactor workspace onFilesChange ([f53ce94](https://github.com/netease/tango/commit/f53ce94c6ed777d45ba6137188bfbc4566e03942))
- update types ([46d5f59](https://github.com/netease/tango/commit/46d5f59b4cd4fa71ed247f5e470e66c8c9f6d4a0))
## [0.2.2](https://github.com/netease/tango/compare/@music163/tango-core@0.2.1...@music163/tango-core@0.2.2) (2023-10-23)
### Bug Fixes
+8 -8
View File
@@ -1,6 +1,6 @@
{
"name": "@music163/tango-core",
"version": "0.2.2",
"version": "0.2.4",
"description": "tango core",
"author": "wwsun <ww.sun@outlook.com>",
"homepage": "",
@@ -24,13 +24,13 @@
"prepublishOnly": "yarn build"
},
"dependencies": {
"@babel/generator": "^7.22.15",
"@babel/parser": "^7.22.15",
"@babel/traverse": "^7.22.15",
"@babel/types": "^7.22.15",
"@music163/tango-helpers": "^0.1.4",
"@types/babel__generator": "^7.6.4",
"@types/babel__traverse": "^7.18.3",
"@babel/generator": "^7.23.0",
"@babel/parser": "^7.23.0",
"@babel/traverse": "^7.23.2",
"@babel/types": "^7.23.0",
"@music163/tango-helpers": "^0.1.6",
"@types/babel__generator": "^7.6.6",
"@types/babel__traverse": "^7.20.3",
"mobx": "6.9.0",
"path-browserify": "^1.0.1"
},
+6
View File
@@ -10,6 +10,10 @@ interface ICreateEngineOptions {
* 默认的模拟器模式
*/
defaultSimulatorMode?: SimulatorNameType;
/**
* 默认激活的侧边栏
*/
defaultActiveSidebarPanel?: string;
}
/**
@@ -20,12 +24,14 @@ interface ICreateEngineOptions {
export function createEngine({
workspace,
defaultSimulatorMode = 'desktop',
defaultActiveSidebarPanel = '',
}: ICreateEngineOptions) {
const engine = new Engine({
workspace,
designer: new Designer({
workspace,
simulator: defaultSimulatorMode,
activeSidebarPanel: defaultActiveSidebarPanel,
}),
});
+12
View File
@@ -20,3 +20,15 @@ const defineStoreHandlerName = 'defineStore';
export function isDefineStore(name: string) {
return defineStoreHandlerName === name;
}
/**
* 是否是 tango 的变量引用
* @example tango.stores.app.name
* @example tango.stores?.app?.name
*
* @param name
* @returns
*/
export function isTangoVariable(name: string) {
return /^tango\??\.(stores|services)\??\./.test(name) && name.split('.').length > 2;
}
+148 -12
View File
@@ -23,15 +23,15 @@ import {
object2node,
} from './parse';
import { isValidComponentName } from '../string';
import { isDefineService, isDefineStore } from '../assert';
import { isDefineService, isDefineStore, isTangoVariable } from '../assert';
import type {
IRouteData,
IStorePropertyData,
IClassPropertyNodeData,
ITangoViewNodeData,
IImportDeclarationPayload,
IServiceFunctionPayload,
InsertChildPositionType,
IImportSpecifierData,
} from '../../types';
import { IdGenerator } from '../id-generator';
@@ -194,7 +194,7 @@ export function queryXFormItemFields(ast: t.File | t.JSXElement) {
* @param node
* @param id
*/
function isJSXElementById(node: t.JSXElement, jsxElementNodeId: string) {
export function isJSXElementById(node: t.JSXElement, jsxElementNodeId: string) {
let isTargetJSXElement = false;
visitJSXElementAttributes(node, (name, value) => {
if (name === SLOT.dnd && value === jsxElementNodeId) {
@@ -448,6 +448,7 @@ function getLastExportDeclarationIndex(node: t.Program) {
/**
* 生成新的 ImportDeclaration
* @deprecated 使用 makeImportDeclaration2 代替
* @param importedModule
* @param importedSourcePath
* @returns
@@ -467,6 +468,27 @@ export function makeImportDeclaration(importedModule: IImportDeclarationPayload)
return t.importDeclaration(newSpecifierNodes, t.stringLiteral(importedModule.sourcePath));
}
export function makeImportDeclaration2(source: string, specifiers: IImportSpecifierData[]) {
const specifierNodes = specifiers.map((item) => {
switch (item.type) {
case 'ImportDefaultSpecifier':
return t.importDefaultSpecifier(t.identifier(item.localName));
case 'ImportSpecifier':
return t.importSpecifier(t.identifier(item.importedName), t.identifier(item.localName));
case 'ImportNamespaceSpecifier':
return t.importNamespaceSpecifier(t.identifier(item.localName));
default:
return;
}
});
return t.importDeclaration(specifierNodes, t.stringLiteral(source));
}
/**
* @deprecated 使用 parseImportDeclaration 代替
* @param node
* @returns
*/
function getImportDeclarationData(node: t.ImportDeclaration): IImportDeclarationPayload {
const sourcePath = node2value(node.source);
let defaultSpecifier;
@@ -487,6 +509,7 @@ function getImportDeclarationData(node: t.ImportDeclaration): IImportDeclaration
/**
* 添加新的导入语句
* @deprecated 使用 addImportDeclaration2 代替
* @param ast
* @param importedModule
* @param importedSourcePath
@@ -504,8 +527,24 @@ export function addImportDeclaration(ast: t.File, importedModule: IImportDeclara
return ast;
}
export function addImportDeclaration2(
ast: t.File,
source: string,
specifiers: IImportSpecifierData[],
) {
traverse(ast, {
Program(path) {
const lastIndex = getLastImportDeclarationIndex(path.node);
const newImportDeclaration = makeImportDeclaration2(source, specifiers);
path.node.body.splice(lastIndex, 0, newImportDeclaration);
path.stop();
},
});
}
/**
* 更新已有的导入语句
* @deprecated 使用 updateImportDeclaration2 代替
* @param ast
* @param importedModule
* @param importedSourcePath
@@ -525,6 +564,24 @@ export function updateImportDeclaration(ast: t.File, importedModule: IImportDecl
return ast;
}
export function updateImportDeclaration2(
ast: t.File,
source: string,
specifiers: IImportSpecifierData[],
) {
traverse(ast, {
ImportDeclaration(path) {
const currentSourcePath = node2value(path.node.source);
if (currentSourcePath === source) {
const newImportDeclaration = makeImportDeclaration2(source, specifiers);
path.replaceWith(newImportDeclaration);
path.stop(); // 只修改匹配到的第一条
}
},
});
return ast;
}
/**
* 新增类属性
* @param ast
@@ -986,7 +1043,12 @@ export function traverseServiceFile(ast: t.File) {
const baseConfig: any = {
encryptFetch: false,
};
const imports: Record<string, IImportSpecifierData[]> = {};
traverse(ast, {
ImportDeclaration(path) {
const { source, specifiers } = parseImportDeclaration(path.node);
imports[source] = specifiers;
},
CallExpression(path) {
const calleeName = keyNode2value(path.node.callee) as string;
if (isDefineService(calleeName)) {
@@ -1003,6 +1065,7 @@ export function traverseServiceFile(ast: t.File) {
},
});
return {
imports,
services,
baseConfig,
};
@@ -1107,6 +1170,7 @@ export function cloneJSXElementWithoutTrackingData(node: t.JSXElement) {
}
export function traverseViewFile(ast: t.File, idGenerator: IdGenerator) {
const imports: Record<string, IImportSpecifierData[]> = {};
const importedModules: Dict<IImportDeclarationPayload | IImportDeclarationPayload[]> = {};
const nodes: Array<ITangoViewNodeData<t.JSXElement>> = [];
const cloneAst = t.cloneNode(ast, true, true);
@@ -1115,12 +1179,38 @@ export function traverseViewFile(ast: t.File, idGenerator: IdGenerator) {
traverse(ast, {
ImportDeclaration(path) {
const declarationData = getImportDeclarationData(path.node);
const exist = importedModules[declarationData.sourcePath];
const { source, specifiers } = parseImportDeclaration(path.node);
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[declarationData.sourcePath] = declarationData;
importedModules[source] = declarationData;
} else {
importedModules[declarationData.sourcePath] = Array.isArray(exist)
importedModules[source] = Array.isArray(exist)
? exist.concat([declarationData])
: [exist, declarationData];
}
@@ -1128,11 +1218,14 @@ export function traverseViewFile(ast: t.File, idGenerator: IdGenerator) {
MemberExpression(path) {
const variable = node2value(path.node, false);
if (
/^tango\.(stores|services)\./.test(variable) &&
variable.split('.').length === 3 &&
!variables.includes(variable)
) {
if (isTangoVariable(variable) && !variables.includes(variable)) {
variables.push(variable);
}
},
OptionalMemberExpression(path) {
const variable = node2value(path.node, false);
if (isTangoVariable(variable) && !variables.includes(variable)) {
variables.push(variable);
}
},
@@ -1183,7 +1276,50 @@ export function traverseViewFile(ast: t.File, idGenerator: IdGenerator) {
ast,
cleanAst,
nodes,
imports,
importedModules,
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,
};
}
+5 -2
View File
@@ -1,5 +1,5 @@
import { Dict } from '@music163/tango-helpers';
import { IImportDeclarationPayload, IImportSpecifierData } from '../types';
import { IImportDeclarationPayload, IImportSpecifierSourceData } from '../types';
/**
* 导入列表解析为导入声明对象
@@ -7,7 +7,10 @@ import { IImportDeclarationPayload, IImportSpecifierData } from '../types';
* @param nameMap
* @returns
*/
export function namesToImportDeclarations(names: string[], nameMap: Dict<IImportSpecifierData>) {
export function namesToImportDeclarations(
names: string[],
nameMap: Dict<IImportSpecifierSourceData>,
) {
const map = {};
names.forEach((name) => {
const mod = nameMap[name];
-1
View File
@@ -33,7 +33,6 @@ export function inferFileType(filename: string): FileType {
return FileType.JsonViewModule;
}
// FIXME: 暂时写死,后续要支持多 services 文件解析
if (/\/services\/.+\.js$/.test(filename)) {
return FileType.ServiceModule;
}
+5 -1
View File
@@ -1,4 +1,8 @@
export * from './models';
export * from './factory';
export * from './helpers';
export * from './types';
export * from './types';
export { default as generator } from '@babel/generator';
export { default as traverse } from '@babel/types';
export * from '@babel/parser';
+36 -18
View File
@@ -3,22 +3,26 @@ import { IWorkspace } from './interfaces';
export type SimulatorNameType = 'desktop' | 'phone';
type SimulatorType = {
export type DesignerViewType = 'design' | 'code';
interface ISimulatorType {
name: SimulatorNameType;
width: number;
height: number;
};
}
type ViewportBoundingType = {
interface IViewportBounding {
width: number;
height: number;
};
}
export type DesignerViewType = 'design' | 'code';
interface IDesignerOptions {
workspace: IWorkspace;
simulator?: SimulatorNameType | ISimulatorType;
activeSidebarPanel?: string;
}
type DesignerOptionsType = { workspace: IWorkspace; simulator?: SimulatorNameType | SimulatorType };
const simulatorTypes: Record<string, SimulatorType> = {
const ISimulatorTypes: Record<string, ISimulatorType> = {
desktop: {
name: 'desktop',
width: 1366,
@@ -35,12 +39,12 @@ export class Designer {
/**
* 当前的沙箱模拟器类型
*/
_simulator: SimulatorType = simulatorTypes.desktop;
_simulator: ISimulatorType = ISimulatorTypes.desktop;
/**
* 当前的视图尺寸
*/
_viewport: ViewportBoundingType = {
_viewport: IViewportBounding = {
width: 1366,
height: 800,
};
@@ -70,9 +74,14 @@ export class Designer {
*/
_isPreview = false;
/**
* 默认展开的侧边栏
*/
defaultActiveSidebarPanel?: string;
private readonly workspace: IWorkspace;
get simulator(): SimulatorType {
get simulator(): ISimulatorType {
return toJS(this._simulator);
}
@@ -100,11 +109,21 @@ export class Designer {
return this._showRightPanel;
}
constructor(options: DesignerOptionsType) {
constructor(options: IDesignerOptions) {
this.workspace = options.workspace;
if (options.simulator) {
this.setSimulator(options.simulator);
const { simulator, activeSidebarPanel: defaultActiveSidebarPanel } = options;
// 默认设计器模式
if (simulator) {
this.setSimulator(simulator);
}
// 默认展开的侧边栏
if (defaultActiveSidebarPanel) {
this.setActiveSidebarPanel(defaultActiveSidebarPanel);
}
makeObservable(this, {
_simulator: observable,
_viewport: observable,
@@ -131,15 +150,15 @@ export class Designer {
});
}
setSimulator(value: SimulatorType | SimulatorNameType) {
setSimulator(value: ISimulatorType | SimulatorNameType) {
if (typeof value === 'string') {
this._simulator = simulatorTypes[value];
this._simulator = ISimulatorTypes[value];
} else {
this._simulator = value;
}
}
setViewport(value: ViewportBoundingType) {
setViewport(value: IViewportBounding) {
this._viewport = value;
}
@@ -154,7 +173,6 @@ export class Designer {
this._activeSidebarPanel = '';
}
}
closeSidebarPanel() {
this._activeSidebarPanel = '';
}
+53 -28
View File
@@ -10,7 +10,7 @@ import {
ITangoConfigPackages,
IPageConfigData,
IServiceFunctionPayload,
IImportSpecifierData,
IImportSpecifierSourceData,
} from '../types';
import { TangoFile, TangoJsonFile } from './file';
import { TangoRouteModule } from './route-module';
@@ -25,7 +25,7 @@ export interface IViewFile {
/**
* 通过导入组件名查找组件来自的包
*/
importMap?: Dict<IImportSpecifierData>;
importMap?: Dict<IImportSpecifierSourceData>;
listImportSources?: () => string[];
listModals?: () => Array<{ label: string; value: string }>;
@@ -148,8 +148,8 @@ export interface IWorkspace {
storeModules?: Record<string, TangoStoreModule>;
serviceModules?: Record<string, TangoServiceModule>;
refresh: (names: string[]) => void;
ready: () => void;
refresh: (names: string[]) => void;
setActiveRoute: (path: string) => void;
setActiveFile: (filename: string) => void;
@@ -157,23 +157,28 @@ export interface IWorkspace {
setComponentPrototypes: (prototypes: Record<string, ComponentPrototypeType>) => void;
getPrototype: (name: string | ComponentPrototypeType) => ComponentPrototypeType;
/**
* 查询节点
* @param id 节点 ID
* @param module 节点所在的模块名
* @returns 返回节点对象
*/
getNode: (id: string, module?: string) => IViewNode;
onFilesChange?: (filenames: string[]) => void;
getFile: (filename: string) => TangoFile;
listFiles: () => Record<string, string>;
addFile: (filename: string, code: string, fileType?: FileType) => void;
// ----------------- 文件操作 -----------------
addFiles: (files: IFileConfig[]) => void;
updateFile: (filename: string, code: string, shouldFormatCode?: boolean) => void;
addFile: (filename: string, code: string, fileType?: FileType) => void;
addServiceFile: (serviceName: string, code: string) => void;
addStoreFile: (storeName: string, code: string) => void;
addViewFile: (viewName: string, code: string) => void;
removeFile: (filename: string) => void;
renameFile: (oldFilename: string, newFilename: string) => void;
renameFolder: (oldFoldername: string, newFoldername: string) => void;
updateFile: (filename: string, code: string, shouldFormatCode?: boolean) => void;
listFiles: () => Record<string, string>;
getFile: (filename: string) => TangoFile;
// ----------------- 节点操作 -----------------
removeSelectedNode: () => void;
cloneSelectedNode: () => void;
copySelectedNode: () => void;
@@ -193,33 +198,47 @@ export interface IWorkspace {
relatedImports?: string[],
) => void;
getServiceFunction?: (serviceKey: string) => object;
listServiceFunctions?: () => Record<string, object>;
removeServiceFunction?: (serviceName: string, modName?: string) => void;
addServiceFunction?: (
/**
* 查询节点
* @param id 节点 ID
* @param module 节点所在的模块名
* @returns 返回节点对象
*/
getNode: (id: string, module?: string) => IViewNode;
// ----------------- 服务函数文件操作 -----------------
getServiceFunction: (serviceKey: string) => object;
listServiceFunctions: () => Record<string, object>;
removeServiceFunction: (serviceName: string, modName?: string) => void;
addServiceFunction: (
payload: IServiceFunctionPayload | IServiceFunctionPayload[],
modName?: string,
) => void;
updateServiceFunction?: (payload: IServiceFunctionPayload, modName?: string) => void;
updateServiceBaseConfig?: (IServiceFunctionPayload: object, modName?: string) => void;
updateServiceFunction: (payload: IServiceFunctionPayload, 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;
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;
copyViewPage: (sourceRoutePath: string, targetPageData: IPageConfigData) => void;
// ----------------- 路由文件操作 -----------------
updateRoute: (sourceRoutePath: string, targetPageData: IPageConfigData) => void;
listDependencies?: () => any;
// ----------------- 依赖包操作 -----------------
updateDependency?: (
addDependency: (data: any) => void;
listDependencies: () => any;
getDependency: (pkgName: string) => object;
updateDependency: (
name: string,
version: string,
options?: {
@@ -228,7 +247,7 @@ export interface IWorkspace {
},
) => void;
removeDependency?: (name: string) => void;
removeDependency: (name: string) => void;
addBizComp?: (
name: string,
@@ -241,6 +260,12 @@ export interface IWorkspace {
removeBizComp?: (name: string) => void;
// ----------------- 其他操作 -----------------
updateModuleCodeByVariablePath?: (variablePath: string, code: string) => void;
// ----------------- getter -----------------
get activeViewModule(): IViewFile;
get pages(): any[];
get bizComps(): string[];
+28 -3
View File
@@ -1,9 +1,16 @@
import * as t from '@babel/types';
import { action, computed, makeObservable, observable } from 'mobx';
import { isNil } from '@music163/tango-helpers';
import { code2ast, ast2code, formatCode } from '../helpers';
import {
code2ast,
ast2code,
formatCode,
traverseFile,
addImportDeclaration2,
updateImportDeclaration2,
} from '../helpers';
import { TangoFile } from './file';
import { IFileConfig } from '../types';
import { IFileConfig, IImportSpecifierData, ImportDeclarationDataType } from '../types';
import { IWorkspace } from './interfaces';
/**
@@ -14,6 +21,11 @@ import { IWorkspace } from './interfaces';
export class TangoModule extends TangoFile {
ast: t.File;
/**
* 导入语句
*/
imports: ImportDeclarationDataType;
constructor(workspace: IWorkspace, props: IFileConfig, isSyncCode = true) {
super(workspace, props, isSyncCode);
}
@@ -39,6 +51,16 @@ export class TangoModule extends TangoFile {
}
}
addImportDeclaration(source: string, specifiers: IImportSpecifierData[]) {
addImportDeclaration2(this.ast, source, specifiers);
return this;
}
updateImportDeclaration(source: string, specifiers: IImportSpecifierData[]) {
updateImportDeclaration2(this.ast, source, specifiers);
return this;
}
/**
* 基于最新的 ast 进行源码同步
*/
@@ -69,7 +91,10 @@ export class TangoModule extends TangoFile {
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() {
const { services, baseConfig } = traverseServiceFile(this.ast);
const { imports, services, baseConfig } = traverseServiceFile(this.ast);
this.imports = imports;
this._serviceFunctions = services;
this._baseConfig = baseConfig;
if (baseConfig.namespace) {
+21 -22
View File
@@ -24,7 +24,8 @@ import {
ITangoViewNodeData,
IImportDeclarationPayload,
InsertChildPositionType,
IImportSpecifierData,
IImportSpecifierSourceData,
ImportDeclarationDataType,
} from '../types';
import { IViewFile, IWorkspace } from './interfaces';
import { TangoModule } from './module';
@@ -38,7 +39,7 @@ export class TangoViewModule extends TangoModule implements IViewFile {
/**
* 通过导入组件名查找组件来自的包
*/
importMap: Dict<IImportSpecifierData>;
importMap: Dict<IImportSpecifierSourceData>;
/**
* 视图中依赖的 tango 变量,仅 stores 和 services
@@ -51,6 +52,7 @@ export class TangoViewModule extends TangoModule implements IViewFile {
private _nodes: Map<string, TangoNode>;
/**
* 导入的模块
* @deprecated
*/
private _importedModules: Dict<IImportDeclarationPayload | IImportDeclarationPayload[]>;
@@ -98,6 +100,7 @@ export class TangoViewModule extends TangoModule implements IViewFile {
ast: newAst,
cleanAst,
nodes,
imports,
importedModules,
variables,
} = traverseViewFile(this.ast, this._idGenerator);
@@ -107,7 +110,8 @@ export class TangoViewModule extends TangoModule implements IViewFile {
this._cleanCode = ast2code(cleanAst);
this._importedModules = importedModules;
this.importMap = this.buildImportMap(importedModules);
this.imports = imports;
this.importMap = this.buildImportMap(imports);
this.variables = variables;
this._nodes.clear();
@@ -127,7 +131,7 @@ export class TangoViewModule extends TangoModule implements IViewFile {
* 依赖列表
*/
listImportSources() {
return Object.keys(this._importedModules);
return Object.keys(this.imports);
}
/**
@@ -169,6 +173,8 @@ export class TangoViewModule extends TangoModule implements IViewFile {
/**
* 基于组件的 prototype 信息更新导入信息
* TODO: 和 Module.addImportDeclaration 中的保持一致
* @deprecated
* @param prototype
* @param shouldUpdateCode
*/
@@ -183,6 +189,8 @@ export class TangoViewModule extends TangoModule implements IViewFile {
/**
* 更新导入的变量(新版)
* TODO: 和 Module.updateImportDeclaration 中的保持一致
* @deprecated
*/
updateImportSpecifiers(importDeclaration: IImportDeclarationPayload) {
const mods = this._importedModules[importDeclaration.sourcePath];
@@ -338,24 +346,15 @@ export class TangoViewModule extends TangoModule implements IViewFile {
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 };
});
}
private 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;
+130 -87
View File
@@ -55,6 +55,10 @@ export interface IWorkspaceOptions {
* 组件描述列表
*/
prototypes?: Record<string, ComponentPrototypeType>;
/**
* 工作区文件变更事件
*/
onFilesChange?: (filenames: string[]) => void;
}
/**
@@ -125,6 +129,11 @@ export class Workspace extends EventTarget implements IWorkspace {
*/
appJson: TangoJsonFile;
/**
* 代码变更回调
*/
onFilesChange?: (filenames: string[]) => void;
/**
* 绑定事件
* TODO: 是否需要自己来管理 listeners,并及时进行 gc
@@ -220,6 +229,7 @@ export class Workspace extends EventTarget implements IWorkspace {
this.activeViewFile = '';
this.files = new Map();
this.isReady = false;
this.onFilesChange = options?.onFilesChange;
if (options?.files) {
this.addFiles(options.files);
@@ -302,7 +312,7 @@ export class Workspace extends EventTarget implements IWorkspace {
}
/**
* 添加一组文件到工作区
* 添加一组文件到工作区,如果文件同名,后面的文件会覆盖前面的文件
* @param files
*/
addFiles(files: IFileConfig[] = []) {
@@ -318,81 +328,99 @@ export class Workspace extends EventTarget implements IWorkspace {
* @param fileType 模块类型
*/
addFile(filename: string, code: string, fileType?: FileType) {
if (!this.files.has(filename)) {
const moduleType = fileType || inferFileType(filename);
const props = {
filename,
code,
type: moduleType,
};
const moduleType = fileType || inferFileType(filename);
const props = {
filename,
code,
type: moduleType,
};
let module;
switch (moduleType) {
case FileType.StoreEntryModule:
module = new TangoStoreEntryModule(this, props);
this.storeEntryModule = module;
break;
case FileType.RouteModule: {
module = new TangoRouteModule(this, props);
this.routeModule = module;
// check if activeRoute exists
const route = module.routes.find((item) => item.path === this.activeRoute);
if (!route) {
this.setActiveRoute(module.routes[0]?.path);
}
break;
let module;
switch (moduleType) {
case FileType.StoreEntryModule:
module = new TangoStoreEntryModule(this, props);
this.storeEntryModule = module;
break;
case FileType.RouteModule: {
module = new TangoRouteModule(this, props);
this.routeModule = module;
// check if activeRoute exists
const route = module.routes.find((item) => item.path === this.activeRoute);
if (!route) {
this.setActiveRoute(module.routes[0]?.path);
}
case FileType.JsxViewModule:
module = new TangoViewModule(this, props);
break;
case FileType.ServiceModule:
module = new TangoServiceModule(this, props);
this.serviceModules[module.name] = module;
break;
case FileType.StoreModule:
module = new TangoStoreModule(this, props);
this.storeModules[module.name] = module;
break;
case FileType.BlockEntryModule: {
const blockName = getBlockNameByFilename(props.filename);
const prototype: ComponentPrototypeType = {
name: blockName,
exportType: 'defaultExport',
package: props.filename,
type: 'block',
};
this.localBlocks[blockName] = props.filename;
this.componentPrototypes.set(blockName, prototype);
module = new TangoViewModule(this, props);
break;
}
case FileType.Module:
module = new TangoJsModule(this, props);
break;
case FileType.Less:
module = new TangoLessFile(this, props);
break;
case FileType.PackageJson:
module = new TangoJsonFile(this, props);
this.packageJson = module;
break;
case FileType.TangoConfigJson:
module = new TangoJsonFile(this, props);
this.tangoConfigJson = module;
break;
case FileType.AppJson:
module = new TangoJsonFile(this, props);
this.appJson = module;
break;
case FileType.Json:
module = new TangoJsonFile(this, props);
break;
default:
module = new TangoFile(this, props);
break;
}
this.files.set(filename, module);
case FileType.JsxViewModule:
module = new TangoViewModule(this, props);
break;
case FileType.ServiceModule:
module = new TangoServiceModule(this, props);
this.serviceModules[module.name] = module;
break;
case FileType.StoreModule:
module = new TangoStoreModule(this, props);
this.storeModules[module.name] = module;
break;
case FileType.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) {
@@ -594,13 +622,10 @@ export class Workspace extends EventTarget implements IWorkspace {
/**
* 添加新的模型文件
* @param name 模型名
* @param code 模型代码
* @deprecated 使用 addStoreFile 代替
*/
addStoreModule(name: string, code: string) {
const filename = getFilepath(name, '/src/stores', '.js');
this.addFile(filename, code);
this.storeEntryModule?.addStore(name).update();
this.addStoreFile(name, code);
}
/**
@@ -717,14 +742,29 @@ export class Workspace extends EventTarget implements IWorkspace {
this.serviceModules[moduleName]?.updateBaseConfig(config).update();
}
addDependency(data: any) {
// TODO: implement it to replace addBizComp & addServiceComp
}
/**
* 获取 package.json 中的依赖列表
* @returns
* TODO: fix this logic to merge dependencies from package.json and tango.config.json
*/
listDependencies() {
return this.packageJson?.getValue('dependencies');
}
getDependency(pkgName: string) {
const packages = this.tangoConfigJson?.getValue('packages');
const dependencies = this.packageJson?.getValue('dependencies'); // 兼容老版本
const detail = {
version: dependencies?.[pkgName],
...(packages?.[pkgName] || {}),
};
return detail;
}
/**
* 更新依赖,没有就添加
* @param name
@@ -1094,13 +1134,6 @@ export class Workspace extends EventTarget implements IWorkspace {
}
this.selectSource.clear();
this.history.push({
message: HistoryMessage.DropNode,
data: {
[targetFile.filename]: targetFile.code,
},
});
}
targetFile.update();
@@ -1109,6 +1142,15 @@ export class Workspace extends EventTarget implements IWorkspace {
}
dragSource.clear();
if (isValidOperation) {
this.history.push({
message: HistoryMessage.DropNode,
data: {
[targetFile.filename]: targetFile.code,
},
});
}
}
/**
@@ -1124,6 +1166,7 @@ export class Workspace extends EventTarget implements IWorkspace {
},
}),
);
this.onFilesChange?.(filenames);
}
/**
@@ -1185,11 +1228,11 @@ export class Workspace extends EventTarget implements IWorkspace {
if (this.files.has(sourceFilePath)) {
// 来源是文件
const file = this.files.get(sourceFilePath);
this.addFile(targetFilePath + '.js', file.cleanCode, file.type);
} else if (this.files.has(sourceFilePath + '/index.js')) {
this.addFile(`${targetFilePath}.js`, file.cleanCode, file.type);
} else if (this.files.has(`${sourceFilePath}/index.js`)) {
// 来源是目录
Array.from(this.files.keys()).forEach((key) => {
if (key.startsWith(sourceFilePath + '/')) {
if (key.startsWith(`${sourceFilePath}/`)) {
const sourceFile = this.getFile(key);
this.addFile(
targetFilePath + key.slice(sourceFilePath.length),
+27 -8
View File
@@ -73,15 +73,40 @@ export interface ITangoViewNodeData<T = JSXElement> {
}
/**
* 导入变量的解析数据
* 导入变量的来源
*/
export interface IImportSpecifierData {
export interface IImportSpecifierSourceData {
source: string;
isDefault?: boolean;
}
/**
* 解析的导入变量数据
*/
export interface IImportSpecifierData {
/**
* 导入名
*/
importedName?: string;
/**
* 本地名
*/
localName: string;
/**
* 类型
*/
type: 'ImportDefaultSpecifier' | 'ImportSpecifier' | 'ImportNamespaceSpecifier';
}
/**
* 解析的导入语句数据
* FIXME: 需要考虑到有两行重复的语句的情况
*/
export type ImportDeclarationDataType = Record<string, IImportSpecifierData[]>;
/**
* 模块导入的参数类型
* @deprecated 使用 IImportSpecifierData 代替
*/
export interface IImportDeclarationPayload {
defaultSpecifier?: string;
@@ -89,12 +114,6 @@ export interface IImportDeclarationPayload {
sourcePath: string;
}
export interface IClassPropertyNodeData {
reference: string;
propertyName: string;
propertyBody: string;
}
/**
* 服务函数参数类型
*/
+3 -4
View File
@@ -126,7 +126,6 @@ describe('helpers', () => {
});
describe('string helpers', () => {
it('value2code: empty array', () => {
expect(value2code([])).toEqual('[]');
});
@@ -151,9 +150,9 @@ describe('string helpers', () => {
it('namesToImportDeclarations', () => {
expect(
namesToImportDeclarations(['Button', 'Box', 'React'], {
Button: { package: '@music163/antd' },
Box: { package: '@music163/antd' },
React: { package: 'react', isDefault: true },
Button: { source: '@music163/antd' },
Box: { source: '@music163/antd' },
React: { source: 'react', isDefault: true },
}),
).toEqual([
{ sourcePath: '@music163/antd', specifiers: ['Button', 'Box'] },
+15
View File
@@ -3,6 +3,21 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.5.3](https://github.com/netease/tango/compare/@music163/tango-designer@0.5.2...@music163/tango-designer@0.5.3) (2023-11-16)
### Bug Fixes
- allow override existing file via addFile ([6cf673e](https://github.com/netease/tango/commit/6cf673e7f6f330613e1d6331256a59900e8cbc68))
## [0.5.2](https://github.com/netease/tango/compare/@music163/tango-designer@0.5.1...@music163/tango-designer@0.5.2) (2023-11-02)
### Bug Fixes
- limit outline tree node icon ([c5840f2](https://github.com/netease/tango/commit/c5840f29b724966930e239fe00ee13b2fb882503))
- outline-panel tree expand bug & add props ([#52](https://github.com/netease/tango/issues/52)) ([b29e235](https://github.com/netease/tango/commit/b29e23599befbd60845053544648b10976a65824))
- resizable-box support horizontal and vertical dragging ([#48](https://github.com/netease/tango/issues/48)) ([fd2fa94](https://github.com/netease/tango/commit/fd2fa943b05e9f1fa57574e703307725c5aed5d8))
- update style of components panel ([11f166a](https://github.com/netease/tango/commit/11f166a2a67313200922a514a8219f34cfafd8ab))
## [0.5.1](https://github.com/netease/tango/compare/@music163/tango-designer@0.5.0...@music163/tango-designer@0.5.1) (2023-10-23)
### Bug Fixes
+11 -11
View File
@@ -1,6 +1,6 @@
{
"name": "@music163/tango-designer",
"version": "0.5.1",
"version": "0.5.3",
"description": "lowcode designer",
"keywords": [
"react"
@@ -32,13 +32,13 @@
},
"dependencies": {
"@ant-design/icons": "^4.8.0",
"@music163/request": "^0.1.0",
"@music163/tango-context": "^0.2.3",
"@music163/tango-core": "^0.2.2",
"@music163/tango-helpers": "^0.1.4",
"@music163/tango-sandbox": "^0.1.7",
"@music163/tango-setting-form": "^0.2.6",
"@music163/tango-ui": "^0.1.7",
"@music163/request": "^0.1.2",
"@music163/tango-context": "^0.2.5",
"@music163/tango-core": "^0.2.4",
"@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",
"antd": "^4.24.2",
"cash-dom": "^8.1.2",
"classnames": "^2.3.2",
@@ -53,9 +53,9 @@
"semver": "^7.3.8"
},
"devDependencies": {
"@types/color": "^3.0.4",
"@types/react-color": "^3.0.6",
"@types/react-resizable": "^3.0.4"
"@types/color": "^3.0.5",
"@types/react-color": "^3.0.9",
"@types/react-resizable": "^3.0.6"
},
"publishConfig": {
"access": "public",
@@ -82,6 +82,7 @@ export function ExpressionSetter(props: ExpressionSetterProps) {
onChange(undefined);
return;
}
if (getVariableContent(code) === value2expressionCode(valueProp)) {
return;
}
@@ -189,15 +190,12 @@ export function ExpressionModal({
<Modal
closable={false}
destroyOnClose
width="70%"
width="60%"
open={visible}
onCancel={onCancel}
onOk={() => {
onOk(exp);
}}
okButtonProps={{
disabled: !!error,
}}
bodyStyle={{
padding: 0,
}}
@@ -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(
({
menuData = emptyMenuData,
@@ -101,8 +93,6 @@ export const ComponentsPanel = observer(
[workspace.bizComps, workspace.blocks, getBizCompName],
);
if (loading) return <StyledSpin tip="正在加载物料列表..." spinning />;
const tabs = Object.keys(menuData).map((key) => ({
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">
<Search placeholder="搜索物料" onChange={setKeyword} />
</Box>
{!keyword ? contentNode : <MaterialList data={allList} filterKeyword={keyword} />}
<Spin spinning={loading} tip="正在加载物料列表...">
{!keyword ? contentNode : <MaterialList data={allList} filterKeyword={keyword} />}
</Spin>
</Box>
);
},
@@ -275,7 +275,7 @@ function ServiceDetailForm({
{...formProps}
>
<Form.Item
label="所属模块"
label="命名空间"
name="moduleName"
rules={[
{
+2 -1
View File
@@ -4,4 +4,5 @@ export * from './components-panel';
export * from './dependency-panel';
export * from './history-panel';
export * from './variable-panel';
export * from './sidebar';
export * from './sidebar';
export * from './resizable-box';
@@ -1,14 +1,34 @@
import React, { useEffect, useMemo, useState } from 'react';
import { Space, Tree } from 'antd';
import { Dropdown, Space, Tree } from 'antd';
import { Box, css } from 'coral-system';
import { IconFont } from '@music163/tango-ui';
import { EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons';
import { EyeOutlined, EyeInvisibleOutlined, EllipsisOutlined } from '@ant-design/icons';
import { observer, useWorkspace } from '@music163/tango-context';
import { DropMethod, ITangoViewNodeData } from '@music163/tango-core';
import { parseDndId } from '@music163/tango-helpers';
import { noop, parseDndId } from '@music163/tango-helpers';
import { useSandboxQuery } from '../../context';
import { buildQueryBySlotId } from '../../helpers';
export interface ComponentsTreeProps {
/**
* 显示/隐藏组件按钮,默认显示
*/
showToggleVisibleIcon?: boolean;
/**
* 页面节点选中回调
*/
onSelect?: (nodeKey: string) => void;
/**
* 节点树更多功能菜单
*/
actionItems?: ActionItem[];
}
interface ActionItem {
item: React.ReactNode;
key: string;
}
const outlineStyle = css`
.ant-tree-title {
color: rgb(85, 85, 85);
@@ -32,6 +52,10 @@ const outlineStyle = css`
.ant-tree-switcher-noop {
display: none;
}
.material-icon {
width: 1em;
height: 1em;
}
`;
const filedNames = {
@@ -51,126 +75,159 @@ const getNodeKeys = (data: ITangoViewNodeData[]) => {
return ids;
};
const OutlineTreeNode: React.FC<any> = observer(({ node }: { node: ITangoViewNodeData }) => {
const workspace = useWorkspace();
const sandboxQuery = useSandboxQuery();
const [visible, setVisible] = useState(true);
const nodeLabel = useMemo(() => {
const { component, index } = parseDndId(node.id);
return `${component}:${index}`;
}, [node.id]);
const componentPrototype = workspace.componentPrototypes.get(node.component);
const icon = componentPrototype?.icon || 'icon-placeholder';
const OutlineTreeNode: React.FC<{ node: ITangoViewNodeData } & ComponentsTreeProps> = observer(
({ node, showToggleVisibleIcon, actionItems }) => {
const workspace = useWorkspace();
const sandboxQuery = useSandboxQuery();
const [visible, setVisible] = useState(true);
const nodeLabel = useMemo(() => {
const { component, index } = parseDndId(node.id);
return `${component}:${index}`;
}, [node.id]);
const componentPrototype = workspace.componentPrototypes.get(node.component);
const icon = componentPrototype?.icon || 'icon-placeholder';
const iconRender = icon.startsWith('icon-') ? (
<IconFont className="material-icon" type={icon} />
) : (
<img src={icon} alt={componentPrototype.name} />
);
const iconRender = icon.startsWith('icon-') ? (
<IconFont className="material-icon" type={icon} />
) : (
<img className="material-icon" src={icon} alt={componentPrototype.name} />
);
const toggleVisible = (nodeId: string) => {
sandboxQuery.getElement(buildQueryBySlotId(nodeId)).style.visibility = visible
? 'hidden'
: 'visible';
setVisible(!visible);
};
const toggleVisible = (nodeId: string) => {
sandboxQuery.getElement(buildQueryBySlotId(nodeId)).style.visibility = visible
? 'hidden'
: 'visible';
setVisible(!visible);
};
const VisibleIcon = visible ? EyeOutlined : EyeInvisibleOutlined;
return (
<Space>
{iconRender} {nodeLabel}
{/* 切换显示 */}
<VisibleIcon
className="material-icon"
onClick={(e) => {
e.stopPropagation();
toggleVisible(node.id);
}}
/>
</Space>
);
});
export const ComponentsTree = observer(() => {
const workspace = useWorkspace();
const sandboxQuery = useSandboxQuery();
const [selectedKeys, setSelectedKeys] = useState<React.Key[]>(
workspace.selectSource.selected.map((item) => item.id),
);
const file = workspace.activeViewModule;
const nodesTree = file.nodesTree as ITangoViewNodeData[];
useEffect(() => {
setSelectedKeys(workspace.selectSource.selected.map((item) => item.id));
}, [workspace.selectSource.selected]);
if (!nodesTree.length) {
const VisibleIcon = visible ? EyeOutlined : EyeInvisibleOutlined;
return (
<Box>
<code>{workspace.activeViewFile}</code>
<Space>
{iconRender} {nodeLabel}
{/* 切换显示 */}
{showToggleVisibleIcon && (
<VisibleIcon
className="material-icon"
onClick={(e) => {
e.stopPropagation();
toggleVisible(node.id);
}}
/>
)}
{/* 更多 ... 功能菜单 */}
{actionItems?.length > 0 && (
<Dropdown
menu={{
items: actionItems.map((_) => ({
key: _.key,
label: _.item,
})),
}}
trigger={['click']}
>
<EllipsisOutlined />
</Dropdown>
)}
</Space>
);
},
);
export const ComponentsTree: React.FC<ComponentsTreeProps> = observer(
({ showToggleVisibleIcon = true, onSelect = noop, actionItems }) => {
const workspace = useWorkspace();
const sandboxQuery = useSandboxQuery();
const [selectedKeys, setSelectedKeys] = useState<React.Key[]>(
workspace.selectSource.selected.map((item) => item.id),
);
const file = workspace.activeViewModule;
const nodesTree = (file?.nodesTree ?? []) as ITangoViewNodeData[];
const [expandedKeys, setExpandedKeys] = useState(getNodeKeys(nodesTree));
useEffect(() => {
setSelectedKeys(workspace.selectSource.selected.map((item) => item.id));
}, [workspace.selectSource.selected]);
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>
);
}
return (
<Box css={outlineStyle}>
<Tree
selectedKeys={selectedKeys}
fieldNames={filedNames}
treeData={nodesTree as any[]}
onSelect={(keys) => {
const data = sandboxQuery.getDraggableParentsData(
buildQueryBySlotId(keys[0] as string),
true,
);
if (data && data.id) {
workspace.selectSource.select({
id: data.id,
name: data.name,
bounding: data.bounding,
parents: data.parents,
});
}
setSelectedKeys(keys);
}}
blockNode
autoExpandParent
expandedKeys={getNodeKeys(nodesTree)}
draggable
onDragStart={(data) => {
const prototype = workspace.componentPrototypes.get(data.node.component);
if (!prototype) {
return;
}
const { canDrag } = prototype.rules || {};
if (canDrag && !canDrag()) {
return;
}
workspace.dragSource.set({
id: data.node.id,
name: data.node.component,
});
}}
onDrop={(data) => {
const dropKey = data.node.key as string;
let method;
if (data.dropToGap) {
// 插入节点的后面
method = DropMethod.InsertAfter;
} else {
// 作为第一个子节点
method = DropMethod.InsertFirstChild;
}
workspace.dragSource.dropTarget.set(
{
id: dropKey,
},
method,
);
workspace.dropNode();
}}
titleRender={(node) => <OutlineTreeNode node={node} />}
/>
</Box>
);
});
},
);
@@ -2,20 +2,26 @@ import React from 'react';
import { Box } from 'coral-system';
import { CollapsePanel } from '@music163/tango-ui';
import { StateTree } from './state-tree';
import { ComponentsTree } from './components-tree';
import { ComponentsTree, ComponentsTreeProps } from './components-tree';
export interface OutlineViewProps {
export interface OutlineViewProps extends ComponentsTreeProps {
/**
* 展示状态视图
*/
showStateView?: boolean;
}
export function OutlinePanel({ showStateView = true }: OutlineViewProps) {
export function OutlinePanel({ showStateView = true, ...treeProps }: OutlineViewProps) {
return (
<Box height="100%" position="relative">
<CollapsePanel title="页面结构" stickyHeader maxHeight="90%" overflowY="auto">
<ComponentsTree />
<CollapsePanel
title="页面结构"
maxHeight={showStateView ? '90%' : '100%'}
collapsed={!showStateView ? false : undefined}
overflowY="auto"
showBottomBorder={showStateView}
>
<ComponentsTree {...treeProps} />
</CollapsePanel>
{showStateView && (
<CollapsePanel title="页面状态" stickyHeader maxHeight="90%" overflowY="auto">
+74 -18
View File
@@ -1,45 +1,101 @@
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import cx from 'classnames';
import { Box, HTMLCoralProps, css } from 'coral-system';
import { Resizable } from 'react-resizable';
import { Resizable, ResizeHandle } from 'react-resizable';
const resizeHandleStyle = css`
export interface ResizableBoxProps {
width?: number;
height?: number;
resizeHandlePosition?: 'left' | 'right' | 'top' | 'bottom';
axis?: 'x' | 'y';
children?: React.ReactNode;
className?: string;
style?: React.CSSProperties;
}
const resizeHandleStyle = (axis: ResizableBoxProps['axis'], barStyle: HTMLCoralProps<'div'>) => css`
position: absolute;
top: 0;
z-index: 999;
width: 4px;
height: 100%;
cursor: col-resize;
cursor: ${axis === 'x' ? 'col-resize' : 'row-resize'};
${barStyle &&
Object.keys(barStyle)
.map((key) => `${key}: ${barStyle[key]};`)
.join('')}
&:hover,
&:active {
background-color: var(--tango-colors-brand);
}
`;
export interface ResizableBoxProps extends HTMLCoralProps<'div'> {
width?: number;
height?: number;
resizeHandlePosition?: 'left' | 'right';
}
const getResizeHandles = (
resizeHandlePosition: ResizableBoxProps['resizeHandlePosition'],
axis: ResizableBoxProps['axis'],
) => {
const handles: ResizeHandle[] = [];
switch (axis) {
case 'x':
handles.push(resizeHandlePosition === 'right' ? 'e' : 'w');
break;
case 'y':
handles.push(resizeHandlePosition === 'bottom' ? 's' : 'n');
break;
default:
break;
}
return handles;
};
export function ResizableBox({
resizeHandlePosition = 'right',
width: widthProp,
height,
height: heightProp,
children,
className,
style,
axis = 'x',
}: ResizableBoxProps) {
const [width, setWidth] = useState(widthProp);
const barStyle = resizeHandlePosition === 'right' ? { right: '-4px' } : { left: '-4px' };
const [height, setHeight] = useState(heightProp);
const barStyle: HTMLCoralProps<'div'> = {
...(axis === 'x'
? {
height: '100%',
width: '4px',
top: '0',
[resizeHandlePosition === 'right' ? 'right' : 'left']: '-4px',
}
: {
height: '4px',
width: '100%',
left: '0',
[resizeHandlePosition === 'bottom' ? 'bottom' : 'top']: '0',
}),
};
useEffect(() => {
setWidth(widthProp);
}, [widthProp]);
useEffect(() => {
setHeight(heightProp);
}, [heightProp]);
return (
<Resizable
axis="x"
axis={axis}
width={width}
resizeHandles={getResizeHandles(resizeHandlePosition, axis)}
height={height}
minConstraints={[150, 150]}
maxConstraints={[window.innerWidth * 0.6, window.innerHeight * 0.8]}
onResize={(e, { size }) => {
setWidth(size.width);
if (axis === 'x') {
setWidth(size.width);
}
if (axis === 'y') {
setHeight(size.height);
}
}}
onResizeStart={() => {
document.body.style.pointerEvents = 'none';
@@ -49,7 +105,7 @@ export function ResizableBox({
document.body.style.pointerEvents = 'auto';
document.body.style.userSelect = 'auto';
}}
handle={<Box className="ResizeHandle" css={resizeHandleStyle} {...barStyle} />}
handle={<Box className="ResizeHandle" css={resizeHandleStyle(axis, barStyle)} />}
>
<div
className={cx('ResizableBox', className)}
+36 -26
View File
@@ -75,8 +75,10 @@ export interface SidebarPanelItemProps
}
function BaseSidebarPanel({ panelWidth: defaultPanelWidth = 280, footer, children }: SidebarProps) {
const designer = useDesigner();
const items = useMemo(() => {
const ret: Record<React.Key, SidebarPanelItemProps> = {};
const ret: Record<string, SidebarPanelItemProps> = {};
React.Children.forEach(children, (child) => {
if (child && React.isValidElement(child)) {
const widget = getWidget(['sidebar', child.key].join('.'));
@@ -90,20 +92,6 @@ function BaseSidebarPanel({ panelWidth: defaultPanelWidth = 280, footer, childre
return ret;
}, [children]);
const designer = useDesigner();
const panel = items[designer.activeSidebarPanel];
const floatPanelStyle: any = panel?.isFloat
? {
position: 'absolute',
left: '50px',
top: 0,
zIndex: 1000,
height: '100%',
boxShadow: 'var(--tango-shadows-lowRight)',
}
: {};
const panelWidth = typeof panel?.width === 'number' ? panel?.width : defaultPanelWidth;
return (
<Box display="flex" flexShrink={0} css={sidebarStyle} className="SidebarPanel">
<Box className="SidebarPanelBar" display="flex" flexDirection="column" position="relative">
@@ -142,19 +130,41 @@ function BaseSidebarPanel({ panelWidth: defaultPanelWidth = 280, footer, childre
{footer}
</Box>
</Box>
{panel ? (
<ResizableBox key={panel.label} width={panelWidth} style={floatPanelStyle}>
<SidebarPanelExpandedContent
isFloat={panel.isFloat}
closeable={panel.isFloat}
onClose={() => {
designer.closeSidebarPanel();
{Object.keys(items).map((key) => {
const panel = items[key];
const floatPanelStyle: any = panel?.isFloat
? {
position: 'absolute',
left: '50px',
top: 0,
zIndex: 1000,
height: '100%',
boxShadow: 'var(--tango-shadows-lowRight)',
}
: {};
const panelWidth = typeof panel?.width === 'number' ? panel?.width : defaultPanelWidth;
return (
<ResizableBox
key={panel.label}
width={panelWidth}
style={{
display: designer.activeSidebarPanel === key ? 'block' : 'none',
...floatPanelStyle,
}}
>
{panel.children}
</SidebarPanelExpandedContent>
</ResizableBox>
) : null}
<SidebarPanelExpandedContent
isFloat={panel.isFloat}
closeable={panel.isFloat}
onClose={() => {
designer.closeSidebarPanel();
}}
>
{panel.children}
</SidebarPanelExpandedContent>
</ResizableBox>
);
})}
</Box>
);
}
@@ -53,7 +53,7 @@ export const VariablePanel = observer(
existNames={storeNames}
onCancel={off}
onSubmit={(values) => {
workspace.addStoreModule(values.name, newStoreTemplate);
workspace.addStoreFile(values.name, newStoreTemplate);
off();
}}
/>
+13 -3
View File
@@ -1,9 +1,11 @@
import React from 'react';
import { Group } from 'coral-system';
import { Modal } from 'antd';
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(() => {
const workspace = useWorkspace();
const designer = useDesigner();
return (
<Group attached>
@@ -13,7 +15,7 @@ export const ModeSwitchTool = observer(() => {
onClick={() => {
designer.setActiveView('design');
}}
tip="设计视图"
tooltip="设计视图"
>
</ToggleButton>
@@ -23,8 +25,16 @@ export const ModeSwitchTool = observer(() => {
onClick={() => {
designer.setActiveView('code'); // 切换到源码视图
designer.setActiveSidebarPanel(''); // 关闭左侧面板
if (workspace.activeFile !== workspace.activeViewFile) {
Modal.confirm({
title: '当前打开的文件与视图不匹配,是否切换到当前视图对应的文件?',
onOk: () => {
workspace.setActiveFile(workspace.activeViewFile);
},
});
}
}}
tip="源码视图"
tooltip="源码视图"
>
</ToggleButton>
+11
View File
@@ -3,6 +3,17 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [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)
### Bug Fixes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@music163/tango-helpers",
"version": "0.1.4",
"version": "0.1.6",
"description": "Shared types, helpers, and hooks of tango-apps",
"keywords": [
"shared",
+14 -5
View File
@@ -238,17 +238,26 @@ export function getVariableContent(str: string) {
return str;
}
// 提供给代码执行环境的全局变量
const patchCode = `
var tango = {
stores: {},
services: {},
config: {},
refs: {},
};
`;
/**
* 将代码放到函数体中进行执行
* @param code
* @returns 函数执行的结果
*/
export function executeCodeAsFunctionBody(code: string) {
export function runCode(code: string) {
let ret;
try {
// eslint-disable-next-line no-new-func
ret = new Function(code);
ret = ret();
ret = new Function(`${patchCode}\n return ${code}`)();
} catch (err) {
// ignore error
}
@@ -261,14 +270,14 @@ const objectWrapperPattern = /^[{\[].*[}\]]$/s;
/**
* 将代码片段转成 js 对象
* @param code 代码文本
* @param isStrict 是否为严格模式
* @param isStrict 是否为严格模式(是否废弃)
* @returns
*/
export function code2object(code: string, isStrict = true) {
// 非严格模式直接执行
// 严格模式下需检测 code 是一个对象
if (!isStrict || (isStrict && objectWrapperPattern.test(code))) {
const ret = executeCodeAsFunctionBody(`return ${code}`);
const ret = runCode(code);
return typeof ret === 'object' ? ret : undefined;
}
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');
return Component;
+3 -3
View File
@@ -42,9 +42,9 @@ describe('string', () => {
expect(isVariableString('{value}')).toBeTruthy();
expect(isVariableString('{"hello"}')).toBeTruthy();
expect(isVariableString('{ foo: "bar" }')).toBeFalsy();
expect(isVariableString('{{ foo: "bar" }}')).toBeTruthy();
expect(isVariableString('{() => {}}')).toBeTruthy();
expect(isVariableString('{() => { console.log(); }}')).toBeTruthy();
expect(isVariableString('{ type: tango.stores?.homePage?.tabKey }')).toBeFalsy();
// expect(isVariableString('{ type: tango.stores.homePage.tabKey }')).toBeFalsy(); // FIXME: fix this case
expect(isVariableString('{ foo: "bar" }')).toBeFalsy();
});
it('getVariableContent', () => {
+10
View File
@@ -3,6 +3,16 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [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)
**Note:** Version bump only for package @music163/tango-sandbox
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@music163/tango-sandbox",
"version": "0.1.7",
"version": "0.1.9",
"description": "sandbox of tango apps",
"author": "wwsun <ww.sun@outlook.com>",
"homepage": "",
@@ -29,14 +29,14 @@
},
"dependencies": {
"@ant-design/icons": "^4.8.0",
"@music163/tango-core": "^0.2.2",
"@music163/tango-helpers": "^0.1.4",
"@music163/tango-core": "^0.2.4",
"@music163/tango-helpers": "^0.1.6",
"crypto-js": "^4.1.1",
"lodash.isequal": "4.5.0",
"react-frame-component": "^5.2.4"
},
"devDependencies": {
"@types/crypto-js": "^4.1.1"
"@types/crypto-js": "^4.1.3"
},
"publishConfig": {
"access": "public",
+3 -1
View File
@@ -21,6 +21,7 @@ import { IFiles, CodeSandboxState, CodeSandboxProps, UnsubscribeFunction } from
// };
export class CodeSandbox extends React.Component<CodeSandboxProps, CodeSandboxState> {
static defaultProps = {
template: 'create-react-app',
skipEval: false,
eventHandlers: {},
externalResources: [] as string[],
@@ -181,9 +182,10 @@ export class CodeSandbox extends React.Component<CodeSandboxProps, CodeSandboxSt
};
render() {
const { style } = this.props;
const { loading, status, iframeId } = this.state;
return (
<div className="CodeSandbox" style={{ position: 'relative', height: '100%' }}>
<div className="CodeSandbox" style={{ position: 'relative', height: '100%', ...style }}>
{loading ? <Loading status={status} /> : null}
<iframe
ref={this.setupFrame}
+1
View File
@@ -504,6 +504,7 @@ export interface CodeSandboxProps {
isFile: (path: string) => Promise<boolean>;
readFile: (path: string) => Promise<string>;
};
style?: React.CSSProperties;
}
export interface ViteSandboxProps {
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [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)
### Bug Fixes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@music163/tango-setting-form",
"version": "0.2.6",
"version": "0.2.8",
"description": "setting form of tango-apps",
"author": "wwsun <ww.sun@outlook.com>",
"homepage": "",
@@ -28,8 +28,8 @@
},
"dependencies": {
"@ant-design/icons": "^4.8.0",
"@music163/tango-helpers": "^0.1.4",
"@music163/tango-ui": "^0.1.7",
"@music163/tango-helpers": "^0.1.6",
"@music163/tango-ui": "^0.1.9",
"antd": "^4.24.2",
"coral-system": "^1.0.5",
"mobx": "6.9.0",
+10
View File
@@ -3,6 +3,16 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [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)
### Bug Fixes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@music163/tango-ui",
"version": "0.1.7",
"version": "0.1.9",
"description": "ui widgets of tango",
"keywords": [
"react",
@@ -38,8 +38,8 @@
"@codemirror/lang-javascript": "^6.2.1",
"@codemirror/lint": "^6.4.2",
"@codemirror/search": "^6.5.4",
"@music163/tango-helpers": "^0.1.4",
"@uiw/react-codemirror": "^4.19.3",
"@music163/tango-helpers": "^0.1.6",
"@uiw/react-codemirror": "^4.21.20",
"antd": "^4.24.2",
"antd-easy-form": "^1.1.3",
"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;
/**
* 是否显示底部边框线
*/
showBottomBorder?: boolean;
}
const headerStyle = css`
@@ -49,6 +53,7 @@ export function CollapsePanel(props: CollapsePanelProps) {
defaultCollapsed = false,
onCollapse,
stickyHeader,
showBottomBorder = true,
...rest
} = props;
const [collapsed, setCollapsed] = useControllableState({
@@ -70,8 +75,15 @@ export function CollapsePanel(props: CollapsePanelProps) {
}
: {};
const CollapsePanelBorderStyle = showBottomBorder
? {
borderBottom: 'solid',
borderBottomColor: 'line2',
}
: {};
return (
<Box className="CollapsePanel" borderBottom="solid" borderBottomColor="line2" {...rest}>
<Box className="CollapsePanel" {...CollapsePanelBorderStyle} {...rest}>
<Box
display="flex"
alignItems="center"
+3 -2
View File
@@ -10,9 +10,10 @@
"@music163/tango-helpers": ["packages/helpers/src/index.ts"],
"@music163/tango-core": ["packages/core/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": ["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