mirror of
https://github.com/NetEase/tango.git
synced 2026-08-31 00:59:32 +08:00
Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 35e258b32a | |||
| 62d403f80a | |||
| 73aded3672 | |||
| 8c07821d93 | |||
| 765ea07cd5 | |||
| a27c241d9c | |||
| 4d6b67eecd | |||
| 6fc889a161 | |||
| c07b554360 | |||
| 149002dddd | |||
| 45b0d856c1 | |||
| 6fc0498901 | |||
| f2919c1aa0 | |||
| 5dd5ab62a8 | |||
| 5e7e285452 | |||
| b3a458a914 | |||
| 1acfa6864b | |||
| baf071f130 | |||
| 6b1d55a11f | |||
| a8357f45b8 | |||
| 9db6358588 | |||
| 5bc2fb2d5a | |||
| 05a0f8f490 | |||
| c884712331 | |||
| e35ff792a0 | |||
| 138cbe9b20 |
@@ -0,0 +1,49 @@
|
||||
name: CI
|
||||
# Event设置为main分支的pull request事件,
|
||||
# 这里的main分支相当于master分支,github项目新建是把main设置为默认分支,我懒得改了所以就保持这样吧
|
||||
on:
|
||||
pull_request:
|
||||
branches: main
|
||||
jobs:
|
||||
# 只需要定义一个job并命名为CI
|
||||
CI:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# 拉取项目代码
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
# 给当前环境下载node
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '18.x'
|
||||
# 检查缓存
|
||||
# 如果key命中缓存则直接将缓存的文件还原到 path 目录,从而减少流水线运行时间
|
||||
# 若 key 没命中缓存时,在当前Job成功完成时将自动创建一个新缓存
|
||||
- name: Cache
|
||||
# 缓存命中结果会存储在steps.[id].outputs.cache-hit里,该变量在继后的step中可读
|
||||
id: cache-dependencies
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
# 缓存文件目录的路径
|
||||
path: |
|
||||
**/node_modules
|
||||
# key中定义缓存标志位的生成方式。runner.OS指当前环境的系统。外加对yarn.lock内容生成哈希码作为key值,如果yarn.lock改变则代表依赖有变化。
|
||||
# 这里用yarn.lock而不是package.json是因为package.json中还有version和description之类的描述项目但和依赖无关的属性
|
||||
key: ${{runner.OS}}-${{hashFiles('**/yarn.lock')}}
|
||||
# 安装依赖
|
||||
- name: Installing Dependencies
|
||||
# 如果缓存标志位没命中,则执行该step。否则就跳过该step
|
||||
if: steps.cache-dependencies.outputs.cache-hit != 'true'
|
||||
run: yarn install
|
||||
# 预构建
|
||||
- name: Prebuild
|
||||
run: yarn build
|
||||
# 运行代码扫描
|
||||
- name: Running Lint
|
||||
# 通过前面章节定义的命令行执行代码扫描
|
||||
run: yarn eslint
|
||||
# 运行自动化测试
|
||||
- name: Running Test
|
||||
# 通过前面章节定义的命令行执行自动化测试
|
||||
run: yarn test
|
||||
@@ -31,32 +31,34 @@ You can view the detailed usage guide through the following links:
|
||||
- Out-of-the-box front-end low-code designer, providing flexible and easy-to-use designer React components
|
||||
- Developed using TypeScript, providing complete type definition files
|
||||
|
||||
## 🌐 Compatibility
|
||||
|
||||
- Modern browsers(Chrome >= 80, Edge >= 80, last 2 safari versions, last 2 firefox versions)
|
||||
|
||||
## 🎯 Milestone
|
||||
|
||||
```mermaid
|
||||
gantt
|
||||
dateFormat YYYY-MM-DD
|
||||
title Tango Release Milestone
|
||||
title Tango 1.0 Milestones
|
||||
excludes weekends
|
||||
%% (`excludes` accepts specific dates in YYYY-MM-DD format, days of the week ("sunday") or "weekends", but not the word "weekdays".)
|
||||
|
||||
section Builder Release
|
||||
Alpha version :active, 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
|
||||
1.0 Alpha : m1, 2023-10-01, 2024-01-31
|
||||
1.0 RC : m2, 2024-02-01, 2024-04-30
|
||||
1.0 Stable : m3, 2024-04-30, 2024-12-31
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> Alpha versions will continue to optimize some implementation protocols and continuously integrate new features from the internal version of NetEase; RC versions will provide a stable parsing protocol and implementation; The 1.0 official version is planned to be released in Q2 2024. After the release of the 1.0 official version, we will continue to iterate on versions and gradually incorporate core capabilities such as process design, component library, and AIGC from the internal version.
|
||||
|
||||
## 🌐 Compatibility
|
||||
|
||||
- Modern browsers(Chrome >= 80, Edge >= 80, last 2 safari versions, last 2 firefox versions)
|
||||
|
||||
## 💻 Development
|
||||
|
||||
### Recommended Development Environment
|
||||
### Environment
|
||||
|
||||
- Node.js >= 16.0.0
|
||||
- Yarn >= 1.22.0
|
||||
- Node `>= 18`
|
||||
- Yarn `>= 1.22 && < 2`
|
||||
|
||||
### Development Quick Start
|
||||
|
||||
|
||||
+13
-11
@@ -31,32 +31,34 @@
|
||||
- 开箱即用的前端低代码设计器,提供灵活易用的设计器 React 组件
|
||||
- 使用 TypeScript 开发,提供完整的类型定义文件
|
||||
|
||||
## 🌐 兼容环境
|
||||
|
||||
- 现代浏览器(Chrome >= 80, Edge >= 80, last 2 safari versions, last 2 firefox versions)
|
||||
|
||||
## 🎯 里程碑
|
||||
|
||||
```mermaid
|
||||
gantt
|
||||
dateFormat YYYY-MM-DD
|
||||
title Tango Release Milestone
|
||||
title Tango 1.0 Milestones
|
||||
excludes weekends
|
||||
%% (`excludes` accepts specific dates in YYYY-MM-DD format, days of the week ("sunday") or "weekends", but not the word "weekdays".)
|
||||
|
||||
section Builder Release
|
||||
Alpha version :active, 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
|
||||
1.0 Alpha : m1, 2023-10-01, 2024-01-31
|
||||
1.0 RC : m2, 2024-02-01, 2024-04-30
|
||||
1.0 Stable : m3, 2024-04-30, 2024-12-31
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> Alpha 版本会持续对一些实现协议进行优化,并持续融合网易内网版本中的新增特性;RC 版本将会提供基本稳定的解析协议和实现;1.0 正式版本计划在 2024 年Q2发布。1.0 正式版发布后我们将会持续进行版本迭代,并逐步融入内网版本中的流程设计、组件库、AIGC 等核心基础能力。
|
||||
|
||||
## 🌐 兼容环境
|
||||
|
||||
- 现代浏览器(Chrome >= 80, Edge >= 80, last 2 safari versions, last 2 firefox versions)
|
||||
|
||||
## 💻 开发
|
||||
|
||||
### 推荐开发环境
|
||||
|
||||
- Node.js >= 16.0.0
|
||||
- Yarn >= 1.22.0
|
||||
- Node `>= 18`
|
||||
- Yarn `>= 1.22 && < 2`
|
||||
|
||||
### 本地开发调试方法
|
||||
|
||||
|
||||
@@ -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,6 +75,18 @@ const tangoConfigJson = {
|
||||
},
|
||||
};
|
||||
|
||||
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";
|
||||
|
||||
@@ -143,6 +155,8 @@ import {
|
||||
FormilyForm,
|
||||
} from "@music163/antd";
|
||||
import { Space } from '@music163/antd';
|
||||
import { LocalButton } from '../components';
|
||||
|
||||
class App extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
@@ -151,6 +165,7 @@ class App extends React.Component {
|
||||
</Section>
|
||||
<Section>
|
||||
<Space>
|
||||
<LocalButton />
|
||||
<Button>button</Button>
|
||||
<Input />
|
||||
</Space>
|
||||
@@ -159,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';
|
||||
|
||||
@@ -170,6 +229,35 @@ export default defineStore({
|
||||
title: 'Page Title',
|
||||
|
||||
array: [1, 2, 3],
|
||||
|
||||
test: function() {
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
}
|
||||
}, 'app');
|
||||
`;
|
||||
|
||||
@@ -246,13 +334,16 @@ export const sampleFiles = [
|
||||
{ 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>
|
||||
@@ -15,9 +15,7 @@ import {
|
||||
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,
|
||||
@@ -35,7 +33,6 @@ const workspace = new Workspace({
|
||||
// 2. 引擎初始化
|
||||
const engine = createEngine({
|
||||
workspace,
|
||||
defaultActiveSidebarPanel: 'outline',
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
@@ -68,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" />
|
||||
@@ -122,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>
|
||||
);
|
||||
}
|
||||
@@ -62,13 +62,13 @@ const config = {
|
||||
({
|
||||
// Replace with your project's social card
|
||||
image: 'img/social-card.png',
|
||||
announcementBar: {
|
||||
id: 'notion_alert',
|
||||
content: '🏗 当前版本为测试版,请暂时不要用于生产环境,正式版将于2023年Q4发布!',
|
||||
backgroundColor: 'var(--ifm-color-primary-contrast-background)',
|
||||
textColor: 'var(--ifm-color-primary-contrast-foreground)',
|
||||
isCloseable: false,
|
||||
},
|
||||
// announcementBar: {
|
||||
// id: 'notion_alert',
|
||||
// content: '🏗 当前版本为测试版,请暂时不要用于生产环境,正式版将于2023年Q4发布!',
|
||||
// backgroundColor: 'var(--ifm-color-primary-contrast-background)',
|
||||
// textColor: 'var(--ifm-color-primary-contrast-foreground)',
|
||||
// isCloseable: false,
|
||||
// },
|
||||
navbar: {
|
||||
title: '',
|
||||
logo: {
|
||||
|
||||
@@ -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,36 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.0.0-alpha.6](https://github.com/netease/tango/compare/@music163/tango-context@1.0.0-alpha.5...@music163/tango-context@1.0.0-alpha.6) (2024-01-16)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
# [1.0.0-alpha.5](https://github.com/netease/tango/compare/@music163/tango-context@1.0.0-alpha.4...@music163/tango-context@1.0.0-alpha.5) (2023-12-29)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- refactor VariableTree & Workspace ([#83](https://github.com/netease/tango/issues/83)) ([8c07821](https://github.com/netease/tango/commit/8c07821d93cea4dfc43f81ca948b845176821184))
|
||||
|
||||
# [1.0.0-alpha.4](https://github.com/netease/tango/compare/@music163/tango-context@1.0.0-alpha.3...@music163/tango-context@1.0.0-alpha.4) (2023-12-26)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
# [1.0.0-alpha.3](https://github.com/netease/tango/compare/@music163/tango-context@1.0.0-alpha.2...@music163/tango-context@1.0.0-alpha.3) (2023-12-25)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
# [1.0.0-alpha.1](https://github.com/netease/tango/compare/@music163/tango-context@0.2.12...@music163/tango-context@1.0.0-alpha.1) (2023-12-12)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
## [0.2.12](https://github.com/netease/tango/compare/@music163/tango-context@0.2.11...@music163/tango-context@0.2.12) (2023-12-04)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
## [0.2.11](https://github.com/netease/tango/compare/@music163/tango-context@0.2.10...@music163/tango-context@0.2.11) (2023-12-04)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
## [0.2.10](https://github.com/netease/tango/compare/@music163/tango-context@0.2.9...@music163/tango-context@0.2.10) (2023-11-30)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-context",
|
||||
"version": "0.2.10",
|
||||
"version": "1.0.0-alpha.6",
|
||||
"description": "react context for tango-apps",
|
||||
"keywords": [
|
||||
"react",
|
||||
@@ -31,9 +31,9 @@
|
||||
"react": ">= 16.8"
|
||||
},
|
||||
"dependencies": {
|
||||
"@music163/tango-core": "^0.2.9",
|
||||
"@music163/tango-helpers": "^0.1.8",
|
||||
"mobx-react-lite": "4.0.2"
|
||||
"@music163/tango-core": "^1.0.0-alpha.6",
|
||||
"@music163/tango-helpers": "^1.0.0-alpha.2",
|
||||
"mobx-react-lite": "4.0.5"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
|
||||
@@ -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,47 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.0.0-alpha.6](https://github.com/netease/tango/compare/@music163/tango-core@1.0.0-alpha.5...@music163/tango-core@1.0.0-alpha.6) (2024-01-16)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 优化变量树实现 ([#90](https://github.com/netease/tango/issues/90)) ([62d403f](https://github.com/netease/tango/commit/62d403f80a5ad08c216bc0c035ffc12c2cf329d2))
|
||||
|
||||
# [1.0.0-alpha.5](https://github.com/netease/tango/compare/@music163/tango-core@1.0.0-alpha.4...@music163/tango-core@1.0.0-alpha.5) (2023-12-29)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- check if store value is valid ([765ea07](https://github.com/netease/tango/commit/765ea07cd5be6528e3b326e4ecb163647774d9e5))
|
||||
- refactor VariableTree & Workspace ([#83](https://github.com/netease/tango/issues/83)) ([8c07821](https://github.com/netease/tango/commit/8c07821d93cea4dfc43f81ca948b845176821184))
|
||||
|
||||
# [1.0.0-alpha.4](https://github.com/netease/tango/compare/@music163/tango-core@1.0.0-alpha.3...@music163/tango-core@1.0.0-alpha.4) (2023-12-26)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- list overflow in expSetter ([4d6b67e](https://github.com/netease/tango/commit/4d6b67eecd02b31f01d1bc896c6eeb83f6b34b35))
|
||||
|
||||
# [1.0.0-alpha.3](https://github.com/netease/tango/compare/@music163/tango-core@1.0.0-alpha.2...@music163/tango-core@1.0.0-alpha.3) (2023-12-25)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- export componentEntry module ([149002d](https://github.com/netease/tango/commit/149002ddddaee859333469f65c054d811bde2f7f))
|
||||
|
||||
# [1.0.0-alpha.1](https://github.com/netease/tango/compare/@music163/tango-core@0.2.11...@music163/tango-core@1.0.0-alpha.1) (2023-12-12)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-core
|
||||
|
||||
## [0.2.11](https://github.com/netease/tango/compare/@music163/tango-core@0.2.10...@music163/tango-core@0.2.11) (2023-12-04)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- update FileType ([c884712](https://github.com/netease/tango/commit/c8847123312bc47ea06575f4e714dfd596893e39))
|
||||
|
||||
## [0.2.10](https://github.com/netease/tango/compare/@music163/tango-core@0.2.9...@music163/tango-core@0.2.10) (2023-12-04)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- add filesFormatter to sandbox & fix update props with imports ([#71](https://github.com/netease/tango/issues/71)) ([138cbe9](https://github.com/netease/tango/commit/138cbe9b203b370aff42c1ae8086d69edacf35e9))
|
||||
|
||||
## [0.2.9](https://github.com/netease/tango/compare/@music163/tango-core@0.2.8...@music163/tango-core@0.2.9) (2023-11-30)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-core",
|
||||
"version": "0.2.9",
|
||||
"version": "1.0.0-alpha.6",
|
||||
"description": "tango core",
|
||||
"author": "wwsun <ww.sun@outlook.com>",
|
||||
"homepage": "",
|
||||
@@ -24,14 +24,14 @@
|
||||
"prepublishOnly": "yarn build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/generator": "^7.23.0",
|
||||
"@babel/parser": "^7.23.0",
|
||||
"@babel/traverse": "^7.23.2",
|
||||
"@babel/types": "^7.23.0",
|
||||
"@music163/tango-helpers": "^0.1.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.2",
|
||||
"@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';
|
||||
|
||||
@@ -997,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);
|
||||
@@ -1006,10 +1006,7 @@ export function serviceConfig2Node(payload: IServiceFunctionPayload) {
|
||||
});
|
||||
}
|
||||
|
||||
export function updateServiceConfigToServiceFile(
|
||||
ast: t.File,
|
||||
config: { [key: string]: IServiceFunctionPayload },
|
||||
) {
|
||||
export function updateServiceConfigToServiceFile(ast: t.File, config: Dict<object>) {
|
||||
traverse(ast, {
|
||||
CallExpression(path) {
|
||||
const calleeName = keyNode2value(path.node.callee) as string;
|
||||
@@ -1330,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',
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
@@ -204,20 +203,23 @@ export interface IWorkspace {
|
||||
|
||||
// ----------------- 服务函数文件操作 -----------------
|
||||
|
||||
getServiceFunction?: (serviceKey: string) => object;
|
||||
listServiceFunctions?: () => Record<string, object>;
|
||||
removeServiceFunction?: (serviceName: string, modName?: string) => void;
|
||||
addServiceFunction?: (
|
||||
payload: IServiceFunctionPayload | IServiceFunctionPayload[],
|
||||
modName?: string,
|
||||
) => void;
|
||||
updateServiceFunction?: (payload: IServiceFunctionPayload, modName?: string) => void;
|
||||
updateServiceBaseConfig?: (IServiceFunctionPayload: object, modName?: string) => void;
|
||||
getServiceFunction?: (serviceKey: string) => {
|
||||
name: string;
|
||||
moduleName: string;
|
||||
config: Dict<object>;
|
||||
};
|
||||
listServiceFunctions?: () => Dict<object>;
|
||||
removeServiceFunction?: (serviceKey: string) => void;
|
||||
addServiceFunction?: (serviceName: string, config: Dict, modName?: string) => void;
|
||||
addServiceFunctions?: (configs: Dict<object>, modName?: string) => void;
|
||||
updateServiceFunction?: (serviceName: string, payload: Dict, modName?: string) => void;
|
||||
updateServiceBaseConfig?: (config: Dict, modName?: string) => void;
|
||||
|
||||
// ----------------- 状态管理文件操作 -----------------
|
||||
addStoreState?: (storeName: string, stateName: string, initValue: string) => void;
|
||||
removeStoreState?: (storeName: string, stateName: string) => void;
|
||||
removeStoreModule?: (storeName: string) => void;
|
||||
removeStoreVariable?: (variablePath: string) => void;
|
||||
updateStoreVariable?: (variablePath: string, code: string) => void;
|
||||
|
||||
// ----------------- 视图文件操作 -----------------
|
||||
|
||||
@@ -256,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);
|
||||
@@ -95,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: Dict<object>) {
|
||||
this.ast = updateServiceConfigToServiceFile(this.ast, configs);
|
||||
return this;
|
||||
}
|
||||
|
||||
updateServiceFunction(payload: IServiceFunctionPayload) {
|
||||
const { name, ...rest } = payload;
|
||||
this.ast = updateServiceConfigToServiceFile(this.ast, { [name]: clone(rest, false) });
|
||||
updateServiceFunction(name: string, payload: object) {
|
||||
this.ast = updateServiceConfigToServiceFile(this.ast, { [name]: clone(payload, false) });
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -2,9 +2,13 @@ import { action, computed, makeObservable, observable } from 'mobx';
|
||||
import { JSXElement } from '@babel/types';
|
||||
import {
|
||||
ComponentPrototypeType,
|
||||
Dict,
|
||||
hasFileExtension,
|
||||
isStoreVariablePath,
|
||||
isString,
|
||||
logger,
|
||||
parseServiceVariablePath,
|
||||
parseStoreVariablePath,
|
||||
uniq,
|
||||
} from '@music163/tango-helpers';
|
||||
import {
|
||||
@@ -14,7 +18,6 @@ import {
|
||||
isPathnameMatchRoute,
|
||||
getJSXElementChildrenNames,
|
||||
namesToImportDeclarations,
|
||||
getBlockNameByFilename,
|
||||
prototype2importDeclarationData,
|
||||
} from '../helpers';
|
||||
import { DropMethod } from './drop-target';
|
||||
@@ -23,19 +26,14 @@ import { TangoNode } from './node';
|
||||
import { TangoJsModule } from './module';
|
||||
import { TangoFile, TangoJsonFile, TangoLessFile } from './file';
|
||||
import { IWorkspace } from './interfaces';
|
||||
import {
|
||||
IFileConfig,
|
||||
FileType,
|
||||
ITangoConfigPackages,
|
||||
IPageConfigData,
|
||||
IServiceFunctionPayload,
|
||||
} from '../types';
|
||||
import { IFileConfig, FileType, ITangoConfigPackages, IPageConfigData } from '../types';
|
||||
import { SelectSource } from './select-source';
|
||||
import { DragSource } from './drag-source';
|
||||
import { TangoRouteModule } from './route-module';
|
||||
import { TangoStoreEntryModule, TangoStoreModule } from './store-module';
|
||||
import { TangoServiceModule } from './service-module';
|
||||
import { TangoViewModule } from './view-module';
|
||||
import { TangoComponentsEntryModule } from './component-module';
|
||||
|
||||
export interface IWorkspaceOptions {
|
||||
/**
|
||||
@@ -73,8 +71,14 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
*/
|
||||
files: Map<string, TangoFile>;
|
||||
|
||||
/**
|
||||
* 组件配置
|
||||
*/
|
||||
componentPrototypes: Map<string, ComponentPrototypeType>;
|
||||
|
||||
/**
|
||||
* 入口文件
|
||||
*/
|
||||
entry: string;
|
||||
|
||||
/**
|
||||
@@ -92,12 +96,6 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
*/
|
||||
activeViewFile: string;
|
||||
|
||||
/**
|
||||
* 本地区块 { [blockName]: filePath }
|
||||
* FIXME: 修正类型 Record<string, TangoBlockModule>
|
||||
*/
|
||||
localBlocks: Record<string, string>;
|
||||
|
||||
/**
|
||||
* 路由配置模块
|
||||
*/
|
||||
@@ -112,6 +110,8 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
|
||||
serviceModules: Record<string, TangoServiceModule> = {};
|
||||
|
||||
componentsEntryModule: TangoComponentsEntryModule;
|
||||
|
||||
/**
|
||||
* package.json 文件
|
||||
*/
|
||||
@@ -197,8 +197,8 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
return list;
|
||||
}
|
||||
|
||||
get blocks() {
|
||||
return Object.keys(this.localBlocks);
|
||||
get localComps(): string[] {
|
||||
return Object.keys(this.componentsEntryModule?.exportList || {});
|
||||
}
|
||||
|
||||
constructor(options?: IWorkspaceOptions) {
|
||||
@@ -207,7 +207,6 @@ 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;
|
||||
@@ -233,8 +232,6 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
activeRoute: observable,
|
||||
activeFile: observable,
|
||||
activeViewFile: observable,
|
||||
localBlocks: observable,
|
||||
blocks: computed,
|
||||
pages: computed,
|
||||
bizComps: computed,
|
||||
setActiveRoute: action,
|
||||
@@ -330,6 +327,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;
|
||||
@@ -351,19 +352,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;
|
||||
@@ -404,8 +392,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) {
|
||||
@@ -504,27 +506,6 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加新的路由
|
||||
*/
|
||||
@@ -616,15 +597,23 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据变量路径更新模块内容
|
||||
* TODO: 改名,不直观
|
||||
* 根据变量路径删除状态变量
|
||||
* @param variablePath
|
||||
*/
|
||||
removeStoreVariable(variablePath: string) {
|
||||
const { storeName, variableName } = parseStoreVariablePath(variablePath);
|
||||
this.removeStoreState(storeName, variableName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据变量路径更新状态变量的值
|
||||
* @param variablePath 变量路径
|
||||
* @param code 变量代码
|
||||
*/
|
||||
updateModuleCodeByVariablePath(variablePath: string, code: string) {
|
||||
if (/^stores\.\w+\.\w+$/.test(variablePath)) {
|
||||
const [, storeName, stateName] = variablePath.split('.');
|
||||
this.storeModules[storeName]?.updateState(stateName, code).update();
|
||||
updateStoreVariable(variablePath: string, code: string) {
|
||||
if (isStoreVariablePath(variablePath)) {
|
||||
const { storeName, variableName } = parseStoreVariablePath(variablePath);
|
||||
this.storeModules[storeName]?.updateState(variableName, code).update();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -635,7 +624,7 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
* @returns
|
||||
*/
|
||||
getServiceFunction(serviceKey: string) {
|
||||
const { name, moduleName } = this.parseServiceKey(serviceKey);
|
||||
const { name, moduleName } = parseServiceVariablePath(serviceKey);
|
||||
if (!name) {
|
||||
return;
|
||||
}
|
||||
@@ -649,9 +638,10 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
|
||||
/**
|
||||
* 获取服务函数的列表
|
||||
* @returns 返回服务函数的列表 { [serviceKey: string]: Dict }
|
||||
*/
|
||||
listServiceFunctions() {
|
||||
const ret: Record<string, object> = {};
|
||||
const ret: Record<string, Dict> = {};
|
||||
Object.keys(this.serviceModules).forEach((moduleName) => {
|
||||
const module = this.serviceModules[moduleName];
|
||||
Object.keys(module.serviceFunctions).forEach((name) => {
|
||||
@@ -665,36 +655,34 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
/**
|
||||
* 更新服务函数
|
||||
*/
|
||||
updateServiceFunction(payload: IServiceFunctionPayload, moduleName = 'index') {
|
||||
this.serviceModules[moduleName].updateServiceFunction(payload).update();
|
||||
updateServiceFunction(serviceName: string, payload: Dict, moduleName = 'index') {
|
||||
this.serviceModules[moduleName].updateServiceFunction(serviceName, payload).update();
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增服务函数,支持批量添加
|
||||
*/
|
||||
addServiceFunction(
|
||||
payload: IServiceFunctionPayload | IServiceFunctionPayload[],
|
||||
moduleName = 'index',
|
||||
) {
|
||||
if (Array.isArray(payload)) {
|
||||
this.serviceModules[moduleName]?.addServiceFunctions(payload).update();
|
||||
} else {
|
||||
this.serviceModules[moduleName]?.addServiceFunction(payload).update();
|
||||
}
|
||||
addServiceFunction(name: string, config: Dict, moduleName = 'index') {
|
||||
this.serviceModules[moduleName]?.addServiceFunction(name, config).update();
|
||||
}
|
||||
|
||||
addServiceFunctions(configs: Dict<Dict>, modName = 'index') {
|
||||
this.serviceModules[modName]?.addServiceFunctions(configs).update();
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除服务函数
|
||||
* @param name
|
||||
*/
|
||||
removeServiceFunction(name: string, moduleName = 'index') {
|
||||
removeServiceFunction(serviceKey: string) {
|
||||
const { moduleName, name } = parseServiceVariablePath(serviceKey);
|
||||
this.serviceModules[moduleName]?.deleteServiceFunction(name).update();
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新服务的基础配置
|
||||
*/
|
||||
updateServiceBaseConfig(config: object, moduleName = 'index') {
|
||||
updateServiceBaseConfig(config: Dict, moduleName = 'index') {
|
||||
this.serviceModules[moduleName]?.updateBaseConfig(config).update();
|
||||
}
|
||||
|
||||
@@ -1226,40 +1214,4 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
logger.error('copyFiles failed, source: %s, target: %s', sourceFilePath, targetFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 serviceKey
|
||||
* @param serviceKey
|
||||
* @returns
|
||||
*
|
||||
* @example services.list => { moduleName: 'index', name: 'list' }
|
||||
* @example services.sub.list => { moduleName: 'sub', name: 'list' }
|
||||
* @example foo => undefined
|
||||
*/
|
||||
private parseServiceKey(serviceKey: string) {
|
||||
const parts = serviceKey.split('.');
|
||||
if (parts[0] !== 'services') {
|
||||
return {};
|
||||
}
|
||||
|
||||
let moduleName = 'index';
|
||||
let name = '';
|
||||
switch (parts.length) {
|
||||
case 2: {
|
||||
name = parts[1];
|
||||
break;
|
||||
}
|
||||
case 3: {
|
||||
moduleName = parts[1];
|
||||
name = parts[2];
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return {
|
||||
moduleName,
|
||||
name,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+33
-12
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入变量的来源
|
||||
*/
|
||||
@@ -113,17 +145,6 @@ export interface IImportDeclarationPayload {
|
||||
sourcePath: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 服务函数参数类型
|
||||
*/
|
||||
export interface IServiceFunctionPayload {
|
||||
[key: string]: any;
|
||||
/**
|
||||
* 服务函数名
|
||||
*/
|
||||
name: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store 属性类型
|
||||
*/
|
||||
|
||||
@@ -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,52 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.0.0-alpha.7](https://github.com/netease/tango/compare/@music163/tango-designer@1.0.0-alpha.6...@music163/tango-designer@1.0.0-alpha.7) (2024-01-16)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 优化变量树实现 ([#90](https://github.com/netease/tango/issues/90)) ([62d403f](https://github.com/netease/tango/commit/62d403f80a5ad08c216bc0c035ffc12c2cf329d2))
|
||||
|
||||
# [1.0.0-alpha.6](https://github.com/netease/tango/compare/@music163/tango-designer@1.0.0-alpha.5...@music163/tango-designer@1.0.0-alpha.6) (2023-12-29)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- check if store value is valid ([765ea07](https://github.com/netease/tango/commit/765ea07cd5be6528e3b326e4ecb163647774d9e5))
|
||||
- refactor VariableTree & Workspace ([#83](https://github.com/netease/tango/issues/83)) ([8c07821](https://github.com/netease/tango/commit/8c07821d93cea4dfc43f81ca948b845176821184))
|
||||
|
||||
# [1.0.0-alpha.5](https://github.com/netease/tango/compare/@music163/tango-designer@1.0.0-alpha.4...@music163/tango-designer@1.0.0-alpha.5) (2023-12-26)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- list overflow in expSetter ([4d6b67e](https://github.com/netease/tango/commit/4d6b67eecd02b31f01d1bc896c6eeb83f6b34b35))
|
||||
|
||||
# [1.0.0-alpha.4](https://github.com/netease/tango/compare/@music163/tango-designer@1.0.0-alpha.3...@music163/tango-designer@1.0.0-alpha.4) (2023-12-25)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-designer
|
||||
|
||||
# [1.0.0-alpha.2](https://github.com/netease/tango/compare/@music163/tango-designer@1.0.0-alpha.1...@music163/tango-designer@1.0.0-alpha.2) (2023-12-13)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- update variable tree with help messages and styling & add tangoConfig schema ([5e7e285](https://github.com/netease/tango/commit/5e7e285452b46888b447991b0e8548b6392acb3a))
|
||||
|
||||
# [1.0.0-alpha.1](https://github.com/netease/tango/compare/@music163/tango-designer@0.5.14...@music163/tango-designer@1.0.0-alpha.1) (2023-12-12)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- add showSearch and showGroups to SettingForm & update internal icons ([#73](https://github.com/netease/tango/issues/73)) ([a8357f4](https://github.com/netease/tango/commit/a8357f45b8b0c1f2b91a856918f6bdecdc65aa73))
|
||||
- render form item without setter & enhance tools ([#76](https://github.com/netease/tango/issues/76)) ([1acfa68](https://github.com/netease/tango/commit/1acfa6864b1faad1a441facc426d8d94b6b090b5))
|
||||
|
||||
## [0.5.14](https://github.com/netease/tango/compare/@music163/tango-designer@0.5.13...@music163/tango-designer@0.5.14) (2023-12-04)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-designer
|
||||
|
||||
## [0.5.13](https://github.com/netease/tango/compare/@music163/tango-designer@0.5.12...@music163/tango-designer@0.5.13) (2023-12-04)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- add filesFormatter to sandbox & fix update props with imports ([#71](https://github.com/netease/tango/issues/71)) ([138cbe9](https://github.com/netease/tango/commit/138cbe9b203b370aff42c1ae8086d69edacf35e9))
|
||||
|
||||
## [0.5.12](https://github.com/netease/tango/compare/@music163/tango-designer@0.5.11...@music163/tango-designer@0.5.12) (2023-11-30)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-designer",
|
||||
"version": "0.5.12",
|
||||
"version": "1.0.0-alpha.7",
|
||||
"description": "lowcode designer",
|
||||
"keywords": [
|
||||
"react"
|
||||
@@ -33,12 +33,12 @@
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^4.8.0",
|
||||
"@music163/request": "^0.1.2",
|
||||
"@music163/tango-context": "^0.2.10",
|
||||
"@music163/tango-core": "^0.2.9",
|
||||
"@music163/tango-helpers": "^0.1.8",
|
||||
"@music163/tango-sandbox": "^0.1.14",
|
||||
"@music163/tango-setting-form": "^0.2.14",
|
||||
"@music163/tango-ui": "^0.1.12",
|
||||
"@music163/tango-context": "^1.0.0-alpha.6",
|
||||
"@music163/tango-core": "^1.0.0-alpha.6",
|
||||
"@music163/tango-helpers": "^1.0.0-alpha.2",
|
||||
"@music163/tango-sandbox": "^1.0.0-alpha.6",
|
||||
"@music163/tango-setting-form": "^1.0.0-alpha.6",
|
||||
"@music163/tango-ui": "^1.0.0-alpha.4",
|
||||
"antd": "^4.24.2",
|
||||
"cash-dom": "^8.1.2",
|
||||
"classnames": "^2.3.2",
|
||||
|
||||
@@ -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,635 +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-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;
|
||||
}
|
||||
|
||||
.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,419 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import {
|
||||
IVariableTreeNode,
|
||||
filterTreeData,
|
||||
isServiceVariablePath,
|
||||
isStoreVariablePath,
|
||||
noop,
|
||||
parseServiceVariablePath,
|
||||
} from '@music163/tango-helpers';
|
||||
import { css, Box, Text } from 'coral-system';
|
||||
import { Button, Popconfirm, Tooltip, Tree } from 'antd';
|
||||
import {
|
||||
CopyOutlined,
|
||||
DeleteOutlined,
|
||||
EyeOutlined,
|
||||
FunctionOutlined,
|
||||
PlusOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { CopyClipboard, Panel, Search } from '@music163/tango-ui';
|
||||
import { AddStoreForm, AddStoreVariableForm } from './add-store';
|
||||
import { AddServiceForm } from './add-service';
|
||||
import {
|
||||
NodeCommonDetail,
|
||||
ValueDefine,
|
||||
ValueDefineProps,
|
||||
ValueDetail,
|
||||
ValueDetailProps,
|
||||
} from './value-detail';
|
||||
import { ValuePreview } from './value-preview';
|
||||
import { ServicePreview } from './service-preview';
|
||||
|
||||
const varTreeStyle = css`
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
|
||||
.ant-tree {
|
||||
font-family: Consolas, Menlo, Courier, monospace;
|
||||
}
|
||||
|
||||
.ant-tree-node-content-wrapper.ant-tree-node-content-wrapper-normal {
|
||||
width: calc(100% - 50px);
|
||||
}
|
||||
|
||||
.ant-tree .ant-tree-treenode {
|
||||
padding: 0;
|
||||
}
|
||||
.ant-tree.ant-tree-directory .ant-tree-treenode::before {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.ant-tree-indent-unit {
|
||||
width: 12px;
|
||||
}
|
||||
`;
|
||||
|
||||
type SelectNodeCallback = (data: IVariableTreeNode) => void;
|
||||
|
||||
type DetailModeType =
|
||||
| 'detail'
|
||||
| 'storeVariableDetail'
|
||||
| 'serviceDetail'
|
||||
| 'addVariable'
|
||||
| 'addStore'
|
||||
| 'addService';
|
||||
|
||||
interface IVariableTreeRenderDetailState {
|
||||
mode: DetailModeType;
|
||||
setMode: (mode: DetailModeType) => void;
|
||||
activeNode: IVariableTreeNode;
|
||||
clear: () => void;
|
||||
}
|
||||
|
||||
export interface VariableTreeProps {
|
||||
defaultValueDetailMode?: ValueDetailProps['defaultMode'];
|
||||
dataSource: IVariableTreeNode[];
|
||||
appContext?: object;
|
||||
serviceModules?: any[];
|
||||
getPreviewValue?: (node: IVariableTreeNode) => unknown;
|
||||
getServiceData?: (serviceKey: string) => object;
|
||||
getServiceNames?: (moduleName: string) => string[];
|
||||
getStoreNames?: () => string[];
|
||||
onSelect?: SelectNodeCallback;
|
||||
onAddStoreVariable?: (storeName: string, data: any) => void;
|
||||
onUpdateStoreVariable?: ValueDefineProps['onSave'];
|
||||
onRemoveStoreVariable?: (variableKey: string) => void;
|
||||
onAddStore?: (newStoreName: string) => void;
|
||||
onAddService?: (data: object) => void;
|
||||
onUpdateService?: (data: object) => void;
|
||||
onRemoveService?: (variableKey: string) => void;
|
||||
onCopy?: (data: IVariableTreeNode) => void;
|
||||
onView?: SelectNodeCallback;
|
||||
height?: number | string;
|
||||
showViewButton?: boolean;
|
||||
/**
|
||||
* 自定义头部
|
||||
*/
|
||||
renderHeaderExtra?: (
|
||||
props: VariableTreeProps,
|
||||
state: IVariableTreeRenderDetailState,
|
||||
) => React.ReactNode;
|
||||
/**
|
||||
* 自定义变量详情渲染
|
||||
* @param props
|
||||
* @param state
|
||||
* @returns
|
||||
*/
|
||||
renderDetail?: (
|
||||
props: VariableTreeProps,
|
||||
state: IVariableTreeRenderDetailState,
|
||||
) => React.ReactNode;
|
||||
}
|
||||
|
||||
export function VariableTree(props: VariableTreeProps) {
|
||||
const {
|
||||
dataSource = [],
|
||||
serviceModules = [],
|
||||
appContext = {},
|
||||
defaultValueDetailMode,
|
||||
onSelect = noop,
|
||||
onAddStoreVariable = noop,
|
||||
onAddStore = noop,
|
||||
onAddService = noop,
|
||||
onUpdateStoreVariable = noop,
|
||||
onUpdateService = noop,
|
||||
onRemoveStoreVariable,
|
||||
onRemoveService,
|
||||
onCopy = noop,
|
||||
onView = noop,
|
||||
getServiceData,
|
||||
getServiceNames,
|
||||
getStoreNames,
|
||||
getPreviewValue = noop,
|
||||
showViewButton,
|
||||
renderHeaderExtra,
|
||||
renderDetail: renderDetailProp,
|
||||
...rest
|
||||
} = props;
|
||||
const [keyword, setKeyword] = useState('');
|
||||
const [activeNode, setActiveNode] = useState<IVariableTreeNode>();
|
||||
const [mode, setMode] = useState<DetailModeType>();
|
||||
const clear = useCallback(() => {
|
||||
setActiveNode(null);
|
||||
setMode(null);
|
||||
}, []);
|
||||
|
||||
const selectNode = useCallback((node: IVariableTreeNode, callback?: SelectNodeCallback) => {
|
||||
if (isStoreVariablePath(node.key)) {
|
||||
setMode('storeVariableDetail');
|
||||
} else if (isServiceVariablePath(node.key)) {
|
||||
setMode('serviceDetail');
|
||||
} else {
|
||||
setMode('detail');
|
||||
}
|
||||
setActiveNode(node);
|
||||
callback?.(node);
|
||||
}, []);
|
||||
|
||||
const treeData = useMemo(() => {
|
||||
const pattern = new RegExp(keyword, 'ig');
|
||||
return keyword
|
||||
? filterTreeData(dataSource, (leaf) => pattern.test(leaf.title), 'children', true)
|
||||
: dataSource;
|
||||
}, [keyword, dataSource]);
|
||||
|
||||
const state = { activeNode, mode, setMode, clear };
|
||||
|
||||
return (
|
||||
<Box display="flex" columnGap="l" className="VariableTree" css={varTreeStyle} {...rest}>
|
||||
<Box className="VariableList" width="40%">
|
||||
{renderHeaderExtra?.(props, state)}
|
||||
<Box mb="m" position="sticky" top="0" bg="white" zIndex={2}>
|
||||
<Search placeholder="请输入变量名" onChange={(val) => setKeyword(val?.trim())} />
|
||||
</Box>
|
||||
<Tree
|
||||
blockNode
|
||||
showIcon={false}
|
||||
defaultExpandAll
|
||||
treeData={treeData}
|
||||
onSelect={(keys, detail) => {
|
||||
selectNode(detail.node, onSelect);
|
||||
}}
|
||||
titleRender={(node) => {
|
||||
const isLeaf = !node.children;
|
||||
|
||||
if (isLeaf) {
|
||||
const showView = node.showViewButton ?? showViewButton;
|
||||
return (
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center">
|
||||
<Text flex="1" truncated>
|
||||
{node.title}
|
||||
{node.type === 'function' && (
|
||||
<Text color="text3" ml="m">
|
||||
<FunctionOutlined />
|
||||
</Text>
|
||||
)}
|
||||
</Text>
|
||||
<Box flex="0 0 72px" textAlign="right">
|
||||
{node.showRemoveButton && (
|
||||
<Popconfirm
|
||||
title={`确认删除吗 ${node.title}?该操作会导致引用此模型的代码报错,请谨慎操作!`}
|
||||
onConfirm={() => {
|
||||
if (isStoreVariablePath(node.key)) {
|
||||
onRemoveStoreVariable(node.key);
|
||||
} else {
|
||||
onRemoveService(node.key);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
/>
|
||||
</Popconfirm>
|
||||
)}
|
||||
<CopyClipboard text={`tango.${node.key.replaceAll('.', '?.')}`}>
|
||||
{({ copied, onClick }) => {
|
||||
const label = copied ? '已复制' : '复制变量路径';
|
||||
return (
|
||||
<Tooltip title={label}>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onClick();
|
||||
onCopy(node);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
}}
|
||||
</CopyClipboard>
|
||||
{showView && (
|
||||
<Tooltip title="查看变量详情">
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<EyeOutlined />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
selectNode(node, onView);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box display="flex" alignItems="center" justifyContent="space-between">
|
||||
<Text mr="m">{node.title}</Text>
|
||||
<Box>
|
||||
{/^\$?stores$/.test(node.key) && (
|
||||
<Tooltip title="新建数据模型">
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setMode('addStore');
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
{node.showAddButton && /^stores\.[a-zA-Z0-9]+$/.test(node.key) && (
|
||||
<Tooltip title={`向 ${node.title} 中添加变量`}>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setActiveNode(node);
|
||||
setMode('addVariable');
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
{/^services(\.[a-zA-Z0-9]+)?$/.test(node.key) && (
|
||||
<Tooltip title={`向 ${node.title} 中添加服务函数`}>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setActiveNode(node);
|
||||
setMode('addService');
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Box className="VariableDetail" flex="1" position="sticky" top="0" overflow="auto">
|
||||
{(function renderDetail() {
|
||||
const customRenderer = renderDetailProp?.(props, state);
|
||||
if (customRenderer) {
|
||||
return customRenderer;
|
||||
}
|
||||
|
||||
if (mode === 'detail') {
|
||||
return <NodeCommonDetail data={activeNode} />;
|
||||
}
|
||||
if (mode === 'storeVariableDetail') {
|
||||
return (
|
||||
<ValueDetail key={activeNode.key} defaultMode={defaultValueDetailMode}>
|
||||
{(previewMode) =>
|
||||
previewMode === 'runtime' ? (
|
||||
<ValuePreview
|
||||
value={getPreviewValue(activeNode)}
|
||||
onCopy={(valuePath) => {
|
||||
return ['tango', activeNode.key.replaceAll('.', '?.'), valuePath].join('.');
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<ValueDefine data={activeNode} onSave={onUpdateStoreVariable} />
|
||||
)
|
||||
}
|
||||
</ValueDetail>
|
||||
);
|
||||
}
|
||||
if (mode === 'serviceDetail') {
|
||||
return (
|
||||
<>
|
||||
<Panel shape="solid" title="服务函数配置">
|
||||
<AddServiceForm
|
||||
key={activeNode.key}
|
||||
serviceModules={serviceModules}
|
||||
serviceNames={(function () {
|
||||
const { moduleName } = parseServiceVariablePath(activeNode.key);
|
||||
return getServiceNames?.(moduleName) || [];
|
||||
})()}
|
||||
initialValues={{
|
||||
...getServiceData?.(activeNode.key),
|
||||
}}
|
||||
onCancel={clear}
|
||||
onSubmit={(values) => {
|
||||
onUpdateService(values);
|
||||
clear();
|
||||
}}
|
||||
/>
|
||||
</Panel>
|
||||
<Panel shape="solid" title="服务函数预览" mt="l">
|
||||
<ServicePreview
|
||||
key={activeNode.key}
|
||||
appContext={appContext}
|
||||
functionKey={activeNode.key}
|
||||
/>
|
||||
</Panel>
|
||||
</>
|
||||
);
|
||||
}
|
||||
if (mode === 'addVariable') {
|
||||
return (
|
||||
<AddStoreVariableForm
|
||||
parentNode={activeNode}
|
||||
onSubmit={(storeName, data) => {
|
||||
onAddStoreVariable(storeName, data);
|
||||
clear();
|
||||
}}
|
||||
onCancel={() => {
|
||||
clear();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (mode === 'addStore') {
|
||||
return (
|
||||
<AddStoreForm
|
||||
storeNames={getStoreNames?.() || []}
|
||||
onSubmit={({ name }) => {
|
||||
onAddStore(name);
|
||||
clear();
|
||||
}}
|
||||
onCancel={() => {
|
||||
clear();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (mode === 'addService') {
|
||||
return (
|
||||
<Panel shape="solid" title="创建服务函数">
|
||||
<AddServiceForm
|
||||
key={activeNode.key}
|
||||
serviceModules={serviceModules}
|
||||
serviceNames={activeNode.children?.map((item) => item.title) || []}
|
||||
initialValues={{
|
||||
moduleName: activeNode.title,
|
||||
}}
|
||||
onCancel={() => {
|
||||
clear();
|
||||
}}
|
||||
onSubmit={(values) => {
|
||||
onAddService(values);
|
||||
clear();
|
||||
}}
|
||||
/>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
})()}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
@@ -13,6 +13,7 @@ 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';
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,13 +6,21 @@ import {
|
||||
isWrappedByExpressionContainer,
|
||||
value2expressionCode,
|
||||
} from '@music163/tango-core';
|
||||
import { getVariableContent, noop, useBoolean, getValue } from '@music163/tango-helpers';
|
||||
import {
|
||||
getVariableContent,
|
||||
noop,
|
||||
useBoolean,
|
||||
getValue,
|
||||
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)) {
|
||||
@@ -77,6 +85,7 @@ export function ExpressionSetter(props: ExpressionSetterProps) {
|
||||
value: valueProp,
|
||||
status,
|
||||
allowClear = true,
|
||||
newStoreTemplate,
|
||||
} = props;
|
||||
const [visible, { on, off }] = useBoolean();
|
||||
const [inputValue, setInputValue] = useState(() => {
|
||||
@@ -143,6 +152,7 @@ export function ExpressionSetter(props: ExpressionSetterProps) {
|
||||
subTitle={modalTip}
|
||||
placeholder={placeholder}
|
||||
autoCompleteOptions={autoCompleteOptions}
|
||||
newStoreTemplate={newStoreTemplate}
|
||||
visible={visible}
|
||||
value={inputValue}
|
||||
onCancel={() => off()}
|
||||
@@ -166,6 +176,10 @@ export interface ExpressionModalProps {
|
||||
onOk?: (value: string) => void;
|
||||
dataSource?: IVariableTreeNode[];
|
||||
autoCompleteOptions?: string[];
|
||||
/**
|
||||
* 新建 store 的模板代码
|
||||
*/
|
||||
newStoreTemplate?: string;
|
||||
}
|
||||
|
||||
export function ExpressionModal({
|
||||
@@ -179,18 +193,19 @@ export function ExpressionModal({
|
||||
value,
|
||||
dataSource,
|
||||
autoCompleteOptions,
|
||||
newStoreTemplate = CODE_TEMPLATES.newStoreTemplate,
|
||||
}: ExpressionModalProps) {
|
||||
const [exp, setExp] = useState(value ?? defaultValue);
|
||||
const [error, setError] = useState('');
|
||||
const workspace = useWorkspace();
|
||||
const onAction = useCallback(
|
||||
(action: string, args: unknown[]) => {
|
||||
workspace[action]?.(...args);
|
||||
},
|
||||
[workspace],
|
||||
);
|
||||
const sandbox = useSandboxQuery();
|
||||
|
||||
const { expressionVariables } = useWorkspaceData();
|
||||
const serviceModules = Object.keys(workspace.serviceModules).map((key) => ({
|
||||
label: key === 'index' ? '默认模块' : key,
|
||||
value: key,
|
||||
}));
|
||||
|
||||
const sandbox = useSandboxQuery();
|
||||
const evaluateContext = sandbox.window;
|
||||
|
||||
const handleExpInputChange = (val: string) => {
|
||||
@@ -231,10 +246,29 @@ export function ExpressionModal({
|
||||
/>
|
||||
{error ? <Text color="red">输入的表达式存在语法错误,请修改后再提交!</Text> : null}
|
||||
</Panel>
|
||||
<Panel title="从变量列表中选中" shape="solid" borderTop="0">
|
||||
<EditableVariableTree
|
||||
<Panel
|
||||
title="从变量列表中选中"
|
||||
shape="solid"
|
||||
borderTop="0"
|
||||
overflow="hidden"
|
||||
bodyProps={{ overflow: 'hidden' }}
|
||||
>
|
||||
<VariableTree
|
||||
height={380}
|
||||
showViewButton
|
||||
dataSource={dataSource || expressionVariables}
|
||||
appContext={sandbox?.window['tango']}
|
||||
getStoreNames={() => Object.keys(workspace.storeModules)}
|
||||
serviceModules={serviceModules}
|
||||
getServiceData={(serviceKey) => {
|
||||
const data = workspace.getServiceFunction(serviceKey);
|
||||
return {
|
||||
name: data.name,
|
||||
moduleName: data.moduleName,
|
||||
method: 'get',
|
||||
...data.config,
|
||||
};
|
||||
}}
|
||||
onSelect={(node) => {
|
||||
if (!node.key) {
|
||||
return;
|
||||
@@ -250,17 +284,28 @@ export function ExpressionModal({
|
||||
}
|
||||
setExp(str);
|
||||
}}
|
||||
onAddVariable={(storeName, data) => {
|
||||
onAction('addStoreState', [storeName, data.name, data.initialValue]);
|
||||
onAddStoreVariable={(storeName, data) => {
|
||||
workspace.addStoreState(storeName, data.name, data.initialValue);
|
||||
}}
|
||||
onDeleteVariable={(storeName, stateName) => {
|
||||
onAction('removeStoreState', [storeName, stateName]);
|
||||
onUpdateStoreVariable={(variableKey, code) => {
|
||||
workspace.updateStoreVariable(variableKey, code);
|
||||
}}
|
||||
onDeleteStore={(storeName) => {
|
||||
onAction('removeStoreModule', [storeName]);
|
||||
onAddStore={(storeName) => {
|
||||
workspace.addStoreFile(storeName, newStoreTemplate);
|
||||
}}
|
||||
onSave={(code, node) => {
|
||||
onAction('updateModuleCodeByVariablePath', [node.key, code]);
|
||||
onRemoveStoreVariable={(variableKey) => {
|
||||
workspace.removeStoreVariable(variableKey);
|
||||
}}
|
||||
onRemoveService={(serviceKey) => {
|
||||
workspace.removeServiceFunction(serviceKey);
|
||||
}}
|
||||
onAddService={(data) => {
|
||||
const { name, moduleName, ...payload } = shapeServiceValues(data);
|
||||
workspace.addServiceFunction(name, payload, moduleName);
|
||||
}}
|
||||
onUpdateService={(data) => {
|
||||
const { name, moduleName, ...payload } = shapeServiceValues(data);
|
||||
workspace.updateServiceFunction(name, payload, moduleName);
|
||||
}}
|
||||
getPreviewValue={(node) => {
|
||||
if (!node || !node.key) {
|
||||
@@ -268,7 +313,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);
|
||||
}
|
||||
|
||||
@@ -5,8 +5,9 @@ import { getValue, isFunction } from '@music163/tango-helpers';
|
||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||
import { MenuOutlined } from '@ant-design/icons';
|
||||
import { useWorkspace, useWorkspaceData } from '@music163/tango-context';
|
||||
import { EditableVariableTreeModal } from '../components';
|
||||
import { VariableTreeModal } from '../components';
|
||||
import { useSandboxQuery } from '../context';
|
||||
import { CODE_TEMPLATES } from '../helpers';
|
||||
|
||||
function object2treeData(
|
||||
val: any,
|
||||
@@ -52,43 +53,40 @@ function traverseTreeData(val: any, callback: (val: any) => void) {
|
||||
}
|
||||
}
|
||||
|
||||
export function ModelSetter({ value, onChange }: FormItemComponentProps) {
|
||||
export function ModelSetter({
|
||||
value,
|
||||
onChange,
|
||||
newStoreTemplate = CODE_TEMPLATES.newStoreTemplate,
|
||||
}: FormItemComponentProps) {
|
||||
const [inputValue, setInputValue] = useState(value);
|
||||
const { modelVariables } = useWorkspaceData();
|
||||
const evaluateContext = useSandboxQuery().window || {};
|
||||
const workspace = useWorkspace();
|
||||
const onAction = useCallback(
|
||||
(action: string, args: unknown[]) => {
|
||||
workspace[action]?.(...args);
|
||||
},
|
||||
[workspace],
|
||||
);
|
||||
const definedVariables = useMemo(() => {
|
||||
const map = new Map();
|
||||
const list: string[] = [];
|
||||
traverseTreeData(modelVariables, (node) => {
|
||||
if (node.key.split('.').length > 1) {
|
||||
map.set(node.key, node);
|
||||
list.push(node.key);
|
||||
}
|
||||
});
|
||||
return map;
|
||||
return list;
|
||||
}, [modelVariables]);
|
||||
|
||||
const variables = evaluateContext['tango']?.stores
|
||||
? [
|
||||
object2treeData(evaluateContext['tango']?.stores, 'stores', 0, 1, (keyPath, val) => {
|
||||
if (keyPath.split('.').length === 2 && definedVariables.has(keyPath)) {
|
||||
return {
|
||||
showAddChildIcon: true,
|
||||
showRemoveIcon: true,
|
||||
};
|
||||
}
|
||||
const ret: any = {};
|
||||
if (isFunction(val)) {
|
||||
// 不可以同步给函数类型变量
|
||||
return {
|
||||
disabled: true,
|
||||
type: 'function',
|
||||
};
|
||||
ret.disabled = true;
|
||||
ret.type = 'function';
|
||||
}
|
||||
|
||||
if (!definedVariables.includes(keyPath)) {
|
||||
ret.showAddButton = false;
|
||||
ret.showRemoveButton = false;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}),
|
||||
]
|
||||
: modelVariables;
|
||||
@@ -122,31 +120,34 @@ export function ModelSetter({ value, onChange }: FormItemComponentProps) {
|
||||
onChange={onInputChange}
|
||||
onBlur={onInputBlur}
|
||||
suffix={
|
||||
<EditableVariableTreeModal
|
||||
<VariableTreeModal
|
||||
title="同步到的变量"
|
||||
trigger={
|
||||
<Tooltip title="从模型列表选择" placement="topRight">
|
||||
<MenuOutlined />
|
||||
</Tooltip>
|
||||
}
|
||||
modes={['preview']}
|
||||
dataSource={variables as any[]}
|
||||
onSelect={(node) => {
|
||||
const modelPath = node.key.split('.').slice(1).join('.');
|
||||
onChange(modelPath);
|
||||
}}
|
||||
onAddVariable={(storeName, data) => {
|
||||
onAction('addStoreState', [storeName, data.name, data.initialValue]);
|
||||
onAddStoreVariable={(storeName, data) => {
|
||||
workspace.addStoreState(storeName, data.name, data.initialValue);
|
||||
}}
|
||||
onDeleteVariable={(storeName, stateName) => {
|
||||
onAction('removeStoreState', [storeName, stateName]);
|
||||
onUpdateStoreVariable={(variableKey, code) => {
|
||||
workspace.updateStoreVariable(variableKey, code);
|
||||
}}
|
||||
onDeleteStore={(storeName) => {
|
||||
onAction('removeStoreModule', [storeName]);
|
||||
onAddStore={(storeName) => {
|
||||
workspace.addStoreFile(storeName, newStoreTemplate);
|
||||
}}
|
||||
onSave={(code, node) => {
|
||||
onAction('updateModuleCodeByVariablePath', [node.key, code]);
|
||||
onRemoveStoreVariable={(variablePath) => {
|
||||
workspace.removeStoreVariable(variablePath);
|
||||
}}
|
||||
onRemoveService={(serviceKey) => {
|
||||
workspace.removeServiceFunction(serviceKey);
|
||||
}}
|
||||
getStoreNames={() => Object.keys(workspace.storeModules)}
|
||||
getPreviewValue={(node) => {
|
||||
if (!node || !node.key) {
|
||||
return;
|
||||
|
||||
@@ -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 } 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}
|
||||
onRemoveService={(variableKey) => {
|
||||
workspace.removeServiceFunction(variableKey);
|
||||
}}
|
||||
onAddService={(data) => {
|
||||
const { name, moduleName, ...payload } = shapeServiceValues(data);
|
||||
workspace.addServiceFunction(name, payload, moduleName);
|
||||
}}
|
||||
onUpdateService={(data) => {
|
||||
const { name, moduleName, ...payload } = shapeServiceValues(data);
|
||||
workspace.updateServiceFunction(name, payload, moduleName);
|
||||
}}
|
||||
getServiceData={(serviceKey) => {
|
||||
const data = workspace.getServiceFunction(serviceKey);
|
||||
return {
|
||||
name: data.name,
|
||||
moduleName: data.moduleName,
|
||||
method: 'get',
|
||||
...data.config,
|
||||
};
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
|
||||
interface ServiceDetailFormProps extends FormProps {
|
||||
serviceKeys?: string[];
|
||||
serviceModules?: any[];
|
||||
onCancel?: () => void;
|
||||
onSubmit?: (values: any, mode: ServiceFunctionOperationModeType) => void;
|
||||
}
|
||||
|
||||
function ServiceDetailForm({
|
||||
serviceKeys = [],
|
||||
serviceModules = [],
|
||||
onCancel,
|
||||
onSubmit,
|
||||
initialValues,
|
||||
...formProps
|
||||
}: ServiceDetailFormProps) {
|
||||
const isModifyMode = !!initialValues?.name; // 是否为更新模式
|
||||
const [disabled, setDisabled] = useState(isModifyMode);
|
||||
const [form] = Form.useForm();
|
||||
// 监听服务类型变化,默认是自定义
|
||||
const formType = Form.useWatch('type', form);
|
||||
|
||||
// 新建的时候,类型变换重置部分字段
|
||||
useEffect(() => {
|
||||
if (!formType) return;
|
||||
!isModifyMode &&
|
||||
form.setFieldsValue({
|
||||
url: undefined,
|
||||
method: undefined,
|
||||
formatter: undefined,
|
||||
});
|
||||
}, [formType, form, isModifyMode]);
|
||||
|
||||
return (
|
||||
<Form
|
||||
form={form}
|
||||
labelCol={6}
|
||||
wrapperCol={17}
|
||||
colon={false}
|
||||
layout="horizontal"
|
||||
initialValues={initialValues}
|
||||
onFinish={(values) => {
|
||||
onSubmit(
|
||||
values,
|
||||
isModifyMode
|
||||
? ServiceFunctionOperationModeType.UPDATE
|
||||
: ServiceFunctionOperationModeType.ADD,
|
||||
);
|
||||
setDisabled(true);
|
||||
}}
|
||||
{...formProps}
|
||||
>
|
||||
<Form.Item
|
||||
label="命名空间"
|
||||
name="moduleName"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
},
|
||||
]}
|
||||
component="select"
|
||||
componentProps={{
|
||||
options: serviceModules,
|
||||
disabled: disabled || isModifyMode,
|
||||
}}
|
||||
/>
|
||||
<Form.Item
|
||||
label="方法名"
|
||||
name="name"
|
||||
rules={[
|
||||
{ required: true },
|
||||
{ pattern: /^[a-z]\w+$/, message: '请输入合法的方法名称' },
|
||||
!isModifyMode && {
|
||||
validator(_, value) {
|
||||
const isExist = serviceKeys.includes(
|
||||
['services', form.getFieldValue('moduleName'), value].join('.'),
|
||||
);
|
||||
return isExist
|
||||
? Promise.reject(new Error('重复的方法名,请换一个!'))
|
||||
: Promise.resolve();
|
||||
},
|
||||
},
|
||||
]}
|
||||
component="input"
|
||||
componentProps={{
|
||||
placeholder: '请输入数据服务调用名称',
|
||||
disabled: disabled || isModifyMode,
|
||||
}}
|
||||
extra={
|
||||
!disabled &&
|
||||
!isModifyMode && (
|
||||
<Box>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
disabled={!form.getFieldValue('url')}
|
||||
onClick={() => {
|
||||
const funcName = getApiDefaultName(form.getFieldValue('url'));
|
||||
form.setFieldValue('name', funcName);
|
||||
}}
|
||||
>
|
||||
使用接口路径自动生成
|
||||
</Button>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Form.Item
|
||||
label="路径"
|
||||
name="url"
|
||||
rules={[{ required: true, type: 'url' }]}
|
||||
component="input"
|
||||
componentProps={{
|
||||
placeholder: '请输入数据服务调用名称',
|
||||
disabled: disabled || isModifyMode,
|
||||
}}
|
||||
/>
|
||||
<Form.Item
|
||||
label="方法"
|
||||
name="method"
|
||||
component="select"
|
||||
componentProps={{
|
||||
options: httpMethods,
|
||||
placeholder: '请选择 HTTP 请求方法',
|
||||
disabled,
|
||||
}}
|
||||
/>
|
||||
<Form.Item
|
||||
label="编码类型"
|
||||
name="requestType"
|
||||
tooltip="设置请求头的 Content-Type,若不设置,则默认变为为 JSON"
|
||||
component="select"
|
||||
componentProps={{
|
||||
options: requestTypes,
|
||||
placeholder: '请求头Content-Type,默认为JSON',
|
||||
disabled,
|
||||
}}
|
||||
/>
|
||||
<Form.Item
|
||||
label="格式化响应"
|
||||
name="formatter"
|
||||
tooltip="提供格式化函数,对结果进行格式化"
|
||||
validateTrigger="onBlur"
|
||||
rules={[
|
||||
{
|
||||
validator(_, value) {
|
||||
if (!value) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return isValidFunctionCode(value)
|
||||
? Promise.resolve()
|
||||
: Promise.reject(new Error('请提供合法的函数代码!'));
|
||||
},
|
||||
},
|
||||
]}
|
||||
extra={
|
||||
!disabled && (
|
||||
<Box>
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: [
|
||||
{ label: '直接返回(默认)', key: 'res => res' },
|
||||
{ label: '返回数据部分(云音乐标准规范)', key: 'res => res.data' },
|
||||
],
|
||||
onClick: ({ key }) => {
|
||||
form.setFieldValue('formatter', key);
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Button type="link" size="small">
|
||||
使用模板函数
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
>
|
||||
<InputCode editable={!disabled} showLineNumbers placeholder="res => res" />
|
||||
</Form.Item>
|
||||
<Form.Item wrapperCol={{ offset: 6, span: 18 }}>
|
||||
{isModifyMode ? (
|
||||
<Space>
|
||||
{!disabled ? (
|
||||
<>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setDisabled(true);
|
||||
}}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button type="primary" htmlType="submit">
|
||||
修改
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<Button
|
||||
onClick={() => {
|
||||
setDisabled(false);
|
||||
}}
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
) : (
|
||||
<Space>
|
||||
<Button onClick={onCancel}>取消</Button>
|
||||
<Button type="primary" htmlType="submit">
|
||||
创建
|
||||
</Button>
|
||||
</Space>
|
||||
)}
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
interface ServicePreviewProps {
|
||||
data?: object;
|
||||
}
|
||||
|
||||
function ServicePreview({ data }: ServicePreviewProps) {
|
||||
let ret: React.ReactNode;
|
||||
|
||||
if (!isNil(data)) {
|
||||
switch (typeof data) {
|
||||
case 'object':
|
||||
ret = <JsonView src={data as object} />;
|
||||
break;
|
||||
default:
|
||||
ret = String(data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const initialRet = <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />;
|
||||
|
||||
return (
|
||||
<Box
|
||||
className="ServicePreviewPanel"
|
||||
overflow="auto"
|
||||
height="auto"
|
||||
minHeight={260}
|
||||
marginTop={10}
|
||||
>
|
||||
{ret || initialRet}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
interface ServiceFunctionPreviewProps {
|
||||
appContext?: any;
|
||||
functionName?: string;
|
||||
}
|
||||
|
||||
function ServiceFunctionPreview({ appContext, functionName }: ServiceFunctionPreviewProps) {
|
||||
const [payload, setPayload] = useState({});
|
||||
const [result, setResult] = useState<any>();
|
||||
const [error, setError] = useState('');
|
||||
return (
|
||||
<Panel
|
||||
title="接口测试"
|
||||
extra={
|
||||
<Space>
|
||||
<Button
|
||||
disabled={!appContext}
|
||||
size="small"
|
||||
onClick={() => {
|
||||
if (!appContext) {
|
||||
setError('执行上下文未准备好,请关闭面板重试');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
appContext.services[functionName](payload).then((data: any) => {
|
||||
setResult(data);
|
||||
});
|
||||
} catch (err) {
|
||||
setError('接口调用失败,请检查参数是否正确');
|
||||
logger.error(err);
|
||||
}
|
||||
}}
|
||||
icon={<PlayCircleOutlined />}
|
||||
>
|
||||
预览
|
||||
</Button>
|
||||
</Space>
|
||||
}
|
||||
borderTop="solid"
|
||||
borderTopColor="line.normal"
|
||||
>
|
||||
<Panel title="请求参数" bodyProps={{ px: 'l' }}>
|
||||
<InputCode
|
||||
placeholder={'添加请求参数,对象格式,如 { key: value }'}
|
||||
editable
|
||||
showLineNumbers
|
||||
onChange={(value: string) => {
|
||||
const obj = code2object(value);
|
||||
setPayload(obj);
|
||||
}}
|
||||
/>
|
||||
</Panel>
|
||||
<Panel title="请求响应" bodyProps={{ px: 'l' }}>
|
||||
{error || (result ? <ServicePreview data={result} /> : '点击预览按钮测试接口返回值')}
|
||||
</Panel>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
export default DataSourceView;
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
@@ -1,55 +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
|
||||
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);
|
||||
@@ -57,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);
|
||||
onRemoveStoreVariable={(variableKey) => {
|
||||
workspace.removeStoreVariable(variableKey);
|
||||
}}
|
||||
onDeleteStore={(storeName) => {
|
||||
workspace.removeStoreModule(storeName);
|
||||
onUpdateStoreVariable={(variableKey, code) => {
|
||||
workspace.updateStoreVariable(variableKey, code);
|
||||
}}
|
||||
onSave={(code, node) => {
|
||||
workspace.updateModuleCodeByVariablePath(node.key, code);
|
||||
}}
|
||||
height="100%"
|
||||
{...restProps}
|
||||
/>
|
||||
)}
|
||||
</Panel>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
interface AddStoreFormProps {
|
||||
existNames?: string[];
|
||||
onCancel?: () => void;
|
||||
onSubmit?: (data: { name: string }) => void;
|
||||
}
|
||||
|
||||
function AddStoreForm({ existNames = [], onCancel, onSubmit }: AddStoreFormProps) {
|
||||
return (
|
||||
<Panel title="添加模型" subTitle="模型可以用来组织一组变量">
|
||||
<Form
|
||||
autoComplete="off"
|
||||
onFinish={(values) => {
|
||||
onSubmit && onSubmit(values);
|
||||
}}
|
||||
>
|
||||
<Form.Item
|
||||
label="模型名称"
|
||||
name="name"
|
||||
rules={[
|
||||
{ required: true },
|
||||
{ pattern: /^[a-z]\w+$/, message: '非法的变量标识符' },
|
||||
{
|
||||
validator(_, value) {
|
||||
const found = existNames.includes(value);
|
||||
return found
|
||||
? Promise.reject(new Error('已存在该模型名,请换一个!'))
|
||||
: Promise.resolve();
|
||||
},
|
||||
},
|
||||
]}
|
||||
component="input"
|
||||
componentProps={{
|
||||
placeholder: '请输入模型名称',
|
||||
}}
|
||||
/>
|
||||
<Form.Item label=" ">
|
||||
<Space>
|
||||
<Button onClick={onCancel}>取消</Button>
|
||||
<Button type="primary" htmlType="submit">
|
||||
保存
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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(() => {
|
||||
@@ -16,7 +16,7 @@ export const HistoryTool = observer(() => {
|
||||
workspace.selectSource.clear();
|
||||
}}
|
||||
>
|
||||
<IconFont type="icon-undo" />
|
||||
<UndoOutlined />
|
||||
</ToggleButton>
|
||||
<ToggleButton
|
||||
shape="ghost"
|
||||
@@ -27,7 +27,7 @@ export const HistoryTool = observer(() => {
|
||||
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>
|
||||
);
|
||||
|
||||
@@ -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(() => {
|
||||
@@ -250,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"
|
||||
@@ -281,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>
|
||||
);
|
||||
|
||||
@@ -3,6 +3,18 @@
|
||||
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.2](https://github.com/netease/tango/compare/@music163/tango-helpers@1.0.0-alpha.1...@music163/tango-helpers@1.0.0-alpha.2) (2024-01-16)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 优化变量树实现 ([#90](https://github.com/netease/tango/issues/90)) ([62d403f](https://github.com/netease/tango/commit/62d403f80a5ad08c216bc0c035ffc12c2cf329d2))
|
||||
|
||||
# [1.0.0-alpha.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.2",
|
||||
"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,56 @@ 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;
|
||||
/**
|
||||
* 是否禁用
|
||||
*/
|
||||
disabled?: boolean;
|
||||
/**
|
||||
* 子结点
|
||||
*/
|
||||
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,34 @@
|
||||
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-sandbox@1.0.0-alpha.5...@music163/tango-sandbox@1.0.0-alpha.6) (2024-01-16)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-sandbox
|
||||
|
||||
# [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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-sandbox",
|
||||
"version": "0.1.14",
|
||||
"version": "1.0.0-alpha.6",
|
||||
"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.9",
|
||||
"@music163/tango-helpers": "^0.1.8",
|
||||
"@music163/tango-core": "^1.0.0-alpha.6",
|
||||
"@music163/tango-helpers": "^1.0.0-alpha.2",
|
||||
"crypto-js": "^4.1.1",
|
||||
"lodash.isequal": "4.5.0",
|
||||
"react-frame-component": "^5.2.4"
|
||||
|
||||
@@ -3,6 +3,37 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [1.0.0-alpha.6](https://github.com/netease/tango/compare/@music163/tango-setting-form@1.0.0-alpha.5...@music163/tango-setting-form@1.0.0-alpha.6) (2024-01-16)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-setting-form
|
||||
|
||||
# [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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-setting-form",
|
||||
"version": "0.2.14",
|
||||
"version": "1.0.0-alpha.6",
|
||||
"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.9",
|
||||
"@music163/tango-helpers": "^0.1.8",
|
||||
"@music163/tango-ui": "^0.1.12",
|
||||
"@music163/tango-core": "^1.0.0-alpha.6",
|
||||
"@music163/tango-helpers": "^1.0.0-alpha.2",
|
||||
"@music163/tango-ui": "^1.0.0-alpha.4",
|
||||
"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}
|
||||
|
||||
@@ -83,7 +83,15 @@ export interface SettingFormProps {
|
||||
*/
|
||||
prototype?: ComponentPrototypeType;
|
||||
/**
|
||||
* 选项分组信息
|
||||
* 是否显示搜索框
|
||||
*/
|
||||
showSearch?: boolean;
|
||||
/**
|
||||
* 是否显示分组导航
|
||||
*/
|
||||
showGroups?: boolean;
|
||||
/**
|
||||
* 选项分组信息,用于对表单项进行分组展示,如未提供,则使用内置的分组信息
|
||||
*/
|
||||
groupOptions?: IFormTabsGroupOption[];
|
||||
/**
|
||||
@@ -98,6 +106,8 @@ export function SettingForm({
|
||||
model: modelProp,
|
||||
defaultValue,
|
||||
onChange = noop,
|
||||
showSearch = true,
|
||||
showGroups = true,
|
||||
groupOptions = internalGroups,
|
||||
}: SettingFormProps) {
|
||||
const [keyword, setKeyword] = useState('');
|
||||
@@ -107,10 +117,8 @@ export function SettingForm({
|
||||
const componentPropGroups = useMemo(() => {
|
||||
return normalizeComponentProps(componentProps);
|
||||
}, [componentProps]);
|
||||
|
||||
const filterProps = useMemo(() => {
|
||||
if (!keyword) {
|
||||
return componentPropGroups[tabKey] || [];
|
||||
}
|
||||
const computedProps = mapTreeData(componentProps, (node) => {
|
||||
const { getProp, ...rest } = node;
|
||||
return {
|
||||
@@ -118,8 +126,19 @@ export function SettingForm({
|
||||
...getProp?.(model),
|
||||
};
|
||||
});
|
||||
|
||||
// 没有进行搜索的情况
|
||||
if (!keyword) {
|
||||
if (showGroups) {
|
||||
return componentPropGroups[tabKey] || [];
|
||||
} else {
|
||||
return computedProps;
|
||||
}
|
||||
}
|
||||
|
||||
// 用户进行搜索的情况
|
||||
return filterComponentProps(computedProps, keyword);
|
||||
}, [keyword, componentProps, tabKey, model]);
|
||||
}, [keyword, componentProps, tabKey, model, showGroups, componentPropGroups]);
|
||||
|
||||
const renderProps = useCallback(
|
||||
(props: ComponentPropType[]) =>
|
||||
@@ -129,15 +148,7 @@ export function SettingForm({
|
||||
...rest,
|
||||
...getProp?.(model),
|
||||
};
|
||||
const FormChild = isValidNestProps(childProto.props)
|
||||
? SettingFormObject
|
||||
: childProto.setter
|
||||
? SettingFormItem
|
||||
: null;
|
||||
|
||||
if (!FormChild) {
|
||||
return null;
|
||||
}
|
||||
const FormChild = isValidNestProps(childProto.props) ? SettingFormObject : SettingFormItem;
|
||||
return <FormChild key={item.name} {...childProto} />;
|
||||
}),
|
||||
[model],
|
||||
@@ -155,15 +166,17 @@ export function SettingForm({
|
||||
<FormModelProvider value={model}>
|
||||
<Box className="SettingForm" mb="xl" css={formStyle}>
|
||||
<Box className="SettingFormHeader" position="sticky" top="0" bg="white" zIndex="2">
|
||||
<Box className="SettingFormSearch" px="l" py="m">
|
||||
<Search
|
||||
placeholder="搜索属性"
|
||||
onChange={(val) => {
|
||||
setKeyword(val?.trim());
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
{!keyword && (
|
||||
{showSearch && (
|
||||
<Box className="SettingFormSearch" px="l" py="m">
|
||||
<Search
|
||||
placeholder="搜索属性"
|
||||
onChange={(val) => {
|
||||
setKeyword(val?.trim());
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
{!keyword && showGroups && (
|
||||
<Box className="SettingFormNav">
|
||||
<Tabs activeKey={tabKey} onChange={setTabKey}>
|
||||
{groupOptions.map((group) => (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { register } from './form-item';
|
||||
import { ExpressionSetter } from './setter';
|
||||
import { ExpressionSetter, TextSetter } from './setter';
|
||||
|
||||
// 预注册基础 Setter
|
||||
register({
|
||||
@@ -7,6 +7,11 @@ register({
|
||||
component: ExpressionSetter,
|
||||
});
|
||||
|
||||
register({
|
||||
name: 'textSetter',
|
||||
component: TextSetter,
|
||||
});
|
||||
|
||||
export * from './form';
|
||||
export * from './form-item';
|
||||
export * from './form-object';
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import React from 'react';
|
||||
import { Input } from 'antd';
|
||||
import { InputCode } from '@music163/tango-ui';
|
||||
import { FormItemComponentProps } from './form-item';
|
||||
|
||||
// TODO: 内置基本的 ExpressionSetter,默认全部 fallback 到此 setter
|
||||
export function ExpressionSetter(props: FormItemComponentProps<string>) {
|
||||
return <div>exp setter</div>;
|
||||
export function ExpressionSetter({ value, onChange, ...rest }: FormItemComponentProps<string>) {
|
||||
return <InputCode onChange={(val) => onChange?.(val)} value={value || ''} {...rest} />;
|
||||
}
|
||||
|
||||
export function TextSetter({ onChange, ...rest }: FormItemComponentProps<string>) {
|
||||
return <Input onChange={(e) => onChange?.(e.target.value)} {...rest} />;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
# Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## 1.0.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))
|
||||
@@ -0,0 +1,3 @@
|
||||
# Tango Config Spec
|
||||
|
||||
This is the specification for the Tango Config format.
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "tango-spec",
|
||||
"version": "1.0.2",
|
||||
"description": "tango config spec",
|
||||
"keywords": [
|
||||
"json",
|
||||
"schema"
|
||||
],
|
||||
"author": "wwsun <ww.sww@outlook.com>",
|
||||
"homepage": "https://github.com/NetEase/tango#readme",
|
||||
"license": "MIT",
|
||||
"main": "index.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/NetEase/tango.git"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "node ./__tests__/spec.test.js"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/NetEase/tango/issues"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": "https://unpkg.com/tango-spec/tango-config.json",
|
||||
"title": "JSON Schema of tango.config.json",
|
||||
"description": "tango.config.json is a configuration file for NetEase Tango apps",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"packages": {
|
||||
"type": "object",
|
||||
"patternProperties": {
|
||||
"^(?:@[a-z0-9-~][a-z0-9-._~]*/)?[a-z0-9-~][a-z0-9-._~]*$": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "package description"
|
||||
},
|
||||
"version": {
|
||||
"type": "string",
|
||||
"description": "package version"
|
||||
},
|
||||
"library": {
|
||||
"type": "string",
|
||||
"description": "package library"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "package type",
|
||||
"enum": ["dependency", "bizDependency", "baseDependency"]
|
||||
},
|
||||
"resources": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "package resources"
|
||||
},
|
||||
"designerResources": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "package designer resources"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": ["version", "library"]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"dataSource": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ox": {
|
||||
"type": "object",
|
||||
"description": "music ox data provider",
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string",
|
||||
"description": "dataSource provider name"
|
||||
},
|
||||
"value": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"app": {
|
||||
"type": "string",
|
||||
"description": "app name"
|
||||
},
|
||||
"branch": {
|
||||
"type": "string",
|
||||
"description": "branch name"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": ["app", "branch"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"proxy": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string",
|
||||
"description": "the path of proxy rule"
|
||||
},
|
||||
"value": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"target": {
|
||||
"type": "string",
|
||||
"description": "the target of proxy rule"
|
||||
},
|
||||
"changeOrigin": {
|
||||
"type": "boolean",
|
||||
"description": "whether change origin of proxy rule"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": ["target"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"externalResources": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": true,
|
||||
"required": ["packages"]
|
||||
}
|
||||
@@ -3,6 +3,22 @@
|
||||
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.4](https://github.com/netease/tango/compare/@music163/tango-ui@1.0.0-alpha.3...@music163/tango-ui@1.0.0-alpha.4) (2024-01-16)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-ui
|
||||
|
||||
# [1.0.0-alpha.3](https://github.com/netease/tango/compare/@music163/tango-ui@1.0.0-alpha.2...@music163/tango-ui@1.0.0-alpha.3) (2023-12-29)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-ui
|
||||
|
||||
# [1.0.0-alpha.1](https://github.com/netease/tango/compare/@music163/tango-ui@0.1.12...@music163/tango-ui@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))
|
||||
- disable search keymap of InputCode ([#72](https://github.com/netease/tango/issues/72)) ([9db6358](https://github.com/netease/tango/commit/9db635858876ee0b9612e4b2ccd996280fc35d3b))
|
||||
- render form item without setter & enhance tools ([#76](https://github.com/netease/tango/issues/76)) ([1acfa68](https://github.com/netease/tango/commit/1acfa6864b1faad1a441facc426d8d94b6b090b5))
|
||||
|
||||
## [0.1.12](https://github.com/netease/tango/compare/@music163/tango-ui@0.1.11...@music163/tango-ui@0.1.12) (2023-11-28)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-ui",
|
||||
"version": "0.1.12",
|
||||
"version": "1.0.0-alpha.4",
|
||||
"description": "ui widgets of tango",
|
||||
"keywords": [
|
||||
"react",
|
||||
@@ -34,14 +34,13 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^4.8.0",
|
||||
"@codemirror/autocomplete": "^6.10.2",
|
||||
"@codemirror/autocomplete": "^6.11.1",
|
||||
"@codemirror/lang-javascript": "^6.2.1",
|
||||
"@codemirror/lint": "^6.4.2",
|
||||
"@codemirror/search": "^6.5.4",
|
||||
"@music163/tango-helpers": "^0.1.8",
|
||||
"@uiw/react-codemirror": "^4.21.20",
|
||||
"@codemirror/search": "^6.5.5",
|
||||
"@music163/tango-helpers": "^1.0.0-alpha.2",
|
||||
"@uiw/react-codemirror": "^4.21.21",
|
||||
"antd": "^4.24.2",
|
||||
"antd-easy-form": "^1.1.3",
|
||||
"coral-system": "^1.0.5",
|
||||
"eslint-linter-browserify": "^8.51.0",
|
||||
"react-json-view": "^1.21.3",
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import { createIcon } from './create-icon';
|
||||
|
||||
const CodeOutlinedSvg = () => (
|
||||
<svg
|
||||
viewBox="0 0 32 32"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em"
|
||||
height="1em"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="m31 16-7 7-1.41-1.41 5.58-5.59-5.58-5.59 1.41-1.41z" />
|
||||
<path d="m1 16 7-7 1.41 1.41-5.58 5.59 5.58 5.59-1.41 1.41z" />
|
||||
<path
|
||||
d="m5.91 15h20.17v2h-20.17z"
|
||||
transform="matrix(.25881905 -.96592583 .96592583 .25881905 -3.6 27.31)"
|
||||
/>
|
||||
<path d="m0 0h32v32h-32z" fill="none" transform="matrix(0 -1 1 0 0 32)" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const CodeOutlined = createIcon(CodeOutlinedSvg, 'CodeOutlined');
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user