mirror of
https://github.com/NetEase/tango.git
synced 2026-09-01 04:19:05 +08:00
Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 73aded3672 | |||
| 8c07821d93 | |||
| 765ea07cd5 | |||
| a27c241d9c | |||
| 4d6b67eecd | |||
| 6fc889a161 | |||
| c07b554360 | |||
| 149002dddd | |||
| 45b0d856c1 | |||
| 6fc0498901 | |||
| f2919c1aa0 | |||
| 5dd5ab62a8 | |||
| 5e7e285452 | |||
| b3a458a914 | |||
| 1acfa6864b | |||
| baf071f130 | |||
| 6b1d55a11f | |||
| a8357f45b8 | |||
| 9db6358588 | |||
| 5bc2fb2d5a | |||
| 05a0f8f490 | |||
| c884712331 | |||
| e35ff792a0 | |||
| 138cbe9b20 | |||
| 979aa34259 | |||
| 76a5a2c659 | |||
| 53ebd89c9a | |||
| 3980810502 | |||
| accd826376 | |||
| 5ca1118f88 | |||
| 14cd4c54a1 | |||
| 7f46137c56 | |||
| 1db52948db | |||
| 2550411c1c | |||
| 640859d36e | |||
| d91763379c | |||
| 588861f649 | |||
| 91b53da38e | |||
| 9b0ada94a0 | |||
| f604996e56 |
@@ -45,18 +45,18 @@ gantt
|
||||
%% (`excludes` accepts specific dates in YYYY-MM-DD format, days of the week ("sunday") or "weekends", but not the word "weekdays".)
|
||||
|
||||
section Builder Release
|
||||
Alpha version :active, des1, 2023-08-10, 2023-08-30
|
||||
Beta version : des2, 2023-09-01, 2023-09-30
|
||||
1.0 RC : des3, after des2, 40d
|
||||
1.0 version : des4, after des3, 21d
|
||||
Alpha version :active, m1, 2023-08-10, 2023-08-30
|
||||
Beta version : m2, 2023-09-01, 2023-09-30
|
||||
1.0 RC : m3, 2023-10-01, 2023-12-15
|
||||
1.0 version : m4, after m3, 10d
|
||||
```
|
||||
|
||||
## 💻 Development
|
||||
|
||||
### Recommended Development Environment
|
||||
### Environment
|
||||
|
||||
- Node.js >= 16.0.0
|
||||
- Yarn >= 1.22.0
|
||||
- Node `>= 18`
|
||||
- Yarn `>= 1.22 && < 2`
|
||||
|
||||
### Development Quick Start
|
||||
|
||||
|
||||
+6
-6
@@ -45,18 +45,18 @@ gantt
|
||||
%% (`excludes` accepts specific dates in YYYY-MM-DD format, days of the week ("sunday") or "weekends", but not the word "weekdays".)
|
||||
|
||||
section Builder Release
|
||||
Alpha version :active, des1, 2023-08-10, 2023-08-30
|
||||
Beta version : des2, 2023-09-01, 2023-09-30
|
||||
1.0 RC : des3, after des2, 40d
|
||||
1.0 version : des4, after des3, 21d
|
||||
Alpha version :active, m1, 2023-08-10, 2023-08-30
|
||||
Beta version : m2, 2023-09-01, 2023-09-30
|
||||
1.0 RC : m3, 2023-10-01, 2023-12-15
|
||||
1.0 version : m4, after m3, 10d
|
||||
```
|
||||
|
||||
## 💻 开发
|
||||
|
||||
### 推荐开发环境
|
||||
|
||||
- Node.js >= 16.0.0
|
||||
- Yarn >= 1.22.0
|
||||
- Node `>= 18`
|
||||
- Yarn `>= 1.22 && < 2`
|
||||
|
||||
### 本地开发调试方法
|
||||
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
PORT=6006
|
||||
HOST=local.netease.com
|
||||
@@ -0,0 +1,9 @@
|
||||
/node_modules
|
||||
/.env.local
|
||||
/.umirc.local.ts
|
||||
/config/config.local.ts
|
||||
/src/.umi
|
||||
/src/.umi-production
|
||||
/src/.umi-test
|
||||
/dist
|
||||
.swc
|
||||
@@ -1,33 +1,17 @@
|
||||
import path from 'path';
|
||||
import { defineConfig } from 'umi';
|
||||
|
||||
const resolvePackageIndex = (relativeEntry: string) =>
|
||||
path.resolve(__dirname, '../../../packages/', relativeEntry);
|
||||
path.resolve(__dirname, '../../packages/', relativeEntry);
|
||||
|
||||
export default {
|
||||
export default defineConfig({
|
||||
routes: [
|
||||
{
|
||||
exact: true,
|
||||
path: '/',
|
||||
component: 'index',
|
||||
name: '首页',
|
||||
},
|
||||
{ path: '/', component: 'index' },
|
||||
{ path: '/docs', component: 'docs' },
|
||||
],
|
||||
devServer: {
|
||||
host: 'local.netease.com',
|
||||
port: 8008,
|
||||
https: {
|
||||
key: path.resolve(__dirname, 'local.netease.com-key.pem'),
|
||||
cert: path.resolve(__dirname, 'local.netease.com.pem'),
|
||||
},
|
||||
headers: { 'Origin-Agent-Cluster': '?0' },
|
||||
},
|
||||
targets: {
|
||||
chrome: 79,
|
||||
firefox: false,
|
||||
safari: false,
|
||||
edge: false,
|
||||
ios: false,
|
||||
},
|
||||
npmClient: 'yarn',
|
||||
mfsu: false,
|
||||
codeSplitting: false,
|
||||
alias: {
|
||||
'@music163/tango-helpers': resolvePackageIndex('helpers/src/index.ts'),
|
||||
'@music163/tango-core': resolvePackageIndex('core/src/index.ts'),
|
||||
@@ -44,6 +28,21 @@ export default {
|
||||
moment: 'moment',
|
||||
antd: 'antd',
|
||||
},
|
||||
headScripts: [
|
||||
'https://cdn.jsdelivr.net/npm/react@18.2.0/umd/react.development.js',
|
||||
'https://cdn.jsdelivr.net/npm/react-dom@18.2.0/umd/react-dom.development.js',
|
||||
'https://cdn.jsdelivr.net/npm/react-is@18.2.0/umd/react-is.production.min.js',
|
||||
'https://cdn.jsdelivr.net/npm/moment/min/moment-with-locales.js',
|
||||
'https://cdn.jsdelivr.net/npm/styled-components@5.3.11/dist/styled-components.js',
|
||||
'https://cdn.jsdelivr.net/npm/antd@4.24.13/dist/antd-with-locales.min.js',
|
||||
'https://cdn.jsdelivr.net/npm/prettier@2.6.0/standalone.js',
|
||||
'https://cdn.jsdelivr.net/npm/prettier@2.6.0/parser-babel.js',
|
||||
],
|
||||
https: {
|
||||
key: path.resolve(__dirname, 'local.netease.com-key.pem'),
|
||||
cert: path.resolve(__dirname, 'local.netease.com.pem'),
|
||||
http2: false,
|
||||
},
|
||||
chainWebpack: (config: any) => {
|
||||
// @see https://github.com/graphql/graphql-js/issues/1272#issuecomment-393903706
|
||||
config.module
|
||||
@@ -61,4 +60,5 @@ export default {
|
||||
config.module.rule('js').include.add(resolvePackageIndex('ui/src'));
|
||||
return config;
|
||||
},
|
||||
};
|
||||
monorepoRedirect: {},
|
||||
});
|
||||
@@ -1,5 +0,0 @@
|
||||
# `playground`
|
||||
|
||||
搭建平台演示示例
|
||||
|
||||
## 说明
|
||||
@@ -2,24 +2,23 @@
|
||||
"name": "playground",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"description": "playground of tango-apps",
|
||||
"license": "MIT",
|
||||
"author": "wwsun <ww.sun@outlook.com>",
|
||||
"files": [
|
||||
"lib"
|
||||
],
|
||||
"author": "Wells <ww.sun@outlook.com>",
|
||||
"scripts": {
|
||||
"build": "COMPRESS=none umi build",
|
||||
"start": "umi dev"
|
||||
"dev": "HOST=local.netease.com PORT=8001 umi dev",
|
||||
"build": "umi build",
|
||||
"postinstall": "umi setup",
|
||||
"setup": "umi setup",
|
||||
"start": "npm run dev"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^4.8.0",
|
||||
"@music163/tango-designer": "*",
|
||||
"@music163/tango-helpers": "*",
|
||||
"antd": "^4.24.2",
|
||||
"coral-system": "^1.0.5"
|
||||
"coral-system": "^1.0.5",
|
||||
"umi": "^4.0.89"
|
||||
},
|
||||
"devDependencies": {
|
||||
"umi": "^3.5.24"
|
||||
"@types/react": "^18.0.33",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"typescript": "^5.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { IApi } from 'umi';
|
||||
|
||||
export default (api: IApi) => {
|
||||
api.addMiddlewares(() => {
|
||||
return (req, res, next) => {
|
||||
res.setHeader('Origin-Agent-Cluster', '?0');
|
||||
next();
|
||||
};
|
||||
});
|
||||
};
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 177 KiB |
@@ -6,29 +6,30 @@ import { registerSetter } from '@music163/tango-designer';
|
||||
import type { ComponentPrototypeType } from '@music163/tango-helpers';
|
||||
import { FooSetter } from '../components';
|
||||
|
||||
export * from './mock-files';
|
||||
|
||||
export const bootHelperVariables = [
|
||||
{
|
||||
key: '$helpers',
|
||||
title: '工具函数',
|
||||
children: [
|
||||
{
|
||||
title: 'setStoreValue',
|
||||
title: '设置变量值',
|
||||
key: '() => tango.setStoreValue("variableName", "variableValue")',
|
||||
type: 'function',
|
||||
help: '设置状态值,tango.setStoreValue("variableName", "variableValue")',
|
||||
},
|
||||
{
|
||||
title: 'getStoreValue',
|
||||
title: '获取变量值',
|
||||
key: '() => tango.getStoreValue("variableName")',
|
||||
type: 'function',
|
||||
help: '获取状态值',
|
||||
},
|
||||
{ title: 'openModal', key: '() => tango.openModal("")', type: 'function' },
|
||||
{ title: 'closeModal', key: '() => tango.closeModal("")', type: 'function' },
|
||||
{ title: 'navigateTo', key: '() => tango.navigateTo("/")', type: 'function' },
|
||||
{ title: 'showToast', key: '() => tango.showToast("hello")', type: 'function' },
|
||||
{ title: 'formatDate', key: '() => tango.formatDate("2022-12-12")', type: 'function' },
|
||||
{ title: 'formatNumber', key: '() => tango.formatDate(9999)', type: 'function' },
|
||||
{ title: '打开弹窗', key: '() => tango.openModal("")', type: 'function' },
|
||||
{ title: '关闭弹窗', key: '() => tango.closeModal("")', type: 'function' },
|
||||
{ title: '切换路由', key: '() => tango.navigateTo("/")', type: 'function' },
|
||||
{
|
||||
title: 'copyToClipboard',
|
||||
title: '拷贝到剪贴板',
|
||||
key: '() => tango.copyToClipboard("hello")',
|
||||
type: 'function',
|
||||
},
|
||||
@@ -75,14 +75,17 @@ const tangoConfigJson = {
|
||||
},
|
||||
};
|
||||
|
||||
const appJson: any = {
|
||||
pages: [
|
||||
{
|
||||
path: '/',
|
||||
name: '首页',
|
||||
},
|
||||
],
|
||||
};
|
||||
const helperCode = `
|
||||
export function registerComponentPrototype(proto) {
|
||||
if (!proto) return;
|
||||
if (!window.localTangoComponentPrototypes) {
|
||||
window.localTangoComponentPrototypes = {};
|
||||
}
|
||||
if (proto.name) {
|
||||
window.localTangoComponentPrototypes[proto.name] = proto;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const routesCode = `
|
||||
import Index from "./pages/list";
|
||||
@@ -152,6 +155,8 @@ import {
|
||||
FormilyForm,
|
||||
} from "@music163/antd";
|
||||
import { Space } from '@music163/antd';
|
||||
import { LocalButton } from '../components';
|
||||
|
||||
class App extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
@@ -160,6 +165,7 @@ class App extends React.Component {
|
||||
</Section>
|
||||
<Section>
|
||||
<Space>
|
||||
<LocalButton />
|
||||
<Button>button</Button>
|
||||
<Input />
|
||||
</Space>
|
||||
@@ -168,9 +174,53 @@ class App extends React.Component {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default definePage(App);
|
||||
`;
|
||||
|
||||
const componentsButtonCode = `
|
||||
import React from 'react';
|
||||
import { registerComponentPrototype } from '../utils';
|
||||
|
||||
export default function MyButton(props) {
|
||||
return <button {...props}>my button</button>
|
||||
}
|
||||
|
||||
registerComponentPrototype({
|
||||
name: 'LocalButton',
|
||||
title: 'Local Button',
|
||||
exportType: 'namedExport',
|
||||
package: '/src/components',
|
||||
props: [
|
||||
{ name: 'background', title: '背景色', setter: 'colorSetter' },
|
||||
],
|
||||
});
|
||||
`;
|
||||
|
||||
const componentsInputCode = `
|
||||
import React from 'react';
|
||||
import { registerComponentPrototype } from '../utils';
|
||||
|
||||
export default function MyInput(props) {
|
||||
return <input {...props} />;
|
||||
}
|
||||
|
||||
registerComponentPrototype({
|
||||
name: 'LocalInput',
|
||||
title: 'Local Input',
|
||||
exportType: 'namedExport',
|
||||
package: '/src/components',
|
||||
props: [
|
||||
{ name: 'color', title: '文本色', setter: 'colorSetter' },
|
||||
],
|
||||
});
|
||||
`;
|
||||
|
||||
const componentsEntryCode = `
|
||||
export { default as LocalButton } from './button';
|
||||
export { default as LocalInput } from './input';
|
||||
`;
|
||||
|
||||
const storeApp = `
|
||||
import { defineStore } from '@music163/tango-boot';
|
||||
|
||||
@@ -179,6 +229,35 @@ export default defineStore({
|
||||
title: 'Page Title',
|
||||
|
||||
array: [1, 2, 3],
|
||||
|
||||
test: function() {
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
}
|
||||
}, 'app');
|
||||
`;
|
||||
|
||||
@@ -187,6 +266,13 @@ import { defineServices } from '@music163/tango-boot';
|
||||
import './sub';
|
||||
|
||||
export default defineServices({
|
||||
longLongLongLongLongLongLongLongGet: {
|
||||
url: 'https://nei.hz.netease.com/api/apimock-v2/cc974ffbaa7a85c77f30e4ce67deb67f/api/getUserProfile',
|
||||
formatter: res => res.data,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
},
|
||||
get: {
|
||||
url: 'https://nei.hz.netease.com/api/apimock-v2/cc974ffbaa7a85c77f30e4ce67deb67f/api/getUserProfile',
|
||||
formatter: res => res.data,
|
||||
@@ -242,20 +328,22 @@ p {
|
||||
|
||||
export const sampleFiles = [
|
||||
{ filename: '/package.json', code: JSON.stringify(packageJson) },
|
||||
{ filename: '/appJson.json', code: JSON.stringify(appJson) },
|
||||
{ filename: '/tango.config.json', code: JSON.stringify(tangoConfigJson) },
|
||||
{ filename: '/README.md', code: '# readme' },
|
||||
{ filename: '/src/index.less', code: lessCode },
|
||||
{ filename: '/src/style.css', code: cssCode },
|
||||
{ filename: '/src/index.js', code: entryCode },
|
||||
{ filename: '/src/pages/list.js', code: viewHomeCode },
|
||||
{ filename: '/src/components/button.js', code: componentsButtonCode },
|
||||
{ filename: '/src/components/input.js', code: componentsInputCode },
|
||||
{ filename: '/src/components/index.js', code: componentsEntryCode },
|
||||
{ filename: '/src/routes.js', code: routesCode },
|
||||
{ filename: '/src/stores/index.js', code: storeIndexCode },
|
||||
{ filename: '/src/stores/app.js', code: storeApp },
|
||||
{ filename: '/src/stores/counter.js', code: storeCounter },
|
||||
{ filename: '/src/services/index.js', code: serviceCode },
|
||||
{ filename: '/src/services/sub.js', code: subServiceCode },
|
||||
{ filename: '/src/utils/index.js', code: `export function foo() {}` },
|
||||
{ filename: '/src/utils/index.js', code: helperCode },
|
||||
];
|
||||
|
||||
export const genDefaultPage = (index: number) => ({
|
||||
@@ -2,4 +2,5 @@
|
||||
@import '~antd/dist/antd.less'; // 引入官方提供的 less 样式入口文件
|
||||
|
||||
@primary-color: #2f54eb;
|
||||
@border-radius-base: 2px;
|
||||
@border-radius-base: 2px;
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
import { Outlet } from 'umi';
|
||||
import './index.less';
|
||||
|
||||
export default function Layout() {
|
||||
return (
|
||||
<div>
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
const DocsPage = () => {
|
||||
return (
|
||||
<div>
|
||||
<p>This is umi docs.</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DocsPage;
|
||||
@@ -1,21 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Tango Playground</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script src="https://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>
|
||||
@@ -12,11 +12,10 @@ import {
|
||||
CodeEditor,
|
||||
Sandbox,
|
||||
DndQuery,
|
||||
themeLight,
|
||||
} from '@music163/tango-designer';
|
||||
import { createEngine, Workspace } from '@music163/tango-core';
|
||||
import { Logo, ProjectDetail, bootHelperVariables } from './share';
|
||||
import { sampleFiles } from '../mock/project';
|
||||
import './index.less';
|
||||
import { Logo, ProjectDetail, bootHelperVariables, sampleFiles } from '../helpers';
|
||||
import {
|
||||
ApiOutlined,
|
||||
AppstoreAddOutlined,
|
||||
@@ -34,7 +33,6 @@ const workspace = new Workspace({
|
||||
// 2. 引擎初始化
|
||||
const engine = createEngine({
|
||||
workspace,
|
||||
defaultActiveSidebarPanel: 'outline',
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
@@ -52,6 +50,7 @@ export default function App() {
|
||||
const [menuData, setMenuData] = useState(false);
|
||||
return (
|
||||
<Designer
|
||||
theme={themeLight}
|
||||
engine={engine}
|
||||
config={{
|
||||
customActionVariables: bootHelperVariables,
|
||||
@@ -66,6 +65,7 @@ export default function App() {
|
||||
<Box px="l">
|
||||
<Toolbar>
|
||||
<Toolbar.Item key="routeSwitch" placement="left" />
|
||||
<Toolbar.Item key="history" placement="left" />
|
||||
<Toolbar.Item key="preview" placement="left" />
|
||||
<Toolbar.Item key="modeSwitch" placement="right" />
|
||||
<Toolbar.Item key="togglePanel" placement="right" />
|
||||
@@ -80,7 +80,6 @@ export default function App() {
|
||||
}
|
||||
>
|
||||
<Sidebar>
|
||||
<Sidebar.Item key="outline" label="结构" icon={<BuildOutlined />} />
|
||||
<Sidebar.Item
|
||||
key="components"
|
||||
label="组件"
|
||||
@@ -90,6 +89,7 @@ export default function App() {
|
||||
loading: menuLoading,
|
||||
}}
|
||||
/>
|
||||
<Sidebar.Item key="outline" label="结构" icon={<BuildOutlined />} />
|
||||
<Sidebar.Item
|
||||
key="variables"
|
||||
label="变量"
|
||||
@@ -120,6 +120,9 @@ export default function App() {
|
||||
workspace.setComponentPrototypes(sandboxWindow.TangoAntd.prototypes);
|
||||
}
|
||||
}
|
||||
if (sandboxWindow.localTangoComponentPrototypes) {
|
||||
workspace.setComponentPrototypes(sandboxWindow.localTangoComponentPrototypes);
|
||||
}
|
||||
setMenuLoading(false);
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json"
|
||||
"extends": "./src/.umi/tsconfig.json"
|
||||
}
|
||||
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
import 'umi/typings';
|
||||
@@ -16,8 +16,8 @@
|
||||
"dependencies": {
|
||||
"@music163/tango-setting-form": "*",
|
||||
"@music163/tango-ui": "*",
|
||||
"mobx": "6.9.0",
|
||||
"mobx-react-lite": "4.0.2"
|
||||
"mobx": "6.12.0",
|
||||
"mobx-react-lite": "4.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@storybook/addon-actions": "^6.5.14",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
import { EditableVariableTree, FormModel, SettingForm } from '@music163/tango-setting-form';
|
||||
import { ComponentPrototypeType, getValue } from '@music163/tango-helpers';
|
||||
import { createServices } from '@music163/request';
|
||||
import React from 'react';
|
||||
import { FormModel, SettingForm } from '@music163/tango-setting-form';
|
||||
import { ComponentPrototypeType } from '@music163/tango-helpers';
|
||||
import { Box } from 'coral-system';
|
||||
import { JsonView } from '@music163/tango-ui';
|
||||
import { toJS } from 'mobx';
|
||||
@@ -9,51 +8,9 @@ import { observer } from 'mobx-react-lite';
|
||||
import { Card } from 'antd';
|
||||
|
||||
export default {
|
||||
title: 'SettingForm/ Setters',
|
||||
title: 'SettingForm',
|
||||
};
|
||||
|
||||
const modelVariables = [
|
||||
{
|
||||
title: 'stores',
|
||||
key: 'stores',
|
||||
selectable: false,
|
||||
children: [
|
||||
{
|
||||
title: 'app',
|
||||
key: 'stores.app',
|
||||
selectable: false,
|
||||
children: [
|
||||
{ title: 'app.title', key: 'stores.app.title', raw: '"hello"' },
|
||||
{ title: 'app.age', key: 'stores.app.age', raw: '20' },
|
||||
{ title: 'app.detail', key: 'stores.app.detail', raw: '{ foo: "foo" }' },
|
||||
{ title: 'app.list', key: 'stores.app.list', type: 'function', raw: '() => {}' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'user',
|
||||
key: 'stores.user',
|
||||
selectable: false,
|
||||
children: [
|
||||
{ title: 'name', key: 'stores.user.name' },
|
||||
{ title: 'age', key: 'stores.user.age' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'services',
|
||||
key: 'services',
|
||||
selectable: false,
|
||||
children: [
|
||||
{
|
||||
title: 'listUsers',
|
||||
key: 'services.listUsers',
|
||||
type: 'function',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const prototype: ComponentPrototypeType = {
|
||||
name: 'Test',
|
||||
exportType: 'namedExport',
|
||||
@@ -257,47 +214,6 @@ const prototype: ComponentPrototypeType = {
|
||||
],
|
||||
};
|
||||
|
||||
const deerService = createServices(
|
||||
{
|
||||
listMy: {
|
||||
url: '/my/upload/list',
|
||||
},
|
||||
listFav: {
|
||||
url: '/my/star/list',
|
||||
},
|
||||
listPub: {
|
||||
url: '/list',
|
||||
},
|
||||
},
|
||||
{
|
||||
baseURL: 'https://febase-openapi.fn.netease.com/deer/api/deer/pic',
|
||||
withCredentials: false, // 解决跨域时必须非*问题
|
||||
headers: {
|
||||
'Febase-Auth': 'dskPVkIRnQ2dEn1DbyxURmUj4rl4BsCh53xFAsJnvVs=',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const context = {
|
||||
stores: {
|
||||
app: {
|
||||
title: 'Sample App',
|
||||
age: 20,
|
||||
detail: {
|
||||
foo: 'foo',
|
||||
bar: 'bar',
|
||||
biz: {
|
||||
x: 'xxx',
|
||||
},
|
||||
},
|
||||
newKey: 'xxx',
|
||||
},
|
||||
foo: {
|
||||
test: 'test string',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 表单值预览
|
||||
*/
|
||||
@@ -310,9 +226,7 @@ export function Basic() {
|
||||
const model = new FormModel(
|
||||
{
|
||||
router: 'www.163.com',
|
||||
expression: {
|
||||
foo: 'foo',
|
||||
},
|
||||
expression: `{ foo: 'foo' }`,
|
||||
object: {
|
||||
name: 'Alice',
|
||||
},
|
||||
@@ -324,19 +238,7 @@ export function Basic() {
|
||||
|
||||
return (
|
||||
<Box display="flex">
|
||||
<SettingForm
|
||||
model={model}
|
||||
remoteServices={{
|
||||
ImageService: deerService as any,
|
||||
}}
|
||||
prototype={prototype}
|
||||
evaluateContext={{
|
||||
tango: context,
|
||||
__UNSAFE_TANGO_CURRENT_PAGE_RENDER_RUNTIME__: { routeData: { params: {}, query: {} } },
|
||||
}}
|
||||
modelVariables={modelVariables}
|
||||
expressionVariables={modelVariables}
|
||||
/>
|
||||
<SettingForm model={model} prototype={prototype} />
|
||||
<Box position="relative">
|
||||
<Card title="表单状态预览" style={{ position: 'sticky', top: 0 }}>
|
||||
<FormValuePreview model={model} />
|
||||
@@ -346,6 +248,14 @@ export function Basic() {
|
||||
);
|
||||
}
|
||||
|
||||
export function Lite() {
|
||||
return (
|
||||
<Box width={320} border="solid">
|
||||
<SettingForm showSearch={false} showGroups={false} prototype={prototype} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export function NoExpressionSwitch() {
|
||||
const model = new FormModel({});
|
||||
return (
|
||||
@@ -354,30 +264,3 @@ export function NoExpressionSwitch() {
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export function Binding() {
|
||||
const [data, setData] = useState<any>({});
|
||||
return (
|
||||
<Box>
|
||||
<Box as="code" bg="highlight" color="white" fontSize="24px">
|
||||
selected: {data?.key}
|
||||
</Box>
|
||||
<EditableVariableTree
|
||||
dataSource={modelVariables as any}
|
||||
getPreviewValue={(node) => {
|
||||
if (!node || !node.key) {
|
||||
return;
|
||||
}
|
||||
if (node.type === 'function') {
|
||||
return node.raw;
|
||||
}
|
||||
const keyPath = node.key.replaceAll('?', '');
|
||||
return getValue(context, keyPath);
|
||||
}}
|
||||
onSelect={setData}
|
||||
onAddVariable={console.log}
|
||||
onSave={console.log}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -28,6 +28,41 @@ export function Basic() {
|
||||
);
|
||||
}
|
||||
|
||||
const code = `
|
||||
function foo() {
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
console.log("test");
|
||||
}
|
||||
`;
|
||||
|
||||
export function Readonly() {
|
||||
return <InputCode shape="inset" readOnly value={code} />;
|
||||
}
|
||||
|
||||
export function Inset() {
|
||||
return <InputCode shape="inset" />;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { ToggleButton, IconFont } from '@music163/tango-ui';
|
||||
import { Box, Flex } from 'coral-system';
|
||||
import { ToggleButton, UndoOutlined, RedoOutlined } from '@music163/tango-ui';
|
||||
import { Box, Group, Flex } from 'coral-system';
|
||||
|
||||
export default {
|
||||
title: 'UI/ToggleButton',
|
||||
@@ -9,28 +9,73 @@ export default {
|
||||
export function Basic() {
|
||||
return (
|
||||
<Box>
|
||||
<Flex gap="l" bg="#efefef" p="l">
|
||||
<Box display="flex" columnGap="l" p="l">
|
||||
<ToggleButton>
|
||||
<IconFont type="icon-undo" />
|
||||
<UndoOutlined />
|
||||
</ToggleButton>
|
||||
<ToggleButton selected>
|
||||
<IconFont type="icon-redo" />
|
||||
<RedoOutlined />
|
||||
</ToggleButton>
|
||||
<ToggleButton disabled>
|
||||
<IconFont type="icon-redo" />
|
||||
<RedoOutlined />
|
||||
</ToggleButton>
|
||||
</Box>
|
||||
<Box display="flex" columnGap="l" p="l">
|
||||
<ToggleButton type="primary">
|
||||
<UndoOutlined />
|
||||
</ToggleButton>
|
||||
<ToggleButton type="primary" selected>
|
||||
<RedoOutlined />
|
||||
</ToggleButton>
|
||||
<ToggleButton type="primary" disabled>
|
||||
<RedoOutlined />
|
||||
</ToggleButton>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export function DarkMode() {
|
||||
return (
|
||||
<Box>
|
||||
<Flex gap="l" bg="#222" p="l">
|
||||
<ToggleButton shape="ghost">
|
||||
<UndoOutlined />
|
||||
</ToggleButton>
|
||||
<ToggleButton shape="ghost" selected>
|
||||
<RedoOutlined />
|
||||
</ToggleButton>
|
||||
<ToggleButton shape="ghost" disabled>
|
||||
<UndoOutlined />
|
||||
</ToggleButton>
|
||||
</Flex>
|
||||
<Flex gap="l" bg="#222" p="l">
|
||||
<ToggleButton shape="ghost">
|
||||
<IconFont type="icon-undo" />
|
||||
<ToggleButton shape="text">
|
||||
<UndoOutlined />
|
||||
</ToggleButton>
|
||||
<ToggleButton shape="ghost" selected>
|
||||
<IconFont type="icon-redo" />
|
||||
<ToggleButton shape="text" selected>
|
||||
<RedoOutlined />
|
||||
</ToggleButton>
|
||||
<ToggleButton shape="ghost" disabled>
|
||||
<IconFont type="icon-undo" />
|
||||
<ToggleButton shape="text" disabled>
|
||||
<RedoOutlined />
|
||||
</ToggleButton>
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export function ButtonGroup() {
|
||||
return (
|
||||
<Group attached>
|
||||
<ToggleButton>
|
||||
<UndoOutlined />
|
||||
</ToggleButton>
|
||||
<ToggleButton selected>
|
||||
<RedoOutlined />
|
||||
</ToggleButton>
|
||||
<ToggleButton disabled>
|
||||
<RedoOutlined />
|
||||
</ToggleButton>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import React from 'react';
|
||||
import { VariableTree } from '@music163/tango-designer';
|
||||
import { Box } from 'coral-system';
|
||||
|
||||
export default {
|
||||
title: 'Designer/VariableTree',
|
||||
};
|
||||
|
||||
const data = [
|
||||
{
|
||||
title: 'Stores',
|
||||
key: 'stores',
|
||||
selectable: false,
|
||||
children: [
|
||||
{
|
||||
title: 'user',
|
||||
key: 'stores.user',
|
||||
children: [
|
||||
{
|
||||
title: 'name',
|
||||
key: 'stores.user.name',
|
||||
raw: '"Tom"',
|
||||
},
|
||||
{
|
||||
title: 'age',
|
||||
key: 'stores.user.age',
|
||||
raw: '18',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'book',
|
||||
key: 'stores.book',
|
||||
children: [
|
||||
{
|
||||
title: 'name',
|
||||
key: 'stores.book.name',
|
||||
raw: '"JavaScript 高级程序设计"',
|
||||
},
|
||||
{
|
||||
title: 'price',
|
||||
key: 'stores.book.price',
|
||||
raw: '99.99',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Services',
|
||||
key: 'services',
|
||||
selectable: false,
|
||||
children: [
|
||||
{
|
||||
title: 'add',
|
||||
key: 'services.add',
|
||||
},
|
||||
{
|
||||
title: 'multiply',
|
||||
key: 'services.multiply',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export function Basic() {
|
||||
return (
|
||||
<Box width={600} border="solid">
|
||||
<VariableTree dataSource={data} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
|
||||
"useWorkspaces": true,
|
||||
"version": "independent",
|
||||
"packages": ["packages/*"],
|
||||
"npmClient": "yarn",
|
||||
|
||||
+22
-18
@@ -6,7 +6,7 @@
|
||||
"apps/*"
|
||||
],
|
||||
"scripts": {
|
||||
"start": "yarn workspace playground start",
|
||||
"start": "yarn workspace playground dev",
|
||||
"start:docs": "yarn workspace storybook storybook",
|
||||
"start:site": "yarn workspace website start",
|
||||
"deploy:site": "yarn workspace website deploy",
|
||||
@@ -18,18 +18,19 @@
|
||||
"test:watch": "jest --watch",
|
||||
"eslint": "eslint packages/**/src/*.{ts,tsx} --cache",
|
||||
"publish": "lerna publish",
|
||||
"ver": "lerna version --no-private --conventional-commits",
|
||||
"release": "yarn eslint && yarn build && yarn run ver && lerna publish from-git",
|
||||
"ver": "lerna version --no-private",
|
||||
"release": "yarn eslint && yarn build && lerna version --no-private --conventional-commits && lerna publish from-git",
|
||||
"release:npm": "yarn eslint && yarn build && yarn run ver && lerna publish from-git",
|
||||
"release:beta": "yarn eslint && yarn build && yarn run ver && lerna publish from-package --dist-tag beta",
|
||||
"up": "yarn upgrade-interactive --latest",
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/preset-env": "^7.23.2",
|
||||
"@babel/preset-react": "^7.22.15",
|
||||
"@babel/preset-typescript": "^7.23.2",
|
||||
"@commitlint/cli": "^17.5.0",
|
||||
"@commitlint/config-conventional": "^17.3.0",
|
||||
"@babel/preset-env": "^7.23.6",
|
||||
"@babel/preset-react": "^7.23.3",
|
||||
"@babel/preset-typescript": "^7.23.3",
|
||||
"@commitlint/cli": "^18.4.3",
|
||||
"@commitlint/config-conventional": "^18.4.3",
|
||||
"@types/jest": "^29.5.7",
|
||||
"@types/lodash-es": "^4.17.10",
|
||||
"@types/lodash.get": "^4.4.8",
|
||||
@@ -38,13 +39,13 @@
|
||||
"@types/react": "^18.2.34",
|
||||
"@types/react-dom": "^18.2.14",
|
||||
"@types/styled-components": "^5.1.29",
|
||||
"@typescript-eslint/eslint-plugin": "^6.9.1",
|
||||
"@typescript-eslint/parser": "^6.9.1",
|
||||
"conventional-changelog-cli": "^2.2.2",
|
||||
"@typescript-eslint/eslint-plugin": "^6.13.2",
|
||||
"@typescript-eslint/parser": "^6.13.2",
|
||||
"conventional-changelog-cli": "^4.1.0",
|
||||
"copyfiles": "^2.4.1",
|
||||
"eslint": "^8.52.0",
|
||||
"eslint": "^8.55.0",
|
||||
"eslint-config-ali": "^14.0.2",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-import-resolver-typescript": "^3.6.1",
|
||||
"eslint-plugin-import": "^2.29.0",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
@@ -53,14 +54,17 @@
|
||||
"husky": "^8.0.3",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"lerna": "^6.6.1",
|
||||
"lint-staged": "^14.0.1",
|
||||
"prettier": "^2.8.7",
|
||||
"lerna": "^8.0.0",
|
||||
"lint-staged": "^15.2.0",
|
||||
"prettier": "^3.1.0",
|
||||
"react": "^17.0.0",
|
||||
"react-dom": "^17.0.0",
|
||||
"styled-components": "^5.3.6",
|
||||
"typedoc": "^0.25.3",
|
||||
"typescript": "^5.2.2"
|
||||
"typedoc": "^0.25.4",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,jsx,ts,tsx}": [
|
||||
|
||||
@@ -3,6 +3,44 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.0.0-alpha.5](https://github.com/netease/tango/compare/@music163/tango-context@1.0.0-alpha.4...@music163/tango-context@1.0.0-alpha.5) (2023-12-29)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- refactor VariableTree & Workspace ([#83](https://github.com/netease/tango/issues/83)) ([8c07821](https://github.com/netease/tango/commit/8c07821d93cea4dfc43f81ca948b845176821184))
|
||||
|
||||
# [1.0.0-alpha.4](https://github.com/netease/tango/compare/@music163/tango-context@1.0.0-alpha.3...@music163/tango-context@1.0.0-alpha.4) (2023-12-26)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
# [1.0.0-alpha.3](https://github.com/netease/tango/compare/@music163/tango-context@1.0.0-alpha.2...@music163/tango-context@1.0.0-alpha.3) (2023-12-25)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
# [1.0.0-alpha.1](https://github.com/netease/tango/compare/@music163/tango-context@0.2.12...@music163/tango-context@1.0.0-alpha.1) (2023-12-12)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
## [0.2.12](https://github.com/netease/tango/compare/@music163/tango-context@0.2.11...@music163/tango-context@0.2.12) (2023-12-04)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
## [0.2.11](https://github.com/netease/tango/compare/@music163/tango-context@0.2.10...@music163/tango-context@0.2.11) (2023-12-04)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
## [0.2.10](https://github.com/netease/tango/compare/@music163/tango-context@0.2.9...@music163/tango-context@0.2.10) (2023-11-30)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
## [0.2.9](https://github.com/netease/tango/compare/@music163/tango-context@0.2.8...@music163/tango-context@0.2.9) (2023-11-29)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
## [0.2.8](https://github.com/netease/tango/compare/@music163/tango-context@0.2.7...@music163/tango-context@0.2.8) (2023-11-27)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
## [0.2.7](https://github.com/netease/tango/compare/@music163/tango-context@0.2.6...@music163/tango-context@0.2.7) (2023-11-23)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-context",
|
||||
"version": "0.2.7",
|
||||
"version": "1.0.0-alpha.5",
|
||||
"description": "react context for tango-apps",
|
||||
"keywords": [
|
||||
"react",
|
||||
@@ -31,9 +31,9 @@
|
||||
"react": ">= 16.8"
|
||||
},
|
||||
"dependencies": {
|
||||
"@music163/tango-core": "^0.2.6",
|
||||
"@music163/tango-helpers": "^0.1.8",
|
||||
"mobx-react-lite": "4.0.2"
|
||||
"@music163/tango-core": "^1.0.0-alpha.5",
|
||||
"@music163/tango-helpers": "^1.0.0-alpha.1",
|
||||
"mobx-react-lite": "4.0.5"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
import type { Engine } from '@music163/tango-core';
|
||||
import { createContext } from '@music163/tango-helpers';
|
||||
|
||||
interface CustomVariableData {
|
||||
key: string;
|
||||
title: string;
|
||||
children?: CustomVariableData[];
|
||||
[key: string]: any;
|
||||
}
|
||||
import { IVariableTreeNode, createContext } from '@music163/tango-helpers';
|
||||
|
||||
export interface ITangoEngineContext {
|
||||
/**
|
||||
@@ -17,8 +10,8 @@ export interface ITangoEngineContext {
|
||||
* 自定义配置数据
|
||||
*/
|
||||
config?: {
|
||||
customActionVariables?: CustomVariableData[];
|
||||
customExpressionVariables?: CustomVariableData[];
|
||||
customActionVariables?: IVariableTreeNode[];
|
||||
customExpressionVariables?: IVariableTreeNode[];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -39,23 +32,25 @@ export const useDesigner = () => {
|
||||
export const useWorkspaceData = () => {
|
||||
const ctx = useTangoEngine();
|
||||
const workspace = useWorkspace();
|
||||
const modelVariables: any[] = []; // 绑定变量列表
|
||||
const storeActionVariables: any[] = []; // 模型中的所有 actions
|
||||
const storeVariables: any[] = []; // 模型中的所有变量
|
||||
const serviceVariables: any[] = []; // 服务中的所有变量
|
||||
const modelVariables: IVariableTreeNode[] = []; // 绑定变量列表
|
||||
const storeActionVariables: IVariableTreeNode[] = []; // 模型中的所有 actions
|
||||
const storeVariables: IVariableTreeNode[] = []; // 模型中的所有变量
|
||||
const serviceVariables: IVariableTreeNode[] = []; // 服务中的所有变量
|
||||
|
||||
Object.values(workspace.storeModules).forEach((file) => {
|
||||
const prefix = `stores.${file.name}`;
|
||||
const states = file.states.map((item) => ({
|
||||
const states: IVariableTreeNode[] = file.states.map((item) => ({
|
||||
title: item.name,
|
||||
key: `${prefix}.${item.name}`,
|
||||
raw: item.code,
|
||||
showRemoveButton: true,
|
||||
}));
|
||||
const actions = file.actions.map((item) => ({
|
||||
const actions: IVariableTreeNode[] = file.actions.map((item) => ({
|
||||
title: item.name,
|
||||
key: `${prefix}.${item.name}`,
|
||||
type: 'function',
|
||||
raw: item.code,
|
||||
showRemoveButton: true,
|
||||
}));
|
||||
|
||||
modelVariables.push({
|
||||
@@ -63,6 +58,7 @@ export const useWorkspaceData = () => {
|
||||
key: prefix,
|
||||
selectable: false,
|
||||
children: states,
|
||||
showAddButton: true,
|
||||
});
|
||||
|
||||
storeActionVariables.push({
|
||||
@@ -77,8 +73,7 @@ export const useWorkspaceData = () => {
|
||||
key: prefix,
|
||||
selectable: false,
|
||||
children: [...states, ...actions],
|
||||
showAddChildIcon: true,
|
||||
showRemoveIcon: true,
|
||||
showAddButton: true,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -88,10 +83,12 @@ export const useWorkspaceData = () => {
|
||||
title: file.name,
|
||||
key: prefix,
|
||||
selectable: false,
|
||||
showAddButton: true,
|
||||
children: Object.keys(file.serviceFunctions || {}).map((key) => ({
|
||||
title: key,
|
||||
key: [prefix, key].join('.'),
|
||||
type: 'function',
|
||||
showRemoveButton: true,
|
||||
})),
|
||||
});
|
||||
});
|
||||
@@ -102,7 +99,7 @@ export const useWorkspaceData = () => {
|
||||
value: item.path,
|
||||
}));
|
||||
|
||||
let actionVariables: CustomVariableData[] = [
|
||||
let actionVariables: IVariableTreeNode[] = [
|
||||
buildVariableOptions('数据模型', '$stores', storeActionVariables),
|
||||
buildVariableOptions('服务函数', '$services', serviceVariables),
|
||||
];
|
||||
@@ -111,7 +108,7 @@ export const useWorkspaceData = () => {
|
||||
actionVariables = actionVariables.concat(ctx.config?.customActionVariables);
|
||||
}
|
||||
|
||||
let expressionVariables: CustomVariableData[] = [
|
||||
let expressionVariables: IVariableTreeNode[] = [
|
||||
buildVariableOptions('数据模型', '$stores', storeVariables),
|
||||
buildVariableOptions('服务函数', '$services', serviceVariables),
|
||||
];
|
||||
@@ -129,7 +126,7 @@ export const useWorkspaceData = () => {
|
||||
};
|
||||
};
|
||||
|
||||
function buildVariableOptions(title: string, key: string, children: any[]) {
|
||||
function buildVariableOptions(title: string, key: string, children: IVariableTreeNode[]) {
|
||||
return {
|
||||
key,
|
||||
title,
|
||||
|
||||
@@ -3,6 +3,60 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.0.0-alpha.5](https://github.com/netease/tango/compare/@music163/tango-core@1.0.0-alpha.4...@music163/tango-core@1.0.0-alpha.5) (2023-12-29)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- check if store value is valid ([765ea07](https://github.com/netease/tango/commit/765ea07cd5be6528e3b326e4ecb163647774d9e5))
|
||||
- refactor VariableTree & Workspace ([#83](https://github.com/netease/tango/issues/83)) ([8c07821](https://github.com/netease/tango/commit/8c07821d93cea4dfc43f81ca948b845176821184))
|
||||
|
||||
# [1.0.0-alpha.4](https://github.com/netease/tango/compare/@music163/tango-core@1.0.0-alpha.3...@music163/tango-core@1.0.0-alpha.4) (2023-12-26)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- list overflow in expSetter ([4d6b67e](https://github.com/netease/tango/commit/4d6b67eecd02b31f01d1bc896c6eeb83f6b34b35))
|
||||
|
||||
# [1.0.0-alpha.3](https://github.com/netease/tango/compare/@music163/tango-core@1.0.0-alpha.2...@music163/tango-core@1.0.0-alpha.3) (2023-12-25)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- export componentEntry module ([149002d](https://github.com/netease/tango/commit/149002ddddaee859333469f65c054d811bde2f7f))
|
||||
|
||||
# [1.0.0-alpha.1](https://github.com/netease/tango/compare/@music163/tango-core@0.2.11...@music163/tango-core@1.0.0-alpha.1) (2023-12-12)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-core
|
||||
|
||||
## [0.2.11](https://github.com/netease/tango/compare/@music163/tango-core@0.2.10...@music163/tango-core@0.2.11) (2023-12-04)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- update FileType ([c884712](https://github.com/netease/tango/commit/c8847123312bc47ea06575f4e714dfd596893e39))
|
||||
|
||||
## [0.2.10](https://github.com/netease/tango/compare/@music163/tango-core@0.2.9...@music163/tango-core@0.2.10) (2023-12-04)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- add filesFormatter to sandbox & fix update props with imports ([#71](https://github.com/netease/tango/issues/71)) ([138cbe9](https://github.com/netease/tango/commit/138cbe9b203b370aff42c1ae8086d69edacf35e9))
|
||||
|
||||
## [0.2.9](https://github.com/netease/tango/compare/@music163/tango-core@0.2.8...@music163/tango-core@0.2.9) (2023-11-30)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- fix render components tree ([76a5a2c](https://github.com/netease/tango/commit/76a5a2c65920bc42b019cd1f32a3cacd0d888638))
|
||||
|
||||
## [0.2.8](https://github.com/netease/tango/compare/@music163/tango-core@0.2.7...@music163/tango-core@0.2.8) (2023-11-29)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- workspace onFilesChange ([#70](https://github.com/netease/tango/issues/70)) ([accd826](https://github.com/netease/tango/commit/accd8263764c811ea8175a9cd341fc6fa6c75967))
|
||||
|
||||
## [0.2.7](https://github.com/netease/tango/compare/@music163/tango-core@0.2.6...@music163/tango-core@0.2.7) (2023-11-27)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- list service functions ([91b53da](https://github.com/netease/tango/commit/91b53da38e362ae320457429037dc347adf90bd3))
|
||||
- parse tango variables in view file ([d917633](https://github.com/netease/tango/commit/d91763379c4ccf9d826b717aa80a07abdaf2e3a2))
|
||||
|
||||
## [0.2.6](https://github.com/netease/tango/compare/@music163/tango-core@0.2.5...@music163/tango-core@0.2.6) (2023-11-23)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-core",
|
||||
"version": "0.2.6",
|
||||
"version": "1.0.0-alpha.5",
|
||||
"description": "tango core",
|
||||
"author": "wwsun <ww.sun@outlook.com>",
|
||||
"homepage": "",
|
||||
@@ -24,14 +24,14 @@
|
||||
"prepublishOnly": "yarn build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/generator": "^7.23.0",
|
||||
"@babel/parser": "^7.23.0",
|
||||
"@babel/traverse": "^7.23.2",
|
||||
"@babel/types": "^7.23.0",
|
||||
"@music163/tango-helpers": "^0.1.8",
|
||||
"@types/babel__generator": "^7.6.6",
|
||||
"@types/babel__traverse": "^7.20.3",
|
||||
"mobx": "6.9.0",
|
||||
"@babel/generator": "^7.23.5",
|
||||
"@babel/parser": "^7.23.5",
|
||||
"@babel/traverse": "^7.23.5",
|
||||
"@babel/types": "^7.23.5",
|
||||
"@music163/tango-helpers": "^1.0.0-alpha.1",
|
||||
"@types/babel__generator": "^7.6.7",
|
||||
"@types/babel__traverse": "^7.20.4",
|
||||
"mobx": "6.12.0",
|
||||
"path-browserify": "^1.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -44,6 +44,11 @@ export function isValidCode(code: string) {
|
||||
/**
|
||||
* 检测代码是否是合法的表达式代码
|
||||
* 表达式是一组代码的集合,它返回一个值;每一个合法的表达式都能计算成某个值
|
||||
* 只要你输入这段代码,可以形成一个值,就算是表达式
|
||||
* @example x = 1; // 1
|
||||
* @example 1=1
|
||||
* @example 'hello'
|
||||
* @example { foo: 'bar' }
|
||||
* @param code
|
||||
* @returns
|
||||
*/
|
||||
|
||||
@@ -22,16 +22,16 @@ import {
|
||||
code2expression,
|
||||
object2node,
|
||||
} from './parse';
|
||||
import { isValidComponentName } from '../string';
|
||||
import { getFullPath, isValidComponentName } from '../string';
|
||||
import { isDefineService, isDefineStore, isTangoVariable } from '../assert';
|
||||
import type {
|
||||
IRouteData,
|
||||
IStorePropertyData,
|
||||
ITangoViewNodeData,
|
||||
IImportDeclarationPayload,
|
||||
IServiceFunctionPayload,
|
||||
InsertChildPositionType,
|
||||
IImportSpecifierData,
|
||||
IExportSpecifierData,
|
||||
} from '../../types';
|
||||
import { IdGenerator } from '../id-generator';
|
||||
|
||||
@@ -688,7 +688,6 @@ export function addRouteToRouteFile(ast: t.File, routePath: string, importFilePa
|
||||
const newNode = t.objectExpression([
|
||||
t.objectProperty(t.identifier('path'), t.stringLiteral(routePath)),
|
||||
t.objectProperty(t.identifier('component'), t.identifier(component)),
|
||||
// FIXME: tango/issues#144 https://g.hz.netease.com/tango/issues/-/issues/144
|
||||
t.objectProperty(t.identifier('exact'), t.booleanLiteral(true)),
|
||||
]);
|
||||
path.node.elements.push(newNode);
|
||||
@@ -998,7 +997,7 @@ export function deleteServiceConfigFromServiceFile(ast: t.File, serviceFunctionN
|
||||
* @param payload
|
||||
* @returns
|
||||
*/
|
||||
export function serviceConfig2Node(payload: IServiceFunctionPayload) {
|
||||
export function serviceConfig2Node(payload: object) {
|
||||
return object2node(payload, (value, key) => {
|
||||
if (key === 'formatter' && value) {
|
||||
return code2expression(value);
|
||||
@@ -1007,10 +1006,7 @@ export function serviceConfig2Node(payload: IServiceFunctionPayload) {
|
||||
});
|
||||
}
|
||||
|
||||
export function updateServiceConfigToServiceFile(
|
||||
ast: t.File,
|
||||
config: { [key: string]: IServiceFunctionPayload },
|
||||
) {
|
||||
export function updateServiceConfigToServiceFile(ast: t.File, config: object) {
|
||||
traverse(ast, {
|
||||
CallExpression(path) {
|
||||
const calleeName = keyNode2value(path.node.callee) as string;
|
||||
@@ -1262,7 +1258,12 @@ export function traverseViewFile(ast: t.File, idGenerator: IdGenerator) {
|
||||
|
||||
MemberExpression(path) {
|
||||
const variable = node2value(path.node, false);
|
||||
if (isTangoVariable(variable) && !variables.includes(variable)) {
|
||||
const parentNode = path.parentPath.node;
|
||||
if (
|
||||
!t.isMemberExpression(parentNode) &&
|
||||
isTangoVariable(variable) &&
|
||||
!variables.includes(variable)
|
||||
) {
|
||||
variables.push(variable);
|
||||
}
|
||||
},
|
||||
@@ -1326,6 +1327,33 @@ export function traverseViewFile(ast: t.File, idGenerator: IdGenerator) {
|
||||
};
|
||||
}
|
||||
|
||||
export function traverseComponentsEntryFile(ast: t.File, baseDir?: string) {
|
||||
const exportMap: Record<string, IExportSpecifierData> = {};
|
||||
traverse(ast, {
|
||||
ExportNamedDeclaration(path) {
|
||||
const node = path.node;
|
||||
let source = node2value(node.source);
|
||||
if (baseDir) {
|
||||
// fix relative source path
|
||||
source = getFullPath(baseDir, source);
|
||||
}
|
||||
node.specifiers.forEach((specifier) => {
|
||||
if (t.isExportSpecifier(specifier)) {
|
||||
const name = keyNode2value(specifier.exported) as string;
|
||||
if (name) {
|
||||
exportMap[name] = {
|
||||
source,
|
||||
exportedName: name,
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return { ast, exportMap };
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析导入语句
|
||||
*/
|
||||
|
||||
@@ -33,6 +33,10 @@ export function inferFileType(filename: string): FileType {
|
||||
return FileType.JsonViewModule;
|
||||
}
|
||||
|
||||
if (/\/(blocks|components)\/index\.js/.test(filename)) {
|
||||
return FileType.ComponentsEntryModule;
|
||||
}
|
||||
|
||||
if (/\/services\/.+\.js$/.test(filename)) {
|
||||
return FileType.ServiceModule;
|
||||
}
|
||||
@@ -49,10 +53,6 @@ export function inferFileType(filename: string): FileType {
|
||||
return FileType.StoreModule;
|
||||
}
|
||||
|
||||
if (/\/blocks\/[\w-]+\/index\.js/.test(filename)) {
|
||||
return FileType.BlockEntryModule;
|
||||
}
|
||||
|
||||
if (/\.jsx?$/.test(filename)) {
|
||||
return FileType.Module;
|
||||
}
|
||||
@@ -170,24 +170,35 @@ export function getBlockNameByFilename(filename: string) {
|
||||
}
|
||||
|
||||
/**
|
||||
* FIXME: 有问题,需要优化下
|
||||
* 基于 from 文件的地址计算 to 文件的相对引用路径
|
||||
* @param from
|
||||
* @param to
|
||||
* 合并两个路径
|
||||
* @param root
|
||||
* @param filename
|
||||
* @returns
|
||||
*/
|
||||
export function getRelativePath(from: string, to: string) {
|
||||
const fromFolder = path.dirname(from);
|
||||
return path.relative(fromFolder, to);
|
||||
export function getFullPath(root: string, filename: string) {
|
||||
return path.join(root, filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算 targetFile 在 sourceFile 中的相对引用路径
|
||||
* @param sourceFile
|
||||
* @param targetFile
|
||||
* @returns
|
||||
*/
|
||||
export function getRelativePath(sourceFile: string, targetFile: string) {
|
||||
sourceFile = path.dirname(sourceFile);
|
||||
return path.relative(sourceFile, targetFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断给定字符串是否是文件路径
|
||||
* @example ./pages/index.js -- yes
|
||||
* @example ../pages/index.js -- yes
|
||||
* @example ../components -- yes
|
||||
* @example /src/pages/index.js -- yes
|
||||
* @example @music163/tango-designer -- no
|
||||
* @param str
|
||||
*/
|
||||
export function isFilepath(str: string) {
|
||||
return /^(\.\.?\/|\/).*\.[a-z]+$/.test(str);
|
||||
return /^(\.\.?\/|\/).*(\.[a-z]+)?$/.test(str);
|
||||
}
|
||||
|
||||
@@ -4,5 +4,5 @@ export * from './helpers';
|
||||
export * from './types';
|
||||
|
||||
export { default as generator } from '@babel/generator';
|
||||
export { default as traverse } from '@babel/types';
|
||||
export { default as traverse } from '@babel/traverse';
|
||||
export * from '@babel/parser';
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import path from 'path';
|
||||
import { action, computed, makeObservable, observable } from 'mobx';
|
||||
import { TangoModule } from './module';
|
||||
import { IWorkspace } from './interfaces';
|
||||
import { IExportSpecifierData, IFileConfig } from '../types';
|
||||
import { traverseComponentsEntryFile } from '../helpers';
|
||||
|
||||
/**
|
||||
* 本地组件目录的入口文件,例如 '/components/index.js' 或 `/blocks/index.js`
|
||||
*/
|
||||
export class TangoComponentsEntryModule extends TangoModule {
|
||||
exportList: Record<string, IExportSpecifierData>;
|
||||
|
||||
constructor(workspace: IWorkspace, props: IFileConfig) {
|
||||
super(workspace, props, false);
|
||||
this.update(props.code, false, false);
|
||||
makeObservable(this, {
|
||||
_code: observable,
|
||||
_cleanCode: observable,
|
||||
exportList: observable,
|
||||
code: computed,
|
||||
cleanCode: computed,
|
||||
update: action,
|
||||
});
|
||||
}
|
||||
|
||||
_analysisAst() {
|
||||
const baseDir = path.dirname(this.filename);
|
||||
const { exportMap } = traverseComponentsEntryFile(this.ast, baseDir);
|
||||
this.exportList = exportMap;
|
||||
Object.keys(this.exportList).forEach((key) => {
|
||||
this.workspace.componentPrototypes.set(key, {
|
||||
name: key,
|
||||
exportType: 'namedExport',
|
||||
package: baseDir,
|
||||
type: 'element',
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -56,6 +56,7 @@ export class TangoFile {
|
||||
this._code = code;
|
||||
this._cleanCode = code;
|
||||
}
|
||||
this.workspace.onFilesChange([this.filename]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,6 +135,7 @@ export class TangoJsonFile extends TangoFile {
|
||||
logger.error(err);
|
||||
}
|
||||
}
|
||||
this.workspace.onFilesChange([this.filename]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,5 +11,6 @@ export * from './route-module';
|
||||
export * from './service-module';
|
||||
export * from './store-module';
|
||||
export * from './view-module';
|
||||
export * from './component-module';
|
||||
export * from './node';
|
||||
export * from './interfaces';
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
InsertChildPositionType,
|
||||
ITangoConfigPackages,
|
||||
IPageConfigData,
|
||||
IServiceFunctionPayload,
|
||||
IImportSpecifierSourceData,
|
||||
IImportSpecifierData,
|
||||
} from '../types';
|
||||
@@ -149,8 +148,6 @@ export interface IWorkspace {
|
||||
setComponentPrototypes: (prototypes: Record<string, ComponentPrototypeType>) => void;
|
||||
getPrototype: (name: string | ComponentPrototypeType) => ComponentPrototypeType;
|
||||
|
||||
onFilesChange?: (filenames: string[]) => void;
|
||||
|
||||
// ----------------- 文件操作 -----------------
|
||||
addFiles: (files: IFileConfig[]) => void;
|
||||
addFile: (filename: string, code: string, fileType?: FileType) => void;
|
||||
@@ -169,6 +166,12 @@ export interface IWorkspace {
|
||||
listFiles: () => Record<string, string>;
|
||||
getFile: (filename: string) => TangoFile;
|
||||
|
||||
/**
|
||||
* 文件变化回调
|
||||
* @param filenames 文件名列表
|
||||
*/
|
||||
onFilesChange: (filenames: string[]) => void;
|
||||
|
||||
// ----------------- 节点操作 -----------------
|
||||
|
||||
removeSelectedNode: () => void;
|
||||
@@ -200,20 +203,23 @@ export interface IWorkspace {
|
||||
|
||||
// ----------------- 服务函数文件操作 -----------------
|
||||
|
||||
getServiceFunction?: (serviceKey: string) => object;
|
||||
getServiceFunction?: (serviceKey: string) => {
|
||||
name: string;
|
||||
moduleName: string;
|
||||
config: object;
|
||||
};
|
||||
listServiceFunctions?: () => Record<string, object>;
|
||||
removeServiceFunction?: (serviceName: string, modName?: string) => void;
|
||||
addServiceFunction?: (
|
||||
payload: IServiceFunctionPayload | IServiceFunctionPayload[],
|
||||
modName?: string,
|
||||
) => void;
|
||||
updateServiceFunction?: (payload: IServiceFunctionPayload, modName?: string) => void;
|
||||
updateServiceBaseConfig?: (IServiceFunctionPayload: object, modName?: string) => void;
|
||||
removeServiceFunction?: (serviceKey: string) => void;
|
||||
addServiceFunction?: (serviceName: string, config: object, modName?: string) => void;
|
||||
addServiceFunctions?: (configs: object, modName?: string) => void;
|
||||
updateServiceFunction?: (serviceName: string, payload: object, modName?: string) => void;
|
||||
updateServiceBaseConfig?: (config: object, modName?: string) => void;
|
||||
|
||||
// ----------------- 状态管理文件操作 -----------------
|
||||
addStoreState?: (storeName: string, stateName: string, initValue: string) => void;
|
||||
removeStoreState?: (storeName: string, stateName: string) => void;
|
||||
removeStoreModule?: (storeName: string) => void;
|
||||
removeStoreVariable?: (variablePath: string) => void;
|
||||
updateStoreVariable?: (variablePath: string, code: string) => void;
|
||||
|
||||
// ----------------- 视图文件操作 -----------------
|
||||
|
||||
@@ -252,15 +258,11 @@ export interface IWorkspace {
|
||||
|
||||
removeBizComp?: (name: string) => void;
|
||||
|
||||
// ----------------- 其他操作 -----------------
|
||||
|
||||
updateModuleCodeByVariablePath?: (variablePath: string, code: string) => void;
|
||||
|
||||
// ----------------- getter -----------------
|
||||
|
||||
get activeViewModule(): IViewFile;
|
||||
get pages(): any[];
|
||||
get bizComps(): string[];
|
||||
get baseComps(): string[];
|
||||
get blocks(): any[];
|
||||
get localComps(): string[];
|
||||
}
|
||||
|
||||
@@ -22,9 +22,9 @@ export class TangoModule extends TangoFile {
|
||||
ast: t.File;
|
||||
|
||||
/**
|
||||
* 导入语句
|
||||
* 导入的依赖列表
|
||||
*/
|
||||
imports: ImportDeclarationDataType;
|
||||
importList: ImportDeclarationDataType;
|
||||
|
||||
constructor(workspace: IWorkspace, props: IFileConfig, isSyncCode = true) {
|
||||
super(workspace, props, isSyncCode);
|
||||
@@ -46,6 +46,8 @@ export class TangoModule extends TangoFile {
|
||||
|
||||
this._analysisAst();
|
||||
|
||||
this.workspace.onFilesChange([this.filename]);
|
||||
|
||||
if (refreshWorkspace) {
|
||||
this.workspace.refresh([this.filename]);
|
||||
}
|
||||
@@ -93,7 +95,7 @@ export class TangoModule extends TangoFile {
|
||||
|
||||
_analysisAst() {
|
||||
const { imports } = traverseFile(this.ast);
|
||||
this.imports = imports;
|
||||
this.importList = imports;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
deleteServiceConfigFromServiceFile,
|
||||
updateBaseConfigToServiceFile,
|
||||
} from '../helpers';
|
||||
import { IFileConfig, IServiceFunctionPayload } from '../types';
|
||||
import { IFileConfig } from '../types';
|
||||
import { IWorkspace } from './interfaces';
|
||||
import { TangoModule } from './module';
|
||||
|
||||
@@ -59,7 +59,7 @@ export class TangoServiceModule extends TangoModule {
|
||||
|
||||
_analysisAst() {
|
||||
const { imports, services, baseConfig } = traverseServiceFile(this.ast);
|
||||
this.imports = imports;
|
||||
this.importList = imports;
|
||||
this._serviceFunctions = services;
|
||||
this._baseConfig = baseConfig;
|
||||
if (baseConfig.namespace) {
|
||||
@@ -67,25 +67,23 @@ export class TangoServiceModule extends TangoModule {
|
||||
}
|
||||
}
|
||||
|
||||
addServiceFunction(payload: IServiceFunctionPayload) {
|
||||
const { name, ...rest } = payload;
|
||||
this.ast = updateServiceConfigToServiceFile(this.ast, { [name]: clone(rest, false) });
|
||||
addServiceFunction(name: string, config: object) {
|
||||
this.ast = updateServiceConfigToServiceFile(this.ast, { [name]: clone(config, false) });
|
||||
return this;
|
||||
}
|
||||
|
||||
addServiceFunctions(payloads: IServiceFunctionPayload[]) {
|
||||
const config = payloads.reduce((acc, cur) => {
|
||||
const { name, ...rest } = cur;
|
||||
acc[name] = clone(rest, false);
|
||||
return acc;
|
||||
}, {});
|
||||
this.ast = updateServiceConfigToServiceFile(this.ast, config);
|
||||
/**
|
||||
* 批量添加服务函数
|
||||
* @param configs { [name: string]: object }
|
||||
* @returns
|
||||
*/
|
||||
addServiceFunctions(configs: object) {
|
||||
this.ast = updateServiceConfigToServiceFile(this.ast, configs);
|
||||
return this;
|
||||
}
|
||||
|
||||
updateServiceFunction(payload: IServiceFunctionPayload) {
|
||||
const { name, ...rest } = payload;
|
||||
this.ast = updateServiceConfigToServiceFile(this.ast, { [name]: clone(rest, false) });
|
||||
updateServiceFunction(name: string, payload: object) {
|
||||
this.ast = updateServiceConfigToServiceFile(this.ast, { [name]: clone(payload, false) });
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
@@ -159,7 +159,7 @@ export class TangoViewModule extends TangoModule implements IViewFile {
|
||||
this._cleanCode = ast2code(cleanAst);
|
||||
|
||||
this._importedModules = importedModules;
|
||||
this.imports = imports;
|
||||
this.importList = imports;
|
||||
this.importMap = buildImportMap(imports);
|
||||
this.variables = variables;
|
||||
|
||||
@@ -180,7 +180,7 @@ export class TangoViewModule extends TangoModule implements IViewFile {
|
||||
* 依赖列表
|
||||
*/
|
||||
listImportSources() {
|
||||
return Object.keys(this.imports);
|
||||
return Object.keys(this.importList);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -227,7 +227,7 @@ export class TangoViewModule extends TangoModule implements IViewFile {
|
||||
* @returns
|
||||
*/
|
||||
addImportSpecifiers(source: string, newSpecifiers: IImportSpecifierData[]) {
|
||||
const existSpecifiers = this.imports[source];
|
||||
const existSpecifiers = this.importList[source];
|
||||
if (existSpecifiers) {
|
||||
const insertedSpecifiers = newSpecifiers.filter((item) => {
|
||||
return !existSpecifiers.find((existItem) => existItem.localName === item.localName);
|
||||
@@ -307,10 +307,24 @@ export class TangoViewModule extends TangoModule implements IViewFile {
|
||||
updateNodeAttributes(nodeId: string, config: Record<string, any>, relatedImports?: string[]) {
|
||||
if (relatedImports && relatedImports.length) {
|
||||
// 导入依赖的组件
|
||||
relatedImports.forEach((name: string) => {
|
||||
const newImportData = relatedImports.reduce((prev, name) => {
|
||||
const proto = this.workspace.getPrototype(name);
|
||||
const { source, specifiers } = prototype2importDeclarationData(proto, this.filename);
|
||||
this.addImportSpecifiers(source, specifiers);
|
||||
const existSpecifiers: IImportSpecifierData[] = prev[source];
|
||||
if (existSpecifiers) {
|
||||
// merge specifiers
|
||||
specifiers.forEach((item) => {
|
||||
if (!existSpecifiers.find((existItem) => existItem.localName === item.localName)) {
|
||||
existSpecifiers.push(item);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
prev[source] = specifiers;
|
||||
}
|
||||
return prev;
|
||||
}, {});
|
||||
Object.keys(newImportData).forEach((source) => {
|
||||
this.addImportSpecifiers(source, newImportData[source]);
|
||||
});
|
||||
}
|
||||
this.ast = updateJSXAttributes(this.ast, nodeId, config);
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { action, computed, makeObservable, observable } from 'mobx';
|
||||
import { JSXElement } from '@babel/types';
|
||||
import {
|
||||
array2object,
|
||||
ComponentPrototypeType,
|
||||
hasFileExtension,
|
||||
isStoreVariablePath,
|
||||
isString,
|
||||
logger,
|
||||
parseServiceVariablePath,
|
||||
parseStoreVariablePath,
|
||||
uniq,
|
||||
} from '@music163/tango-helpers';
|
||||
import {
|
||||
@@ -15,8 +17,6 @@ import {
|
||||
isPathnameMatchRoute,
|
||||
getJSXElementChildrenNames,
|
||||
namesToImportDeclarations,
|
||||
getBlockNameByFilename,
|
||||
getPrivilegeCode,
|
||||
prototype2importDeclarationData,
|
||||
} from '../helpers';
|
||||
import { DropMethod } from './drop-target';
|
||||
@@ -25,19 +25,14 @@ import { TangoNode } from './node';
|
||||
import { TangoJsModule } from './module';
|
||||
import { TangoFile, TangoJsonFile, TangoLessFile } from './file';
|
||||
import { IWorkspace } from './interfaces';
|
||||
import {
|
||||
IFileConfig,
|
||||
FileType,
|
||||
ITangoConfigPackages,
|
||||
IPageConfigData,
|
||||
IServiceFunctionPayload,
|
||||
} from '../types';
|
||||
import { IFileConfig, FileType, ITangoConfigPackages, IPageConfigData } from '../types';
|
||||
import { SelectSource } from './select-source';
|
||||
import { DragSource } from './drag-source';
|
||||
import { TangoRouteModule } from './route-module';
|
||||
import { TangoStoreEntryModule, TangoStoreModule } from './store-module';
|
||||
import { TangoServiceModule } from './service-module';
|
||||
import { TangoViewModule } from './view-module';
|
||||
import { TangoComponentsEntryModule } from './component-module';
|
||||
|
||||
export interface IWorkspaceOptions {
|
||||
/**
|
||||
@@ -59,7 +54,7 @@ export interface IWorkspaceOptions {
|
||||
/**
|
||||
* 工作区文件变更事件
|
||||
*/
|
||||
onFilesChange?: (filenames: string[]) => void;
|
||||
onFilesChange?: IWorkspace['onFilesChange'];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -75,8 +70,14 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
*/
|
||||
files: Map<string, TangoFile>;
|
||||
|
||||
/**
|
||||
* 组件配置
|
||||
*/
|
||||
componentPrototypes: Map<string, ComponentPrototypeType>;
|
||||
|
||||
/**
|
||||
* 入口文件
|
||||
*/
|
||||
entry: string;
|
||||
|
||||
/**
|
||||
@@ -94,12 +95,6 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
*/
|
||||
activeViewFile: string;
|
||||
|
||||
/**
|
||||
* 本地区块 { [blockName]: filePath }
|
||||
* FIXME: 修正类型 Record<string, TangoBlockModule>
|
||||
*/
|
||||
localBlocks: Record<string, string>;
|
||||
|
||||
/**
|
||||
* 路由配置模块
|
||||
*/
|
||||
@@ -114,6 +109,8 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
|
||||
serviceModules: Record<string, TangoServiceModule> = {};
|
||||
|
||||
componentsEntryModule: TangoComponentsEntryModule;
|
||||
|
||||
/**
|
||||
* package.json 文件
|
||||
*/
|
||||
@@ -124,17 +121,6 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
*/
|
||||
tangoConfigJson: TangoJsonFile;
|
||||
|
||||
/**
|
||||
* appJson.json 文件
|
||||
* FIXME: 是否保留 ???
|
||||
*/
|
||||
appJson: TangoJsonFile;
|
||||
|
||||
/**
|
||||
* 代码变更回调
|
||||
*/
|
||||
onFilesChange?: (filenames: string[]) => void;
|
||||
|
||||
/**
|
||||
* 绑定事件
|
||||
* TODO: 是否需要自己来管理 listeners,并及时进行 gc
|
||||
@@ -170,15 +156,12 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
* 获取页面列表
|
||||
*/
|
||||
get pages() {
|
||||
const appJsonPages = this.appJson?.getValue('pages');
|
||||
const pagesMap = array2object(appJsonPages || [], (item) => item.path);
|
||||
const ret: IPageConfigData[] = [];
|
||||
this.routeModule?.routes.forEach((item) => {
|
||||
const data = pagesMap[item.path];
|
||||
if (item.path !== '*') {
|
||||
ret.push({
|
||||
...data,
|
||||
path: item.path,
|
||||
name: item.component,
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -213,8 +196,8 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
return list;
|
||||
}
|
||||
|
||||
get blocks() {
|
||||
return Object.keys(this.localBlocks);
|
||||
get localComps(): string[] {
|
||||
return Object.keys(this.componentsEntryModule?.exportList || {});
|
||||
}
|
||||
|
||||
constructor(options?: IWorkspaceOptions) {
|
||||
@@ -223,14 +206,17 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
this.selectSource = new SelectSource(this);
|
||||
this.dragSource = new DragSource(this);
|
||||
this.componentPrototypes = new Map();
|
||||
this.localBlocks = {};
|
||||
this.entry = options?.entry;
|
||||
this.activeRoute = options?.defaultActiveRoute || '/';
|
||||
this.activeFile = options?.entry;
|
||||
this.activeViewFile = '';
|
||||
this.files = new Map();
|
||||
this.isReady = false;
|
||||
this.onFilesChange = options?.onFilesChange;
|
||||
|
||||
if (options?.onFilesChange) {
|
||||
// 使用用户提供的 onFilesChange
|
||||
this.onFilesChange = options.onFilesChange;
|
||||
}
|
||||
|
||||
if (options?.files) {
|
||||
this.addFiles(options.files);
|
||||
@@ -245,8 +231,6 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
activeRoute: observable,
|
||||
activeFile: observable,
|
||||
activeViewFile: observable,
|
||||
localBlocks: observable,
|
||||
blocks: computed,
|
||||
pages: computed,
|
||||
bizComps: computed,
|
||||
setActiveRoute: action,
|
||||
@@ -342,6 +326,10 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
module = new TangoStoreEntryModule(this, props);
|
||||
this.storeEntryModule = module;
|
||||
break;
|
||||
case FileType.ComponentsEntryModule:
|
||||
module = new TangoComponentsEntryModule(this, props);
|
||||
this.componentsEntryModule = module;
|
||||
break;
|
||||
case FileType.RouteModule: {
|
||||
module = new TangoRouteModule(this, props);
|
||||
this.routeModule = module;
|
||||
@@ -363,19 +351,6 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
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;
|
||||
@@ -390,10 +365,6 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
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;
|
||||
@@ -420,8 +391,22 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
this.storeEntryModule.addStore(storeName).update();
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加视图文件
|
||||
* @param viewName 文件名
|
||||
* @param code 代码
|
||||
*/
|
||||
addViewFile(viewName: string, code: string) {
|
||||
// TODO: implement it
|
||||
const viewRoute = viewName.startsWith('/') ? viewName : `/${viewName}`;
|
||||
const filename = `/src/pages/${viewName}.js`;
|
||||
this.addFile(filename, code);
|
||||
this.addRoute(
|
||||
{
|
||||
name: viewName,
|
||||
path: viewRoute,
|
||||
},
|
||||
filename,
|
||||
);
|
||||
}
|
||||
|
||||
updateFile(filename: string, code: string, shouldFormatCode = false) {
|
||||
@@ -517,51 +502,14 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
this.routeModule.removeRoute(routePath).update();
|
||||
this.setActiveRoute(this.routeModule.routes[0]?.path || '/');
|
||||
}
|
||||
// remove appJson page
|
||||
this.appJson
|
||||
?.setValue('pages', (pages) => {
|
||||
return (pages as IPageConfigData[]).filter((page) => page.path !== routePath);
|
||||
})
|
||||
.update();
|
||||
this.removeFile(filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加新的视图文件
|
||||
* @deprecated 使用 addViewFile 代替
|
||||
* FIXME: 重构这个逻辑
|
||||
* @param route 视图名
|
||||
* @param code 视图代码
|
||||
*/
|
||||
addViewPage(routeConfig: string | IPageConfigData, code: string) {
|
||||
const config =
|
||||
typeof routeConfig === 'string'
|
||||
? {
|
||||
name: routeConfig,
|
||||
path: routeConfig.startsWith('/') ? routeConfig : `/${routeConfig}`,
|
||||
}
|
||||
: routeConfig;
|
||||
|
||||
const filename = getFilepath(config.path, '/src/pages', '.js');
|
||||
this.addFile(filename, code);
|
||||
this.addRoute(config, filename);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加新的路由
|
||||
*/
|
||||
addRoute(routeData: IPageConfigData, importFilePath: string) {
|
||||
this.routeModule?.addRoute(routeData.path, importFilePath).update();
|
||||
this.appJson
|
||||
?.setValue('pages', (pages) => {
|
||||
(pages as IPageConfigData[]).push({
|
||||
name: routeData.name || routeData.path,
|
||||
path: routeData.path,
|
||||
privilegeCode: getPrivilegeCode(this.packageJson?.getValue('name'), routeData.path),
|
||||
});
|
||||
return pages;
|
||||
})
|
||||
.update();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -573,18 +521,6 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
if (sourceRoutePath !== targetPageData.path) {
|
||||
this.routeModule?.updateRoute(sourceRoutePath, targetPageData.path).update();
|
||||
}
|
||||
this.appJson
|
||||
?.setValue('pages', (pages) => {
|
||||
for (const page of pages as IPageConfigData[]) {
|
||||
if (page.path === sourceRoutePath) {
|
||||
page.path = targetPageData.path;
|
||||
page.name = targetPageData.name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return pages;
|
||||
})
|
||||
.update();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -660,15 +596,23 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据变量路径更新模块内容
|
||||
* TODO: 改名,不直观
|
||||
* 根据变量路径删除状态变量
|
||||
* @param variablePath
|
||||
*/
|
||||
removeStoreVariable(variablePath: string) {
|
||||
const { storeName, variableName } = parseStoreVariablePath(variablePath);
|
||||
this.removeStoreState(storeName, variableName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据变量路径更新状态变量的值
|
||||
* @param variablePath 变量路径
|
||||
* @param code 变量代码
|
||||
*/
|
||||
updateModuleCodeByVariablePath(variablePath: string, code: string) {
|
||||
if (/^stores\.\w+\.\w+$/.test(variablePath)) {
|
||||
const [, storeName, stateName] = variablePath.split('.');
|
||||
this.storeModules[storeName]?.updateState(stateName, code).update();
|
||||
updateStoreVariable(variablePath: string, code: string) {
|
||||
if (isStoreVariablePath(variablePath)) {
|
||||
const { storeName, variableName } = parseStoreVariablePath(variablePath);
|
||||
this.storeModules[storeName]?.updateState(variableName, code).update();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -679,7 +623,7 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
* @returns
|
||||
*/
|
||||
getServiceFunction(serviceKey: string) {
|
||||
const { name, moduleName } = this.parseServiceKey(serviceKey);
|
||||
const { name, moduleName } = parseServiceVariablePath(serviceKey);
|
||||
if (!name) {
|
||||
return;
|
||||
}
|
||||
@@ -696,12 +640,10 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
*/
|
||||
listServiceFunctions() {
|
||||
const ret: Record<string, object> = {};
|
||||
Object.values(this.serviceModules).forEach((module) => {
|
||||
Object.keys(this.serviceModules).forEach((moduleName) => {
|
||||
const module = this.serviceModules[moduleName];
|
||||
Object.keys(module.serviceFunctions).forEach((name) => {
|
||||
const serviceKey =
|
||||
module.baseConfig.namespace !== 'index'
|
||||
? name
|
||||
: [module.baseConfig.namespace, name].join('.');
|
||||
const serviceKey = moduleName === 'index' ? name : [moduleName, name].join('.');
|
||||
ret[serviceKey] = module.serviceFunctions[name];
|
||||
});
|
||||
});
|
||||
@@ -711,29 +653,27 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
/**
|
||||
* 更新服务函数
|
||||
*/
|
||||
updateServiceFunction(payload: IServiceFunctionPayload, moduleName = 'index') {
|
||||
this.serviceModules[moduleName].updateServiceFunction(payload).update();
|
||||
updateServiceFunction(serviceName: string, payload: object, moduleName = 'index') {
|
||||
this.serviceModules[moduleName].updateServiceFunction(serviceName, payload).update();
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增服务函数,支持批量添加
|
||||
*/
|
||||
addServiceFunction(
|
||||
payload: IServiceFunctionPayload | IServiceFunctionPayload[],
|
||||
moduleName = 'index',
|
||||
) {
|
||||
if (Array.isArray(payload)) {
|
||||
this.serviceModules[moduleName]?.addServiceFunctions(payload).update();
|
||||
} else {
|
||||
this.serviceModules[moduleName]?.addServiceFunction(payload).update();
|
||||
}
|
||||
addServiceFunction(name: string, config: object, moduleName = 'index') {
|
||||
this.serviceModules[moduleName]?.addServiceFunction(name, config).update();
|
||||
}
|
||||
|
||||
addServiceFunctions(configs: object, modName = 'index') {
|
||||
this.serviceModules[modName]?.addServiceFunctions(configs).update();
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除服务函数
|
||||
* @param name
|
||||
*/
|
||||
removeServiceFunction(name: string, moduleName = 'index') {
|
||||
removeServiceFunction(serviceKey: string) {
|
||||
const { moduleName, name } = parseServiceVariablePath(serviceKey);
|
||||
this.serviceModules[moduleName]?.deleteServiceFunction(name).update();
|
||||
}
|
||||
|
||||
@@ -1177,6 +1117,10 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
}
|
||||
}
|
||||
|
||||
onFilesChange(filenams: string[]) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新目标文件
|
||||
* @param filenames
|
||||
@@ -1190,7 +1134,6 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
},
|
||||
}),
|
||||
);
|
||||
this.onFilesChange?.(filenames);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1269,40 +1212,4 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
logger.error('copyFiles failed, source: %s, target: %s', sourceFilePath, targetFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 serviceKey
|
||||
* @param serviceKey
|
||||
* @returns
|
||||
*
|
||||
* @example services.list => { moduleName: 'index', name: 'list' }
|
||||
* @example services.sub.list => { moduleName: 'sub', name: 'list' }
|
||||
* @example foo => undefined
|
||||
*/
|
||||
private parseServiceKey(serviceKey: string) {
|
||||
const parts = serviceKey.split('.');
|
||||
if (parts[0] !== 'services') {
|
||||
return {};
|
||||
}
|
||||
|
||||
let moduleName = 'index';
|
||||
let name = '';
|
||||
switch (parts.length) {
|
||||
case 2: {
|
||||
name = parts[1];
|
||||
break;
|
||||
}
|
||||
case 3: {
|
||||
moduleName = parts[1];
|
||||
name = parts[2];
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return {
|
||||
moduleName,
|
||||
name,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+34
-14
@@ -10,11 +10,25 @@ export enum FileType {
|
||||
Module = 'module',
|
||||
StoreEntryModule = 'storeEntryModule',
|
||||
RouteModule = 'routeModule',
|
||||
BlockEntryModule = 'blockEntryModule',
|
||||
ServiceModule = 'serviceModule',
|
||||
StoreModule = 'storeModule',
|
||||
|
||||
// 组件配置文件
|
||||
ComponentPrototypeModule = 'componentPrototypeModule',
|
||||
// 组件运行调试入口文件,一般为 `/app.js`
|
||||
ComponentDemoEntryModule = 'componentDemoEntryModule',
|
||||
/**
|
||||
* 本地组件目录的入口文件
|
||||
*/
|
||||
ComponentsEntryModule = 'componentsEntryModule',
|
||||
/**
|
||||
* @deprecated 已废弃
|
||||
*/
|
||||
BlockEntryModule = 'blockEntryModule',
|
||||
|
||||
// jsx 类型视图文件
|
||||
JsxViewModule = 'jsxViewModule',
|
||||
// json 类型视图文件
|
||||
JsonViewModule = 'jsonViewModule',
|
||||
|
||||
// 非 js 文件
|
||||
@@ -72,6 +86,24 @@ export interface ITangoViewNodeData<T = JSXElement> {
|
||||
children?: Array<ITangoViewNodeData<T>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出变量数据
|
||||
*/
|
||||
export interface IExportSpecifierData {
|
||||
/**
|
||||
* 来源
|
||||
*/
|
||||
source: string;
|
||||
/**
|
||||
* exported name
|
||||
*/
|
||||
exportedName: string;
|
||||
/**
|
||||
* local name
|
||||
*/
|
||||
localName?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入变量的来源
|
||||
*/
|
||||
@@ -100,7 +132,6 @@ export interface IImportSpecifierData {
|
||||
|
||||
/**
|
||||
* 解析的导入语句数据
|
||||
* FIXME: 需要考虑到有两行重复的语句的情况
|
||||
*/
|
||||
export type ImportDeclarationDataType = Record<string, IImportSpecifierData[]>;
|
||||
|
||||
@@ -114,17 +145,6 @@ export interface IImportDeclarationPayload {
|
||||
sourcePath: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 服务函数参数类型
|
||||
*/
|
||||
export interface IServiceFunctionPayload {
|
||||
[key: string]: any;
|
||||
/**
|
||||
* 服务函数名
|
||||
*/
|
||||
name: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store 属性类型
|
||||
*/
|
||||
@@ -193,7 +213,7 @@ export interface IPageConfigData {
|
||||
*/
|
||||
name?: string;
|
||||
/**
|
||||
* PMS 权限码
|
||||
* 权限码
|
||||
*/
|
||||
privilegeCode?: string;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ describe('ast helpers', () => {
|
||||
it('isValidExpression', () => {
|
||||
expect(isValidExpressionCode('() => { }')).toBeTruthy();
|
||||
expect(isValidExpressionCode('1')).toBeTruthy();
|
||||
expect(isValidExpressionCode('a = 1')).toBeTruthy();
|
||||
expect(isValidExpressionCode('1 + 1')).toBeTruthy();
|
||||
expect(isValidExpressionCode('"hello"')).toBeTruthy();
|
||||
expect(isValidExpressionCode('false')).toBeTruthy();
|
||||
expect(isValidExpressionCode('{ bizId: "vip", type: "category" }')).toBeTruthy();
|
||||
|
||||
@@ -129,8 +129,10 @@ describe('string helpers', () => {
|
||||
expect(getRelativePath('/src/pages/index.js', '/src/blocks/sample-block/index.js')).toEqual(
|
||||
'../blocks/sample-block/index.js',
|
||||
);
|
||||
// TODO: fix me
|
||||
// expect(getRelativePath('/src/pages/', '/src/pages/index.js')).toEqual('./index.js');
|
||||
expect(getRelativePath('/src/pages/index.js', '/src/components')).toEqual('../components');
|
||||
expect(getRelativePath('/src/pages/index.js', '/src/components/input.js')).toEqual(
|
||||
'../components/input.js',
|
||||
);
|
||||
});
|
||||
|
||||
it('getFilepath', () => {
|
||||
@@ -143,7 +145,9 @@ describe('string helpers', () => {
|
||||
expect(isFilepath('./pages/index.js')).toBeTruthy();
|
||||
expect(isFilepath('../pages/index.js')).toBeTruthy();
|
||||
expect(isFilepath('./pages/index.css')).toBeTruthy();
|
||||
expect(isFilepath('/src/pages/index.js')).toBeTruthy();
|
||||
expect(isFilepath('./src/pages/index.js')).toBeTruthy();
|
||||
expect(isFilepath('./src/components')).toBeTruthy();
|
||||
expect(isFilepath('../src/components')).toBeTruthy();
|
||||
expect(isFilepath('path')).toBeFalsy();
|
||||
expect(isFilepath('path-browserify')).toBeFalsy();
|
||||
expect(isFilepath('@music/one')).toBeFalsy();
|
||||
|
||||
@@ -3,6 +3,80 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.0.0-alpha.6](https://github.com/netease/tango/compare/@music163/tango-designer@1.0.0-alpha.5...@music163/tango-designer@1.0.0-alpha.6) (2023-12-29)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- check if store value is valid ([765ea07](https://github.com/netease/tango/commit/765ea07cd5be6528e3b326e4ecb163647774d9e5))
|
||||
- refactor VariableTree & Workspace ([#83](https://github.com/netease/tango/issues/83)) ([8c07821](https://github.com/netease/tango/commit/8c07821d93cea4dfc43f81ca948b845176821184))
|
||||
|
||||
# [1.0.0-alpha.5](https://github.com/netease/tango/compare/@music163/tango-designer@1.0.0-alpha.4...@music163/tango-designer@1.0.0-alpha.5) (2023-12-26)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- list overflow in expSetter ([4d6b67e](https://github.com/netease/tango/commit/4d6b67eecd02b31f01d1bc896c6eeb83f6b34b35))
|
||||
|
||||
# [1.0.0-alpha.4](https://github.com/netease/tango/compare/@music163/tango-designer@1.0.0-alpha.3...@music163/tango-designer@1.0.0-alpha.4) (2023-12-25)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-designer
|
||||
|
||||
# [1.0.0-alpha.2](https://github.com/netease/tango/compare/@music163/tango-designer@1.0.0-alpha.1...@music163/tango-designer@1.0.0-alpha.2) (2023-12-13)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- update variable tree with help messages and styling & add tangoConfig schema ([5e7e285](https://github.com/netease/tango/commit/5e7e285452b46888b447991b0e8548b6392acb3a))
|
||||
|
||||
# [1.0.0-alpha.1](https://github.com/netease/tango/compare/@music163/tango-designer@0.5.14...@music163/tango-designer@1.0.0-alpha.1) (2023-12-12)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- add showSearch and showGroups to SettingForm & update internal icons ([#73](https://github.com/netease/tango/issues/73)) ([a8357f4](https://github.com/netease/tango/commit/a8357f45b8b0c1f2b91a856918f6bdecdc65aa73))
|
||||
- render form item without setter & enhance tools ([#76](https://github.com/netease/tango/issues/76)) ([1acfa68](https://github.com/netease/tango/commit/1acfa6864b1faad1a441facc426d8d94b6b090b5))
|
||||
|
||||
## [0.5.14](https://github.com/netease/tango/compare/@music163/tango-designer@0.5.13...@music163/tango-designer@0.5.14) (2023-12-04)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-designer
|
||||
|
||||
## [0.5.13](https://github.com/netease/tango/compare/@music163/tango-designer@0.5.12...@music163/tango-designer@0.5.13) (2023-12-04)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- add filesFormatter to sandbox & fix update props with imports ([#71](https://github.com/netease/tango/issues/71)) ([138cbe9](https://github.com/netease/tango/commit/138cbe9b203b370aff42c1ae8086d69edacf35e9))
|
||||
|
||||
## [0.5.12](https://github.com/netease/tango/compare/@music163/tango-designer@0.5.11...@music163/tango-designer@0.5.12) (2023-11-30)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- fix render components tree ([76a5a2c](https://github.com/netease/tango/commit/76a5a2c65920bc42b019cd1f32a3cacd0d888638))
|
||||
|
||||
## [0.5.11](https://github.com/netease/tango/compare/@music163/tango-designer@0.5.10...@music163/tango-designer@0.5.11) (2023-11-29)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-designer
|
||||
|
||||
## [0.5.10](https://github.com/netease/tango/compare/@music163/tango-designer@0.5.9...@music163/tango-designer@0.5.10) (2023-11-28)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- update default theme ([14cd4c5](https://github.com/netease/tango/commit/14cd4c54a1e5eed834d01921e186bdba9bbe7922))
|
||||
|
||||
## [0.5.9](https://github.com/netease/tango/compare/@music163/tango-designer@0.5.8...@music163/tango-designer@0.5.9) (2023-11-28)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- update theme ([2550411](https://github.com/netease/tango/commit/2550411c1c93037931d44aa9f7822ffe8caa900b))
|
||||
|
||||
## [0.5.8](https://github.com/netease/tango/compare/@music163/tango-designer@0.5.7...@music163/tango-designer@0.5.8) (2023-11-27)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- event setter ([588861f](https://github.com/netease/tango/commit/588861f6492b45954e29ceed950567781b822492))
|
||||
|
||||
## [0.5.7](https://github.com/netease/tango/compare/@music163/tango-designer@0.5.6...@music163/tango-designer@0.5.7) (2023-11-23)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- update varTreeNode style ([#69](https://github.com/netease/tango/issues/69)) ([f604996](https://github.com/netease/tango/commit/f604996e56c9e4d58ff7f80cd98c3753986f081f))
|
||||
|
||||
## [0.5.6](https://github.com/netease/tango/compare/@music163/tango-designer@0.5.5...@music163/tango-designer@0.5.6) (2023-11-23)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-designer
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-designer",
|
||||
"version": "0.5.6",
|
||||
"version": "1.0.0-alpha.6",
|
||||
"description": "lowcode designer",
|
||||
"keywords": [
|
||||
"react"
|
||||
@@ -33,12 +33,12 @@
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^4.8.0",
|
||||
"@music163/request": "^0.1.2",
|
||||
"@music163/tango-context": "^0.2.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",
|
||||
"@music163/tango-context": "^1.0.0-alpha.5",
|
||||
"@music163/tango-core": "^1.0.0-alpha.5",
|
||||
"@music163/tango-helpers": "^1.0.0-alpha.1",
|
||||
"@music163/tango-sandbox": "^1.0.0-alpha.5",
|
||||
"@music163/tango-setting-form": "^1.0.0-alpha.5",
|
||||
"@music163/tango-ui": "^1.0.0-alpha.3",
|
||||
"antd": "^4.24.2",
|
||||
"cash-dom": "^8.1.2",
|
||||
"classnames": "^2.3.2",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './drag-box';
|
||||
export * from './input-kv';
|
||||
export * from './variable-tree';
|
||||
export * from './variable-tree-modal';
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import React, { useState } from 'react';
|
||||
import { ModalProps, Modal } from 'antd';
|
||||
import { Box } from 'coral-system';
|
||||
import { IVariableTreeNode, noop, useBoolean } from '@music163/tango-helpers';
|
||||
import { VariableTree, VariableTreeProps } from './variable-tree';
|
||||
|
||||
interface VariableTreeModalProps extends VariableTreeProps {
|
||||
trigger?: React.ReactElement;
|
||||
title?: ModalProps['title'];
|
||||
modalProps?: ModalProps;
|
||||
}
|
||||
|
||||
export function VariableTreeModal({
|
||||
trigger,
|
||||
title,
|
||||
modalProps,
|
||||
onSelect = noop,
|
||||
...rest
|
||||
}: VariableTreeModalProps) {
|
||||
const [node, setNode] = useState<IVariableTreeNode>();
|
||||
const [visible, { on, off }] = useBoolean(false);
|
||||
return (
|
||||
<Box>
|
||||
{React.cloneElement(trigger, { onClick: on })}
|
||||
<Modal
|
||||
title={title}
|
||||
open={visible}
|
||||
onCancel={off}
|
||||
okButtonProps={{
|
||||
disabled: !node,
|
||||
}}
|
||||
onOk={() => {
|
||||
if (node) {
|
||||
onSelect(node);
|
||||
off();
|
||||
}
|
||||
}}
|
||||
width="60%"
|
||||
{...modalProps}
|
||||
>
|
||||
<VariableTree height={400} onSelect={setNode} {...rest} />
|
||||
</Modal>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -1,631 +0,0 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import {
|
||||
Tree,
|
||||
Button,
|
||||
Form,
|
||||
Input,
|
||||
Space,
|
||||
ModalProps,
|
||||
Modal,
|
||||
Radio,
|
||||
Empty,
|
||||
Popconfirm,
|
||||
Tooltip,
|
||||
} from 'antd';
|
||||
import { Box, Text, css } from 'coral-system';
|
||||
import {
|
||||
PlusOutlined,
|
||||
FunctionOutlined,
|
||||
DeleteOutlined,
|
||||
CopyOutlined,
|
||||
EyeOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import {
|
||||
filterTreeData,
|
||||
isFunction,
|
||||
isNil,
|
||||
isString,
|
||||
noop,
|
||||
useBoolean,
|
||||
} from '@music163/tango-helpers';
|
||||
import {
|
||||
Panel,
|
||||
InputCode,
|
||||
Search,
|
||||
JsonView,
|
||||
JsonViewProps,
|
||||
CopyClipboard,
|
||||
} from '@music163/tango-ui';
|
||||
import { isValidExpressionCode } from '@music163/tango-core';
|
||||
|
||||
export interface IVariableTreeNode {
|
||||
/**
|
||||
* 唯一标识符
|
||||
*/
|
||||
key: string;
|
||||
/**
|
||||
* 标题
|
||||
*/
|
||||
title?: string;
|
||||
/**
|
||||
* 是否可选中
|
||||
*/
|
||||
selectable?: boolean;
|
||||
/**
|
||||
* 展示添加子节点的图标
|
||||
*/
|
||||
showAddChildIcon?: boolean;
|
||||
/**
|
||||
* 展示删除删除图标
|
||||
*/
|
||||
showDeleteIcon?: boolean;
|
||||
/**
|
||||
* 结点类型,用来展示图标
|
||||
*/
|
||||
type?: 'function' | 'property';
|
||||
/**
|
||||
* 定义的原始值
|
||||
*/
|
||||
raw?: any;
|
||||
/**
|
||||
* 子结点
|
||||
*/
|
||||
children?: IVariableTreeNode[];
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
type VariableModelType = 'preview' | 'define' | 'add';
|
||||
|
||||
export interface EditableVariableTreeProps {
|
||||
/**
|
||||
* 允许的变量面板
|
||||
*/
|
||||
modes?: VariableModelType[];
|
||||
/**
|
||||
* 默认的变量面板
|
||||
*/
|
||||
defaultMode?: VariableModelType;
|
||||
/**
|
||||
* 数据源
|
||||
*/
|
||||
dataSource?: VariableTreeProps['dataSource'];
|
||||
/**
|
||||
* 点击添加变量的回调
|
||||
*/
|
||||
onAddVariable?: AddNodeFormProps['onSubmit'];
|
||||
/**
|
||||
* 预览值取值函数
|
||||
*/
|
||||
getPreviewValue?: (node: IVariableTreeNode) => unknown;
|
||||
/**
|
||||
* 选择列表项时的回调
|
||||
*/
|
||||
onSelect?: (data: IVariableTreeNode) => void;
|
||||
/**
|
||||
* 保存结点定义的回调
|
||||
*/
|
||||
onSave?: NodeDefineProps['onSave'];
|
||||
/**
|
||||
* 删除结点定义的回调
|
||||
*/
|
||||
onDeleteVariable?: (storeName: string, stateName: string) => void;
|
||||
/**
|
||||
* 删除模型时的回调
|
||||
*/
|
||||
onDeleteStore?: (storeName: string) => void;
|
||||
/**
|
||||
* 高度
|
||||
*/
|
||||
height?: number | string;
|
||||
/**
|
||||
* 搜索框的样式
|
||||
*/
|
||||
searchStyle?: React.CSSProperties;
|
||||
/**
|
||||
* 搜索框的后置结点
|
||||
*/
|
||||
searchAddonAfter?: React.ReactNode;
|
||||
}
|
||||
|
||||
const previewOptions = [
|
||||
{ label: '运行时', value: 'preview' },
|
||||
{ label: '定义', value: 'define' },
|
||||
];
|
||||
|
||||
export function EditableVariableTree({
|
||||
height,
|
||||
searchAddonAfter,
|
||||
searchStyle,
|
||||
dataSource,
|
||||
onAddVariable = noop,
|
||||
onDeleteVariable = noop,
|
||||
onDeleteStore = noop,
|
||||
getPreviewValue = noop,
|
||||
onSelect = noop,
|
||||
onSave = noop,
|
||||
modes = ['add', 'preview', 'define'],
|
||||
defaultMode = 'preview',
|
||||
}: EditableVariableTreeProps) {
|
||||
const [keyword, setKeyword] = useState('');
|
||||
const [node, setNode] = useState<IVariableTreeNode>();
|
||||
const [mode, setMode] = useState<EditableVariableTreeProps['defaultMode']>(defaultMode);
|
||||
const hasPanelSwitch = modes.includes('define') && modes.includes('preview');
|
||||
|
||||
const treeData = useMemo(() => {
|
||||
const pattern = new RegExp(keyword, 'ig');
|
||||
return keyword
|
||||
? filterTreeData(dataSource, (leaf) => pattern.test(leaf.title), 'children', true)
|
||||
: dataSource;
|
||||
}, [keyword, dataSource]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
className="EditableVariableTree"
|
||||
display="flex"
|
||||
overflow="auto"
|
||||
height={height}
|
||||
position="relative"
|
||||
>
|
||||
<Box p="l" width="40%" borderRight="solid" borderColor="line.normal">
|
||||
<Box mb="m" position="sticky" top="0" bg="white" zIndex={2}>
|
||||
<Input.Group compact>
|
||||
<Search
|
||||
placeholder="请输入变量名"
|
||||
onChange={(val) => setKeyword(val?.trim())}
|
||||
style={searchStyle}
|
||||
/>
|
||||
{searchAddonAfter}
|
||||
</Input.Group>
|
||||
</Box>
|
||||
<VariableTree
|
||||
dataSource={treeData}
|
||||
showViewIcon
|
||||
onSelect={(item) => {
|
||||
setNode(item);
|
||||
mode === 'add' && setMode(defaultMode);
|
||||
onSelect(item);
|
||||
}}
|
||||
onView={(item) => {
|
||||
setNode(item);
|
||||
mode === 'add' && setMode(defaultMode);
|
||||
}}
|
||||
onAdd={(item) => {
|
||||
setNode(item);
|
||||
setMode('add');
|
||||
}}
|
||||
onRemove={(item) => {
|
||||
const [type, storeName] = item.key.split('.');
|
||||
setNode(null);
|
||||
onDeleteStore(storeName);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
{node ? (
|
||||
<Panel
|
||||
title={
|
||||
{
|
||||
preview: '变量值预览',
|
||||
add: '添加变量',
|
||||
define: '变量定义',
|
||||
}[mode]
|
||||
}
|
||||
extra={
|
||||
hasPanelSwitch && mode !== 'add' ? (
|
||||
<Radio.Group
|
||||
optionType="button"
|
||||
buttonStyle="solid"
|
||||
size="small"
|
||||
value={mode}
|
||||
onChange={(e) => setMode(e.target.value)}
|
||||
options={previewOptions}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
width="60%"
|
||||
borderRadius="m"
|
||||
position="sticky"
|
||||
top="0"
|
||||
bodyProps={{ px: 'm' }}
|
||||
>
|
||||
{mode === 'preview' && (
|
||||
<ValuePreview
|
||||
value={getPreviewValue(node)}
|
||||
onSelect={(valuePath) => {
|
||||
return ['tango', node.key.replaceAll('.', '?.'), valuePath].join('.');
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{mode === 'define' && (
|
||||
<NodeDefineForm
|
||||
node={node}
|
||||
onSave={onSave}
|
||||
onDelete={(item) => {
|
||||
const [type, storeName, stateName] = item.key.split('.');
|
||||
onDeleteVariable(storeName, stateName);
|
||||
setNode(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{mode === 'add' && (
|
||||
<AddNodeForm
|
||||
parentNode={node}
|
||||
onCancel={() => {
|
||||
setMode(defaultMode);
|
||||
setNode(null);
|
||||
}}
|
||||
onSubmit={(storeName, data) => {
|
||||
onAddVariable(storeName, data);
|
||||
setMode(defaultMode);
|
||||
setNode(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Panel>
|
||||
) : (
|
||||
<Panel title="提示" flex="1" position="sticky" top="0">
|
||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="请从左侧列表中选择一个变量" />
|
||||
</Panel>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
interface EditableVariableTreeModalProps extends EditableVariableTreeProps {
|
||||
trigger?: React.ReactElement;
|
||||
title?: ModalProps['title'];
|
||||
modalProps?: ModalProps;
|
||||
}
|
||||
|
||||
export function EditableVariableTreeModal({
|
||||
trigger,
|
||||
title,
|
||||
modalProps,
|
||||
onSelect = noop,
|
||||
...rest
|
||||
}: EditableVariableTreeModalProps) {
|
||||
const [node, setNode] = useState<IVariableTreeNode>();
|
||||
const [visible, { on, off }] = useBoolean(false);
|
||||
return (
|
||||
<Box>
|
||||
{React.cloneElement(trigger, { onClick: on })}
|
||||
<Modal
|
||||
title={title}
|
||||
open={visible}
|
||||
onCancel={off}
|
||||
okButtonProps={{
|
||||
disabled: !node,
|
||||
}}
|
||||
onOk={() => {
|
||||
if (node) {
|
||||
onSelect(node);
|
||||
off();
|
||||
}
|
||||
}}
|
||||
width="60%"
|
||||
{...modalProps}
|
||||
>
|
||||
<EditableVariableTree height={480} onSelect={setNode} {...rest} />
|
||||
</Modal>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const varTreeStyle = css`
|
||||
overflow: auto;
|
||||
|
||||
.ant-tree {
|
||||
font-family: Consolas, Menlo, Courier, monospace;
|
||||
}
|
||||
|
||||
.ant-tree .ant-tree-treenode {
|
||||
padding: 0;
|
||||
}
|
||||
.ant-tree.ant-tree-directory .ant-tree-treenode::before {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.ant-tree-indent-unit {
|
||||
width: 12px;
|
||||
}
|
||||
|
||||
.anticon-function {
|
||||
margin-left: 4px;
|
||||
color: var(--tango-colors-text2);
|
||||
}
|
||||
`;
|
||||
|
||||
interface VariableTreeProps {
|
||||
dataSource?: IVariableTreeNode[];
|
||||
onSelect?: (data: IVariableTreeNode) => void;
|
||||
onAdd?: (data: IVariableTreeNode) => void;
|
||||
onRemove?: (data: IVariableTreeNode) => void;
|
||||
onCopy?: (data: IVariableTreeNode) => void;
|
||||
onView?: (data: IVariableTreeNode) => void;
|
||||
showDeleteIcon?: boolean;
|
||||
showViewIcon?: boolean;
|
||||
}
|
||||
|
||||
export function VariableTree({
|
||||
dataSource = [],
|
||||
onSelect = noop,
|
||||
onAdd = noop,
|
||||
onRemove = noop,
|
||||
onCopy = noop,
|
||||
onView = noop,
|
||||
showDeleteIcon = false,
|
||||
showViewIcon = false,
|
||||
}: VariableTreeProps) {
|
||||
return (
|
||||
<Box className="VariableTree" css={varTreeStyle}>
|
||||
<Tree
|
||||
blockNode
|
||||
showIcon={false}
|
||||
defaultExpandAll
|
||||
treeData={dataSource}
|
||||
onSelect={(keys, detail) => {
|
||||
onSelect(detail.node);
|
||||
}}
|
||||
titleRender={(node) => {
|
||||
const isLeaf = !node.children;
|
||||
|
||||
if (isLeaf) {
|
||||
const isDeletable = node.showDeleteIcon ?? showDeleteIcon;
|
||||
return (
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center">
|
||||
<Text flex="1" truncated>
|
||||
{node.title}
|
||||
{node.type === 'function' && <FunctionOutlined />}
|
||||
</Text>
|
||||
<Box flex="0 0 72px" textAlign="right">
|
||||
{isDeletable && (
|
||||
<Popconfirm
|
||||
title="确认删除吗?该操作会导致引用此模型的代码报错,请谨慎操作!"
|
||||
onConfirm={() => onRemove(node)}
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
/>
|
||||
</Popconfirm>
|
||||
)}
|
||||
<CopyClipboard text={`tango.${node.key.replaceAll('.', '?.')}`}>
|
||||
{({ copied, onClick }) => {
|
||||
const label = copied ? '已复制' : '复制变量路径';
|
||||
return (
|
||||
<Tooltip title={label}>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onClick();
|
||||
onCopy(node);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}}
|
||||
</CopyClipboard>
|
||||
{showViewIcon && (
|
||||
<Tooltip title="查看变量详情">
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<EyeOutlined />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onView(node);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box display="flex" alignItems="center" justifyContent="space-between">
|
||||
<Text mr="m">{node.title}</Text>
|
||||
<Box>
|
||||
{node.showAddChildIcon && (
|
||||
<Tooltip title={`向 ${node.title} 中添加变量`}>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onAdd(node);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
interface NodePreviewProps {
|
||||
value?: unknown;
|
||||
/**
|
||||
* 选择预览结点的回调
|
||||
*/
|
||||
onSelect?: JsonViewProps['onCopy'];
|
||||
}
|
||||
|
||||
export function ValuePreview({ value, onSelect }: NodePreviewProps) {
|
||||
if (isNil(value)) {
|
||||
return <span>暂时没有可预览的数据</span>;
|
||||
}
|
||||
|
||||
if (isFunction(value)) {
|
||||
return <span>暂不支持预览函数</span>;
|
||||
}
|
||||
|
||||
if (typeof value === 'object') {
|
||||
return <JsonView src={value} enableCopy onCopy={onSelect} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<InputCode
|
||||
shape="inset"
|
||||
value={isString(value) ? `"${value}"` : String(value)}
|
||||
editable={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface NodeDefineProps {
|
||||
node: IVariableTreeNode;
|
||||
onSave?: (code: string, node: IVariableTreeNode) => void;
|
||||
onDelete?: (node: IVariableTreeNode) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 变量值定义面板
|
||||
*/
|
||||
function NodeDefineForm({ node, onSave = noop, onDelete = noop }: NodeDefineProps) {
|
||||
const [value, setValue] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [editable, { on, off }] = useBoolean();
|
||||
if (isNil(node.raw)) {
|
||||
return <Empty description="缺失定义数据" />;
|
||||
}
|
||||
return (
|
||||
<Box>
|
||||
<InputCode
|
||||
shape="inset"
|
||||
value={node.raw}
|
||||
onChange={(nextValue) => setValue(nextValue)}
|
||||
editable={editable}
|
||||
onBlur={() => {
|
||||
if (!isValidExpressionCode(value)) {
|
||||
setError('代码格式错误,请检查代码语法!');
|
||||
} else {
|
||||
setError('');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Box color="red" my="m">
|
||||
{error}
|
||||
</Box>
|
||||
<Box mt="l">
|
||||
{editable ? (
|
||||
<Space>
|
||||
<Button
|
||||
onClick={() => {
|
||||
off();
|
||||
setError('');
|
||||
}}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
if (error) {
|
||||
return;
|
||||
}
|
||||
|
||||
onSave(value, node);
|
||||
off();
|
||||
}}
|
||||
>
|
||||
保存
|
||||
</Button>
|
||||
</Space>
|
||||
) : (
|
||||
<Space>
|
||||
<Button onClick={on}>编辑</Button>
|
||||
<Popconfirm
|
||||
title="确认删除此变量吗?该操作会导致引用该变量的代码报错!"
|
||||
onConfirm={() => {
|
||||
onDelete(node);
|
||||
}}
|
||||
>
|
||||
<Button danger>删除</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
interface AddNodeFormProps {
|
||||
parentNode: IVariableTreeNode;
|
||||
onCancel?: React.MouseEventHandler<HTMLButtonElement>;
|
||||
onSubmit?: (storeName: string, data: { name: string; initialValue: string }) => void;
|
||||
}
|
||||
|
||||
function AddNodeForm({ parentNode, onCancel, onSubmit }: AddNodeFormProps) {
|
||||
return (
|
||||
<Form
|
||||
layout="vertical"
|
||||
autoComplete="off"
|
||||
validateTrigger="onBlur"
|
||||
onFinish={(values) => {
|
||||
if (onSubmit && parentNode) {
|
||||
const [type, storeName] = parentNode.key.split('.');
|
||||
onSubmit(storeName, values);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Form.Item label="所属模型">
|
||||
<Input value={parentNode?.key} disabled />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="变量名"
|
||||
name="name"
|
||||
rules={[
|
||||
{ required: true },
|
||||
{ pattern: /^\w+$/, message: '非法的变量标识符' },
|
||||
{
|
||||
validator(_, value) {
|
||||
const found = parentNode.children.find((item) => item.title === value);
|
||||
return found
|
||||
? Promise.reject(new Error('和已有变量名冲突,请换一个名字!'))
|
||||
: Promise.resolve();
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input placeholder="请输入变量名" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="初值"
|
||||
name="initialValue"
|
||||
rules={[
|
||||
{ required: true },
|
||||
{
|
||||
validator(_, value) {
|
||||
return isValidExpressionCode(value)
|
||||
? Promise.resolve()
|
||||
: Promise.reject(new Error('代码存在语法错误,请输入合法的代码片段!'));
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputCode shape="inset" placeholder="请输入初值" />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Space>
|
||||
<Button onClick={onCancel}>取消</Button>
|
||||
<Button type="primary" htmlType="submit">
|
||||
保存
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
import { isValidFunctionCode, url2serviceName } from '@music163/tango-helpers';
|
||||
import { InputCode } from '@music163/tango-ui';
|
||||
import { Button, Dropdown, Form, FormProps, Input, Select, Space } from 'antd';
|
||||
import { Box } from 'coral-system';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
interface AddServiceFormProps extends FormProps {
|
||||
initialValues?: object;
|
||||
onSubmit: (values: object) => void;
|
||||
onCancel: () => void;
|
||||
serviceModules: object[];
|
||||
serviceNames: string[];
|
||||
}
|
||||
|
||||
export function AddServiceForm({
|
||||
initialValues,
|
||||
onSubmit,
|
||||
onCancel,
|
||||
serviceModules,
|
||||
serviceNames,
|
||||
...formProps
|
||||
}: AddServiceFormProps) {
|
||||
const isModifyMode = !!initialValues?.['name']; // 是否为更新模式
|
||||
const [disabled, setDisabled] = useState(isModifyMode);
|
||||
const [form] = Form.useForm();
|
||||
|
||||
return (
|
||||
<Form
|
||||
form={form}
|
||||
labelCol={{ span: 6 }}
|
||||
wrapperCol={{ span: 17 }}
|
||||
colon={false}
|
||||
layout="horizontal"
|
||||
initialValues={initialValues}
|
||||
onFinish={(values) => {
|
||||
onSubmit(values);
|
||||
}}
|
||||
{...formProps}
|
||||
>
|
||||
<Form.Item
|
||||
label="命名空间"
|
||||
name="moduleName"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Select options={serviceModules} disabled={disabled || isModifyMode} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="方法名"
|
||||
name="name"
|
||||
rules={[
|
||||
{ required: true },
|
||||
{ pattern: /^[a-z]\w+$/, message: '请输入合法的方法名称' },
|
||||
!isModifyMode && {
|
||||
validator(_, value) {
|
||||
const isExist = serviceNames.includes(value);
|
||||
return isExist
|
||||
? Promise.reject(new Error('重复的方法名,请换一个!'))
|
||||
: Promise.resolve();
|
||||
},
|
||||
},
|
||||
]}
|
||||
extra={
|
||||
!disabled && (
|
||||
<Box>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
const url = form.getFieldValue('url');
|
||||
if (!url) {
|
||||
return;
|
||||
}
|
||||
|
||||
const funcName = url2serviceName(url);
|
||||
form.setFieldValue('name', funcName);
|
||||
}}
|
||||
>
|
||||
基于接口路径自动生成
|
||||
</Button>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
>
|
||||
<Input placeholder="请输入数据服务调用名称" disabled={disabled || isModifyMode} />
|
||||
</Form.Item>
|
||||
<Form.Item label="路径" name="url" rules={[{ required: true, type: 'url' }]}>
|
||||
<Input placeholder="请输入数据服务调用名称" disabled={disabled || isModifyMode} />
|
||||
</Form.Item>
|
||||
<Form.Item label="方法" name="method">
|
||||
<Select
|
||||
options={[
|
||||
{ label: 'GET', value: 'get' },
|
||||
{ label: 'POST', value: 'post' },
|
||||
]}
|
||||
placeholder="请选择 HTTP 请求方法"
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="格式化响应"
|
||||
name="formatter"
|
||||
tooltip="提供格式化函数,对结果进行格式化"
|
||||
validateTrigger="onBlur"
|
||||
rules={[
|
||||
{
|
||||
validator(_, value) {
|
||||
if (!value) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return isValidFunctionCode(value)
|
||||
? Promise.resolve()
|
||||
: Promise.reject(new Error('请提供合法的函数代码!'));
|
||||
},
|
||||
},
|
||||
]}
|
||||
extra={
|
||||
!disabled && (
|
||||
<Box>
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{ label: '直接返回(默认)', key: 'res => res' },
|
||||
{ label: '返回数据部分(云音乐标准规范)', key: 'res => res.data' },
|
||||
],
|
||||
onClick: ({ key }) => {
|
||||
form.setFieldValue('formatter', key);
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Button type="link" size="small">
|
||||
使用模板函数
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
>
|
||||
<InputCode showLineNumbers placeholder="res => res" editable={!disabled} />
|
||||
</Form.Item>
|
||||
<Form.Item wrapperCol={{ offset: 6, span: 18 }}>
|
||||
{isModifyMode ? (
|
||||
<Space>
|
||||
{!disabled ? (
|
||||
<>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setDisabled(true);
|
||||
}}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button type="primary" htmlType="submit">
|
||||
修改
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<Button
|
||||
onClick={() => {
|
||||
setDisabled(false);
|
||||
}}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
) : (
|
||||
<Space>
|
||||
<Button onClick={onCancel}>取消</Button>
|
||||
<Button type="primary" htmlType="submit">
|
||||
创建
|
||||
</Button>
|
||||
</Space>
|
||||
)}
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import React from 'react';
|
||||
import { Button, Form, Input, Space } from 'antd';
|
||||
import { isValidExpressionCode } from '@music163/tango-core';
|
||||
import { InputCode, Panel } from '@music163/tango-ui';
|
||||
import { IVariableTreeNode } from '@music163/tango-helpers';
|
||||
|
||||
export interface AddStoreVariableFormProps {
|
||||
parentNode: IVariableTreeNode;
|
||||
onCancel?: React.MouseEventHandler<HTMLButtonElement>;
|
||||
onSubmit?: (storeName: string, data: { name: string; initialValue: string }) => void;
|
||||
}
|
||||
|
||||
export function AddStoreVariableForm({
|
||||
parentNode,
|
||||
onCancel,
|
||||
onSubmit,
|
||||
}: AddStoreVariableFormProps) {
|
||||
return (
|
||||
<Form
|
||||
layout="vertical"
|
||||
autoComplete="off"
|
||||
validateTrigger="onBlur"
|
||||
onFinish={(values) => {
|
||||
if (onSubmit && parentNode) {
|
||||
const [type, storeName] = parentNode.key.split('.');
|
||||
onSubmit(storeName, values);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Form.Item label="所属模型">
|
||||
<Input value={parentNode?.key} disabled />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="变量名"
|
||||
name="name"
|
||||
rules={[
|
||||
{ required: true },
|
||||
{ pattern: /^\w+$/, message: '非法的变量标识符' },
|
||||
{
|
||||
validator(_, value) {
|
||||
const found = parentNode.children.find((item) => item.title === value);
|
||||
return found
|
||||
? Promise.reject(new Error('和已有变量名冲突,请换一个名字!'))
|
||||
: Promise.resolve();
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input placeholder="请输入变量名" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="初值"
|
||||
name="initialValue"
|
||||
rules={[
|
||||
{ required: true },
|
||||
{
|
||||
validator(_, value) {
|
||||
return value && isValidExpressionCode(value)
|
||||
? Promise.resolve()
|
||||
: Promise.reject(new Error('代码存在语法错误,请输入合法的代码片段!'));
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<InputCode shape="inset" placeholder="请输入初值" />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Space>
|
||||
<Button onClick={onCancel}>取消</Button>
|
||||
<Button type="primary" htmlType="submit">
|
||||
保存
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
export interface AddStoreFormProps {
|
||||
storeNames?: string[];
|
||||
onCancel?: () => void;
|
||||
onSubmit?: (data: { name: string }) => void;
|
||||
}
|
||||
|
||||
export function AddStoreForm({ storeNames = [], onCancel, onSubmit }: AddStoreFormProps) {
|
||||
return (
|
||||
<Panel title="添加模型" subTitle="模型可以用来组织一组变量" bodyProps={{ p: 'l' }}>
|
||||
<Form
|
||||
autoComplete="off"
|
||||
colon={false}
|
||||
onFinish={(values) => {
|
||||
onSubmit && onSubmit(values);
|
||||
}}
|
||||
>
|
||||
<Form.Item
|
||||
label="模型名称"
|
||||
name="name"
|
||||
rules={[
|
||||
{ required: true },
|
||||
{ pattern: /^[a-z]\w+$/, message: '非法的变量标识符' },
|
||||
{
|
||||
validator(_, value) {
|
||||
const found = storeNames.includes(value);
|
||||
return found
|
||||
? Promise.reject(new Error('已存在该模型名,请换一个!'))
|
||||
: Promise.resolve();
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input placeholder="请输入模型名称" />
|
||||
</Form.Item>
|
||||
<Form.Item label=" ">
|
||||
<Space>
|
||||
<Button onClick={onCancel}>取消</Button>
|
||||
<Button type="primary" htmlType="submit">
|
||||
保存
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,360 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import {
|
||||
IVariableTreeNode,
|
||||
filterTreeData,
|
||||
isServiceVariablePath,
|
||||
isStoreVariablePath,
|
||||
noop,
|
||||
parseServiceVariablePath,
|
||||
} from '@music163/tango-helpers';
|
||||
import { css, Box, Text } from 'coral-system';
|
||||
import { Button, Popconfirm, Tooltip, Tree } from 'antd';
|
||||
import {
|
||||
CopyOutlined,
|
||||
DeleteOutlined,
|
||||
EyeOutlined,
|
||||
FunctionOutlined,
|
||||
PlusOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { CopyClipboard, Panel, Search } from '@music163/tango-ui';
|
||||
import { AddStoreForm, AddStoreVariableForm } from './add-store';
|
||||
import { AddServiceForm } from './add-service';
|
||||
import {
|
||||
NodeCommonDetail,
|
||||
ValueDefine,
|
||||
ValueDefineProps,
|
||||
ValueDetail,
|
||||
ValueDetailProps,
|
||||
} from './value-detail';
|
||||
import { ValuePreview } from './value-preview';
|
||||
import { ServicePreview } from './service-preview';
|
||||
|
||||
const varTreeStyle = css`
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
|
||||
.ant-tree {
|
||||
font-family: Consolas, Menlo, Courier, monospace;
|
||||
}
|
||||
|
||||
.ant-tree-node-content-wrapper.ant-tree-node-content-wrapper-normal {
|
||||
width: calc(100% - 50px);
|
||||
}
|
||||
|
||||
.ant-tree .ant-tree-treenode {
|
||||
padding: 0;
|
||||
}
|
||||
.ant-tree.ant-tree-directory .ant-tree-treenode::before {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.ant-tree-indent-unit {
|
||||
width: 12px;
|
||||
}
|
||||
`;
|
||||
|
||||
type SelectNodeCallback = (data: IVariableTreeNode) => void;
|
||||
|
||||
export interface VariableTreeProps {
|
||||
defaultValueDetailMode?: ValueDetailProps['defaultMode'];
|
||||
dataSource: IVariableTreeNode[];
|
||||
appContext?: object;
|
||||
serviceModules?: any[];
|
||||
getPreviewValue?: (node: IVariableTreeNode) => unknown;
|
||||
getServiceData?: (serviceKey: string) => object;
|
||||
getServiceNames?: (moduleName: string) => string[];
|
||||
getStoreNames?: () => string[];
|
||||
onSelect?: SelectNodeCallback;
|
||||
onAddStoreVariable?: (storeName: string, data: any) => void;
|
||||
onAddStore?: (newStoreName: string) => void;
|
||||
onAddService?: (data: object) => void;
|
||||
onRemoveVariable?: (variableKey: string) => void;
|
||||
onUpdateVariable?: ValueDefineProps['onSave'];
|
||||
onUpdateService?: (data: object) => void;
|
||||
onCopy?: (data: IVariableTreeNode) => void;
|
||||
onView?: SelectNodeCallback;
|
||||
height?: number | string;
|
||||
showViewButton?: boolean;
|
||||
}
|
||||
|
||||
export function VariableTree({
|
||||
dataSource = [],
|
||||
serviceModules = [],
|
||||
appContext = {},
|
||||
defaultValueDetailMode,
|
||||
onSelect = noop,
|
||||
onAddStoreVariable = noop,
|
||||
onAddStore = noop,
|
||||
onAddService = noop,
|
||||
onRemoveVariable = noop,
|
||||
onUpdateVariable = noop,
|
||||
onUpdateService = noop,
|
||||
onCopy = noop,
|
||||
onView = noop,
|
||||
getServiceData,
|
||||
getServiceNames,
|
||||
getStoreNames,
|
||||
getPreviewValue = noop,
|
||||
showViewButton,
|
||||
...rest
|
||||
}: VariableTreeProps) {
|
||||
const [keyword, setKeyword] = useState('');
|
||||
const [activeNode, setActiveNode] = useState<IVariableTreeNode>();
|
||||
const [mode, setMode] = useState<
|
||||
'detail' | 'storeVariableDetail' | 'serviceDetail' | 'addVariable' | 'addStore' | 'addService'
|
||||
>();
|
||||
const clear = useCallback(() => {
|
||||
setActiveNode(null);
|
||||
setMode(null);
|
||||
}, []);
|
||||
|
||||
const selectNode = useCallback((node: IVariableTreeNode, callback?: SelectNodeCallback) => {
|
||||
if (isStoreVariablePath(node.key)) {
|
||||
setMode('storeVariableDetail');
|
||||
} else if (isServiceVariablePath(node.key)) {
|
||||
setMode('serviceDetail');
|
||||
} else {
|
||||
setMode('detail');
|
||||
}
|
||||
setActiveNode(node);
|
||||
callback?.(node);
|
||||
}, []);
|
||||
|
||||
const treeData = useMemo(() => {
|
||||
const pattern = new RegExp(keyword, 'ig');
|
||||
return keyword
|
||||
? filterTreeData(dataSource, (leaf) => pattern.test(leaf.title), 'children', true)
|
||||
: dataSource;
|
||||
}, [keyword, dataSource]);
|
||||
|
||||
return (
|
||||
<Box display="flex" columnGap="l" className="VariableTree" css={varTreeStyle} {...rest}>
|
||||
<Box className="VariableList" width="40%">
|
||||
<Box mb="m" position="sticky" top="0" bg="white" zIndex={2}>
|
||||
<Search placeholder="请输入变量名" onChange={(val) => setKeyword(val?.trim())} />
|
||||
</Box>
|
||||
<Tree
|
||||
blockNode
|
||||
showIcon={false}
|
||||
defaultExpandAll
|
||||
treeData={treeData}
|
||||
onSelect={(keys, detail) => {
|
||||
selectNode(detail.node, onSelect);
|
||||
}}
|
||||
titleRender={(node) => {
|
||||
const isLeaf = !node.children;
|
||||
|
||||
if (isLeaf) {
|
||||
const showView = node.showViewButton ?? showViewButton;
|
||||
return (
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center">
|
||||
<Text flex="1" truncated>
|
||||
{node.title}
|
||||
{node.type === 'function' && (
|
||||
<Text color="text3" ml="m">
|
||||
<FunctionOutlined />
|
||||
</Text>
|
||||
)}
|
||||
</Text>
|
||||
<Box flex="0 0 72px" textAlign="right">
|
||||
{node.showRemoveButton && (
|
||||
<Popconfirm
|
||||
title={`确认删除吗 ${node.title}?该操作会导致引用此模型的代码报错,请谨慎操作!`}
|
||||
onConfirm={() => {
|
||||
onRemoveVariable(node.key);
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
/>
|
||||
</Popconfirm>
|
||||
)}
|
||||
<CopyClipboard text={`tango.${node.key.replaceAll('.', '?.')}`}>
|
||||
{({ copied, onClick }) => {
|
||||
const label = copied ? '已复制' : '复制变量路径';
|
||||
return (
|
||||
<Tooltip title={label}>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onClick();
|
||||
onCopy(node);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}}
|
||||
</CopyClipboard>
|
||||
{showView && (
|
||||
<Tooltip title="查看变量详情">
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<EyeOutlined />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
selectNode(node, onView);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box display="flex" alignItems="center" justifyContent="space-between">
|
||||
<Text mr="m">{node.title}</Text>
|
||||
<Box>
|
||||
{/^\$?stores$/.test(node.key) && (
|
||||
<Tooltip title="新建数据模型">
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setMode('addStore');
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
{node.showAddButton && /^stores\.[a-zA-Z0-9]+$/.test(node.key) && (
|
||||
<Tooltip title={`向 ${node.title} 中添加变量`}>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setActiveNode(node);
|
||||
setMode('addVariable');
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
{/^services(\.[a-zA-Z0-9]+)?$/.test(node.key) && (
|
||||
<Tooltip title={`向 ${node.title} 中添加服务函数`}>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setActiveNode(node);
|
||||
setMode('addService');
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Box className="VariableDetail" flex="1" position="sticky" top="0" overflow="auto">
|
||||
{mode === 'detail' && <NodeCommonDetail data={activeNode} />}
|
||||
{mode === 'storeVariableDetail' && (
|
||||
<ValueDetail key={activeNode.key} defaultMode={defaultValueDetailMode}>
|
||||
{(previewMode) =>
|
||||
previewMode === 'runtime' ? (
|
||||
<ValuePreview
|
||||
value={getPreviewValue(activeNode)}
|
||||
onCopy={(valuePath) => {
|
||||
return ['tango', activeNode.key.replaceAll('.', '?.'), valuePath].join('.');
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<ValueDefine data={activeNode} onSave={onUpdateVariable} />
|
||||
)
|
||||
}
|
||||
</ValueDetail>
|
||||
)}
|
||||
{mode === 'serviceDetail' && (
|
||||
<>
|
||||
<Panel shape="solid" title="服务函数配置">
|
||||
<AddServiceForm
|
||||
key={activeNode.key}
|
||||
serviceModules={serviceModules}
|
||||
serviceNames={(function () {
|
||||
const { moduleName } = parseServiceVariablePath(activeNode.key);
|
||||
return getServiceNames?.(moduleName) || [];
|
||||
})()}
|
||||
initialValues={{
|
||||
...getServiceData?.(activeNode.key),
|
||||
}}
|
||||
onCancel={clear}
|
||||
onSubmit={(values) => {
|
||||
onUpdateService(values);
|
||||
clear();
|
||||
}}
|
||||
/>
|
||||
</Panel>
|
||||
<Panel shape="solid" title="服务函数预览" mt="l">
|
||||
<ServicePreview
|
||||
key={activeNode.key}
|
||||
appContext={appContext}
|
||||
functionKey={activeNode.key}
|
||||
/>
|
||||
</Panel>
|
||||
</>
|
||||
)}
|
||||
{mode === 'addVariable' && (
|
||||
<Panel shape="solid" title="添加变量">
|
||||
<AddStoreVariableForm
|
||||
parentNode={activeNode}
|
||||
onSubmit={(storeName, data) => {
|
||||
onAddStoreVariable(storeName, data);
|
||||
clear();
|
||||
}}
|
||||
onCancel={() => {
|
||||
clear();
|
||||
}}
|
||||
/>
|
||||
</Panel>
|
||||
)}
|
||||
{mode === 'addStore' && (
|
||||
<AddStoreForm
|
||||
storeNames={getStoreNames?.() || []}
|
||||
onSubmit={({ name }) => {
|
||||
onAddStore(name);
|
||||
clear();
|
||||
}}
|
||||
onCancel={() => {
|
||||
clear();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{mode === 'addService' && (
|
||||
<Panel shape="solid" title="创建服务函数">
|
||||
<AddServiceForm
|
||||
key={activeNode.key}
|
||||
serviceModules={serviceModules}
|
||||
serviceNames={activeNode.children?.map((item) => item.title) || []}
|
||||
initialValues={{
|
||||
moduleName: activeNode.title,
|
||||
}}
|
||||
onCancel={() => {
|
||||
clear();
|
||||
}}
|
||||
onSubmit={(values) => {
|
||||
onAddService(values);
|
||||
clear();
|
||||
}}
|
||||
/>
|
||||
</Panel>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Box } from 'coral-system';
|
||||
import { Button, Empty } from 'antd';
|
||||
import { PlayCircleOutlined } from '@ant-design/icons';
|
||||
import { InputCode, Panel, JsonView } from '@music163/tango-ui';
|
||||
import { isNil, logger, code2object, getValue } from '@music163/tango-helpers';
|
||||
|
||||
export interface ServicePreviewProps {
|
||||
appContext?: any;
|
||||
functionKey?: string;
|
||||
}
|
||||
|
||||
export function ServicePreview({ appContext, functionKey }: ServicePreviewProps) {
|
||||
const [payload, setPayload] = useState({});
|
||||
const [result, setResult] = useState<any>();
|
||||
const [error, setError] = useState('');
|
||||
return (
|
||||
<Box>
|
||||
<Panel title="请求参数" bodyProps={{ px: 'l' }}>
|
||||
<InputCode
|
||||
placeholder={'添加请求参数,对象格式,如 { key: value }'}
|
||||
editable
|
||||
showLineNumbers
|
||||
onChange={(value: string) => {
|
||||
const obj = code2object(value);
|
||||
setPayload(obj);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
block
|
||||
type="primary"
|
||||
style={{ margin: '8px 0' }}
|
||||
disabled={!appContext}
|
||||
onClick={() => {
|
||||
if (!appContext) {
|
||||
setError('执行上下文未准备好,请关闭面板重试');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const fn = getValue(appContext, functionKey);
|
||||
fn(payload).then((data: any) => {
|
||||
setError('');
|
||||
setResult(data);
|
||||
});
|
||||
} catch (err) {
|
||||
setError('接口调用失败,请检查参数是否正确');
|
||||
logger.error(err);
|
||||
}
|
||||
}}
|
||||
icon={<PlayCircleOutlined />}
|
||||
>
|
||||
预览
|
||||
</Button>
|
||||
</Panel>
|
||||
<Panel title="请求响应" bodyProps={{ px: 'l' }}>
|
||||
{error || (result ? <ResponseDataPreview data={result} /> : '点击预览按钮测试接口返回值')}
|
||||
</Panel>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
interface ResponseDataPreviewProps {
|
||||
data?: object;
|
||||
}
|
||||
|
||||
function ResponseDataPreview({ data }: ResponseDataPreviewProps) {
|
||||
let ret: React.ReactNode;
|
||||
|
||||
if (!isNil(data)) {
|
||||
switch (typeof data) {
|
||||
case 'object':
|
||||
ret = <JsonView src={data as object} />;
|
||||
break;
|
||||
default:
|
||||
ret = String(data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const initialRet = <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />;
|
||||
|
||||
return (
|
||||
<Box
|
||||
className="ResponseDataPreview"
|
||||
overflow="auto"
|
||||
height="auto"
|
||||
minHeight={260}
|
||||
marginTop={10}
|
||||
>
|
||||
{ret || initialRet}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
import React, { useState } from 'react';
|
||||
import { IVariableTreeNode, isNil, noop, useBoolean } from '@music163/tango-helpers';
|
||||
import { Empty, Space, Button, Radio, Alert } from 'antd';
|
||||
import { Box } from 'coral-system';
|
||||
import { InputCode, Panel } from '@music163/tango-ui';
|
||||
import { isValidExpressionCode } from '@music163/tango-core';
|
||||
|
||||
const previewOptions = [
|
||||
{ label: '运行时', value: 'runtime' },
|
||||
{ label: '定义', value: 'define' },
|
||||
];
|
||||
|
||||
export type ValueDetailModeType = 'runtime' | 'define';
|
||||
|
||||
export interface ValueDetailProps {
|
||||
defaultMode?: ValueDetailModeType;
|
||||
children: (mode: ValueDetailModeType) => React.ReactNode;
|
||||
}
|
||||
|
||||
export function ValueDetail({ defaultMode = 'runtime', children }: ValueDetailProps) {
|
||||
const [mode, setMode] = useState<ValueDetailModeType>(defaultMode);
|
||||
return (
|
||||
<Panel
|
||||
title="变量详情"
|
||||
shape="solid"
|
||||
extra={
|
||||
<Radio.Group
|
||||
optionType="button"
|
||||
buttonStyle="solid"
|
||||
size="small"
|
||||
value={mode}
|
||||
onChange={(e) => {
|
||||
setMode(e.target.value);
|
||||
}}
|
||||
options={previewOptions}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{children(mode)}
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
export interface ValueDefineProps {
|
||||
data: IVariableTreeNode;
|
||||
onSave?: (variableKey: string, code: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 变量值定义面板
|
||||
*/
|
||||
export function ValueDefine({ data, onSave = noop }: ValueDefineProps) {
|
||||
const [value, setValue] = useState(data.raw);
|
||||
const [error, setError] = useState('');
|
||||
const [editable, { on, off }] = useBoolean();
|
||||
if (isNil(data.raw)) {
|
||||
return <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="缺失定义数据" />;
|
||||
}
|
||||
return (
|
||||
<Box>
|
||||
<InputCode
|
||||
shape="inset"
|
||||
value={value}
|
||||
onChange={(nextValue) => setValue(nextValue)}
|
||||
readOnly={!editable}
|
||||
onBlur={() => {
|
||||
if (!value) {
|
||||
setError('输入内容不可为空!');
|
||||
} else if (value && !isValidExpressionCode(`foo = ${value}`)) {
|
||||
// 这里先保证输入是一个合法的表达式
|
||||
setError('代码格式错误,请检查代码语法!');
|
||||
} else {
|
||||
setError('');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Box color="red" my="m">
|
||||
{error}
|
||||
</Box>
|
||||
<Box mt="l">
|
||||
{editable ? (
|
||||
<Space>
|
||||
<Button
|
||||
onClick={() => {
|
||||
off();
|
||||
setError('');
|
||||
}}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => {
|
||||
if (error) {
|
||||
return;
|
||||
}
|
||||
|
||||
onSave(data.key, value);
|
||||
off();
|
||||
}}
|
||||
>
|
||||
保存
|
||||
</Button>
|
||||
</Space>
|
||||
) : (
|
||||
<Button onClick={on}>编辑</Button>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export interface NodeCommonDetailProps {
|
||||
data: IVariableTreeNode;
|
||||
}
|
||||
|
||||
export function NodeCommonDetail({ data }: NodeCommonDetailProps) {
|
||||
if (!data?.help) {
|
||||
return <div />;
|
||||
}
|
||||
return <Alert type="info" message={`使用说明:${data.help}`} closable />;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import { isFunction, isNil, isString } from '@music163/tango-helpers';
|
||||
import { InputCode, JsonView, JsonViewProps } from '@music163/tango-ui';
|
||||
import { Empty } from 'antd';
|
||||
|
||||
interface ValuePreviewProps {
|
||||
value?: unknown;
|
||||
/**
|
||||
* 选择预览结点的回调
|
||||
*/
|
||||
onCopy?: JsonViewProps['onCopy'];
|
||||
}
|
||||
|
||||
export function ValuePreview({ value, onCopy }: ValuePreviewProps) {
|
||||
if (isNil(value)) {
|
||||
return <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂时没有可预览的数据" />;
|
||||
}
|
||||
|
||||
if (isFunction(value)) {
|
||||
return <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="暂不支持预览函数" />;
|
||||
}
|
||||
|
||||
if (typeof value === 'object') {
|
||||
return <JsonView src={value} enableCopy onCopy={onCopy} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<InputCode
|
||||
shape="inset"
|
||||
value={isString(value) ? `"${value}"` : String(value)}
|
||||
editable={false}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -36,6 +36,8 @@ export function DesignerPanel(props: DesignerPanelProps) {
|
||||
height="48px"
|
||||
bg="colors.custom.topNavBg"
|
||||
color="colors.custom.topNavColor"
|
||||
borderBottom="solid"
|
||||
borderColor="colors.custom.topNavBorderColor"
|
||||
className="DesignerPanelHeader"
|
||||
>
|
||||
<Box display="flex" alignItems="center">
|
||||
|
||||
@@ -3,9 +3,9 @@ import { SystemProvider, extendTheme } from 'coral-system';
|
||||
import { ConfigProvider } from 'antd';
|
||||
import { TangoEngineProvider, ITangoEngineContext } from '@music163/tango-context';
|
||||
import { createFromIconfontCN } from '@ant-design/icons';
|
||||
import { DesignerProvider, IDesignerContext } from './context';
|
||||
import { theme as baseTheme } from './theme';
|
||||
import zhCN from 'antd/lib/locale/zh_CN';
|
||||
import { DesignerProvider, IDesignerContext } from './context';
|
||||
import defaultTheme from './themes/default';
|
||||
|
||||
export interface DesignerProps extends IDesignerContext, ITangoEngineContext {
|
||||
/**
|
||||
@@ -34,7 +34,7 @@ export function Designer(props: DesignerProps) {
|
||||
iconfontScriptUrl = '//at.alicdn.com/t/c/font_2891794_lzc7rtwuzf.js',
|
||||
children,
|
||||
} = props;
|
||||
const theme = useMemo(() => extendTheme(themeProp, baseTheme), [themeProp]);
|
||||
const theme = useMemo(() => extendTheme(themeProp, defaultTheme), [themeProp]);
|
||||
useEffect(() => {
|
||||
createFromIconfontCN({
|
||||
scriptUrl: iconfontScriptUrl,
|
||||
|
||||
@@ -72,7 +72,6 @@ export function getElementData(
|
||||
const bounding = getElementBoundingData(element, relativeContainer);
|
||||
const data = getElementDataProps(element);
|
||||
const dnd = parseDndId(data.dnd);
|
||||
// FIXME: 是否改为按需执行,仅 dropTarget 需要 display 信息,且目前 display 信息并不准确
|
||||
const display = getElementCSSDisplay(element);
|
||||
return {
|
||||
id: dnd.id,
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export * from './dom';
|
||||
export * from './template';
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
const newStoreTemplate = `
|
||||
import { defineStore } from '@music163/tango-boot';
|
||||
export default defineStore({
|
||||
});
|
||||
`;
|
||||
|
||||
export const CODE_TEMPLATES = {
|
||||
newStoreTemplate,
|
||||
};
|
||||
@@ -12,6 +12,8 @@ export * from './sidebar';
|
||||
export * from './toolbar';
|
||||
export * from './selection-menu';
|
||||
export * from './widgets';
|
||||
export * from './themes';
|
||||
export * from './components';
|
||||
|
||||
export { register as registerSetter } from '@music163/tango-setting-form';
|
||||
|
||||
|
||||
@@ -17,6 +17,12 @@ const navigatorStyle = css`
|
||||
|
||||
.navigatorInput {
|
||||
flex: 1;
|
||||
border-color: transparent;
|
||||
background-color: var(--tango-colors-fill2);
|
||||
|
||||
&:focus {
|
||||
background-color: #fff;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -68,7 +74,7 @@ export class Navigator extends React.Component<NavigatorProps, NavigatorState> {
|
||||
className="navigator"
|
||||
bg="white"
|
||||
px="l"
|
||||
py="4px"
|
||||
py="m"
|
||||
borderBottom="solid"
|
||||
borderBottomColor="line.normal"
|
||||
css={navigatorStyle}
|
||||
|
||||
@@ -23,6 +23,12 @@ export type SandboxProps = Omit<CodeSandboxProps, 'files' | 'eventHandlers' | 'o
|
||||
* tangoConfigJson 处理器
|
||||
*/
|
||||
configFormatter?: IMergeTangoConfigJsonConfig['formatter'];
|
||||
/**
|
||||
* 文件格式化器
|
||||
* @param files 沙箱文件列表
|
||||
* @returns 修改后的文件列表
|
||||
*/
|
||||
filesFormatter?: (files: CodeSandboxProps['files']) => CodeSandboxProps['files'];
|
||||
sandboxType?: 'design' | 'preview';
|
||||
mode?: 'single' | 'combined';
|
||||
injectScript?: string;
|
||||
@@ -41,6 +47,7 @@ const LANDING_PAGE_PATH = '/__background_landing_page__';
|
||||
function useSandbox({
|
||||
isPreview: isPreviewProp,
|
||||
configFormatter,
|
||||
filesFormatter,
|
||||
onViewChange,
|
||||
onMessage: onMessageProp,
|
||||
onLoad: onLoadProp,
|
||||
@@ -79,7 +86,12 @@ function useSandbox({
|
||||
prev[filename] = { code };
|
||||
return prev;
|
||||
}, {});
|
||||
|
||||
files = fixSandboxFiles(files, workspace.entry);
|
||||
if (filesFormatter) {
|
||||
// 支持用户对传入的文件进行修改
|
||||
files = filesFormatter(files);
|
||||
}
|
||||
|
||||
const onMessage = (data: any) => onMessageProp && onMessageProp(data, getSandboxConfig());
|
||||
const onLoad = () => onLoadProp && onLoadProp(getSandboxConfig());
|
||||
@@ -413,6 +425,7 @@ function fixSandboxFiles(files: Record<string, { code: string }>, entry = '/src/
|
||||
`,
|
||||
};
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { AutoComplete } from 'antd';
|
||||
import { ActionSelect } from '@music163/tango-ui';
|
||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||
import { useWorkspace, useWorkspaceData } from '@music163/tango-context';
|
||||
import { ExpressionModal } from './expression-setter';
|
||||
import { ExpressionModal, getWrappedExpressionCode } from './expression-setter';
|
||||
|
||||
enum EventAction {
|
||||
NoAction = 'noAction',
|
||||
@@ -47,8 +47,9 @@ export function EventSetter(props: EventSetterProps) {
|
||||
|
||||
const handleChange = useCallback<FormItemComponentProps['onChange']>(
|
||||
(nextValue: any, ...args) => {
|
||||
if (nextValue !== value) {
|
||||
onChange(nextValue, ...args);
|
||||
const ret = getWrappedExpressionCode(nextValue);
|
||||
if (ret !== value) {
|
||||
onChange(ret, ...args);
|
||||
}
|
||||
},
|
||||
[onChange, value],
|
||||
@@ -83,7 +84,7 @@ export function EventSetter(props: EventSetterProps) {
|
||||
onCancel={() => setExpModalVisible(false)}
|
||||
onOk={(nextValue) => {
|
||||
setExpModalVisible(false);
|
||||
handleChange(`{${nextValue}}`);
|
||||
handleChange(nextValue);
|
||||
}}
|
||||
dataSource={actionVariables}
|
||||
/>
|
||||
|
||||
@@ -6,13 +6,22 @@ import {
|
||||
isWrappedByExpressionContainer,
|
||||
value2expressionCode,
|
||||
} from '@music163/tango-core';
|
||||
import { getVariableContent, noop, useBoolean, getValue } from '@music163/tango-helpers';
|
||||
import {
|
||||
getVariableContent,
|
||||
noop,
|
||||
useBoolean,
|
||||
getValue,
|
||||
isStoreVariablePath,
|
||||
IVariableTreeNode,
|
||||
} from '@music163/tango-helpers';
|
||||
import { CloseCircleFilled, ExpandAltOutlined } from '@ant-design/icons';
|
||||
import { IconButton, Panel, InputCode } from '@music163/tango-ui';
|
||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||
import { useWorkspace, useWorkspaceData } from '@music163/tango-context';
|
||||
import { EditableVariableTree, IVariableTreeNode } from '../components';
|
||||
import { VariableTree } from '../components';
|
||||
import { useSandboxQuery } from '../context';
|
||||
import { CODE_TEMPLATES } from '../helpers';
|
||||
import { shapeServiceValues } from '../sidebar/datasource-panel/interface-config';
|
||||
|
||||
export const expressionValueValidate = (value: string) => {
|
||||
if (isWrappedByExpressionContainer(value)) {
|
||||
@@ -34,6 +43,23 @@ export const jsonValueValidate = (value: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 返回 `{}` 包裹后的表达式代码
|
||||
* @param code 原始代码
|
||||
* @returns
|
||||
*/
|
||||
export function getWrappedExpressionCode(code: string) {
|
||||
let ret;
|
||||
if (!code) {
|
||||
// do nothing
|
||||
} else if (isWrappedByExpressionContainer(code)) {
|
||||
ret = code;
|
||||
} else {
|
||||
ret = `{${code}}`;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
const suffixStyle = css`
|
||||
.anticon-close-circle {
|
||||
color: rgba(0, 0, 0, 0.25);
|
||||
@@ -60,6 +86,7 @@ export function ExpressionSetter(props: ExpressionSetterProps) {
|
||||
value: valueProp,
|
||||
status,
|
||||
allowClear = true,
|
||||
newStoreTemplate,
|
||||
} = props;
|
||||
const [visible, { on, off }] = useBoolean();
|
||||
const [inputValue, setInputValue] = useState(() => {
|
||||
@@ -79,15 +106,7 @@ export function ExpressionSetter(props: ExpressionSetterProps) {
|
||||
return;
|
||||
}
|
||||
|
||||
let ret;
|
||||
|
||||
if (!code) {
|
||||
// do nothing
|
||||
} else if (isWrappedByExpressionContainer(code)) {
|
||||
ret = code;
|
||||
} else {
|
||||
ret = `{${code}}`;
|
||||
}
|
||||
const ret = getWrappedExpressionCode(code);
|
||||
|
||||
if (ret === valueProp) {
|
||||
return;
|
||||
@@ -134,11 +153,12 @@ export function ExpressionSetter(props: ExpressionSetterProps) {
|
||||
subTitle={modalTip}
|
||||
placeholder={placeholder}
|
||||
autoCompleteOptions={autoCompleteOptions}
|
||||
newStoreTemplate={newStoreTemplate}
|
||||
visible={visible}
|
||||
value={inputValue}
|
||||
onCancel={() => off()}
|
||||
onOk={(value) => {
|
||||
onChange(value);
|
||||
change(value);
|
||||
off();
|
||||
}}
|
||||
/>
|
||||
@@ -157,6 +177,10 @@ export interface ExpressionModalProps {
|
||||
onOk?: (value: string) => void;
|
||||
dataSource?: IVariableTreeNode[];
|
||||
autoCompleteOptions?: string[];
|
||||
/**
|
||||
* 新建 store 的模板代码
|
||||
*/
|
||||
newStoreTemplate?: string;
|
||||
}
|
||||
|
||||
export function ExpressionModal({
|
||||
@@ -170,18 +194,19 @@ export function ExpressionModal({
|
||||
value,
|
||||
dataSource,
|
||||
autoCompleteOptions,
|
||||
newStoreTemplate = CODE_TEMPLATES.newStoreTemplate,
|
||||
}: ExpressionModalProps) {
|
||||
const [exp, setExp] = useState(value ?? defaultValue);
|
||||
const [error, setError] = useState('');
|
||||
const workspace = useWorkspace();
|
||||
const onAction = useCallback(
|
||||
(action: string, args: unknown[]) => {
|
||||
workspace[action]?.(...args);
|
||||
},
|
||||
[workspace],
|
||||
);
|
||||
const sandbox = useSandboxQuery();
|
||||
|
||||
const { expressionVariables } = useWorkspaceData();
|
||||
const serviceModules = Object.keys(workspace.serviceModules).map((key) => ({
|
||||
label: key === 'index' ? '默认模块' : key,
|
||||
value: key,
|
||||
}));
|
||||
|
||||
const sandbox = useSandboxQuery();
|
||||
const evaluateContext = sandbox.window;
|
||||
|
||||
const handleExpInputChange = (val: string) => {
|
||||
@@ -222,10 +247,29 @@ export function ExpressionModal({
|
||||
/>
|
||||
{error ? <Text color="red">输入的表达式存在语法错误,请修改后再提交!</Text> : null}
|
||||
</Panel>
|
||||
<Panel title="从变量列表中选中" shape="solid" borderTop="0">
|
||||
<EditableVariableTree
|
||||
<Panel
|
||||
title="从变量列表中选中"
|
||||
shape="solid"
|
||||
borderTop="0"
|
||||
overflow="hidden"
|
||||
bodyProps={{ overflow: 'hidden' }}
|
||||
>
|
||||
<VariableTree
|
||||
height={380}
|
||||
showViewButton
|
||||
dataSource={dataSource || expressionVariables}
|
||||
appContext={sandbox?.window['tango']}
|
||||
getStoreNames={() => Object.keys(workspace.storeModules)}
|
||||
serviceModules={serviceModules}
|
||||
getServiceData={(serviceKey) => {
|
||||
const data = workspace.getServiceFunction(serviceKey);
|
||||
return {
|
||||
name: data.name,
|
||||
moduleName: data.moduleName,
|
||||
method: 'get',
|
||||
...data.config,
|
||||
};
|
||||
}}
|
||||
onSelect={(node) => {
|
||||
if (!node.key) {
|
||||
return;
|
||||
@@ -241,17 +285,29 @@ export function ExpressionModal({
|
||||
}
|
||||
setExp(str);
|
||||
}}
|
||||
onAddVariable={(storeName, data) => {
|
||||
onAction('addStoreState', [storeName, data.name, data.initialValue]);
|
||||
onAddStoreVariable={(storeName, data) => {
|
||||
workspace.addStoreState(storeName, data.name, data.initialValue);
|
||||
}}
|
||||
onDeleteVariable={(storeName, stateName) => {
|
||||
onAction('removeStoreState', [storeName, stateName]);
|
||||
onUpdateVariable={(variableKey, code) => {
|
||||
workspace.updateStoreVariable(variableKey, code);
|
||||
}}
|
||||
onDeleteStore={(storeName) => {
|
||||
onAction('removeStoreModule', [storeName]);
|
||||
onAddStore={(storeName) => {
|
||||
workspace.addStoreFile(storeName, newStoreTemplate);
|
||||
}}
|
||||
onSave={(code, node) => {
|
||||
onAction('updateModuleCodeByVariablePath', [node.key, code]);
|
||||
onRemoveVariable={(variablePath) => {
|
||||
if (isStoreVariablePath(variablePath)) {
|
||||
workspace.removeStoreVariable(variablePath);
|
||||
} else {
|
||||
workspace.removeServiceFunction(variablePath);
|
||||
}
|
||||
}}
|
||||
onAddService={(data) => {
|
||||
const { name, moduleName, ...payload } = shapeServiceValues(data);
|
||||
workspace.addServiceFunction(name, payload, moduleName);
|
||||
}}
|
||||
onUpdateService={(data) => {
|
||||
const { name, moduleName, ...payload } = shapeServiceValues(data);
|
||||
workspace.updateServiceFunction(name, payload, moduleName);
|
||||
}}
|
||||
getPreviewValue={(node) => {
|
||||
if (!node || !node.key) {
|
||||
@@ -259,7 +315,7 @@ export function ExpressionModal({
|
||||
}
|
||||
|
||||
if (node.type === 'function') {
|
||||
return node.raw;
|
||||
return;
|
||||
}
|
||||
|
||||
return getValue(evaluateContext['tango'], node.key);
|
||||
|
||||
@@ -168,4 +168,6 @@ const setters: IFormItemCreateOptions[] = [
|
||||
{ name: 'borderSetter', component: BorderSetter },
|
||||
];
|
||||
|
||||
setters.forEach(register);
|
||||
export function registerBuiltinSetters() {
|
||||
setters.forEach(register);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Box } from 'coral-system';
|
||||
import { Input, Tooltip } from 'antd';
|
||||
import { getValue, isFunction } from '@music163/tango-helpers';
|
||||
import { getValue, isFunction, isStoreVariablePath } from '@music163/tango-helpers';
|
||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||
import { MenuOutlined } from '@ant-design/icons';
|
||||
import { useWorkspace, useWorkspaceData } from '@music163/tango-context';
|
||||
import { EditableVariableTreeModal } from '../components';
|
||||
import { VariableTreeModal } from '../components';
|
||||
import { useSandboxQuery } from '../context';
|
||||
import { CODE_TEMPLATES } from '../helpers';
|
||||
|
||||
function object2treeData(
|
||||
val: any,
|
||||
@@ -52,43 +53,40 @@ function traverseTreeData(val: any, callback: (val: any) => void) {
|
||||
}
|
||||
}
|
||||
|
||||
export function ModelSetter({ value, onChange }: FormItemComponentProps) {
|
||||
export function ModelSetter({
|
||||
value,
|
||||
onChange,
|
||||
newStoreTemplate = CODE_TEMPLATES.newStoreTemplate,
|
||||
}: FormItemComponentProps) {
|
||||
const [inputValue, setInputValue] = useState(value);
|
||||
const { modelVariables } = useWorkspaceData();
|
||||
const evaluateContext = useSandboxQuery().window || {};
|
||||
const workspace = useWorkspace();
|
||||
const onAction = useCallback(
|
||||
(action: string, args: unknown[]) => {
|
||||
workspace[action]?.(...args);
|
||||
},
|
||||
[workspace],
|
||||
);
|
||||
const definedVariables = useMemo(() => {
|
||||
const map = new Map();
|
||||
const list: string[] = [];
|
||||
traverseTreeData(modelVariables, (node) => {
|
||||
if (node.key.split('.').length > 1) {
|
||||
map.set(node.key, node);
|
||||
list.push(node.key);
|
||||
}
|
||||
});
|
||||
return map;
|
||||
return list;
|
||||
}, [modelVariables]);
|
||||
|
||||
const variables = evaluateContext['tango']?.stores
|
||||
? [
|
||||
object2treeData(evaluateContext['tango']?.stores, 'stores', 0, 1, (keyPath, val) => {
|
||||
if (keyPath.split('.').length === 2 && definedVariables.has(keyPath)) {
|
||||
return {
|
||||
showAddChildIcon: true,
|
||||
showRemoveIcon: true,
|
||||
};
|
||||
}
|
||||
const ret: any = {};
|
||||
if (isFunction(val)) {
|
||||
// 不可以同步给函数类型变量
|
||||
return {
|
||||
disabled: true,
|
||||
type: 'function',
|
||||
};
|
||||
ret.disabled = true;
|
||||
ret.type = 'function';
|
||||
}
|
||||
|
||||
if (!definedVariables.includes(keyPath)) {
|
||||
ret.showAddButton = false;
|
||||
ret.showRemoveButton = false;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}),
|
||||
]
|
||||
: modelVariables;
|
||||
@@ -122,31 +120,35 @@ export function ModelSetter({ value, onChange }: FormItemComponentProps) {
|
||||
onChange={onInputChange}
|
||||
onBlur={onInputBlur}
|
||||
suffix={
|
||||
<EditableVariableTreeModal
|
||||
<VariableTreeModal
|
||||
title="同步到的变量"
|
||||
trigger={
|
||||
<Tooltip title="从模型列表选择" placement="topRight">
|
||||
<MenuOutlined />
|
||||
</Tooltip>
|
||||
}
|
||||
modes={['preview']}
|
||||
dataSource={variables as any[]}
|
||||
onSelect={(node) => {
|
||||
const modelPath = node.key.split('.').slice(1).join('.');
|
||||
onChange(modelPath);
|
||||
}}
|
||||
onAddVariable={(storeName, data) => {
|
||||
onAction('addStoreState', [storeName, data.name, data.initialValue]);
|
||||
onAddStoreVariable={(storeName, data) => {
|
||||
workspace.addStoreState(storeName, data.name, data.initialValue);
|
||||
}}
|
||||
onDeleteVariable={(storeName, stateName) => {
|
||||
onAction('removeStoreState', [storeName, stateName]);
|
||||
onUpdateVariable={(variableKey, code) => {
|
||||
workspace.updateStoreVariable(variableKey, code);
|
||||
}}
|
||||
onDeleteStore={(storeName) => {
|
||||
onAction('removeStoreModule', [storeName]);
|
||||
onAddStore={(storeName) => {
|
||||
workspace.addStoreFile(storeName, newStoreTemplate);
|
||||
}}
|
||||
onSave={(code, node) => {
|
||||
onAction('updateModuleCodeByVariablePath', [node.key, code]);
|
||||
onRemoveVariable={(variablePath) => {
|
||||
if (isStoreVariablePath(variablePath)) {
|
||||
workspace.removeStoreVariable(variablePath);
|
||||
} else {
|
||||
workspace.removeServiceFunction(variablePath);
|
||||
}
|
||||
}}
|
||||
getStoreNames={() => Object.keys(workspace.storeModules)}
|
||||
getPreviewValue={(node) => {
|
||||
if (!node || !node.key) {
|
||||
return;
|
||||
|
||||
@@ -4,7 +4,9 @@ import { SettingForm, FormModel, SettingFormProps } from '@music163/tango-settin
|
||||
import { Panel } from '@music163/tango-ui';
|
||||
import { clone } from '@music163/tango-helpers';
|
||||
import { observer, useDesigner, useWorkspace } from '@music163/tango-context';
|
||||
import './setters';
|
||||
import { registerBuiltinSetters } from './setters';
|
||||
|
||||
registerBuiltinSetters();
|
||||
|
||||
export interface SettingPanelProps extends SettingFormProps {
|
||||
title?: React.ReactNode;
|
||||
|
||||
@@ -13,7 +13,7 @@ import { QuestionCircleOutlined } from '@ant-design/icons';
|
||||
import { Button, Empty, Spin, Popover } from 'antd';
|
||||
import { getDragGhostElement } from '../helpers';
|
||||
|
||||
type MenuKeyType = 'common' | 'atom' | 'snippet' | 'block' | 'bizComp';
|
||||
type MenuKeyType = 'common' | 'atom' | 'snippet' | 'bizComp' | 'localComp';
|
||||
type MenuValueType = Array<{ title: string; items: string[] }>;
|
||||
export type MenuDataType = PartialRecord<MenuKeyType, MenuValueType>;
|
||||
|
||||
@@ -26,10 +26,6 @@ export interface ComponentsPanelProps {
|
||||
* 展示业务组件分类
|
||||
*/
|
||||
showBizComps?: boolean;
|
||||
/**
|
||||
* 展示基础组件分类
|
||||
*/
|
||||
showBlocks?: boolean;
|
||||
/**
|
||||
* 动态加载物料 loading,避免未加载完成用户点击空列表
|
||||
*/
|
||||
@@ -47,7 +43,7 @@ const localeMap = {
|
||||
atom: '原子组件',
|
||||
snippet: '组合',
|
||||
bizComp: '业务组件',
|
||||
block: '区块',
|
||||
localComp: '本地组件',
|
||||
};
|
||||
|
||||
const emptyMenuData: MenuDataType = {
|
||||
@@ -78,19 +74,18 @@ export const ComponentsPanel = observer(
|
||||
({
|
||||
menuData = emptyMenuData,
|
||||
showBizComps = true,
|
||||
showBlocks = true,
|
||||
getBizCompName = upperCamelCase,
|
||||
loading = false,
|
||||
}: ComponentsPanelProps) => {
|
||||
const [keyword, setKeyword] = useState<string>('');
|
||||
const allList = useFlatMenuData<MenuDataType>(menuData);
|
||||
const workspace = useWorkspace();
|
||||
const [bizCompData, blockData] = useMemo(
|
||||
const [bizCompData, localCompData] = useMemo(
|
||||
() => [
|
||||
[{ title: '已安装业务组件', items: workspace.bizComps.map(getBizCompName) }],
|
||||
[{ title: '项目区块', items: workspace.blocks }],
|
||||
[{ title: '本地组件', items: workspace.localComps }],
|
||||
],
|
||||
[workspace.bizComps, workspace.blocks, getBizCompName],
|
||||
[workspace.bizComps, workspace.localComps, getBizCompName],
|
||||
);
|
||||
|
||||
const tabs = Object.keys(menuData).map((key) => ({
|
||||
@@ -106,11 +101,12 @@ export const ComponentsPanel = observer(
|
||||
children: <MaterialList type="bizComp" data={bizCompData} />,
|
||||
});
|
||||
}
|
||||
if (showBlocks) {
|
||||
|
||||
if (localCompData.length) {
|
||||
tabs.push({
|
||||
key: 'block',
|
||||
label: localeMap.block,
|
||||
children: <MaterialList type="block" data={blockData} />,
|
||||
key: 'localComps',
|
||||
label: localeMap.localComp,
|
||||
children: <MaterialList data={localCompData} />,
|
||||
});
|
||||
}
|
||||
const contentNode =
|
||||
@@ -145,7 +141,7 @@ interface MaterialListProps {
|
||||
/**
|
||||
* 物料类型
|
||||
*/
|
||||
type?: 'common' | 'bizComp' | 'block';
|
||||
type?: 'common' | 'bizComp' | 'localComp';
|
||||
}
|
||||
|
||||
function MaterialList({ data, filterKeyword, type = 'common' }: MaterialListProps) {
|
||||
@@ -175,7 +171,7 @@ function MaterialList({ data, filterKeyword, type = 'common' }: MaterialListProp
|
||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="没有匹配到任何组件" />
|
||||
)}
|
||||
<Grid
|
||||
columns={type === 'block' ? 1 : 2}
|
||||
columns={type === 'localComp' ? 1 : 2}
|
||||
spacing="1px"
|
||||
bg="background.normal"
|
||||
padding="0"
|
||||
@@ -185,9 +181,7 @@ function MaterialList({ data, filterKeyword, type = 'common' }: MaterialListProp
|
||||
if (!prototype) {
|
||||
logger.log(`<${item}> prototype not found!`);
|
||||
}
|
||||
return prototype ? (
|
||||
<MaterialGrid key={item} data={prototype} type={type} />
|
||||
) : null;
|
||||
return prototype ? <MaterialGrid key={item} data={prototype} /> : null;
|
||||
})}
|
||||
{addBlock && <Box bg="white" />}
|
||||
</Grid>
|
||||
@@ -201,7 +195,6 @@ function MaterialList({ data, filterKeyword, type = 'common' }: MaterialListProp
|
||||
|
||||
interface MaterialProps {
|
||||
data: ComponentPrototypeType;
|
||||
type: 'common' | 'atom' | 'snippet' | 'bizComp' | 'block';
|
||||
}
|
||||
|
||||
const StyledCommonGridItem = styled.div`
|
||||
@@ -219,11 +212,6 @@ const StyledCommonGridItem = styled.div`
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
&[class~='block-item'] {
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.material-icon {
|
||||
font-size: 40px;
|
||||
}
|
||||
@@ -260,7 +248,7 @@ const StyledCommonGridItem = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
function MaterialGrid({ data, type }: MaterialProps) {
|
||||
function MaterialGrid({ data }: MaterialProps) {
|
||||
const workspace = useWorkspace();
|
||||
|
||||
const handleDragStart = (e: React.DragEvent) => {
|
||||
@@ -286,7 +274,6 @@ function MaterialGrid({ data, type }: MaterialProps) {
|
||||
data-name={data.name}
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
className={type === 'block' ? 'block-item' : null}
|
||||
>
|
||||
{icon.startsWith('icon-') ? (
|
||||
<IconFont className="material-icon" type={data.icon || 'icon-placeholder'} />
|
||||
@@ -296,7 +283,7 @@ function MaterialGrid({ data, type }: MaterialProps) {
|
||||
<Text fontSize="12px" lineHeight="1.5">
|
||||
{data.title}
|
||||
</Text>
|
||||
<Text fontSize="12px" color={type === 'block' ? 'inherit' : 'gray.50'}>
|
||||
<Text fontSize="12px" color="gray.50">
|
||||
{data.name}
|
||||
</Text>
|
||||
{data.docs || data.help ? (
|
||||
|
||||
@@ -4,8 +4,6 @@ import { Tabs } from '@music163/tango-ui';
|
||||
import InterfaceConfig from './interface-config';
|
||||
import ProxyConfig from './proxy-config';
|
||||
|
||||
export * from './interface-config';
|
||||
|
||||
export function DataSourcePanel(props: Record<string, any>) {
|
||||
return (
|
||||
<Box className="DataSourceView" height="100%" overflowY="auto">
|
||||
|
||||
@@ -1,538 +1,60 @@
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import React from 'react';
|
||||
import { observer, useWorkspace, useWorkspaceData } from '@music163/tango-context';
|
||||
import { Box, css } from 'coral-system';
|
||||
import { Button, FormProps, Empty, Space, Dropdown } from 'antd';
|
||||
import { PlayCircleOutlined } from '@ant-design/icons';
|
||||
import { Form, InputCode, Panel, JsonView, Search } from '@music163/tango-ui';
|
||||
import {
|
||||
isNil,
|
||||
getVariableContent,
|
||||
isValidFunctionCode,
|
||||
logger,
|
||||
code2object,
|
||||
filterTreeData,
|
||||
} from '@music163/tango-helpers';
|
||||
import { Box } from 'coral-system';
|
||||
import { getVariableContent, parseServiceVariablePath } from '@music163/tango-helpers';
|
||||
import { isWrappedByExpressionContainer } from '@music163/tango-core';
|
||||
import { useSandboxQuery } from '../../context';
|
||||
import { VariableTree } from '../../components';
|
||||
import { useSandboxQuery } from '../../context';
|
||||
|
||||
/*
|
||||
* 服务函数的操作类型
|
||||
*/
|
||||
enum ServiceFunctionOperationModeType {
|
||||
ADD = 'add',
|
||||
UPDATE = 'update',
|
||||
DELETE = 'delete',
|
||||
}
|
||||
|
||||
/**
|
||||
* 服务函数 HTTP Type
|
||||
* 云音乐网关只支持 get 和 post
|
||||
*/
|
||||
enum ServiceFunctionMethodType {
|
||||
GET = 'GET',
|
||||
// PUT = 'PUT',
|
||||
POST = 'POST',
|
||||
// PATCH = 'PATCH',
|
||||
// DELETE = 'DELETE',
|
||||
}
|
||||
|
||||
/**
|
||||
* requestType
|
||||
* headers: Content-Type
|
||||
*/
|
||||
enum DataServiceRequestType {
|
||||
'application/json' = 'json',
|
||||
'application/x-www-form-urlencoded' = 'x-www-form-urlencoded',
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 api 路径转换为默认的驼峰方法名
|
||||
*/
|
||||
export const getApiDefaultName = (url: string) =>
|
||||
url
|
||||
// 去除 api + 模块名前缀
|
||||
// - 云音乐 api 规范为 /api/模块名/
|
||||
// - 后端公技基本使用 /模块名/api/
|
||||
// - 中台类服务似乎常用 /api/middle/模块名/
|
||||
// TODO: 是否需要去除模块名?一般同一个应用是同一个模块名,去掉可以缩减方法名长度,但确实有模块名不一样且后面的路径完全一致的接口
|
||||
// 目前的实现是去除了模块名,只干掉 /api/middle/ 和 /api/backend/ 这种常用前缀
|
||||
.replace(/^\/[^/]+?\/api\/|^\/api\/middle\/|^\/api\/backend\/|^\/api\//, '')
|
||||
// 去除路由参数
|
||||
.replace(/\/\{.*?\}/, '')
|
||||
// 忽略下划线与减号,将后面的字符转成大驼峰
|
||||
.replace(/[-/_]+\w/g, (str) => str.replace(/[-/_]+/, '').toUpperCase())
|
||||
// 首字母转小写
|
||||
.replace(/^./, (str) => str.toLowerCase())
|
||||
// 方法名以数字开头,添加 api 前缀
|
||||
.replace(/^\d/, (str) => `api${str}`);
|
||||
|
||||
interface DataServiceViewProps {
|
||||
onAdd?: (values: Record<string, string> | Array<Record<string, string>>) => void;
|
||||
onUpdate?: (values: Record<string, string>) => void;
|
||||
onDelete?: (values: Record<string, string>) => void;
|
||||
}
|
||||
|
||||
// http 方法类型
|
||||
const httpMethods = Object.keys(ServiceFunctionMethodType).map((key) => ({
|
||||
label: key,
|
||||
value: key,
|
||||
}));
|
||||
|
||||
// requestType 类型
|
||||
const requestTypes = Object.keys(DataServiceRequestType).map((key) => ({
|
||||
label: key,
|
||||
value: DataServiceRequestType[key],
|
||||
}));
|
||||
|
||||
const detailFormStyle = css`
|
||||
.ant-form-item {
|
||||
margin-bottom: 12px;
|
||||
// 移除掉不必要的属性
|
||||
export function shapeServiceValues(val: any) {
|
||||
const shapeValues = { ...val };
|
||||
delete shapeValues.type;
|
||||
// 兼容旧版,如果 formatter 包裹了 {} 则删掉首尾
|
||||
if (shapeValues.formatter && isWrappedByExpressionContainer(shapeValues.formatter)) {
|
||||
shapeValues.formatter = getVariableContent(shapeValues.formatter);
|
||||
}
|
||||
`;
|
||||
return shapeValues;
|
||||
}
|
||||
|
||||
const DataSourceView = observer(({ onAdd, onUpdate, onDelete }: DataServiceViewProps) => {
|
||||
const [serviceData, setServiceData] = useState<any>();
|
||||
const [keyword, setKeyword] = useState<string>();
|
||||
const workspace = useWorkspace();
|
||||
const DataSourceView = observer(() => {
|
||||
const sandbox = useSandboxQuery();
|
||||
const workspace = useWorkspace();
|
||||
const { serviceVariables } = useWorkspaceData();
|
||||
const serviceModules = Object.keys(workspace.serviceModules).map((key) => ({
|
||||
label: key === 'index' ? '默认模块' : key,
|
||||
value: key,
|
||||
}));
|
||||
const serviceFunctions = serviceVariables.reduce((acc, cur) => {
|
||||
acc = acc.concat(cur.children.map((item: any) => item.key));
|
||||
return acc;
|
||||
}, []);
|
||||
const dataSource = useMemo(() => {
|
||||
if (!keyword) {
|
||||
return serviceVariables;
|
||||
}
|
||||
return filterTreeData(
|
||||
serviceVariables,
|
||||
(leaf) => leaf.title.includes(keyword),
|
||||
'children',
|
||||
true,
|
||||
);
|
||||
}, [serviceVariables, keyword]);
|
||||
|
||||
const isAddMode = serviceData && !serviceData.name;
|
||||
|
||||
return (
|
||||
<Box className="ServiceFunctionList" display="flex" borderTopColor="line.normal">
|
||||
<Box width="35%" overflow="auto" borderRight="solid" borderColor="line.normal">
|
||||
<Panel
|
||||
height="100%"
|
||||
bodyProps={{
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
p="m"
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
rowGap="m"
|
||||
>
|
||||
<Button
|
||||
block
|
||||
onClick={() => {
|
||||
setServiceData({});
|
||||
}}
|
||||
>
|
||||
新建服务函数
|
||||
</Button>
|
||||
<Search placeholder="搜索服务函数" onChange={setKeyword} />
|
||||
</Box>
|
||||
<VariableTree
|
||||
dataSource={dataSource}
|
||||
showDeleteIcon
|
||||
onSelect={(item) => {
|
||||
setServiceData({ key: item.key, ...workspace.getServiceFunction(item.key) });
|
||||
}}
|
||||
onRemove={(item) => {
|
||||
workspace.removeServiceFunction(item.key);
|
||||
onDelete?.(item);
|
||||
}}
|
||||
/>
|
||||
</Panel>
|
||||
</Box>
|
||||
<Box width="65%" overflow="auto" css={detailFormStyle}>
|
||||
{serviceData && (
|
||||
<>
|
||||
<Panel title={isAddMode ? '新建服务函数' : '服务函数详情'}>
|
||||
<ServiceDetailForm
|
||||
key={serviceData.key || 'createSF'}
|
||||
serviceModules={serviceModules}
|
||||
serviceKeys={serviceFunctions}
|
||||
initialValues={
|
||||
isAddMode
|
||||
? {
|
||||
moduleName: 'index',
|
||||
}
|
||||
: {
|
||||
name: serviceData.name,
|
||||
moduleName: serviceData.moduleName,
|
||||
method: 'get',
|
||||
...serviceData.config,
|
||||
}
|
||||
}
|
||||
onCancel={() => {
|
||||
setServiceData(undefined);
|
||||
}}
|
||||
onSubmit={(values, mode: ServiceFunctionOperationModeType) => {
|
||||
function shapeServiceValues(val: any) {
|
||||
const shapeValues = { ...val };
|
||||
delete shapeValues.type;
|
||||
// 兼容旧版,如果 formatter 包裹了 {} 则删掉首尾
|
||||
if (
|
||||
shapeValues.formatter &&
|
||||
isWrappedByExpressionContainer(shapeValues.formatter)
|
||||
) {
|
||||
shapeValues.formatter = getVariableContent(shapeValues.formatter);
|
||||
}
|
||||
return shapeValues;
|
||||
}
|
||||
// 移除掉不必要的属性
|
||||
const { moduleName, ...data } = shapeServiceValues(values);
|
||||
if (mode === ServiceFunctionOperationModeType.ADD) {
|
||||
workspace.addServiceFunction(data, moduleName);
|
||||
onAdd && onAdd(values);
|
||||
} else if (mode === ServiceFunctionOperationModeType.UPDATE) {
|
||||
workspace.updateServiceFunction(data, moduleName);
|
||||
onUpdate && onUpdate(values);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Panel>
|
||||
{serviceData?.key ? (
|
||||
<ServiceFunctionPreview
|
||||
key={serviceData.key}
|
||||
appContext={sandbox?.window['tango']}
|
||||
functionName={serviceData.key}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
{!serviceData && (
|
||||
<Box py="xxl">
|
||||
<Empty description="请从左侧选择数据服务函数或新建数据服务函数" />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
<Box className="ServiceFunctionList" p="m">
|
||||
<VariableTree
|
||||
dataSource={serviceVariables}
|
||||
appContext={sandbox?.window['tango']}
|
||||
serviceModules={serviceModules}
|
||||
onRemoveVariable={(variableKey) => {
|
||||
workspace.removeServiceFunction(variableKey);
|
||||
}}
|
||||
onAddService={(data) => {
|
||||
const { name, moduleName, ...payload } = shapeServiceValues(data);
|
||||
workspace.addServiceFunction(name, payload, moduleName);
|
||||
}}
|
||||
onUpdateService={(data) => {
|
||||
const { name, moduleName, ...payload } = shapeServiceValues(data);
|
||||
workspace.updateServiceFunction(name, payload, moduleName);
|
||||
}}
|
||||
getServiceData={(serviceKey) => {
|
||||
const data = workspace.getServiceFunction(serviceKey);
|
||||
return {
|
||||
name: data.name,
|
||||
moduleName: data.moduleName,
|
||||
method: 'get',
|
||||
...data.config,
|
||||
};
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
|
||||
interface ServiceDetailFormProps extends FormProps {
|
||||
serviceKeys?: string[];
|
||||
serviceModules?: any[];
|
||||
onCancel?: () => void;
|
||||
onSubmit?: (values: any, mode: ServiceFunctionOperationModeType) => void;
|
||||
}
|
||||
|
||||
function ServiceDetailForm({
|
||||
serviceKeys = [],
|
||||
serviceModules = [],
|
||||
onCancel,
|
||||
onSubmit,
|
||||
initialValues,
|
||||
...formProps
|
||||
}: ServiceDetailFormProps) {
|
||||
const isModifyMode = !!initialValues?.name; // 是否为更新模式
|
||||
const [disabled, setDisabled] = useState(isModifyMode);
|
||||
const [form] = Form.useForm();
|
||||
// 监听服务类型变化,默认是自定义
|
||||
const formType = Form.useWatch('type', form);
|
||||
|
||||
// 新建的时候,类型变换重置部分字段
|
||||
useEffect(() => {
|
||||
if (!formType) return;
|
||||
!isModifyMode &&
|
||||
form.setFieldsValue({
|
||||
url: undefined,
|
||||
method: undefined,
|
||||
formatter: undefined,
|
||||
});
|
||||
}, [formType, form, isModifyMode]);
|
||||
|
||||
return (
|
||||
<Form
|
||||
form={form}
|
||||
labelCol={6}
|
||||
wrapperCol={17}
|
||||
colon={false}
|
||||
layout="horizontal"
|
||||
initialValues={initialValues}
|
||||
onFinish={(values) => {
|
||||
onSubmit(
|
||||
values,
|
||||
isModifyMode
|
||||
? ServiceFunctionOperationModeType.UPDATE
|
||||
: ServiceFunctionOperationModeType.ADD,
|
||||
);
|
||||
setDisabled(true);
|
||||
}}
|
||||
{...formProps}
|
||||
>
|
||||
<Form.Item
|
||||
label="命名空间"
|
||||
name="moduleName"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
},
|
||||
]}
|
||||
component="select"
|
||||
componentProps={{
|
||||
options: serviceModules,
|
||||
disabled: disabled || isModifyMode,
|
||||
}}
|
||||
/>
|
||||
<Form.Item
|
||||
label="方法名"
|
||||
name="name"
|
||||
rules={[
|
||||
{ required: true },
|
||||
{ pattern: /^[a-z]\w+$/, message: '请输入合法的方法名称' },
|
||||
!isModifyMode && {
|
||||
validator(_, value) {
|
||||
const isExist = serviceKeys.includes(
|
||||
['services', form.getFieldValue('moduleName'), value].join('.'),
|
||||
);
|
||||
return isExist
|
||||
? Promise.reject(new Error('重复的方法名,请换一个!'))
|
||||
: Promise.resolve();
|
||||
},
|
||||
},
|
||||
]}
|
||||
component="input"
|
||||
componentProps={{
|
||||
placeholder: '请输入数据服务调用名称',
|
||||
disabled: disabled || isModifyMode,
|
||||
}}
|
||||
extra={
|
||||
!disabled &&
|
||||
!isModifyMode && (
|
||||
<Box>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
disabled={!form.getFieldValue('url')}
|
||||
onClick={() => {
|
||||
const funcName = getApiDefaultName(form.getFieldValue('url'));
|
||||
form.setFieldValue('name', funcName);
|
||||
}}
|
||||
>
|
||||
使用接口路径自动生成
|
||||
</Button>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Form.Item
|
||||
label="路径"
|
||||
name="url"
|
||||
rules={[{ required: true, type: 'url' }]}
|
||||
component="input"
|
||||
componentProps={{
|
||||
placeholder: '请输入数据服务调用名称',
|
||||
disabled: disabled || isModifyMode,
|
||||
}}
|
||||
/>
|
||||
<Form.Item
|
||||
label="方法"
|
||||
name="method"
|
||||
component="select"
|
||||
componentProps={{
|
||||
options: httpMethods,
|
||||
placeholder: '请选择 HTTP 请求方法',
|
||||
disabled,
|
||||
}}
|
||||
/>
|
||||
<Form.Item
|
||||
label="编码类型"
|
||||
name="requestType"
|
||||
tooltip="设置请求头的 Content-Type,若不设置,则默认变为为 JSON"
|
||||
component="select"
|
||||
componentProps={{
|
||||
options: requestTypes,
|
||||
placeholder: '请求头Content-Type,默认为JSON',
|
||||
disabled,
|
||||
}}
|
||||
/>
|
||||
<Form.Item
|
||||
label="格式化响应"
|
||||
name="formatter"
|
||||
tooltip="提供格式化函数,对结果进行格式化"
|
||||
validateTrigger="onBlur"
|
||||
rules={[
|
||||
{
|
||||
validator(_, value) {
|
||||
if (!value) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return isValidFunctionCode(value)
|
||||
? Promise.resolve()
|
||||
: Promise.reject(new Error('请提供合法的函数代码!'));
|
||||
},
|
||||
},
|
||||
]}
|
||||
extra={
|
||||
!disabled && (
|
||||
<Box>
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{ label: '直接返回(默认)', key: 'res => res' },
|
||||
{ label: '返回数据部分(云音乐标准规范)', key: 'res => res.data' },
|
||||
],
|
||||
onClick: ({ key }) => {
|
||||
form.setFieldValue('formatter', key);
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Button type="link" size="small">
|
||||
使用模板函数
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
>
|
||||
<InputCode editable={!disabled} showLineNumbers placeholder="res => res" />
|
||||
</Form.Item>
|
||||
<Form.Item wrapperCol={{ offset: 6, span: 18 }}>
|
||||
{isModifyMode ? (
|
||||
<Space>
|
||||
{!disabled ? (
|
||||
<>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setDisabled(true);
|
||||
}}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button type="primary" htmlType="submit">
|
||||
修改
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<Button
|
||||
onClick={() => {
|
||||
setDisabled(false);
|
||||
}}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
) : (
|
||||
<Space>
|
||||
<Button onClick={onCancel}>取消</Button>
|
||||
<Button type="primary" htmlType="submit">
|
||||
创建
|
||||
</Button>
|
||||
</Space>
|
||||
)}
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
interface ServicePreviewProps {
|
||||
data?: object;
|
||||
}
|
||||
|
||||
function ServicePreview({ data }: ServicePreviewProps) {
|
||||
let ret: React.ReactNode;
|
||||
|
||||
if (!isNil(data)) {
|
||||
switch (typeof data) {
|
||||
case 'object':
|
||||
ret = <JsonView src={data as object} />;
|
||||
break;
|
||||
default:
|
||||
ret = String(data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const initialRet = <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />;
|
||||
|
||||
return (
|
||||
<Box
|
||||
className="ServicePreviewPanel"
|
||||
overflow="auto"
|
||||
height="auto"
|
||||
minHeight={260}
|
||||
marginTop={10}
|
||||
>
|
||||
{ret || initialRet}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
interface ServiceFunctionPreviewProps {
|
||||
appContext?: any;
|
||||
functionName?: string;
|
||||
}
|
||||
|
||||
function ServiceFunctionPreview({ appContext, functionName }: ServiceFunctionPreviewProps) {
|
||||
const [payload, setPayload] = useState({});
|
||||
const [result, setResult] = useState<any>();
|
||||
const [error, setError] = useState('');
|
||||
return (
|
||||
<Panel
|
||||
title="接口测试"
|
||||
extra={
|
||||
<Space>
|
||||
<Button
|
||||
disabled={!appContext}
|
||||
size="small"
|
||||
onClick={() => {
|
||||
if (!appContext) {
|
||||
setError('执行上下文未准备好,请关闭面板重试');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
appContext.services[functionName](payload).then((data: any) => {
|
||||
setResult(data);
|
||||
});
|
||||
} catch (err) {
|
||||
setError('接口调用失败,请检查参数是否正确');
|
||||
logger.error(err);
|
||||
}
|
||||
}}
|
||||
icon={<PlayCircleOutlined />}
|
||||
>
|
||||
预览
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
borderTop="solid"
|
||||
borderTopColor="line.normal"
|
||||
>
|
||||
<Panel title="请求参数" bodyProps={{ px: 'l' }}>
|
||||
<InputCode
|
||||
placeholder={'添加请求参数,对象格式,如 { key: value }'}
|
||||
editable
|
||||
showLineNumbers
|
||||
onChange={(value: string) => {
|
||||
const obj = code2object(value);
|
||||
setPayload(obj);
|
||||
}}
|
||||
/>
|
||||
</Panel>
|
||||
<Panel title="请求响应" bodyProps={{ px: 'l' }}>
|
||||
{error || (result ? <ServicePreview data={result} /> : '点击预览按钮测试接口返回值')}
|
||||
</Panel>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
export default DataSourceView;
|
||||
|
||||
@@ -366,19 +366,19 @@ function RenderItem({
|
||||
</Popconfirm>,
|
||||
]
|
||||
: baseNeedUpgrade
|
||||
? [
|
||||
<Popconfirm
|
||||
title={`确认升级 ${record.name} 到 ${basePackage.version} 吗?`}
|
||||
onConfirm={() => onDepUpgrade({ version: basePackage.version })}
|
||||
>
|
||||
<a key="upgrade">升级</a>
|
||||
</Popconfirm>,
|
||||
]
|
||||
: [
|
||||
<Text key="latest" color={'rgba(0, 0, 0, 0.45)'}>
|
||||
{!isUndefined(baseNeedUpgrade) && '已是最新'}
|
||||
</Text>,
|
||||
]
|
||||
? [
|
||||
<Popconfirm
|
||||
title={`确认升级 ${record.name} 到 ${basePackage.version} 吗?`}
|
||||
onConfirm={() => onDepUpgrade({ version: basePackage.version })}
|
||||
>
|
||||
<a key="upgrade">升级</a>
|
||||
</Popconfirm>,
|
||||
]
|
||||
: [
|
||||
<Text key="latest" color={'rgba(0, 0, 0, 0.45)'}>
|
||||
{!isUndefined(baseNeedUpgrade) && '已是最新'}
|
||||
</Text>,
|
||||
]
|
||||
}
|
||||
>
|
||||
<List.Item.Meta
|
||||
@@ -453,7 +453,7 @@ function AddDependencyModal({
|
||||
|
||||
const onFinishCallback = (name?: string) => {
|
||||
name && message.success(`${name} 添加成功`);
|
||||
// FIXME: 修复沙箱添加组件后 HMR 失效的 bug,添加完刷新
|
||||
// TIP: 修复沙箱添加组件后 HMR 失效的 bug,添加完刷新
|
||||
sandbox.reload();
|
||||
off();
|
||||
};
|
||||
|
||||
@@ -148,6 +148,12 @@ export const ComponentsTree: React.FC<ComponentsTreeProps> = observer(
|
||||
setSelectedKeys(workspace.selectSource.selected.map((item) => item.id));
|
||||
}, [workspace.selectSource.selected]);
|
||||
|
||||
useEffect(() => {
|
||||
setExpandedKeys(getNodeKeys(nodesTree));
|
||||
// nodeTree 更新后重新计算expandKeys
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [nodesTree?.[0]?.id]);
|
||||
|
||||
if (!nodesTree.length) {
|
||||
return (
|
||||
<Box>
|
||||
@@ -156,12 +162,6 @@ export const ComponentsTree: React.FC<ComponentsTreeProps> = observer(
|
||||
);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setExpandedKeys(getNodeKeys(nodesTree));
|
||||
// nodeTree 更新后重新计算expandKeys
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [nodesTree?.[0]?.id]);
|
||||
|
||||
return (
|
||||
<Box css={outlineStyle}>
|
||||
<Tree
|
||||
@@ -230,4 +230,4 @@ export const ComponentsTree: React.FC<ComponentsTreeProps> = observer(
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
);
|
||||
);
|
||||
|
||||
@@ -1,56 +1,39 @@
|
||||
import React from 'react';
|
||||
import { Button, Space } from 'antd';
|
||||
import { Button } from 'antd';
|
||||
import { FileAddOutlined } from '@ant-design/icons';
|
||||
import { useBoolean } from '@music163/tango-helpers';
|
||||
import { Panel, Form, IconButton } from '@music163/tango-ui';
|
||||
import { FileAddOutlined, QuestionCircleOutlined } from '@ant-design/icons';
|
||||
import { Panel } from '@music163/tango-ui';
|
||||
import { observer, useWorkspace, useWorkspaceData } from '@music163/tango-context';
|
||||
import { EditableVariableTree, EditableVariableTreeProps } from '../components';
|
||||
import { VariableTree } from '../components';
|
||||
import { CODE_TEMPLATES } from '../helpers';
|
||||
import { AddStoreForm } from '../components/variable-tree/add-store';
|
||||
|
||||
const defaultStoreTemplate = `
|
||||
import { defineStore } from '@music163/tango-boot';
|
||||
export default defineStore({
|
||||
});
|
||||
`;
|
||||
|
||||
export interface VariablePanelProps extends EditableVariableTreeProps {
|
||||
wrapperHeight?: number | string;
|
||||
export interface VariablePanelProps {
|
||||
newStoreTemplate?: string;
|
||||
}
|
||||
|
||||
export const VariablePanel = observer(
|
||||
({
|
||||
wrapperHeight = '100%',
|
||||
newStoreTemplate = defaultStoreTemplate,
|
||||
...restProps
|
||||
}: VariablePanelProps) => {
|
||||
({ newStoreTemplate = CODE_TEMPLATES.newStoreTemplate }: VariablePanelProps) => {
|
||||
const [isAdd, { on, off }] = useBoolean();
|
||||
const workspace = useWorkspace();
|
||||
const { storeVariables } = useWorkspaceData();
|
||||
const storeNames = storeVariables.map((item) => item.title);
|
||||
const storeNames = storeVariables.map((item) => item.title) as string[];
|
||||
|
||||
return (
|
||||
<Panel
|
||||
shape="solid"
|
||||
className="ModelView"
|
||||
height={wrapperHeight}
|
||||
height="100%"
|
||||
title="视图模型与变量管理"
|
||||
subTitle={
|
||||
<IconButton
|
||||
tooltip="如何使用"
|
||||
icon={<QuestionCircleOutlined />}
|
||||
href="https://music-doc.st.netease.com/st/tango-docs/docs/guide/basic/model"
|
||||
/>
|
||||
}
|
||||
extra={
|
||||
<Button size="small" type="text" icon={<FileAddOutlined />} onClick={on}>
|
||||
<Button size="small" type="primary" icon={<FileAddOutlined />} onClick={on}>
|
||||
新建模型
|
||||
</Button>
|
||||
}
|
||||
bodyProps={{ p: 0 }}
|
||||
bodyProps={{ p: 'm' }}
|
||||
>
|
||||
{isAdd ? (
|
||||
<AddStoreForm
|
||||
existNames={storeNames}
|
||||
storeNames={storeNames}
|
||||
onCancel={off}
|
||||
onSubmit={(values) => {
|
||||
workspace.addStoreFile(values.name, newStoreTemplate);
|
||||
@@ -58,75 +41,21 @@ export const VariablePanel = observer(
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<EditableVariableTree
|
||||
modes={['add', 'define']}
|
||||
defaultMode="define"
|
||||
<VariableTree
|
||||
defaultValueDetailMode="define"
|
||||
dataSource={storeVariables}
|
||||
onAddVariable={(storeName, data) => {
|
||||
onAddStoreVariable={(storeName, data) => {
|
||||
workspace.addStoreState(storeName, data.name, data.initialValue);
|
||||
}}
|
||||
onDeleteVariable={(storeName, stateName) => {
|
||||
workspace.removeStoreState(storeName, stateName);
|
||||
onRemoveVariable={(variableKey) => {
|
||||
workspace.removeStoreVariable(variableKey);
|
||||
}}
|
||||
onDeleteStore={(storeName) => {
|
||||
workspace.removeStoreModule(storeName);
|
||||
onUpdateVariable={(variableKey, code) => {
|
||||
workspace.updateStoreVariable(variableKey, code);
|
||||
}}
|
||||
onSave={(code, node) => {
|
||||
workspace.updateModuleCodeByVariablePath(node.key, code);
|
||||
}}
|
||||
height="100%"
|
||||
{...restProps}
|
||||
/>
|
||||
)}
|
||||
</Panel>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
interface AddStoreFormProps {
|
||||
existNames?: string[];
|
||||
onCancel?: () => void;
|
||||
onSubmit?: (data: { name: string }) => void;
|
||||
}
|
||||
|
||||
function AddStoreForm({ existNames = [], onCancel, onSubmit }: AddStoreFormProps) {
|
||||
return (
|
||||
<Panel title="添加模型" subTitle="模型可以用来组织一组变量">
|
||||
<Form
|
||||
autoComplete="off"
|
||||
onFinish={(values) => {
|
||||
onSubmit && onSubmit(values);
|
||||
}}
|
||||
>
|
||||
<Form.Item
|
||||
label="模型名称"
|
||||
name="name"
|
||||
rules={[
|
||||
{ required: true },
|
||||
{ pattern: /^[a-z]\w+$/, message: '非法的变量标识符' },
|
||||
{
|
||||
validator(_, value) {
|
||||
const found = existNames.includes(value);
|
||||
return found
|
||||
? Promise.reject(new Error('已存在该模型名,请换一个!'))
|
||||
: Promise.resolve();
|
||||
},
|
||||
},
|
||||
]}
|
||||
component="input"
|
||||
componentProps={{
|
||||
placeholder: '请输入模型名称',
|
||||
}}
|
||||
/>
|
||||
<Form.Item label=" ">
|
||||
<Space>
|
||||
<Button onClick={onCancel}>取消</Button>
|
||||
<Button type="primary" htmlType="submit">
|
||||
保存
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { extendTheme } from 'coral-system';
|
||||
|
||||
export const theme = extendTheme({
|
||||
export default extendTheme({
|
||||
colors: {
|
||||
primary: {
|
||||
10: '#e8f3ff',
|
||||
@@ -27,11 +27,16 @@ export const theme = extendTheme({
|
||||
custom: {
|
||||
topNavBg: '#222',
|
||||
topNavColor: '#fff',
|
||||
topNavBorderColor: '#222',
|
||||
toolbarDividerColor: 'gray.60',
|
||||
toolbarButtonBg: 'rgba(223, 223, 223, 0.08)',
|
||||
toolbarButtonBgHover: '#4080ff',
|
||||
toolbarButtonTextColor: '#f5f5f5',
|
||||
toolbarButtonBgDisabled: 'rgba(223,223,223, 0.08)',
|
||||
toolbarButtonBgActive: '#4080ff',
|
||||
toolbarButtonTextColor: '#FFF',
|
||||
toolbarButtonTextColorHover: '#FFF',
|
||||
toolbarButtonTextColorDisabled: '#bfbfbf',
|
||||
toolbarButtonTextColorDisabled: 'hsla(0,0%,100%,0.3)',
|
||||
toolbarButtonTextColorActive: '#fff',
|
||||
sidebarBg: '#fff',
|
||||
sidebarExpandBg: '#fff',
|
||||
sidebarItemActiveBg: '#f2f3f5',
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default as themeDefault } from './default';
|
||||
export { default as themeLight } from './light';
|
||||
@@ -0,0 +1,25 @@
|
||||
import { extendTheme } from 'coral-system';
|
||||
|
||||
export default extendTheme({
|
||||
colors: {
|
||||
custom: {
|
||||
topNavBg: '#FFF',
|
||||
topNavColor: '#333',
|
||||
topNavBorderColor: 'rgb(229,230,235)',
|
||||
toolbarDividerColor: 'rgb(229,230,235)',
|
||||
toolbarButtonBg: 'rgb(242,243,245)',
|
||||
toolbarButtonBgHover: 'rgb(229,230,235)',
|
||||
toolbarButtonBgDisabled: 'rgb(247,248,250)',
|
||||
toolbarButtonBgActive: 'colors.brand',
|
||||
toolbarButtonTextColor: 'colors.text2',
|
||||
toolbarButtonTextColorHover: 'colors.text2',
|
||||
toolbarButtonTextColorDisabled: 'colors.text4',
|
||||
toolbarButtonTextColorActive: '#FFF',
|
||||
sidebarBg: '#fff',
|
||||
sidebarExpandBg: '#fff',
|
||||
sidebarItemActiveBg: '#f2f3f5',
|
||||
sidebarItemHoverBg: '#f2f3f5',
|
||||
viewportBg: '#f0f2f5',
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Box } from 'coral-system';
|
||||
import { ToggleButton, IconFont } from '@music163/tango-ui';
|
||||
import { ToggleButton, RedoOutlined, UndoOutlined } from '@music163/tango-ui';
|
||||
import { observer, useWorkspace } from '@music163/tango-context';
|
||||
|
||||
export const HistoryTool = observer(() => {
|
||||
@@ -10,24 +10,24 @@ export const HistoryTool = observer(() => {
|
||||
<ToggleButton
|
||||
shape="ghost"
|
||||
disabled={!workspace.history.couldBack || workspace.history.index === 0}
|
||||
tip="撤销"
|
||||
tooltip="撤销"
|
||||
onClick={() => {
|
||||
workspace.history.back();
|
||||
workspace.selectSource.clear();
|
||||
}}
|
||||
>
|
||||
<IconFont type="icon-undo" />
|
||||
<UndoOutlined />
|
||||
</ToggleButton>
|
||||
<ToggleButton
|
||||
shape="ghost"
|
||||
disabled={!workspace.history.couldForward}
|
||||
tip="重做"
|
||||
tooltip="重做"
|
||||
onClick={() => {
|
||||
workspace.history.forward();
|
||||
workspace.selectSource.clear();
|
||||
}}
|
||||
>
|
||||
<IconFont type="icon-redo" />
|
||||
<RedoOutlined />
|
||||
</ToggleButton>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import React from 'react';
|
||||
import { Group } from 'coral-system';
|
||||
import { Modal } from 'antd';
|
||||
import { ToggleButton } from '@music163/tango-ui';
|
||||
import { ToggleButton, CodeOutlined } from '@music163/tango-ui';
|
||||
import { observer, useDesigner, useWorkspace } from '@music163/tango-context';
|
||||
import { BorderOutlined } from '@ant-design/icons';
|
||||
|
||||
export const ModeSwitchTool = observer(() => {
|
||||
const workspace = useWorkspace();
|
||||
@@ -17,7 +18,7 @@ export const ModeSwitchTool = observer(() => {
|
||||
}}
|
||||
tooltip="设计视图"
|
||||
>
|
||||
设计
|
||||
<BorderOutlined />
|
||||
</ToggleButton>
|
||||
<ToggleButton
|
||||
shape="ghost"
|
||||
@@ -36,7 +37,7 @@ export const ModeSwitchTool = observer(() => {
|
||||
}}
|
||||
tooltip="源码视图"
|
||||
>
|
||||
源码
|
||||
<CodeOutlined />
|
||||
</ToggleButton>
|
||||
</Group>
|
||||
);
|
||||
|
||||
@@ -17,7 +17,7 @@ export const PreviewTool = observer(() => {
|
||||
designer.setActiveView('design');
|
||||
}
|
||||
}}
|
||||
tip="预览"
|
||||
tooltip="预览"
|
||||
>
|
||||
<EyeOutlined />
|
||||
</ToggleButton>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { Button, Popconfirm, Modal, ModalProps } from 'antd';
|
||||
import { Button, Popconfirm, Modal, ModalProps, Form, Input } from 'antd';
|
||||
import { Box, css, Group, Text } from 'coral-system';
|
||||
import { FileOutlined, DeleteOutlined, CopyOutlined, SettingOutlined } from '@ant-design/icons';
|
||||
import { array2object, callAll } from '@music163/tango-helpers';
|
||||
import { IPageConfigData } from '@music163/tango-core';
|
||||
import { Form, ToggleButton } from '@music163/tango-ui';
|
||||
import { ToggleButton } from '@music163/tango-ui';
|
||||
import { observer, useWorkspace } from '@music163/tango-context';
|
||||
|
||||
export const RouteSwitchTool = observer(() => {
|
||||
@@ -71,6 +71,7 @@ function PageSelect({ value, onSelect, onRemove, onUpdate, onCopy, options }: Pa
|
||||
}}
|
||||
/>
|
||||
}
|
||||
icon={<FileOutlined />}
|
||||
>
|
||||
{current?.name || current?.label || value}
|
||||
</ToggleButton>
|
||||
@@ -249,17 +250,13 @@ function CopyViewModalForm({
|
||||
}}
|
||||
>
|
||||
<Form
|
||||
wrapperCol={20}
|
||||
wrapperCol={{ span: 20 }}
|
||||
form={form}
|
||||
initialValues={{ name: current?.name, path: current?.path }}
|
||||
>
|
||||
<Form.Item
|
||||
label="页面标题"
|
||||
name="name"
|
||||
required
|
||||
component="input"
|
||||
componentProps={{ placeholder: '请输入标题' }}
|
||||
/>
|
||||
<Form.Item label="页面标题" name="name" required>
|
||||
<Input placeholder="请输入标题" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="页面路由"
|
||||
name="path"
|
||||
@@ -280,11 +277,9 @@ function CopyViewModalForm({
|
||||
message: '非法的路由地址',
|
||||
},
|
||||
]}
|
||||
component="input"
|
||||
componentProps={{
|
||||
placeholder: '输入页面路由',
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<Input placeholder="请输入页面路由" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import React from 'react';
|
||||
import { observer, useDesigner } from '@music163/tango-context';
|
||||
import { Box } from 'coral-system';
|
||||
import { IconFont, ToggleButton } from '@music163/tango-ui';
|
||||
import {
|
||||
OpenPanelFilledLeftOutlined,
|
||||
OpenPanelFilledRightOutlined,
|
||||
OpenPanelLeftOutlined,
|
||||
OpenPanelRightOutlined,
|
||||
ToggleButton,
|
||||
} from '@music163/tango-ui';
|
||||
|
||||
export const TogglePanelTool = observer(() => {
|
||||
const designer = useDesigner();
|
||||
@@ -14,20 +20,15 @@ export const TogglePanelTool = observer(() => {
|
||||
designer.setActiveSidebarPanel(designer.activeSidebarPanel ? '' : 'outline');
|
||||
}}
|
||||
>
|
||||
<IconFont
|
||||
type={
|
||||
designer.activeSidebarPanel ? 'icon-open-panel-filled-left' : 'icon-open-panel-left'
|
||||
}
|
||||
/>
|
||||
{designer.activeSidebarPanel ? <OpenPanelFilledLeftOutlined /> : <OpenPanelLeftOutlined />}
|
||||
</ToggleButton>
|
||||
<ToggleButton
|
||||
tooltip="隐藏/显示右侧面板"
|
||||
shape="ghost"
|
||||
onClick={() => designer.toggleRightPanel()}
|
||||
disabled={designer.activeView === 'code'}
|
||||
>
|
||||
<IconFont
|
||||
type={designer.showRightPanel ? 'icon-open-panel-filled-right' : 'icon-open-panel-right'}
|
||||
/>
|
||||
{designer.showRightPanel ? <OpenPanelFilledRightOutlined /> : <OpenPanelRightOutlined />}
|
||||
</ToggleButton>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -81,7 +81,7 @@ function ToolbarItem({ placement, widgetProps, children }: ToolbarItemProps) {
|
||||
}
|
||||
|
||||
function Separator() {
|
||||
return <Box className="Separator" width={1} height={16} bg="gray.60" />;
|
||||
return <Box className="Separator" width={1} height={16} bg="colors.custom.toolbarDividerColor" />;
|
||||
}
|
||||
|
||||
Toolbar.Item = ToolbarItem;
|
||||
|
||||
@@ -17,7 +17,7 @@ export const ViewportSwitchTool = observer(() => {
|
||||
onClick={() => {
|
||||
designer.setSimulator('desktop');
|
||||
}}
|
||||
tip="桌面视图"
|
||||
tooltip="桌面视图"
|
||||
>
|
||||
<DesktopOutlined />
|
||||
</ToggleButton>
|
||||
@@ -27,7 +27,7 @@ export const ViewportSwitchTool = observer(() => {
|
||||
onClick={() => {
|
||||
designer.setSimulator('phone');
|
||||
}}
|
||||
tip="手机视图"
|
||||
tooltip="手机视图"
|
||||
>
|
||||
<MobileOutlined />
|
||||
</ToggleButton>
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.0.0-alpha.1](https://github.com/netease/tango/compare/@music163/tango-helpers@1.0.0-alpha.0...@music163/tango-helpers@1.0.0-alpha.1) (2023-12-29)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- refactor VariableTree & Workspace ([#83](https://github.com/netease/tango/issues/83)) ([8c07821](https://github.com/netease/tango/commit/8c07821d93cea4dfc43f81ca948b845176821184))
|
||||
|
||||
## [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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-helpers",
|
||||
"version": "0.1.8",
|
||||
"version": "1.0.0-alpha.1",
|
||||
"description": "Shared types, helpers, and hooks of tango-apps",
|
||||
"keywords": [
|
||||
"shared",
|
||||
|
||||
@@ -43,3 +43,21 @@ export function isPromise(obj: any) {
|
||||
export function isNil(val: any) {
|
||||
return val == null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否是状态变量的 path
|
||||
* @param key
|
||||
* @returns
|
||||
*/
|
||||
export function isStoreVariablePath(key: string) {
|
||||
return /^stores\.[a-zA-Z0-9]+\.\w+$/.test(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否是服务变量的 path
|
||||
* @param key
|
||||
* @returns
|
||||
*/
|
||||
export function isServiceVariablePath(key: string) {
|
||||
return /^services\.[a-zA-Z0-9]+/.test(key);
|
||||
}
|
||||
|
||||
@@ -301,3 +301,84 @@ export function getCodeBlockFormMarkdown(markdown: string) {
|
||||
return match[2];
|
||||
}
|
||||
}
|
||||
|
||||
export function url2serviceName(url: string) {
|
||||
if (url.startsWith('http')) {
|
||||
// 去除域名前缀
|
||||
url = url
|
||||
.replace(/https?:\/\//, '')
|
||||
.split('/')
|
||||
.slice(1)
|
||||
.join('/');
|
||||
}
|
||||
|
||||
return (
|
||||
url
|
||||
// 去除 api + 模块名前缀
|
||||
// - 云音乐 api 规范为 /api/模块名/
|
||||
// - 后端公技基本使用 /模块名/api/
|
||||
// - 中台类服务似乎常用 /api/middle/模块名/
|
||||
// 目前的实现是去除了模块名,只干掉 /api/middle/ 和 /api/backend/ 这种常用前缀
|
||||
.replace(/^\/[^/]+?\/api\/|^\/api\/middle\/|^\/api\/backend\/|^\/api\//, '')
|
||||
// 去除路由参数
|
||||
.replace(/\/\{.*?\}/, '')
|
||||
// 忽略下划线与减号,将后面的字符转成大驼峰
|
||||
.replace(/[-/_]+\w/g, (str) => str.replace(/[-/_]+/, '').toUpperCase())
|
||||
// 首字母转小写
|
||||
.replace(/^./, (str) => str.toLowerCase())
|
||||
// 方法名以数字开头,添加 api 前缀
|
||||
.replace(/^\d/, (str) => `api${str}`)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析状态变量的 path
|
||||
* @example stores.foo.bar => { storeName: 'foo', variableName: 'bar' }
|
||||
* @example stores.user.count => { storeName: 'user', variableName: 'count' }
|
||||
*
|
||||
* @param variablePath
|
||||
* @returns
|
||||
*/
|
||||
export function parseStoreVariablePath(variablePath: string) {
|
||||
const [, storeName, variableName] = variablePath.split('.');
|
||||
return {
|
||||
storeName,
|
||||
variableName,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析服务变量的 path
|
||||
* @param variablePath
|
||||
* @returns
|
||||
*
|
||||
* @example services.list => { moduleName: 'index', name: 'list' }
|
||||
* @example services.sub.list => { moduleName: 'sub', name: 'list' }
|
||||
* @example foo => undefined
|
||||
*/
|
||||
export function parseServiceVariablePath(variablePath: string) {
|
||||
const parts = variablePath.split('.');
|
||||
if (parts[0] !== 'services') {
|
||||
return {};
|
||||
}
|
||||
|
||||
let moduleName = 'index';
|
||||
let name = '';
|
||||
switch (parts.length) {
|
||||
case 2: {
|
||||
name = parts[1];
|
||||
break;
|
||||
}
|
||||
case 3: {
|
||||
moduleName = parts[1];
|
||||
name = parts[2];
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return {
|
||||
moduleName,
|
||||
name,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -33,6 +33,52 @@ export type OptionType = {
|
||||
relatedImports?: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
* 变量树节点类型
|
||||
*/
|
||||
export interface IVariableTreeNode {
|
||||
/**
|
||||
* 唯一标识符
|
||||
*/
|
||||
key: string;
|
||||
/**
|
||||
* 标题
|
||||
*/
|
||||
title?: string;
|
||||
/**
|
||||
* 辅助提示信息
|
||||
*/
|
||||
help?: string;
|
||||
/**
|
||||
* 是否可选中
|
||||
*/
|
||||
selectable?: boolean;
|
||||
/**
|
||||
* 展示删除按钮
|
||||
*/
|
||||
showRemoveButton?: boolean;
|
||||
/**
|
||||
* 展示添加按钮
|
||||
*/
|
||||
showAddButton?: boolean;
|
||||
/**
|
||||
* 展示查看按钮
|
||||
*/
|
||||
showViewButton?: boolean;
|
||||
/**
|
||||
* 结点类型,用来展示图标
|
||||
*/
|
||||
type?: 'function' | 'property';
|
||||
/**
|
||||
* 定义的原始值
|
||||
*/
|
||||
raw?: any;
|
||||
/**
|
||||
* 子结点
|
||||
*/
|
||||
children?: IVariableTreeNode[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 服务函数类型
|
||||
*/
|
||||
|
||||
@@ -43,7 +43,7 @@ describe('string', () => {
|
||||
expect(isVariableString('{"hello"}')).toBeTruthy();
|
||||
expect(isVariableString('{ foo: "bar" }')).toBeFalsy();
|
||||
expect(isVariableString('{ type: tango.stores?.homePage?.tabKey }')).toBeFalsy();
|
||||
// expect(isVariableString('{ type: tango.stores.homePage.tabKey }')).toBeFalsy(); // FIXME: fix this case
|
||||
// expect(isVariableString('{ type: tango.stores.homePage.tabKey }')).toBeFalsy(); // TIP: failed
|
||||
expect(isVariableString('{ foo: "bar" }')).toBeFalsy();
|
||||
});
|
||||
|
||||
|
||||
@@ -3,6 +3,42 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.0.0-alpha.5](https://github.com/netease/tango/compare/@music163/tango-sandbox@1.0.0-alpha.4...@music163/tango-sandbox@1.0.0-alpha.5) (2023-12-29)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-sandbox
|
||||
|
||||
# [1.0.0-alpha.4](https://github.com/netease/tango/compare/@music163/tango-sandbox@1.0.0-alpha.3...@music163/tango-sandbox@1.0.0-alpha.4) (2023-12-26)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-sandbox
|
||||
|
||||
# [1.0.0-alpha.3](https://github.com/netease/tango/compare/@music163/tango-sandbox@1.0.0-alpha.2...@music163/tango-sandbox@1.0.0-alpha.3) (2023-12-25)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-sandbox
|
||||
|
||||
# [1.0.0-alpha.1](https://github.com/netease/tango/compare/@music163/tango-sandbox@0.1.16...@music163/tango-sandbox@1.0.0-alpha.1) (2023-12-12)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-sandbox
|
||||
|
||||
## [0.1.16](https://github.com/netease/tango/compare/@music163/tango-sandbox@0.1.15...@music163/tango-sandbox@0.1.16) (2023-12-04)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-sandbox
|
||||
|
||||
## [0.1.15](https://github.com/netease/tango/compare/@music163/tango-sandbox@0.1.14...@music163/tango-sandbox@0.1.15) (2023-12-04)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-sandbox
|
||||
|
||||
## [0.1.14](https://github.com/netease/tango/compare/@music163/tango-sandbox@0.1.13...@music163/tango-sandbox@0.1.14) (2023-11-30)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-sandbox
|
||||
|
||||
## [0.1.13](https://github.com/netease/tango/compare/@music163/tango-sandbox@0.1.12...@music163/tango-sandbox@0.1.13) (2023-11-29)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-sandbox
|
||||
|
||||
## [0.1.12](https://github.com/netease/tango/compare/@music163/tango-sandbox@0.1.11...@music163/tango-sandbox@0.1.12) (2023-11-27)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-sandbox
|
||||
|
||||
## [0.1.11](https://github.com/netease/tango/compare/@music163/tango-sandbox@0.1.10...@music163/tango-sandbox@0.1.11) (2023-11-23)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-sandbox
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-sandbox",
|
||||
"version": "0.1.11",
|
||||
"version": "1.0.0-alpha.5",
|
||||
"description": "sandbox of tango apps",
|
||||
"author": "wwsun <ww.sun@outlook.com>",
|
||||
"homepage": "",
|
||||
@@ -29,8 +29,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^4.8.0",
|
||||
"@music163/tango-core": "^0.2.6",
|
||||
"@music163/tango-helpers": "^0.1.8",
|
||||
"@music163/tango-core": "^1.0.0-alpha.5",
|
||||
"@music163/tango-helpers": "^1.0.0-alpha.1",
|
||||
"crypto-js": "^4.1.1",
|
||||
"lodash.isequal": "4.5.0",
|
||||
"react-frame-component": "^5.2.4"
|
||||
|
||||
@@ -3,6 +3,54 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.0.0-alpha.5](https://github.com/netease/tango/compare/@music163/tango-setting-form@1.0.0-alpha.4...@music163/tango-setting-form@1.0.0-alpha.5) (2023-12-29)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-setting-form
|
||||
|
||||
# [1.0.0-alpha.4](https://github.com/netease/tango/compare/@music163/tango-setting-form@1.0.0-alpha.3...@music163/tango-setting-form@1.0.0-alpha.4) (2023-12-26)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-setting-form
|
||||
|
||||
# [1.0.0-alpha.3](https://github.com/netease/tango/compare/@music163/tango-setting-form@1.0.0-alpha.2...@music163/tango-setting-form@1.0.0-alpha.3) (2023-12-25)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-setting-form
|
||||
|
||||
# [1.0.0-alpha.1](https://github.com/netease/tango/compare/@music163/tango-setting-form@0.2.16...@music163/tango-setting-form@1.0.0-alpha.1) (2023-12-12)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- add showSearch and showGroups to SettingForm & update internal icons ([#73](https://github.com/netease/tango/issues/73)) ([a8357f4](https://github.com/netease/tango/commit/a8357f45b8b0c1f2b91a856918f6bdecdc65aa73))
|
||||
- render form item without setter & enhance tools ([#76](https://github.com/netease/tango/issues/76)) ([1acfa68](https://github.com/netease/tango/commit/1acfa6864b1faad1a441facc426d8d94b6b090b5))
|
||||
|
||||
## [0.2.16](https://github.com/netease/tango/compare/@music163/tango-setting-form@0.2.15...@music163/tango-setting-form@0.2.16) (2023-12-04)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-setting-form
|
||||
|
||||
## [0.2.15](https://github.com/netease/tango/compare/@music163/tango-setting-form@0.2.14...@music163/tango-setting-form@0.2.15) (2023-12-04)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-setting-form
|
||||
|
||||
## [0.2.14](https://github.com/netease/tango/compare/@music163/tango-setting-form@0.2.13...@music163/tango-setting-form@0.2.14) (2023-11-30)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-setting-form
|
||||
|
||||
## [0.2.13](https://github.com/netease/tango/compare/@music163/tango-setting-form@0.2.12...@music163/tango-setting-form@0.2.13) (2023-11-29)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- workspace onFilesChange ([#70](https://github.com/netease/tango/issues/70)) ([accd826](https://github.com/netease/tango/commit/accd8263764c811ea8175a9cd341fc6fa6c75967))
|
||||
|
||||
## [0.2.12](https://github.com/netease/tango/compare/@music163/tango-setting-form@0.2.11...@music163/tango-setting-form@0.2.12) (2023-11-28)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- update theme ([2550411](https://github.com/netease/tango/commit/2550411c1c93037931d44aa9f7822ffe8caa900b))
|
||||
- update type ([1db5294](https://github.com/netease/tango/commit/1db52948dbdc28f4533eb968f7da1bf6d16173d4))
|
||||
|
||||
## [0.2.11](https://github.com/netease/tango/compare/@music163/tango-setting-form@0.2.10...@music163/tango-setting-form@0.2.11) (2023-11-27)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-setting-form
|
||||
|
||||
## [0.2.10](https://github.com/netease/tango/compare/@music163/tango-setting-form@0.2.9...@music163/tango-setting-form@0.2.10) (2023-11-23)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-setting-form
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-setting-form",
|
||||
"version": "0.2.10",
|
||||
"version": "1.0.0-alpha.5",
|
||||
"description": "setting form of tango-apps",
|
||||
"author": "wwsun <ww.sun@outlook.com>",
|
||||
"homepage": "",
|
||||
@@ -28,13 +28,13 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^4.8.0",
|
||||
"@music163/tango-core": "^0.2.6",
|
||||
"@music163/tango-helpers": "^0.1.8",
|
||||
"@music163/tango-ui": "^0.1.11",
|
||||
"@music163/tango-core": "^1.0.0-alpha.5",
|
||||
"@music163/tango-helpers": "^1.0.0-alpha.1",
|
||||
"@music163/tango-ui": "^1.0.0-alpha.3",
|
||||
"antd": "^4.24.2",
|
||||
"coral-system": "^1.0.5",
|
||||
"mobx": "6.9.0",
|
||||
"mobx-react-lite": "4.0.2"
|
||||
"mobx": "6.12.0",
|
||||
"mobx-react-lite": "4.0.5"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
|
||||
@@ -8,14 +8,15 @@ import {
|
||||
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';
|
||||
import { ToggleButton, CodeOutlined } from '@music163/tango-ui';
|
||||
import { InputProps } from 'antd';
|
||||
import { useFormModel, useFormVariable } from './context';
|
||||
import { FormControl } from './form-ui';
|
||||
import { Box, Text } from 'coral-system';
|
||||
|
||||
export interface FormItemProps extends ComponentPropType {
|
||||
extra?: React.ReactNode;
|
||||
footer?: React.ReactNode;
|
||||
}
|
||||
|
||||
export interface FormItemComponentProps<T = any> {
|
||||
@@ -76,6 +77,7 @@ export function createFormItem(options: IFormItemCreateOptions) {
|
||||
getVisible: getVisibleProp,
|
||||
getSetterProps: getSetterPropsProp,
|
||||
extra,
|
||||
footer,
|
||||
}: FormItemProps) {
|
||||
const { disableSwitchExpressionSetter } = useFormVariable();
|
||||
const model = useFormModel();
|
||||
@@ -106,8 +108,11 @@ export function createFormItem(options: IFormItemCreateOptions) {
|
||||
|
||||
let expProps = {};
|
||||
|
||||
// FIXME: 重新考虑这段代码的位置
|
||||
if (['expressionSetter', 'actionSetter', 'eventSetter'].includes(setter) || isVariable) {
|
||||
// FIXME: 重新考虑这段代码的位置,外置这个逻辑
|
||||
if (
|
||||
['expressionSetter', 'expSetter', 'actionSetter', 'eventSetter'].includes(setter) ||
|
||||
isVariable
|
||||
) {
|
||||
expProps = {
|
||||
modalTitle: title,
|
||||
modalTip: tip,
|
||||
@@ -141,7 +146,8 @@ export function createFormItem(options: IFormItemCreateOptions) {
|
||||
docs={docs}
|
||||
error={field.error}
|
||||
extra={
|
||||
<>
|
||||
<Box>
|
||||
{extra}
|
||||
{!disableVariableSetter ? (
|
||||
<ToggleButton
|
||||
borderRadius="s"
|
||||
@@ -153,12 +159,12 @@ export function createFormItem(options: IFormItemCreateOptions) {
|
||||
selected={isVariable}
|
||||
onClick={() => toggleIsVariable()}
|
||||
>
|
||||
<IconFont type="icon-brackets-curly" />
|
||||
<CodeOutlined />
|
||||
</ToggleButton>
|
||||
) : null}
|
||||
{extra}
|
||||
</>
|
||||
</Box>
|
||||
}
|
||||
footer={footer}
|
||||
data-setter={setterName}
|
||||
data-field={name}
|
||||
>
|
||||
@@ -187,10 +193,6 @@ export function register(config: IFormItemCreateOptions) {
|
||||
});
|
||||
}
|
||||
|
||||
const iconStyle = {
|
||||
color: 'red',
|
||||
};
|
||||
|
||||
export function SettingFormItem(props: FormItemProps) {
|
||||
const { setter } = props;
|
||||
const Comp = REGISTERED_FORM_ITEM_MAP[setter];
|
||||
@@ -199,10 +201,10 @@ export function SettingFormItem(props: FormItemProps) {
|
||||
return (
|
||||
<Fallback
|
||||
{...props}
|
||||
extra={
|
||||
<Tooltip title={`${props.name}: invalid setter ${props.setter}`}>
|
||||
<QuestionCircleOutlined color="red" style={iconStyle} />
|
||||
</Tooltip>
|
||||
footer={
|
||||
<Text color="red" mt="m">
|
||||
{props.setter} is invalid
|
||||
</Text>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -7,6 +7,7 @@ import { isNil, isString } from '@music163/tango-helpers';
|
||||
export interface FormControlProps extends Omit<FormLabelProps, 'type'> {
|
||||
visible: boolean;
|
||||
extra?: React.ReactNode;
|
||||
footer?: React.ReactNode;
|
||||
error?: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
@@ -18,6 +19,7 @@ export function FormControl({
|
||||
tip,
|
||||
docs,
|
||||
extra,
|
||||
footer,
|
||||
error,
|
||||
children,
|
||||
...rest
|
||||
@@ -29,6 +31,7 @@ export function FormControl({
|
||||
{extra}
|
||||
</Box>
|
||||
<Box>{children}</Box>
|
||||
{footer}
|
||||
{!!error && (
|
||||
<Box mt="m" color="red">
|
||||
{error}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user