mirror of
https://github.com/NetEase/tango.git
synced 2026-08-31 00:59:32 +08:00
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fb26540f27 | |||
| 558f2cd0a6 | |||
| 8a1234e565 | |||
| 630c486eb8 | |||
| 324f450851 | |||
| d6ffb50e85 | |||
| 2906efc8fc | |||
| 2259fe77fc | |||
| dbbd1dddc7 | |||
| 14e17ebbed | |||
| 4a06eb20cb | |||
| 089a482f75 | |||
| afb94d6a30 | |||
| b9403cfec6 | |||
| 6cf673e7f6 | |||
| 653386dcc0 | |||
| 208e275bb1 | |||
| 77f4145adf | |||
| 11f166a2a6 | |||
| 0452fdb7f1 | |||
| 0068d3426e | |||
| c5840f29b7 | |||
| 963bfe1ab8 | |||
| ae806aee35 | |||
| b29e23599b | |||
| f53ce94c6e | |||
| d775a5003a | |||
| 46d5f59b4c | |||
| 0c0d95fe58 | |||
| 9dd11f7a6d | |||
| fd2fa943b0 | |||
| 70fc6f8030 | |||
| 4f7233bd6c |
@@ -2,11 +2,12 @@
|
||||
"extends": ["eslint-config-ali/typescript/react", "prettier"],
|
||||
"ignorePatterns": ["**/dist/**/*", "**/lib/**/*", "**/node_modules/**/*", "scripts/**/*"],
|
||||
"rules": {
|
||||
"@typescript-eslint/consistent-type-definitions": "off",
|
||||
"@typescript-eslint/dot-notation": "off",
|
||||
"@typescript-eslint/no-unused-vars": "warn",
|
||||
"import/no-cycle": "off",
|
||||
"no-nested-ternary": "off",
|
||||
"no-useless-return":"off",
|
||||
"no-useless-return": "off",
|
||||
"no-param-reassign": "off",
|
||||
"prefer-destructuring": "off"
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
[](https://github.com/NetEase/tango/blob/main/LICENSE)
|
||||
[](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,16 +90,23 @@ Please read the [github contribution guide](https://docs.github.com/en/get-start
|
||||
- Commit and push your code
|
||||
- Open a Pull Request
|
||||
|
||||
## 📈 Usage Trends
|
||||
|
||||
[Usage Trends of Tango Packages](https://npm-compare.com/@music163/tango-helpers,@music163/tango-context,@music163/tango-core,@music163/tango-setting-form,@music163/tango-sandbox,@music163/tango-ui,@music163/tango-designer)
|
||||
|
||||
## 💗 Acknowledgments
|
||||
|
||||
Thanks to the NetEase Cloud Music Front-end team, Public Technology team, Live Broadcasting Technology team, and all the colleagues who participated in the Tango project.
|
||||
Thanks to the NetEase Cloud Music Front-end team, Public Technology team, Live Broadcasting Technology team, and all the developers who participated in the Tango project.
|
||||
|
||||
Thank you to CodeSandbox for providing the [Sandpack](https://sandpack.codesandbox.io/) project, which provides powerful online code execution capabilities for Tango.
|
||||
|
||||
## 📣 Product Promotion
|
||||
|
||||

|
||||
|
||||
Don't waste your time restoring the UI, try the NetEase Cloud Music "Seal D2C" development tool! Easily turn your design into code, support React, RN, Vue, WeChat apps and other multi-end scenarios, what you see is what you get!
|
||||
|
||||
Experience "Seal D2C" now:
|
||||
|
||||
- I'm a Figma user: <https://www.figma.com/community/plugin/1174548852019950797/seal-figma-to-code-d2c/>
|
||||
- I'm a MasterGo user: <https://mastergo.com/community/plugin/98956774428196/>
|
||||
|
||||
## 📄 License
|
||||
|
||||
This project is licensed under the terms of the [MIT license](./LICENSE)
|
||||
|
||||
+14
-6
@@ -5,7 +5,7 @@
|
||||
<h1 align="center">Tango 低代码设计器</h1>
|
||||
<div align="center">
|
||||
|
||||
一个源码驱动的低代码设计器,来自网易云音乐技术团队
|
||||
一个源码驱动的低代码设计器框架
|
||||
|
||||
[](https://github.com/NetEase/tango/blob/main/LICENSE)
|
||||
[](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,16 +91,23 @@ yarn start
|
||||
- 合并修改 git rebase master
|
||||
- 发起 Pull Request
|
||||
|
||||
## 📈 使用量趋势
|
||||
|
||||
[Tango 使用量趋势](https://npm-compare.com/@music163/tango-helpers,@music163/tango-context,@music163/tango-core,@music163/tango-setting-form,@music163/tango-sandbox,@music163/tango-ui,@music163/tango-designer)
|
||||
|
||||
## 💗 致谢
|
||||
|
||||
感谢网易云音乐公共技术团队,大前端团队,直播技术团队,以及所有参与过 Tango 项目的同学们。
|
||||
感谢网易云音乐公共技术团队,大前端团队,直播技术团队,以及所有参与过 Tango 项目的开发者。
|
||||
|
||||
感谢 CodeSandbox 提供的 [Sandpack](https://sandpack.codesandbox.io/) 项目,为 Tango 提供了强大的基于浏览器的代码构建与执行能力。
|
||||
|
||||
## 📣 产品推广
|
||||
|
||||

|
||||
|
||||
不要再浪费时间还原 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)
|
||||
|
||||
@@ -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',
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -145,6 +151,7 @@ import {
|
||||
Input,
|
||||
FormilyForm,
|
||||
} from "@music163/antd";
|
||||
import { Space } from '@music163/antd';
|
||||
class App extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
@@ -152,8 +159,10 @@ class App extends React.Component {
|
||||
<Section title="Section Title">
|
||||
</Section>
|
||||
<Section>
|
||||
<Space>
|
||||
<Button>button</Button>
|
||||
<Input />
|
||||
</Space>
|
||||
</Section>
|
||||
</Page>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -34,6 +34,7 @@ const workspace = new Workspace({
|
||||
// 2. 引擎初始化
|
||||
const engine = createEngine({
|
||||
workspace,
|
||||
defaultActiveSidebarPanel: 'outline',
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -137,7 +137,7 @@ const config = {
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '更多',
|
||||
title: '相关资源',
|
||||
items: [
|
||||
{
|
||||
label: '博客',
|
||||
@@ -149,6 +149,19 @@ const config = {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '更多产品',
|
||||
items: [
|
||||
{
|
||||
label: '海豹 D2C - Figma 插件',
|
||||
to: 'https://www.figma.com/community/plugin/1174548852019950797/seal-figma-to-code-d2c',
|
||||
},
|
||||
{
|
||||
label: '海豹 D2C - MasterGo 插件',
|
||||
to: 'https://mastergo.com/community/plugin/98956774428196/',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
logo: {
|
||||
alt: 'NetEase Cloud Music',
|
||||
|
||||
@@ -24,7 +24,7 @@ const timelines = [
|
||||
{
|
||||
icon: null,
|
||||
title: '1.0 RC',
|
||||
date: '2023.10.30',
|
||||
date: '2023.11.30',
|
||||
description: translate({
|
||||
id: 'homepage.timeline.rc',
|
||||
message: '核心 API 基本稳定,不再发生 BR,发布 1.0 RC 版本。',
|
||||
@@ -33,7 +33,7 @@ const timelines = [
|
||||
{
|
||||
icon: null,
|
||||
title: '1.0',
|
||||
date: 'Before 2023.12.25',
|
||||
date: 'Before 2023.12.29',
|
||||
description: translate({
|
||||
id: 'homepage.timeline.stable',
|
||||
message: 'API 完全稳定,提供良好的社区支持,可用于生产环境。',
|
||||
|
||||
+1
-1
@@ -154,7 +154,7 @@ const config = {
|
||||
// testLocationInResults: false,
|
||||
|
||||
// The glob patterns Jest uses to detect test files
|
||||
testMatch: ['**/__tests__/**/*.[jt]s?(x)', '**/?(*.)+(spec|test).[tj]s?(x)'],
|
||||
testMatch: ['**/tests/**/*.[jt]s?(x)', '**/?(*.)+(spec|test).[tj]s?(x)'],
|
||||
|
||||
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
|
||||
// testPathIgnorePatterns: [
|
||||
|
||||
+21
-24
@@ -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}": [
|
||||
|
||||
@@ -3,6 +3,24 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.2.7](https://github.com/netease/tango/compare/@music163/tango-context@0.2.6...@music163/tango-context@0.2.7) (2023-11-23)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
## [0.2.6](https://github.com/netease/tango/compare/@music163/tango-context@0.2.5...@music163/tango-context@0.2.6) (2023-11-20)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
## [0.2.5](https://github.com/netease/tango/compare/@music163/tango-context@0.2.4...@music163/tango-context@0.2.5) (2023-11-16)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
## [0.2.4](https://github.com/netease/tango/compare/@music163/tango-context@0.2.3...@music163/tango-context@0.2.4) (2023-11-02)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- optional context config ([70fc6f8](https://github.com/netease/tango/commit/70fc6f8030202f5d340281c0424752044ed0fb83))
|
||||
|
||||
## [0.2.3](https://github.com/netease/tango/compare/@music163/tango-context@0.2.2...@music163/tango-context@0.2.3) (2023-10-23)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-context",
|
||||
"version": "0.2.3",
|
||||
"version": "0.2.7",
|
||||
"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.6",
|
||||
"@music163/tango-helpers": "^0.1.8",
|
||||
"mobx-react-lite": "4.0.2"
|
||||
},
|
||||
"publishConfig": {
|
||||
|
||||
@@ -16,9 +16,9 @@ export interface ITangoEngineContext {
|
||||
/**
|
||||
* 自定义配置数据
|
||||
*/
|
||||
config: {
|
||||
customActionVariables: CustomVariableData[];
|
||||
customExpressionVariables: CustomVariableData[];
|
||||
config?: {
|
||||
customActionVariables?: CustomVariableData[];
|
||||
customExpressionVariables?: CustomVariableData[];
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,39 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.2.6](https://github.com/netease/tango/compare/@music163/tango-core@0.2.5...@music163/tango-core@0.2.6) (2023-11-23)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- prototype to import declaration ([#67](https://github.com/netease/tango/issues/67)) ([8a1234e](https://github.com/netease/tango/commit/8a1234e565489c92f041356b7a06339eaeee48df))
|
||||
- refactor update import specifiers ([#68](https://github.com/netease/tango/issues/68)) ([558f2cd](https://github.com/netease/tango/commit/558f2cd0a692c6bbc866d08250d25e2619af183f))
|
||||
|
||||
## [0.2.5](https://github.com/netease/tango/compare/@music163/tango-core@0.2.4...@music163/tango-core@0.2.5) (2023-11-20)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- refactor parse expression ([#61](https://github.com/netease/tango/issues/61)) ([dbbd1dd](https://github.com/netease/tango/commit/dbbd1dddc75c532b7c9710ab0941c8680100f093))
|
||||
|
||||
## [0.2.4](https://github.com/netease/tango/compare/@music163/tango-core@0.2.3...@music163/tango-core@0.2.4) (2023-11-16)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- allow override existing file via addFile ([6cf673e](https://github.com/netease/tango/commit/6cf673e7f6f330613e1d6331256a59900e8cbc68))
|
||||
- export isJSXElementById from traverse ([#54](https://github.com/netease/tango/issues/54)) ([208e275](https://github.com/netease/tango/commit/208e275bb1cd98ed21d816c26179f9ebc11df223))
|
||||
- parse variables from view ([089a482](https://github.com/netease/tango/commit/089a482f750e9d9a7743a6641d6e39989347d318))
|
||||
- update types ([653386d](https://github.com/netease/tango/commit/653386dcc0b064b41915548129952eeabe53019f))
|
||||
|
||||
## [0.2.3](https://github.com/netease/tango/compare/@music163/tango-core@0.2.2...@music163/tango-core@0.2.3) (2023-11-02)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- add onFilesChange to workspace ([#50](https://github.com/netease/tango/issues/50)) ([d775a50](https://github.com/netease/tango/commit/d775a5003ab1a14f801d3b38f7187ea15ef7d74d))
|
||||
- **core:** create store without entry file ([ae806ae](https://github.com/netease/tango/commit/ae806aee3583ef5f957836e15d715035c4556867))
|
||||
- **core:** logging drop action ([963bfe1](https://github.com/netease/tango/commit/963bfe1ab80fa4114637963534b2891a27538120))
|
||||
- designer model add defaultActiveSidebarPanel ([#51](https://github.com/netease/tango/issues/51)) ([9dd11f7](https://github.com/netease/tango/commit/9dd11f7a6d5807c80a63c84c1ad0df5bc1ce9558))
|
||||
- refactor workspace onFilesChange ([f53ce94](https://github.com/netease/tango/commit/f53ce94c6ed777d45ba6137188bfbc4566e03942))
|
||||
- update types ([46d5f59](https://github.com/netease/tango/commit/46d5f59b4cd4fa71ed247f5e470e66c8c9f6d4a0))
|
||||
|
||||
## [0.2.2](https://github.com/netease/tango/compare/@music163/tango-core@0.2.1...@music163/tango-core@0.2.2) (2023-10-23)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-core",
|
||||
"version": "0.2.2",
|
||||
"version": "0.2.6",
|
||||
"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.8",
|
||||
"@types/babel__generator": "^7.6.6",
|
||||
"@types/babel__traverse": "^7.20.3",
|
||||
"mobx": "6.9.0",
|
||||
"path-browserify": "^1.0.1"
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { isValidExpressionCode } from './ast';
|
||||
|
||||
const defineServiceHandlerNames = ['defineServices', 'createServices'];
|
||||
const sfHandlerPattern = new RegExp(`^(${defineServiceHandlerNames.join('|')})$`);
|
||||
|
||||
@@ -20,3 +22,28 @@ const defineStoreHandlerName = 'defineStore';
|
||||
export function isDefineStore(name: string) {
|
||||
return defineStoreHandlerName === name;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否是 tango 的变量引用
|
||||
* @example tango.stores.app.name
|
||||
* @example tango.stores?.app?.name
|
||||
*
|
||||
* @param name
|
||||
* @returns
|
||||
*/
|
||||
export function isTangoVariable(name: string) {
|
||||
return /^tango\??\.(stores|services)\??\./.test(name) && name.split('.').length > 2;
|
||||
}
|
||||
|
||||
const templatePattern = /^{(.+)}$/s;
|
||||
|
||||
/**
|
||||
* 判断给定字符串是否被表达式容器`{expCode}`包裹
|
||||
* @param code
|
||||
*/
|
||||
export function isWrappedByExpressionContainer(code: string, isStrict = true) {
|
||||
if (isStrict && isValidExpressionCode(code)) {
|
||||
return false;
|
||||
}
|
||||
return templatePattern.test(code);
|
||||
}
|
||||
|
||||
@@ -6,10 +6,10 @@ import * as t from '@babel/types';
|
||||
import {
|
||||
logger,
|
||||
isValidObjectString,
|
||||
isVariableString,
|
||||
getVariableContent,
|
||||
isPlainObject,
|
||||
} from '@music163/tango-helpers';
|
||||
import { isWrappedByExpressionContainer } from '../assert';
|
||||
|
||||
// @see https://babeljs.io/docs/en/babel-parser#pluginss
|
||||
const babelParserConfig: ParserOptions = {
|
||||
@@ -22,7 +22,6 @@ const babelParserConfig: ParserOptions = {
|
||||
'classProperties',
|
||||
'asyncGenerators',
|
||||
'functionBind',
|
||||
'functionSent',
|
||||
'dynamicImport',
|
||||
'optionalChaining',
|
||||
],
|
||||
@@ -44,6 +43,7 @@ export function isValidCode(code: string) {
|
||||
|
||||
/**
|
||||
* 检测代码是否是合法的表达式代码
|
||||
* 表达式是一组代码的集合,它返回一个值;每一个合法的表达式都能计算成某个值
|
||||
* @param code
|
||||
* @returns
|
||||
*/
|
||||
@@ -91,8 +91,8 @@ export function code2expression(code: string) {
|
||||
try {
|
||||
expNode = t.cloneNode(parseExpression(code, babelParserConfig), false, true);
|
||||
} catch (err) {
|
||||
console.error('invalid code', err);
|
||||
expNode = t.identifier('undefined');
|
||||
logger.error('invalid code', err);
|
||||
// expNode = t.identifier('undefined');
|
||||
}
|
||||
return expNode;
|
||||
}
|
||||
@@ -103,7 +103,7 @@ export function code2expression(code: string) {
|
||||
* @returns File
|
||||
*/
|
||||
export function expressionCode2ast(code: string) {
|
||||
if (isVariableString(code)) {
|
||||
if (isWrappedByExpressionContainer(code)) {
|
||||
code = getVariableContent(code);
|
||||
}
|
||||
const node = code2expression(code);
|
||||
@@ -130,7 +130,7 @@ export function value2node(
|
||||
ret = t.numericLiteral(value);
|
||||
break;
|
||||
case 'string':
|
||||
if (isVariableString(value)) {
|
||||
if (isWrappedByExpressionContainer(value)) {
|
||||
// 再检查是否是表达式容器,例如 {this.foo}, {1}
|
||||
const innerString = getVariableContent(value);
|
||||
ret = code2expression(innerString);
|
||||
@@ -201,7 +201,7 @@ export function value2jsxAttributeValueNode(value: any) {
|
||||
if (isValidObjectString(value)) {
|
||||
// 先检查是否是对象字符串
|
||||
ret = t.jsxExpressionContainer(code2expression(value));
|
||||
} else if (isVariableString(value)) {
|
||||
} else if (isWrappedByExpressionContainer(value)) {
|
||||
// 再检查是否是表达式容器,例如 {this.foo}, {1}
|
||||
const innerString = getVariableContent(value);
|
||||
ret = t.jsxExpressionContainer(code2expression(innerString));
|
||||
@@ -221,7 +221,7 @@ export function value2jsxChildrenValueNode(value: any) {
|
||||
let ret: t.JSXElement | t.JSXFragment | t.JSXExpressionContainer | t.JSXSpreadChild | t.JSXText;
|
||||
switch (typeof value) {
|
||||
case 'string':
|
||||
if (isVariableString(value)) {
|
||||
if (isWrappedByExpressionContainer(value)) {
|
||||
const innerString = getVariableContent(value);
|
||||
ret = t.jsxExpressionContainer(code2expression(innerString));
|
||||
} else {
|
||||
|
||||
@@ -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,11 +448,12 @@ function getLastExportDeclarationIndex(node: t.Program) {
|
||||
|
||||
/**
|
||||
* 生成新的 ImportDeclaration
|
||||
* @deprecated 使用 makeImportDeclaration2 代替
|
||||
* @param importedModule
|
||||
* @param importedSourcePath
|
||||
* @returns
|
||||
*/
|
||||
export function makeImportDeclaration(importedModule: IImportDeclarationPayload) {
|
||||
export function makeImportDeclarationLegacy(importedModule: IImportDeclarationPayload) {
|
||||
const specifierNodes =
|
||||
importedModule.specifiers?.map((localName) =>
|
||||
t.importSpecifier(t.identifier(localName), t.identifier(localName)),
|
||||
@@ -467,6 +468,36 @@ export function makeImportDeclaration(importedModule: IImportDeclarationPayload)
|
||||
return t.importDeclaration(newSpecifierNodes, t.stringLiteral(importedModule.sourcePath));
|
||||
}
|
||||
|
||||
function specifierData2node(data: IImportSpecifierData) {
|
||||
switch (data.type) {
|
||||
case 'ImportDefaultSpecifier':
|
||||
return t.importDefaultSpecifier(t.identifier(data.localName));
|
||||
case 'ImportSpecifier':
|
||||
return t.importSpecifier(
|
||||
t.identifier(data.importedName || data.localName),
|
||||
t.identifier(data.localName),
|
||||
);
|
||||
case 'ImportNamespaceSpecifier':
|
||||
return t.importNamespaceSpecifier(t.identifier(data.localName));
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function specifierDataList2nodes(specifiers: IImportSpecifierData[]) {
|
||||
return specifiers.map((item) => specifierData2node(item)).filter((item) => !!item);
|
||||
}
|
||||
|
||||
export function makeImportDeclaration(source: string, specifiers: IImportSpecifierData[]) {
|
||||
const specifierNodes = specifierDataList2nodes(specifiers);
|
||||
return t.importDeclaration(specifierNodes, t.stringLiteral(source));
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 使用 parseImportDeclaration 代替
|
||||
* @param node
|
||||
* @returns
|
||||
*/
|
||||
function getImportDeclarationData(node: t.ImportDeclaration): IImportDeclarationPayload {
|
||||
const sourcePath = node2value(node.source);
|
||||
let defaultSpecifier;
|
||||
@@ -487,16 +518,33 @@ function getImportDeclarationData(node: t.ImportDeclaration): IImportDeclaration
|
||||
|
||||
/**
|
||||
* 添加新的导入语句
|
||||
* @deprecated 使用 addImportDeclaration2 代替
|
||||
* @param ast
|
||||
* @param importedModule
|
||||
* @param importedSourcePath
|
||||
* @returns
|
||||
*/
|
||||
export function addImportDeclaration(ast: t.File, importedModule: IImportDeclarationPayload) {
|
||||
export function addImportDeclarationLegacy(ast: t.File, importedModule: IImportDeclarationPayload) {
|
||||
traverse(ast, {
|
||||
Program(path) {
|
||||
const lastIndex = getLastImportDeclarationIndex(path.node);
|
||||
const newImportDeclaration = makeImportDeclaration(importedModule);
|
||||
const newImportDeclaration = makeImportDeclarationLegacy(importedModule);
|
||||
path.node.body.splice(lastIndex, 0, newImportDeclaration);
|
||||
path.stop();
|
||||
},
|
||||
});
|
||||
return ast;
|
||||
}
|
||||
|
||||
export function addImportDeclaration(
|
||||
ast: t.File,
|
||||
source: string,
|
||||
specifiers: IImportSpecifierData[],
|
||||
) {
|
||||
traverse(ast, {
|
||||
Program(path) {
|
||||
const lastIndex = getLastImportDeclarationIndex(path.node);
|
||||
const newImportDeclaration = makeImportDeclaration(source, specifiers);
|
||||
path.node.body.splice(lastIndex, 0, newImportDeclaration);
|
||||
path.stop();
|
||||
},
|
||||
@@ -506,17 +554,21 @@ export function addImportDeclaration(ast: t.File, importedModule: IImportDeclara
|
||||
|
||||
/**
|
||||
* 更新已有的导入语句
|
||||
* @deprecated 使用 updateImportDeclaration2 代替
|
||||
* @param ast
|
||||
* @param importedModule
|
||||
* @param importedSourcePath
|
||||
* @returns
|
||||
*/
|
||||
export function updateImportDeclaration(ast: t.File, importedModule: IImportDeclarationPayload) {
|
||||
export function updateImportDeclarationLegacy(
|
||||
ast: t.File,
|
||||
importedModule: IImportDeclarationPayload,
|
||||
) {
|
||||
traverse(ast, {
|
||||
ImportDeclaration(path) {
|
||||
const currentSourcePath = node2value(path.node.source);
|
||||
if (currentSourcePath === importedModule.sourcePath) {
|
||||
const newImportDeclaration = makeImportDeclaration(importedModule);
|
||||
const newImportDeclaration = makeImportDeclarationLegacy(importedModule);
|
||||
path.replaceWith(newImportDeclaration);
|
||||
path.stop(); // 只修改匹配到的第一条
|
||||
}
|
||||
@@ -525,6 +577,50 @@ export function updateImportDeclaration(ast: t.File, importedModule: IImportDecl
|
||||
return ast;
|
||||
}
|
||||
|
||||
export function updateImportDeclaration(
|
||||
ast: t.File,
|
||||
source: string,
|
||||
specifiers: IImportSpecifierData[],
|
||||
) {
|
||||
traverse(ast, {
|
||||
ImportDeclaration(path) {
|
||||
const currentSourcePath = node2value(path.node.source);
|
||||
if (currentSourcePath === source) {
|
||||
const newImportDeclaration = makeImportDeclaration(source, specifiers);
|
||||
path.replaceWith(newImportDeclaration);
|
||||
path.stop(); // 只修改匹配到的第一条
|
||||
}
|
||||
},
|
||||
});
|
||||
return ast;
|
||||
}
|
||||
|
||||
/**
|
||||
* 再已有的导入语句中添加新的导入符号
|
||||
* @param ast
|
||||
* @param source
|
||||
* @param newSpecifiers
|
||||
* @returns
|
||||
*/
|
||||
export function insertImportSpecifiers(
|
||||
ast: t.File,
|
||||
source: string,
|
||||
newSpecifiers: IImportSpecifierData[],
|
||||
) {
|
||||
traverse(ast, {
|
||||
ImportDeclaration(path) {
|
||||
const currentSourcePath = node2value(path.node.source);
|
||||
if (currentSourcePath === source) {
|
||||
const nodes = specifierDataList2nodes(newSpecifiers);
|
||||
path.node.specifiers.push(...nodes);
|
||||
// 只在匹配到的第一个导入声明语句添加即可,不再重复执行
|
||||
path.stop();
|
||||
}
|
||||
},
|
||||
});
|
||||
return ast;
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增类属性
|
||||
* @param ast
|
||||
@@ -986,7 +1082,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 +1104,7 @@ export function traverseServiceFile(ast: t.File) {
|
||||
},
|
||||
});
|
||||
return {
|
||||
imports,
|
||||
services,
|
||||
baseConfig,
|
||||
};
|
||||
@@ -1107,6 +1209,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 +1218,43 @@ export function traverseViewFile(ast: t.File, idGenerator: IdGenerator) {
|
||||
|
||||
traverse(ast, {
|
||||
ImportDeclaration(path) {
|
||||
const declarationData = getImportDeclarationData(path.node);
|
||||
const exist = importedModules[declarationData.sourcePath];
|
||||
if (!exist) {
|
||||
importedModules[declarationData.sourcePath] = declarationData;
|
||||
const { source, specifiers } = parseImportDeclaration(path.node);
|
||||
if (imports[source]) {
|
||||
// 存在重复的导入语句,合并导入符号列表
|
||||
imports[source] = imports[source].concat(specifiers);
|
||||
} else {
|
||||
importedModules[declarationData.sourcePath] = Array.isArray(exist)
|
||||
imports[source] = specifiers;
|
||||
}
|
||||
|
||||
// FIXME: 下面的逻辑兼容旧的逻辑,后续需要移除掉
|
||||
const declarationData = specifiers.reduce(
|
||||
(prev, cur) => {
|
||||
switch (cur.type) {
|
||||
case 'ImportDefaultSpecifier':
|
||||
prev.defaultSpecifier = cur.localName;
|
||||
break;
|
||||
case 'ImportSpecifier':
|
||||
prev.specifiers.push(cur.localName);
|
||||
break;
|
||||
case 'ImportNamespaceSpecifier':
|
||||
prev.specifiers.push(cur.localName);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return prev;
|
||||
},
|
||||
{
|
||||
defaultSpecifier: undefined,
|
||||
specifiers: [],
|
||||
sourcePath: source,
|
||||
},
|
||||
);
|
||||
const exist = importedModules[source];
|
||||
if (!exist) {
|
||||
importedModules[source] = declarationData;
|
||||
} else {
|
||||
importedModules[source] = Array.isArray(exist)
|
||||
? exist.concat([declarationData])
|
||||
: [exist, declarationData];
|
||||
}
|
||||
@@ -1128,11 +1262,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 +1320,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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { getVariableContent, isVariableString } from '@music163/tango-helpers';
|
||||
import { value2node, expression2code } from './ast';
|
||||
import { getVariableContent } from '@music163/tango-helpers';
|
||||
import { value2node, expression2code, isValidExpressionCode } from './ast';
|
||||
import { isWrappedByExpressionContainer } from './assert';
|
||||
|
||||
/**
|
||||
* 将 js value 转换为代码字符串
|
||||
@@ -36,7 +37,9 @@ export function value2expressionCode(val: any) {
|
||||
|
||||
switch (typeof val) {
|
||||
case 'string': {
|
||||
if (isVariableString(val)) {
|
||||
if (isValidExpressionCode(val)) {
|
||||
ret = val;
|
||||
} else if (isWrappedByExpressionContainer(val, false)) {
|
||||
ret = getVariableContent(val);
|
||||
} else if (isStringCode(val)) {
|
||||
ret = val;
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -3,16 +3,51 @@ import {
|
||||
ComponentPropType,
|
||||
ComponentPrototypeType,
|
||||
isNil,
|
||||
isVariableString,
|
||||
logger,
|
||||
uuid,
|
||||
} from '@music163/tango-helpers';
|
||||
import { getRelativePath, isFilepath } from './string';
|
||||
import type { IImportDeclarationPayload } from '../types';
|
||||
import type { IImportDeclarationPayload, IImportSpecifierData } from '../types';
|
||||
import { code2expression } from './ast';
|
||||
import { isWrappedByExpressionContainer } from './assert';
|
||||
|
||||
export function prototype2importDeclarationData(
|
||||
prototype: ComponentPrototypeType,
|
||||
relativeFilepath?: string,
|
||||
): { source: string; specifiers: IImportSpecifierData[] } {
|
||||
let source = prototype.package;
|
||||
if (relativeFilepath && isFilepath(source)) {
|
||||
source = getRelativePath(relativeFilepath, source);
|
||||
}
|
||||
if (source.endsWith('.js')) {
|
||||
source = source.slice(0, -3);
|
||||
}
|
||||
|
||||
const specifiers: IImportSpecifierData[] = [];
|
||||
|
||||
if (prototype.exportType === 'defaultExport') {
|
||||
specifiers.push({
|
||||
localName: prototype.name,
|
||||
type: 'ImportDefaultSpecifier',
|
||||
});
|
||||
} else {
|
||||
[prototype.name, ...(prototype.relatedImports || [])].forEach((item) => {
|
||||
specifiers.push({
|
||||
localName: item,
|
||||
type: 'ImportSpecifier',
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
source,
|
||||
specifiers,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据组件的 prototype 生成 ImportDeclarationPayload
|
||||
* @deprecated
|
||||
*/
|
||||
export function getImportDeclarationPayloadByPrototype(
|
||||
prototype: ComponentPrototypeType,
|
||||
@@ -35,6 +70,9 @@ export function getImportDeclarationPayloadByPrototype(
|
||||
if (relativeFilepath && isFilepath(sourcePath)) {
|
||||
sourcePath = getRelativePath(relativeFilepath, sourcePath);
|
||||
}
|
||||
if (sourcePath.endsWith('.js')) {
|
||||
sourcePath = sourcePath.slice(0, -3);
|
||||
}
|
||||
|
||||
return {
|
||||
defaultSpecifier,
|
||||
@@ -82,7 +120,7 @@ function getPropKeyValuePair(item: ComponentPropType, generateValue: (...args: a
|
||||
break;
|
||||
}
|
||||
case 'string': {
|
||||
if (!isVariableString(value)) {
|
||||
if (!isWrappedByExpressionContainer(value)) {
|
||||
// 不是变量字符串
|
||||
value = `"${value}"`;
|
||||
} else {
|
||||
|
||||
@@ -33,7 +33,6 @@ export function inferFileType(filename: string): FileType {
|
||||
return FileType.JsonViewModule;
|
||||
}
|
||||
|
||||
// FIXME: 暂时写死,后续要支持多 services 文件解析
|
||||
if (/\/services\/.+\.js$/.test(filename)) {
|
||||
return FileType.ServiceModule;
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 = '';
|
||||
}
|
||||
|
||||
@@ -5,11 +5,11 @@ import { DragSource } from './drag-source';
|
||||
import {
|
||||
IFileConfig,
|
||||
FileType,
|
||||
IImportDeclarationPayload,
|
||||
InsertChildPositionType,
|
||||
ITangoConfigPackages,
|
||||
IPageConfigData,
|
||||
IServiceFunctionPayload,
|
||||
IImportSpecifierSourceData,
|
||||
IImportSpecifierData,
|
||||
} from '../types';
|
||||
import { TangoFile, TangoJsonFile } from './file';
|
||||
@@ -25,7 +25,7 @@ export interface IViewFile {
|
||||
/**
|
||||
* 通过导入组件名查找组件来自的包
|
||||
*/
|
||||
importMap?: Dict<IImportSpecifierData>;
|
||||
importMap?: Dict<IImportSpecifierSourceData>;
|
||||
|
||||
listImportSources?: () => string[];
|
||||
listModals?: () => Array<{ label: string; value: string }>;
|
||||
@@ -33,6 +33,14 @@ export interface IViewFile {
|
||||
|
||||
update: (code?: string, isFormatCode?: boolean, refreshWorkspace?: boolean) => void;
|
||||
|
||||
/**
|
||||
* 添加新的导入符号
|
||||
* @param source 导入来源
|
||||
* @param newSpecifiers 新导入符号列表
|
||||
* @returns this
|
||||
*/
|
||||
addImportSpecifiers: (source: string, newSpecifiers: IImportSpecifierData[]) => IViewFile;
|
||||
|
||||
getNode: (targetNodeId: string) => IViewNode;
|
||||
|
||||
removeNode: (targetNodeId: string) => IViewFile;
|
||||
@@ -41,31 +49,15 @@ export interface IViewFile {
|
||||
targetNodeId: string,
|
||||
newNode: any,
|
||||
position?: InsertChildPositionType,
|
||||
sourcePrototype?: string | ComponentPrototypeType,
|
||||
) => IViewFile;
|
||||
|
||||
insertAfter: (
|
||||
targetNodeId: string,
|
||||
newNode: any,
|
||||
sourcePrototype?: string | ComponentPrototypeType,
|
||||
) => IViewFile;
|
||||
insertAfter: (targetNodeId: string, newNode: any) => IViewFile;
|
||||
|
||||
insertBefore: (
|
||||
targetNodeId: string,
|
||||
newNode: any,
|
||||
sourcePrototype?: string | ComponentPrototypeType,
|
||||
) => IViewFile;
|
||||
insertBefore: (targetNodeId: string, newNode: any) => IViewFile;
|
||||
|
||||
replaceNode: (
|
||||
targetNodeId: string,
|
||||
newNode: any,
|
||||
sourcePrototype?: string | ComponentPrototypeType,
|
||||
) => IViewFile;
|
||||
replaceNode: (targetNodeId: string, newNode: any) => IViewFile;
|
||||
|
||||
replaceViewChildren: (
|
||||
rawNodes: any[],
|
||||
importDeclarations?: IImportDeclarationPayload[],
|
||||
) => IViewFile;
|
||||
replaceViewChildren: (rawNodes: any[]) => IViewFile;
|
||||
|
||||
updateNodeAttribute: (
|
||||
nodeId: string,
|
||||
@@ -148,8 +140,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 +149,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,6 +190,16 @@ export interface IWorkspace {
|
||||
relatedImports?: string[],
|
||||
) => void;
|
||||
|
||||
/**
|
||||
* 查询节点
|
||||
* @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;
|
||||
@@ -203,21 +210,25 @@ export interface IWorkspace {
|
||||
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;
|
||||
|
||||
// ----------------- 依赖包操作 -----------------
|
||||
|
||||
addDependency?: (data: any) => void;
|
||||
listDependencies?: () => any;
|
||||
getDependency?: (pkgName: string) => object;
|
||||
|
||||
updateDependency?: (
|
||||
name: string,
|
||||
@@ -241,6 +252,12 @@ export interface IWorkspace {
|
||||
|
||||
removeBizComp?: (name: string) => void;
|
||||
|
||||
// ----------------- 其他操作 -----------------
|
||||
|
||||
updateModuleCodeByVariablePath?: (variablePath: string, code: string) => void;
|
||||
|
||||
// ----------------- getter -----------------
|
||||
|
||||
get activeViewModule(): IViewFile;
|
||||
get pages(): any[];
|
||||
get bizComps(): string[];
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
import * as t from '@babel/types';
|
||||
import { action, computed, makeObservable, observable } from 'mobx';
|
||||
import { isNil } from '@music163/tango-helpers';
|
||||
import { code2ast, ast2code, formatCode } from '../helpers';
|
||||
import {
|
||||
code2ast,
|
||||
ast2code,
|
||||
formatCode,
|
||||
traverseFile,
|
||||
addImportDeclaration,
|
||||
updateImportDeclaration,
|
||||
} from '../helpers';
|
||||
import { TangoFile } from './file';
|
||||
import { IFileConfig } from '../types';
|
||||
import { IFileConfig, IImportSpecifierData, ImportDeclarationDataType } from '../types';
|
||||
import { IWorkspace } from './interfaces';
|
||||
|
||||
/**
|
||||
@@ -14,6 +21,11 @@ import { IWorkspace } from './interfaces';
|
||||
export class TangoModule extends TangoFile {
|
||||
ast: t.File;
|
||||
|
||||
/**
|
||||
* 导入语句
|
||||
*/
|
||||
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[]) {
|
||||
this.ast = addImportDeclaration(this.ast, source, specifiers);
|
||||
return this;
|
||||
}
|
||||
|
||||
updateImportDeclaration(source: string, specifiers: IImportSpecifierData[]) {
|
||||
this.ast = updateImportDeclaration(this.ast, source, specifiers);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于最新的 ast 进行源码同步
|
||||
*/
|
||||
@@ -69,7 +91,10 @@ export class TangoModule extends TangoFile {
|
||||
this.ast = code2ast(code);
|
||||
}
|
||||
|
||||
_analysisAst() {}
|
||||
_analysisAst() {
|
||||
const { imports } = traverseFile(this.ast);
|
||||
this.imports = imports;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as t from '@babel/types';
|
||||
import { action, computed, makeObservable, observable, toJS } from 'mobx';
|
||||
import { ComponentPrototypeType, Dict } from '@music163/tango-helpers';
|
||||
import { Dict } from '@music163/tango-helpers';
|
||||
import {
|
||||
ast2code,
|
||||
traverseViewFile,
|
||||
@@ -8,15 +8,17 @@ import {
|
||||
insertSiblingAfterJSXElement,
|
||||
appendChildToJSXElement,
|
||||
addImportDeclaration,
|
||||
updateImportDeclaration,
|
||||
replaceJSXElement,
|
||||
getImportDeclarationPayloadByPrototype,
|
||||
removeUnusedImportSpecifiers,
|
||||
insertSiblingBeforeJSXElement,
|
||||
replaceRootJSXElementChildren,
|
||||
IdGenerator,
|
||||
updateJSXAttributes,
|
||||
queryXFormItemFields,
|
||||
prototype2importDeclarationData,
|
||||
insertImportSpecifiers,
|
||||
addImportDeclarationLegacy,
|
||||
updateImportDeclarationLegacy,
|
||||
} from '../helpers';
|
||||
import { TangoNode } from './node';
|
||||
import {
|
||||
@@ -24,11 +26,59 @@ import {
|
||||
ITangoViewNodeData,
|
||||
IImportDeclarationPayload,
|
||||
InsertChildPositionType,
|
||||
IImportSpecifierSourceData,
|
||||
ImportDeclarationDataType,
|
||||
IImportSpecifierData,
|
||||
} from '../types';
|
||||
import { IViewFile, IWorkspace } from './interfaces';
|
||||
import { TangoModule } from './module';
|
||||
|
||||
/**
|
||||
* 导入信息转为 变量名->来源 的 map 结构
|
||||
* @param importedModules
|
||||
* @returns
|
||||
*/
|
||||
function buildImportMap(importedModules: ImportDeclarationDataType) {
|
||||
const map: Dict<IImportSpecifierSourceData> = {};
|
||||
Object.keys(importedModules).forEach((source) => {
|
||||
const specifiers = importedModules[source];
|
||||
specifiers?.forEach((specifier) => {
|
||||
map[specifier.localName] = {
|
||||
source,
|
||||
isDefault: specifier.type === 'ImportDefaultSpecifier',
|
||||
};
|
||||
});
|
||||
});
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将节点列表转换为 tree data 嵌套数组
|
||||
* @param list
|
||||
*/
|
||||
function nodeListToTreeData(list: ITangoViewNodeData[]) {
|
||||
const map: Record<string, ITangoViewNodeData> = {};
|
||||
|
||||
list.forEach((item) => {
|
||||
// 如果不存在,则初始化
|
||||
if (!map[item.id]) {
|
||||
map[item.id] = {
|
||||
...item,
|
||||
children: [],
|
||||
};
|
||||
}
|
||||
|
||||
// 是否找到父节点,找到则塞进去
|
||||
if (item.parentId && map[item.parentId]) {
|
||||
map[item.parentId].children.push(map[item.id]);
|
||||
}
|
||||
});
|
||||
|
||||
// 保留根节点
|
||||
const ret = Object.values(map).filter((item) => !item.parentId);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* 视图模块
|
||||
*/
|
||||
@@ -38,7 +88,7 @@ export class TangoViewModule extends TangoModule implements IViewFile {
|
||||
/**
|
||||
* 通过导入组件名查找组件来自的包
|
||||
*/
|
||||
importMap: Dict<IImportSpecifierData>;
|
||||
importMap: Dict<IImportSpecifierSourceData>;
|
||||
|
||||
/**
|
||||
* 视图中依赖的 tango 变量,仅 stores 和 services
|
||||
@@ -51,6 +101,7 @@ export class TangoViewModule extends TangoModule implements IViewFile {
|
||||
private _nodes: Map<string, TangoNode>;
|
||||
/**
|
||||
* 导入的模块
|
||||
* @deprecated
|
||||
*/
|
||||
private _importedModules: Dict<IImportDeclarationPayload | IImportDeclarationPayload[]>;
|
||||
|
||||
@@ -98,6 +149,7 @@ export class TangoViewModule extends TangoModule implements IViewFile {
|
||||
ast: newAst,
|
||||
cleanAst,
|
||||
nodes,
|
||||
imports,
|
||||
importedModules,
|
||||
variables,
|
||||
} = traverseViewFile(this.ast, this._idGenerator);
|
||||
@@ -107,7 +159,8 @@ export class TangoViewModule extends TangoModule implements IViewFile {
|
||||
this._cleanCode = ast2code(cleanAst);
|
||||
|
||||
this._importedModules = importedModules;
|
||||
this.importMap = this.buildImportMap(importedModules);
|
||||
this.imports = imports;
|
||||
this.importMap = buildImportMap(imports);
|
||||
this.variables = variables;
|
||||
|
||||
this._nodes.clear();
|
||||
@@ -127,7 +180,7 @@ export class TangoViewModule extends TangoModule implements IViewFile {
|
||||
* 依赖列表
|
||||
*/
|
||||
listImportSources() {
|
||||
return Object.keys(this._importedModules);
|
||||
return Object.keys(this.imports);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -168,23 +221,30 @@ export class TangoViewModule extends TangoModule implements IViewFile {
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于组件的 prototype 信息更新导入信息
|
||||
* @param prototype
|
||||
* @param shouldUpdateCode
|
||||
* 添加导入符号
|
||||
* @param source
|
||||
* @param newSpecifiers
|
||||
* @returns
|
||||
*/
|
||||
updateImportSpecifiersByPrototype(sourcePrototype: string | ComponentPrototypeType) {
|
||||
const prototype = this.workspace.getPrototype(sourcePrototype);
|
||||
if (prototype) {
|
||||
const importDeclaration = getImportDeclarationPayloadByPrototype(prototype, this.filename);
|
||||
this.updateImportSpecifiers(importDeclaration);
|
||||
addImportSpecifiers(source: string, newSpecifiers: IImportSpecifierData[]) {
|
||||
const existSpecifiers = this.imports[source];
|
||||
if (existSpecifiers) {
|
||||
const insertedSpecifiers = newSpecifiers.filter((item) => {
|
||||
return !existSpecifiers.find((existItem) => existItem.localName === item.localName);
|
||||
});
|
||||
this.ast = insertImportSpecifiers(this.ast, source, insertedSpecifiers);
|
||||
} else {
|
||||
// 不存在导入来源,直接添加新的导入语句
|
||||
this.ast = addImportDeclaration(this.ast, source, newSpecifiers);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新导入的变量(新版)
|
||||
* 更新导入的变量
|
||||
* @deprecated 使用 updateImportDeclaration 代替
|
||||
*/
|
||||
updateImportSpecifiers(importDeclaration: IImportDeclarationPayload) {
|
||||
updateImportSpecifiersLegacy(importDeclaration: IImportDeclarationPayload) {
|
||||
const mods = this._importedModules[importDeclaration.sourcePath];
|
||||
let ast;
|
||||
// 如果模块已存在,需要去重
|
||||
@@ -199,12 +259,12 @@ export class TangoViewModule extends TangoModule implements IViewFile {
|
||||
(name) => !specifiers.includes(name),
|
||||
);
|
||||
|
||||
ast = updateImportDeclaration(this.ast, {
|
||||
ast = updateImportDeclarationLegacy(this.ast, {
|
||||
...importDeclaration,
|
||||
specifiers: newSpecifiers.concat(targetMod.specifiers),
|
||||
});
|
||||
} else {
|
||||
ast = addImportDeclaration(this.ast, importDeclaration);
|
||||
ast = addImportDeclarationLegacy(this.ast, importDeclaration);
|
||||
}
|
||||
this.ast = ast;
|
||||
return this;
|
||||
@@ -249,7 +309,8 @@ export class TangoViewModule extends TangoModule implements IViewFile {
|
||||
// 导入依赖的组件
|
||||
relatedImports.forEach((name: string) => {
|
||||
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);
|
||||
@@ -260,43 +321,25 @@ export class TangoViewModule extends TangoModule implements IViewFile {
|
||||
* 插入子节点的最后面
|
||||
* @param targetNodeId
|
||||
* @param newNode
|
||||
* @param sourceName
|
||||
* @param position
|
||||
* @returns
|
||||
*/
|
||||
insertChild(
|
||||
targetNodeId: string,
|
||||
newNode: t.JSXElement,
|
||||
position: InsertChildPositionType = 'last',
|
||||
sourceName: string | ComponentPrototypeType,
|
||||
) {
|
||||
this.ast = appendChildToJSXElement(this.ast, targetNodeId, newNode, position);
|
||||
if (sourceName) {
|
||||
this.updateImportSpecifiersByPrototype(sourceName);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
insertAfter(
|
||||
targetNodeId: string,
|
||||
newNode: t.JSXElement,
|
||||
sourceName?: string | ComponentPrototypeType,
|
||||
) {
|
||||
insertAfter(targetNodeId: string, newNode: t.JSXElement) {
|
||||
this.ast = insertSiblingAfterJSXElement(this.ast, targetNodeId, newNode);
|
||||
if (sourceName) {
|
||||
this.updateImportSpecifiersByPrototype(sourceName);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
insertBefore(
|
||||
targetNodeId: string,
|
||||
newNode: t.JSXElement,
|
||||
sourceName?: string | ComponentPrototypeType,
|
||||
) {
|
||||
insertBefore(targetNodeId: string, newNode: t.JSXElement) {
|
||||
this.ast = insertSiblingBeforeJSXElement(this.ast, targetNodeId, newNode);
|
||||
if (sourceName) {
|
||||
this.updateImportSpecifiersByPrototype(sourceName);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -304,22 +347,15 @@ export class TangoViewModule extends TangoModule implements IViewFile {
|
||||
* 替换目标节点为新节点
|
||||
* @param targetNodeId
|
||||
* @param newNode
|
||||
* @param sourcePrototype
|
||||
*/
|
||||
replaceNode(
|
||||
targetNodeId: string,
|
||||
newNode: t.JSXElement,
|
||||
sourceName?: string | ComponentPrototypeType,
|
||||
) {
|
||||
replaceNode(targetNodeId: string, newNode: t.JSXElement) {
|
||||
this.ast = replaceJSXElement(this.ast, targetNodeId, newNode);
|
||||
if (sourceName) {
|
||||
this.updateImportSpecifiersByPrototype(sourceName);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 替换 jsx 跟结点的子元素
|
||||
* @deprecated 不推荐使用
|
||||
*/
|
||||
replaceViewChildren(
|
||||
childrenNodes: t.JSXElement[],
|
||||
@@ -331,60 +367,10 @@ export class TangoViewModule extends TangoModule implements IViewFile {
|
||||
|
||||
if (importDeclarations?.length) {
|
||||
importDeclarations.forEach((item) => {
|
||||
this.updateImportSpecifiers(item);
|
||||
this.updateImportSpecifiersLegacy(item);
|
||||
});
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
private buildImportMap(
|
||||
importedModules: Dict<IImportDeclarationPayload | IImportDeclarationPayload[]>,
|
||||
) {
|
||||
const map: Dict<IImportSpecifierData> = {};
|
||||
Object.keys(importedModules).forEach((modName) => {
|
||||
const mod = importedModules[modName];
|
||||
(Array.isArray(mod) ? mod : [mod]).forEach((item) => {
|
||||
if (item.defaultSpecifier) {
|
||||
map[item.defaultSpecifier] = {
|
||||
source: modName,
|
||||
isDefault: true,
|
||||
};
|
||||
}
|
||||
if (item.specifiers.length) {
|
||||
item.specifiers.forEach((spe) => {
|
||||
map[spe] = { source: modName };
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将节点列表转换为 tree data 嵌套数组
|
||||
* @param list
|
||||
*/
|
||||
function nodeListToTreeData(list: ITangoViewNodeData[]) {
|
||||
const map: Record<string, ITangoViewNodeData> = {};
|
||||
|
||||
list.forEach((item) => {
|
||||
// 如果不存在,则初始化
|
||||
if (!map[item.id]) {
|
||||
map[item.id] = {
|
||||
...item,
|
||||
children: [],
|
||||
};
|
||||
}
|
||||
|
||||
// 是否找到父节点,找到则塞进去
|
||||
if (item.parentId && map[item.parentId]) {
|
||||
map[item.parentId].children.push(map[item.id]);
|
||||
}
|
||||
});
|
||||
|
||||
// 保留根节点
|
||||
const ret = Object.values(map).filter((item) => !item.parentId);
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
namesToImportDeclarations,
|
||||
getBlockNameByFilename,
|
||||
getPrivilegeCode,
|
||||
prototype2importDeclarationData,
|
||||
} from '../helpers';
|
||||
import { DropMethod } from './drop-target';
|
||||
import { HistoryMessage, TangoHistory } from './history';
|
||||
@@ -55,6 +56,10 @@ export interface IWorkspaceOptions {
|
||||
* 组件描述列表
|
||||
*/
|
||||
prototypes?: Record<string, ComponentPrototypeType>;
|
||||
/**
|
||||
* 工作区文件变更事件
|
||||
*/
|
||||
onFilesChange?: (filenames: string[]) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -125,6 +130,11 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
*/
|
||||
appJson: TangoJsonFile;
|
||||
|
||||
/**
|
||||
* 代码变更回调
|
||||
*/
|
||||
onFilesChange?: (filenames: string[]) => void;
|
||||
|
||||
/**
|
||||
* 绑定事件
|
||||
* TODO: 是否需要自己来管理 listeners,并及时进行 gc
|
||||
@@ -220,6 +230,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 +313,7 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加一组文件到工作区
|
||||
* 添加一组文件到工作区,如果文件同名,后面的文件会覆盖前面的文件
|
||||
* @param files
|
||||
*/
|
||||
addFiles(files: IFileConfig[] = []) {
|
||||
@@ -318,81 +329,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) {
|
||||
@@ -499,7 +528,8 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
|
||||
/**
|
||||
* 添加新的视图文件
|
||||
* @deprecated
|
||||
* @deprecated 使用 addViewFile 代替
|
||||
* FIXME: 重构这个逻辑
|
||||
* @param route 视图名
|
||||
* @param code 视图代码
|
||||
*/
|
||||
@@ -594,13 +624,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 +744,29 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
this.serviceModules[moduleName]?.updateBaseConfig(config).update();
|
||||
}
|
||||
|
||||
addDependency(data: any) {
|
||||
// TODO: implement it to replace addBizComp & addServiceComp
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 package.json 中的依赖列表
|
||||
* @returns
|
||||
* TODO: fix this logic to merge dependencies from package.json and tango.config.json
|
||||
*/
|
||||
listDependencies() {
|
||||
return this.packageJson?.getValue('dependencies');
|
||||
}
|
||||
|
||||
getDependency(pkgName: string) {
|
||||
const packages = this.tangoConfigJson?.getValue('packages');
|
||||
const dependencies = this.packageJson?.getValue('dependencies'); // 兼容老版本
|
||||
const detail = {
|
||||
version: dependencies?.[pkgName],
|
||||
...(packages?.[pkgName] || {}),
|
||||
};
|
||||
return detail;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新依赖,没有就添加
|
||||
* @param name
|
||||
@@ -909,7 +951,7 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
);
|
||||
|
||||
importDeclarations.forEach((importDeclaration) => {
|
||||
this.activeViewModule.updateImportSpecifiers(importDeclaration);
|
||||
this.activeViewModule.updateImportSpecifiersLegacy(importDeclaration);
|
||||
});
|
||||
|
||||
this.copyTempNodes.forEach((node) => {
|
||||
@@ -960,7 +1002,11 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
const sourcePrototype = this.getPrototype(sourceName);
|
||||
const newNode = prototype2jsxElement(sourcePrototype);
|
||||
const file = this.getNode(targetNodeId).file;
|
||||
file.insertChild(targetNodeId, newNode, 'last', sourcePrototype).update();
|
||||
const { source, specifiers } = prototype2importDeclarationData(sourcePrototype, file.filename);
|
||||
file
|
||||
.insertChild(targetNodeId, newNode, 'last')
|
||||
.addImportSpecifiers(source, specifiers)
|
||||
.update();
|
||||
this.history.push({
|
||||
message: HistoryMessage.InsertNode,
|
||||
data: {
|
||||
@@ -980,7 +1026,8 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
const sourcePrototype = this.getPrototype(sourceName);
|
||||
const newNode = prototype2jsxElement(sourcePrototype);
|
||||
const file = this.getNode(targetNodeId).file;
|
||||
file.replaceNode(targetNodeId, newNode, sourcePrototype).update();
|
||||
const { source, specifiers } = prototype2importDeclarationData(sourcePrototype, file.filename);
|
||||
file.replaceNode(targetNodeId, newNode).addImportSpecifiers(source, specifiers).update();
|
||||
this.history.push({
|
||||
message: HistoryMessage.ReplaceNode,
|
||||
data: {
|
||||
@@ -998,7 +1045,14 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
if (insertedPrototype) {
|
||||
const newNode = prototype2jsxElement(insertedPrototype);
|
||||
const file = this.selectSource.file;
|
||||
file.insertChild(this.selectSource.first.id, newNode, 'last', insertedPrototype).update();
|
||||
const { source, specifiers } = prototype2importDeclarationData(
|
||||
insertedPrototype,
|
||||
file.filename,
|
||||
);
|
||||
file
|
||||
.insertChild(this.selectSource.first.id, newNode, 'last')
|
||||
.addImportSpecifiers(source, specifiers)
|
||||
.update();
|
||||
this.history.push({
|
||||
message: HistoryMessage.InsertNode,
|
||||
data: {
|
||||
@@ -1054,30 +1108,40 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
const targetFile = dropTarget.node?.file;
|
||||
const sourceFile = dragSource.node?.file;
|
||||
|
||||
// dragSourcePrototype to importDeclarations
|
||||
const { source, specifiers } = prototype2importDeclarationData(
|
||||
dragSourcePrototype,
|
||||
targetFile.filename,
|
||||
);
|
||||
|
||||
let isValidOperation = true;
|
||||
switch (dropTarget.method) {
|
||||
// 直接往目标节点的 children 里添加一个节点
|
||||
case DropMethod.InsertChild: {
|
||||
targetFile.insertChild(dropTarget.id, newNode, 'last', dragSourcePrototype);
|
||||
targetFile
|
||||
.insertChild(dropTarget.id, newNode, 'last')
|
||||
.addImportSpecifiers(source, specifiers);
|
||||
break;
|
||||
}
|
||||
case DropMethod.InsertFirstChild: {
|
||||
targetFile.insertChild(dropTarget.id, newNode, 'first', dragSourcePrototype);
|
||||
targetFile
|
||||
.insertChild(dropTarget.id, newNode, 'first')
|
||||
.addImportSpecifiers(source, specifiers);
|
||||
break;
|
||||
}
|
||||
// 往目标节点的后边插入一个节点
|
||||
case DropMethod.InsertAfter: {
|
||||
targetFile.insertAfter(dropTarget.id, newNode, dragSourcePrototype);
|
||||
targetFile.insertAfter(dropTarget.id, newNode).addImportSpecifiers(source, specifiers);
|
||||
break;
|
||||
}
|
||||
// 往目标节点的前方插入一个节点
|
||||
case DropMethod.InsertBefore: {
|
||||
targetFile.insertBefore(dropTarget.id, newNode, dragSourcePrototype);
|
||||
targetFile.insertBefore(dropTarget.id, newNode).addImportSpecifiers(source, specifiers);
|
||||
break;
|
||||
}
|
||||
// 替换目标节点
|
||||
case DropMethod.ReplaceNode: {
|
||||
targetFile.replaceNode(dropTarget.id, newNode, dragSourcePrototype);
|
||||
targetFile.replaceNode(dropTarget.id, newNode).addImportSpecifiers(source, specifiers);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
@@ -1094,13 +1158,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 +1166,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 +1190,7 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
},
|
||||
}),
|
||||
);
|
||||
this.onFilesChange?.(filenames);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1185,11 +1252,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),
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 服务函数参数类型
|
||||
*/
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -3,15 +3,16 @@ import {
|
||||
serviceConfig2Node,
|
||||
isValidCode,
|
||||
isValidExpressionCode,
|
||||
code2expression,
|
||||
} from '../src/helpers';
|
||||
|
||||
describe('helpers', () => {
|
||||
test('isValidCode', () => {
|
||||
describe('ast helpers', () => {
|
||||
it('isValidCode', () => {
|
||||
expect(isValidCode('() => { hello world }')).toBeFalsy();
|
||||
expect(isValidCode('function() {}')).toBeFalsy();
|
||||
});
|
||||
|
||||
test('isValidExpression', () => {
|
||||
it('isValidExpression', () => {
|
||||
expect(isValidExpressionCode('() => { }')).toBeTruthy();
|
||||
expect(isValidExpressionCode('1')).toBeTruthy();
|
||||
expect(isValidExpressionCode('"hello"')).toBeTruthy();
|
||||
@@ -20,9 +21,17 @@ describe('helpers', () => {
|
||||
expect(isValidExpressionCode('[1,2,3]')).toBeTruthy();
|
||||
expect(isValidExpressionCode('<div>hello</div>')).toBeTruthy();
|
||||
expect(isValidExpressionCode('<div>hello</div>')).toBeTruthy();
|
||||
|
||||
expect(isValidExpressionCode('{1}')).toBeFalsy();
|
||||
expect(isValidExpressionCode('{"1"}')).toBeFalsy();
|
||||
expect(isValidExpressionCode('{ 1+1 }')).toBeFalsy();
|
||||
expect(isValidExpressionCode('{<div>aaa</div>}')).toBeFalsy();
|
||||
expect(isValidExpressionCode('{() => {}}')).toBeFalsy();
|
||||
expect(isValidExpressionCode('{[1,2,3]}')).toBeFalsy();
|
||||
expect(isValidExpressionCode('{tango.stores.app.title}')).toBeFalsy();
|
||||
});
|
||||
|
||||
test('object2node', () => {
|
||||
it('object2node', () => {
|
||||
const node = object2node({
|
||||
url: '/api/backend/clientversion/appmarket/list',
|
||||
method: 'POST',
|
||||
@@ -40,4 +49,22 @@ describe('helpers', () => {
|
||||
} as any);
|
||||
expect(node.type).toEqual('ObjectExpression');
|
||||
});
|
||||
|
||||
it('code2expression', () => {
|
||||
expect(code2expression('')).toBeUndefined();
|
||||
expect(code2expression('{tango.stores.app}')).toBeUndefined();
|
||||
|
||||
expect(code2expression('{ type: window.bar }').type).toEqual('ObjectExpression');
|
||||
expect(code2expression('() => {};').type).toEqual('ArrowFunctionExpression');
|
||||
expect(code2expression('<Button>hello</Button>').type).toBe('JSXElement');
|
||||
expect(code2expression('<BreadcrumbItem children="节点名称" />').type).toBe('JSXElement');
|
||||
|
||||
const arrayCode = `
|
||||
[
|
||||
{ label: 'foo', value: 'foo' },
|
||||
{ label: 'bar', value: 'bar' },
|
||||
]
|
||||
`;
|
||||
expect(code2expression(arrayCode).type).toEqual('ArrayExpression');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -25,40 +25,6 @@ describe('helpers', () => {
|
||||
expect(code2ast('function App() {}').type).toEqual('File');
|
||||
});
|
||||
|
||||
it('code2expression: null', () => {
|
||||
expect(code2expression('')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('code2expression: object', () => {
|
||||
const code = `
|
||||
{
|
||||
foo: 'bar',
|
||||
}
|
||||
`;
|
||||
const node = code2expression(code);
|
||||
expect(node.type).toEqual('ObjectExpression');
|
||||
});
|
||||
|
||||
it('code2expression: arrow function', () => {
|
||||
expect(code2expression('() => {};').type).toEqual('ArrowFunctionExpression');
|
||||
});
|
||||
|
||||
it('code2expression: list', () => {
|
||||
const code = `
|
||||
[
|
||||
{ label: 'foo', value: 'foo' },
|
||||
{ label: 'bar', value: 'bar' },
|
||||
]
|
||||
`;
|
||||
const node = code2expression(code);
|
||||
expect(node.type).toEqual('ArrayExpression');
|
||||
});
|
||||
|
||||
it('code2expression: jsxElement', () => {
|
||||
const node = code2expression('<Button>hello</Button>');
|
||||
expect(node.type).toEqual('JSXElement');
|
||||
});
|
||||
|
||||
it('parse jsxElement attributes', () => {
|
||||
const node = code2expression(
|
||||
"<XColumn dataIndex='col' enumMap={{ 1: '已解决', 2: '未解决' }} />",
|
||||
@@ -73,11 +39,6 @@ describe('helpers', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('code2expression: closed jsxElement', () => {
|
||||
const node = code2expression('<BreadcrumbItem children="节点名称" />');
|
||||
expect(node.type).toEqual('JSXElement');
|
||||
});
|
||||
|
||||
it('value2node: number', () => {
|
||||
expect(value2node(1).type).toEqual('NumericLiteral');
|
||||
});
|
||||
@@ -126,7 +87,6 @@ describe('helpers', () => {
|
||||
});
|
||||
|
||||
describe('string helpers', () => {
|
||||
|
||||
it('value2code: empty array', () => {
|
||||
expect(value2code([])).toEqual('[]');
|
||||
});
|
||||
@@ -151,9 +111,9 @@ describe('string helpers', () => {
|
||||
it('namesToImportDeclarations', () => {
|
||||
expect(
|
||||
namesToImportDeclarations(['Button', 'Box', 'React'], {
|
||||
Button: { package: '@music163/antd' },
|
||||
Box: { package: '@music163/antd' },
|
||||
React: { package: 'react', isDefault: true },
|
||||
Button: { source: '@music163/antd' },
|
||||
Box: { source: '@music163/antd' },
|
||||
React: { source: 'react', isDefault: true },
|
||||
}),
|
||||
).toEqual([
|
||||
{ sourcePath: '@music163/antd', specifiers: ['Button', 'Box'] },
|
||||
|
||||
@@ -3,6 +3,37 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.5.6](https://github.com/netease/tango/compare/@music163/tango-designer@0.5.5...@music163/tango-designer@0.5.6) (2023-11-23)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-designer
|
||||
|
||||
## [0.5.5](https://github.com/netease/tango/compare/@music163/tango-designer@0.5.4...@music163/tango-designer@0.5.5) (2023-11-20)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- pass down sandbox props ([d6ffb50](https://github.com/netease/tango/commit/d6ffb50e85573cb17ee52a797b84de5e6cedb044))
|
||||
|
||||
## [0.5.4](https://github.com/netease/tango/compare/@music163/tango-designer@0.5.3...@music163/tango-designer@0.5.4) (2023-11-20)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- refactor parse expression ([#61](https://github.com/netease/tango/issues/61)) ([dbbd1dd](https://github.com/netease/tango/commit/dbbd1dddc75c532b7c9710ab0941c8680100f093))
|
||||
|
||||
## [0.5.3](https://github.com/netease/tango/compare/@music163/tango-designer@0.5.2...@music163/tango-designer@0.5.3) (2023-11-16)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- allow override existing file via addFile ([6cf673e](https://github.com/netease/tango/commit/6cf673e7f6f330613e1d6331256a59900e8cbc68))
|
||||
|
||||
## [0.5.2](https://github.com/netease/tango/compare/@music163/tango-designer@0.5.1...@music163/tango-designer@0.5.2) (2023-11-02)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- limit outline tree node icon ([c5840f2](https://github.com/netease/tango/commit/c5840f29b724966930e239fe00ee13b2fb882503))
|
||||
- outline-panel tree expand bug & add props ([#52](https://github.com/netease/tango/issues/52)) ([b29e235](https://github.com/netease/tango/commit/b29e23599befbd60845053544648b10976a65824))
|
||||
- resizable-box support horizontal and vertical dragging ([#48](https://github.com/netease/tango/issues/48)) ([fd2fa94](https://github.com/netease/tango/commit/fd2fa943b05e9f1fa57574e703307725c5aed5d8))
|
||||
- update style of components panel ([11f166a](https://github.com/netease/tango/commit/11f166a2a67313200922a514a8219f34cfafd8ab))
|
||||
|
||||
## [0.5.1](https://github.com/netease/tango/compare/@music163/tango-designer@0.5.0...@music163/tango-designer@0.5.1) (2023-10-23)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-designer",
|
||||
"version": "0.5.1",
|
||||
"version": "0.5.6",
|
||||
"description": "lowcode designer",
|
||||
"keywords": [
|
||||
"react"
|
||||
@@ -32,13 +32,13 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^4.8.0",
|
||||
"@music163/request": "^0.1.0",
|
||||
"@music163/tango-context": "^0.2.3",
|
||||
"@music163/tango-core": "^0.2.2",
|
||||
"@music163/tango-helpers": "^0.1.4",
|
||||
"@music163/tango-sandbox": "^0.1.7",
|
||||
"@music163/tango-setting-form": "^0.2.6",
|
||||
"@music163/tango-ui": "^0.1.7",
|
||||
"@music163/request": "^0.1.2",
|
||||
"@music163/tango-context": "^0.2.7",
|
||||
"@music163/tango-core": "^0.2.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",
|
||||
"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",
|
||||
|
||||
@@ -372,11 +372,11 @@ export function VariableTree({
|
||||
const isDeletable = node.showDeleteIcon ?? showDeleteIcon;
|
||||
return (
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center">
|
||||
<Text>
|
||||
<Text flex="1" truncated>
|
||||
{node.title}
|
||||
{node.type === 'function' && <FunctionOutlined />}
|
||||
</Text>
|
||||
<Box>
|
||||
<Box flex="0 0 72px" textAlign="right">
|
||||
{isDeletable && (
|
||||
<Popconfirm
|
||||
title="确认删除吗?该操作会导致引用此模型的代码报错,请谨慎操作!"
|
||||
|
||||
@@ -119,6 +119,7 @@ const PreviewSandbox = observer(
|
||||
startRoute,
|
||||
injectScript,
|
||||
sandboxType: 'preview',
|
||||
...rest,
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -166,6 +167,7 @@ const DesignSandbox = observer(
|
||||
startRoute,
|
||||
injectScript,
|
||||
sandboxType: 'design',
|
||||
...rest,
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -4,7 +4,6 @@ import { AutoComplete } from 'antd';
|
||||
import { ActionSelect } from '@music163/tango-ui';
|
||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||
import { useWorkspace, useWorkspaceData } from '@music163/tango-context';
|
||||
import { wrapCode } from '@music163/tango-helpers';
|
||||
import { ExpressionModal } from './expression-setter';
|
||||
|
||||
enum EventAction {
|
||||
@@ -84,7 +83,7 @@ export function EventSetter(props: EventSetterProps) {
|
||||
onCancel={() => setExpModalVisible(false)}
|
||||
onOk={(nextValue) => {
|
||||
setExpModalVisible(false);
|
||||
handleChange(wrapCode(nextValue));
|
||||
handleChange(`{${nextValue}}`);
|
||||
}}
|
||||
dataSource={actionVariables}
|
||||
/>
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { Box, Text, css } from 'coral-system';
|
||||
import { Modal } from 'antd';
|
||||
import { isValidExpressionCode, value2expressionCode } from '@music163/tango-core';
|
||||
import {
|
||||
isVariableString,
|
||||
getVariableContent,
|
||||
noop,
|
||||
useBoolean,
|
||||
getValue,
|
||||
wrapCode,
|
||||
} from '@music163/tango-helpers';
|
||||
isValidExpressionCode,
|
||||
isWrappedByExpressionContainer,
|
||||
value2expressionCode,
|
||||
} from '@music163/tango-core';
|
||||
import { getVariableContent, noop, useBoolean, getValue } from '@music163/tango-helpers';
|
||||
import { CloseCircleFilled, ExpandAltOutlined } from '@ant-design/icons';
|
||||
import { IconButton, Panel, InputCode } from '@music163/tango-ui';
|
||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||
@@ -18,7 +15,7 @@ import { EditableVariableTree, IVariableTreeNode } from '../components';
|
||||
import { useSandboxQuery } from '../context';
|
||||
|
||||
export const expressionValueValidate = (value: string) => {
|
||||
if (isVariableString(value)) {
|
||||
if (isWrappedByExpressionContainer(value)) {
|
||||
const exp = getVariableContent(value);
|
||||
if (!isValidExpressionCode(exp)) {
|
||||
return '表达式存在语法错误!';
|
||||
@@ -27,7 +24,7 @@ export const expressionValueValidate = (value: string) => {
|
||||
};
|
||||
|
||||
export const jsonValueValidate = (value: string) => {
|
||||
if (isVariableString(value)) {
|
||||
if (isWrappedByExpressionContainer(value)) {
|
||||
const jsonStr = getVariableContent(value);
|
||||
try {
|
||||
JSON.parse(jsonStr);
|
||||
@@ -78,15 +75,25 @@ export function ExpressionSetter(props: ExpressionSetterProps) {
|
||||
|
||||
const change = useCallback(
|
||||
(code: string) => {
|
||||
if (!code) {
|
||||
onChange(undefined);
|
||||
return;
|
||||
}
|
||||
if (getVariableContent(code) === value2expressionCode(valueProp)) {
|
||||
if (code === valueProp) {
|
||||
return;
|
||||
}
|
||||
|
||||
onChange(wrapCode(code));
|
||||
let ret;
|
||||
|
||||
if (!code) {
|
||||
// do nothing
|
||||
} else if (isWrappedByExpressionContainer(code)) {
|
||||
ret = code;
|
||||
} else {
|
||||
ret = `{${code}}`;
|
||||
}
|
||||
|
||||
if (ret === valueProp) {
|
||||
return;
|
||||
}
|
||||
|
||||
onChange(ret);
|
||||
},
|
||||
[valueProp, onChange],
|
||||
);
|
||||
@@ -189,15 +196,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>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -6,13 +6,13 @@ import { PlayCircleOutlined } from '@ant-design/icons';
|
||||
import { Form, InputCode, Panel, JsonView, Search } from '@music163/tango-ui';
|
||||
import {
|
||||
isNil,
|
||||
isVariableString,
|
||||
getVariableContent,
|
||||
isValidFunctionCode,
|
||||
logger,
|
||||
code2object,
|
||||
filterTreeData,
|
||||
} from '@music163/tango-helpers';
|
||||
import { isWrappedByExpressionContainer } from '@music163/tango-core';
|
||||
import { useSandboxQuery } from '../../context';
|
||||
import { VariableTree } from '../../components';
|
||||
|
||||
@@ -187,7 +187,10 @@ const DataSourceView = observer(({ onAdd, onUpdate, onDelete }: DataServiceViewP
|
||||
const shapeValues = { ...val };
|
||||
delete shapeValues.type;
|
||||
// 兼容旧版,如果 formatter 包裹了 {} 则删掉首尾
|
||||
if (shapeValues.formatter && isVariableString(shapeValues.formatter)) {
|
||||
if (
|
||||
shapeValues.formatter &&
|
||||
isWrappedByExpressionContainer(shapeValues.formatter)
|
||||
) {
|
||||
shapeValues.formatter = getVariableContent(shapeValues.formatter);
|
||||
}
|
||||
return shapeValues;
|
||||
@@ -275,7 +278,7 @@ function ServiceDetailForm({
|
||||
{...formProps}
|
||||
>
|
||||
<Form.Item
|
||||
label="所属模块"
|
||||
label="命名空间"
|
||||
name="moduleName"
|
||||
rules={[
|
||||
{
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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();
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -3,6 +3,29 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
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)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-helpers",
|
||||
"version": "0.1.4",
|
||||
"version": "0.1.8",
|
||||
"description": "Shared types, helpers, and hooks of tango-apps",
|
||||
"keywords": [
|
||||
"shared",
|
||||
|
||||
@@ -182,6 +182,8 @@ const templatePattern = /^{(.+)}$/s;
|
||||
|
||||
/**
|
||||
* 判断给定字符串是否为变量字符串
|
||||
* @deprecated 使用 isWrappedByExpressionContainer 代替
|
||||
*
|
||||
* @example {[]}
|
||||
* @example {{}}
|
||||
* @example {this.foo}
|
||||
@@ -191,6 +193,7 @@ const templatePattern = /^{(.+)}$/s;
|
||||
*/
|
||||
export function isVariableString(str: string) {
|
||||
// 先检查是否是简单的对象
|
||||
// FIXME: 这里有问题,如果代码中有引用,会被误判
|
||||
if (code2object(str)) {
|
||||
return false;
|
||||
}
|
||||
@@ -205,6 +208,8 @@ export function isVariableString(str: string) {
|
||||
* @example "hello" => {"hello"}
|
||||
* @example () => {} => {() => {}}
|
||||
*
|
||||
* @deprecated 有问题,不要使用
|
||||
*
|
||||
* @param code 输入代码
|
||||
* @returns 加上花括号后的代码
|
||||
*/
|
||||
@@ -238,17 +243,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 +275,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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -33,94 +33,11 @@ export type OptionType = {
|
||||
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 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 };
|
||||
|
||||
/**
|
||||
@@ -163,63 +80,3 @@ export type SetterOnChangeDetailType = {
|
||||
relatedImports?: string[];
|
||||
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
|
||||
*/
|
||||
|
||||
@@ -15,8 +15,8 @@ export type PartialRecord<K extends keyof any, T> = {
|
||||
[P in K]?: T;
|
||||
};
|
||||
|
||||
export type ReactComponentProps = {
|
||||
export interface ReactComponentProps {
|
||||
children?: React.ReactNode;
|
||||
style?: React.CSSProperties;
|
||||
className?: string;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -3,6 +3,24 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [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)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-sandbox
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-sandbox",
|
||||
"version": "0.1.7",
|
||||
"version": "0.1.11",
|
||||
"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.6",
|
||||
"@music163/tango-helpers": "^0.1.8",
|
||||
"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",
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -504,6 +504,7 @@ export interface CodeSandboxProps {
|
||||
isFile: (path: string) => Promise<boolean>;
|
||||
readFile: (path: string) => Promise<string>;
|
||||
};
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
export interface ViteSandboxProps {
|
||||
|
||||
@@ -3,6 +3,24 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [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)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-setting-form",
|
||||
"version": "0.2.6",
|
||||
"version": "0.2.10",
|
||||
"description": "setting form of tango-apps",
|
||||
"author": "wwsun <ww.sun@outlook.com>",
|
||||
"homepage": "",
|
||||
@@ -28,8 +28,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^4.8.0",
|
||||
"@music163/tango-helpers": "^0.1.4",
|
||||
"@music163/tango-ui": "^0.1.7",
|
||||
"@music163/tango-core": "^0.2.6",
|
||||
"@music163/tango-helpers": "^0.1.8",
|
||||
"@music163/tango-ui": "^0.1.11",
|
||||
"antd": "^4.24.2",
|
||||
"coral-system": "^1.0.5",
|
||||
"mobx": "6.9.0",
|
||||
|
||||
@@ -4,10 +4,10 @@ import { observer } from 'mobx-react-lite';
|
||||
import {
|
||||
clone,
|
||||
ComponentPropType,
|
||||
isVariableString,
|
||||
SetterOnChangeDetailType,
|
||||
useBoolean,
|
||||
} from '@music163/tango-helpers';
|
||||
import { isWrappedByExpressionContainer } from '@music163/tango-core';
|
||||
import { IconFont, ToggleButton } from '@music163/tango-ui';
|
||||
import { QuestionCircleOutlined } from '@ant-design/icons';
|
||||
import { InputProps, Tooltip } from 'antd';
|
||||
@@ -83,7 +83,7 @@ export function createFormItem(options: IFormItemCreateOptions) {
|
||||
const value = toJS(field.value ?? defaultValue);
|
||||
const disableVariableSetter = disableSwitchExpressionSetter ?? disableVariableSetterProp;
|
||||
const [isVariable, { toggle: toggleIsVariable }] = useBoolean(
|
||||
() => !disableVariableSetter && isVariableString(value),
|
||||
() => !disableVariableSetter && isWrappedByExpressionContainer(value),
|
||||
);
|
||||
|
||||
const setterName = isVariable ? 'expressionSetter' : setter;
|
||||
|
||||
@@ -3,6 +3,24 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
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)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-ui",
|
||||
"version": "0.1.7",
|
||||
"version": "0.1.11",
|
||||
"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.8",
|
||||
"@uiw/react-codemirror": "^4.21.20",
|
||||
"antd": "^4.24.2",
|
||||
"antd-easy-form": "^1.1.3",
|
||||
"coral-system": "^1.0.5",
|
||||
|
||||
@@ -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
@@ -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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user