Compare commits

...

5 Commits

Author SHA1 Message Date
Wells
73aded3672 chore(release): publish
- @music163/tango-context@1.0.0-alpha.5
 - @music163/tango-core@1.0.0-alpha.5
 - @music163/tango-designer@1.0.0-alpha.6
 - @music163/tango-helpers@1.0.0-alpha.1
 - @music163/tango-sandbox@1.0.0-alpha.5
 - @music163/tango-setting-form@1.0.0-alpha.5
 - @music163/tango-ui@1.0.0-alpha.3
2023-12-29 17:41:58 +08:00
Wells
8c07821d93 fix: refactor VariableTree & Workspace (#83)
* fix: refactor VariableTree

* fix: update

* fix: update

* fix: update

* fix: update

* fix: refactor IVariableTreeNode

* fix: update

* fix: refactor workspace

---------

Co-authored-by: wwsun <ww.sww@outlook.com>
2023-12-29 17:30:11 +08:00
wwsun
765ea07cd5 fix: check if store value is valid 2023-12-26 15:43:20 +08:00
wwsun
a27c241d9c chore(release): publish
- @music163/tango-context@1.0.0-alpha.4
 - @music163/tango-core@1.0.0-alpha.4
 - @music163/tango-designer@1.0.0-alpha.5
 - @music163/tango-sandbox@1.0.0-alpha.4
 - @music163/tango-setting-form@1.0.0-alpha.4
2023-12-26 14:38:54 +08:00
wwsun
4d6b67eecd fix: list overflow in expSetter 2023-12-26 14:36:13 +08:00
46 changed files with 1582 additions and 1412 deletions

View File

@@ -229,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');
`;

View File

@@ -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" />;
}

View File

@@ -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>
);
}

View File

@@ -3,6 +3,16 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [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

View File

@@ -1,6 +1,6 @@
{
"name": "@music163/tango-context",
"version": "1.0.0-alpha.3",
"version": "1.0.0-alpha.5",
"description": "react context for tango-apps",
"keywords": [
"react",
@@ -31,8 +31,8 @@
"react": ">= 16.8"
},
"dependencies": {
"@music163/tango-core": "^1.0.0-alpha.3",
"@music163/tango-helpers": "^1.0.0-alpha.0",
"@music163/tango-core": "^1.0.0-alpha.5",
"@music163/tango-helpers": "^1.0.0-alpha.1",
"mobx-react-lite": "4.0.5"
},
"publishConfig": {

View File

@@ -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,

View File

@@ -3,6 +3,19 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.0.0-alpha.5](https://github.com/netease/tango/compare/@music163/tango-core@1.0.0-alpha.4...@music163/tango-core@1.0.0-alpha.5) (2023-12-29)
### Bug Fixes
- check if store value is valid ([765ea07](https://github.com/netease/tango/commit/765ea07cd5be6528e3b326e4ecb163647774d9e5))
- refactor VariableTree & Workspace ([#83](https://github.com/netease/tango/issues/83)) ([8c07821](https://github.com/netease/tango/commit/8c07821d93cea4dfc43f81ca948b845176821184))
# [1.0.0-alpha.4](https://github.com/netease/tango/compare/@music163/tango-core@1.0.0-alpha.3...@music163/tango-core@1.0.0-alpha.4) (2023-12-26)
### Bug Fixes
- list overflow in expSetter ([4d6b67e](https://github.com/netease/tango/commit/4d6b67eecd02b31f01d1bc896c6eeb83f6b34b35))
# [1.0.0-alpha.3](https://github.com/netease/tango/compare/@music163/tango-core@1.0.0-alpha.2...@music163/tango-core@1.0.0-alpha.3) (2023-12-25)
### Bug Fixes

View File

@@ -1,6 +1,6 @@
{
"name": "@music163/tango-core",
"version": "1.0.0-alpha.3",
"version": "1.0.0-alpha.5",
"description": "tango core",
"author": "wwsun <ww.sun@outlook.com>",
"homepage": "",
@@ -28,7 +28,7 @@
"@babel/parser": "^7.23.5",
"@babel/traverse": "^7.23.5",
"@babel/types": "^7.23.5",
"@music163/tango-helpers": "^1.0.0-alpha.0",
"@music163/tango-helpers": "^1.0.0-alpha.1",
"@types/babel__generator": "^7.6.7",
"@types/babel__traverse": "^7.20.4",
"mobx": "6.12.0",

View File

@@ -44,6 +44,11 @@ export function isValidCode(code: string) {
/**
* 检测代码是否是合法的表达式代码
* 表达式是一组代码的集合,它返回一个值;每一个合法的表达式都能计算成某个值
* 只要你输入这段代码,可以形成一个值,就算是表达式
* @example x = 1; // 1
* @example 1=1
* @example 'hello'
* @example { foo: 'bar' }
* @param code
* @returns
*/

View File

@@ -29,7 +29,6 @@ import type {
IStorePropertyData,
ITangoViewNodeData,
IImportDeclarationPayload,
IServiceFunctionPayload,
InsertChildPositionType,
IImportSpecifierData,
IExportSpecifierData,
@@ -998,7 +997,7 @@ export function deleteServiceConfigFromServiceFile(ast: t.File, serviceFunctionN
* @param payload
* @returns
*/
export function serviceConfig2Node(payload: IServiceFunctionPayload) {
export function serviceConfig2Node(payload: object) {
return object2node(payload, (value, key) => {
if (key === 'formatter' && value) {
return code2expression(value);
@@ -1007,10 +1006,7 @@ export function serviceConfig2Node(payload: IServiceFunctionPayload) {
});
}
export function updateServiceConfigToServiceFile(
ast: t.File,
config: { [key: string]: IServiceFunctionPayload },
) {
export function updateServiceConfigToServiceFile(ast: t.File, config: object) {
traverse(ast, {
CallExpression(path) {
const calleeName = keyNode2value(path.node.callee) as string;

View File

@@ -8,7 +8,6 @@ import {
InsertChildPositionType,
ITangoConfigPackages,
IPageConfigData,
IServiceFunctionPayload,
IImportSpecifierSourceData,
IImportSpecifierData,
} from '../types';
@@ -204,20 +203,23 @@ export interface IWorkspace {
// ----------------- 服务函数文件操作 -----------------
getServiceFunction?: (serviceKey: string) => object;
getServiceFunction?: (serviceKey: string) => {
name: string;
moduleName: string;
config: object;
};
listServiceFunctions?: () => Record<string, object>;
removeServiceFunction?: (serviceName: string, modName?: string) => void;
addServiceFunction?: (
payload: IServiceFunctionPayload | IServiceFunctionPayload[],
modName?: string,
) => void;
updateServiceFunction?: (payload: IServiceFunctionPayload, modName?: string) => void;
updateServiceBaseConfig?: (IServiceFunctionPayload: object, modName?: string) => void;
removeServiceFunction?: (serviceKey: string) => void;
addServiceFunction?: (serviceName: string, config: object, modName?: string) => void;
addServiceFunctions?: (configs: object, modName?: string) => void;
updateServiceFunction?: (serviceName: string, payload: object, modName?: string) => void;
updateServiceBaseConfig?: (config: object, modName?: string) => void;
// ----------------- 状态管理文件操作 -----------------
addStoreState?: (storeName: string, stateName: string, initValue: string) => void;
removeStoreState?: (storeName: string, stateName: string) => void;
removeStoreModule?: (storeName: string) => void;
removeStoreVariable?: (variablePath: string) => void;
updateStoreVariable?: (variablePath: string, code: string) => void;
// ----------------- 视图文件操作 -----------------
@@ -256,10 +258,6 @@ export interface IWorkspace {
removeBizComp?: (name: string) => void;
// ----------------- 其他操作 -----------------
updateModuleCodeByVariablePath?: (variablePath: string, code: string) => void;
// ----------------- getter -----------------
get activeViewModule(): IViewFile;

View File

@@ -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';
@@ -67,25 +67,23 @@ export class TangoServiceModule extends TangoModule {
}
}
addServiceFunction(payload: IServiceFunctionPayload) {
const { name, ...rest } = payload;
this.ast = updateServiceConfigToServiceFile(this.ast, { [name]: clone(rest, false) });
addServiceFunction(name: string, config: object) {
this.ast = updateServiceConfigToServiceFile(this.ast, { [name]: clone(config, false) });
return this;
}
addServiceFunctions(payloads: IServiceFunctionPayload[]) {
const config = payloads.reduce((acc, cur) => {
const { name, ...rest } = cur;
acc[name] = clone(rest, false);
return acc;
}, {});
this.ast = updateServiceConfigToServiceFile(this.ast, config);
/**
* 批量添加服务函数
* @param configs { [name: string]: object }
* @returns
*/
addServiceFunctions(configs: object) {
this.ast = updateServiceConfigToServiceFile(this.ast, configs);
return this;
}
updateServiceFunction(payload: IServiceFunctionPayload) {
const { name, ...rest } = payload;
this.ast = updateServiceConfigToServiceFile(this.ast, { [name]: clone(rest, false) });
updateServiceFunction(name: string, payload: object) {
this.ast = updateServiceConfigToServiceFile(this.ast, { [name]: clone(payload, false) });
return this;
}

View File

@@ -3,8 +3,11 @@ import { JSXElement } from '@babel/types';
import {
ComponentPrototypeType,
hasFileExtension,
isStoreVariablePath,
isString,
logger,
parseServiceVariablePath,
parseStoreVariablePath,
uniq,
} from '@music163/tango-helpers';
import {
@@ -22,13 +25,7 @@ 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';
@@ -200,7 +197,7 @@ export class Workspace extends EventTarget implements IWorkspace {
}
get localComps(): string[] {
return Object.keys(this.componentsEntryModule.exportList);
return Object.keys(this.componentsEntryModule?.exportList || {});
}
constructor(options?: IWorkspaceOptions) {
@@ -599,15 +596,23 @@ export class Workspace extends EventTarget implements IWorkspace {
}
/**
* 根据变量路径更新模块内容
* TODO: 改名,不直观
* 根据变量路径删除状态变量
* @param variablePath
*/
removeStoreVariable(variablePath: string) {
const { storeName, variableName } = parseStoreVariablePath(variablePath);
this.removeStoreState(storeName, variableName);
}
/**
* 根据变量路径更新状态变量的值
* @param variablePath 变量路径
* @param code 变量代码
*/
updateModuleCodeByVariablePath(variablePath: string, code: string) {
if (/^stores\.\w+\.\w+$/.test(variablePath)) {
const [, storeName, stateName] = variablePath.split('.');
this.storeModules[storeName]?.updateState(stateName, code).update();
updateStoreVariable(variablePath: string, code: string) {
if (isStoreVariablePath(variablePath)) {
const { storeName, variableName } = parseStoreVariablePath(variablePath);
this.storeModules[storeName]?.updateState(variableName, code).update();
}
}
@@ -618,7 +623,7 @@ export class Workspace extends EventTarget implements IWorkspace {
* @returns
*/
getServiceFunction(serviceKey: string) {
const { name, moduleName } = this.parseServiceKey(serviceKey);
const { name, moduleName } = parseServiceVariablePath(serviceKey);
if (!name) {
return;
}
@@ -648,29 +653,27 @@ export class Workspace extends EventTarget implements IWorkspace {
/**
* 更新服务函数
*/
updateServiceFunction(payload: IServiceFunctionPayload, moduleName = 'index') {
this.serviceModules[moduleName].updateServiceFunction(payload).update();
updateServiceFunction(serviceName: string, payload: object, moduleName = 'index') {
this.serviceModules[moduleName].updateServiceFunction(serviceName, payload).update();
}
/**
* 新增服务函数,支持批量添加
*/
addServiceFunction(
payload: IServiceFunctionPayload | IServiceFunctionPayload[],
moduleName = 'index',
) {
if (Array.isArray(payload)) {
this.serviceModules[moduleName]?.addServiceFunctions(payload).update();
} else {
this.serviceModules[moduleName]?.addServiceFunction(payload).update();
}
addServiceFunction(name: string, config: object, moduleName = 'index') {
this.serviceModules[moduleName]?.addServiceFunction(name, config).update();
}
addServiceFunctions(configs: object, modName = 'index') {
this.serviceModules[modName]?.addServiceFunctions(configs).update();
}
/**
* 删除服务函数
* @param name
*/
removeServiceFunction(name: string, moduleName = 'index') {
removeServiceFunction(serviceKey: string) {
const { moduleName, name } = parseServiceVariablePath(serviceKey);
this.serviceModules[moduleName]?.deleteServiceFunction(name).update();
}
@@ -1209,40 +1212,4 @@ export class Workspace extends EventTarget implements IWorkspace {
logger.error('copyFiles failed, source: %s, target: %s', sourceFilePath, targetFilePath);
}
}
/**
* 解析 serviceKey
* @param serviceKey
* @returns
*
* @example services.list => { moduleName: 'index', name: 'list' }
* @example services.sub.list => { moduleName: 'sub', name: 'list' }
* @example foo => undefined
*/
private parseServiceKey(serviceKey: string) {
const parts = serviceKey.split('.');
if (parts[0] !== 'services') {
return {};
}
let moduleName = 'index';
let name = '';
switch (parts.length) {
case 2: {
name = parts[1];
break;
}
case 3: {
moduleName = parts[1];
name = parts[2];
break;
}
default:
break;
}
return {
moduleName,
name,
};
}
}

View File

@@ -145,17 +145,6 @@ export interface IImportDeclarationPayload {
sourcePath: string;
}
/**
* 服务函数参数类型
*/
export interface IServiceFunctionPayload {
[key: string]: any;
/**
* 服务函数名
*/
name: string;
}
/**
* Store 属性类型
*/

View File

@@ -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();

View File

@@ -131,7 +131,7 @@ describe('string helpers', () => {
);
expect(getRelativePath('/src/pages/index.js', '/src/components')).toEqual('../components');
expect(getRelativePath('/src/pages/index.js', '/src/components/input.js')).toEqual(
'../components/index.js',
'../components/input.js',
);
});

View File

@@ -3,6 +3,19 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.0.0-alpha.6](https://github.com/netease/tango/compare/@music163/tango-designer@1.0.0-alpha.5...@music163/tango-designer@1.0.0-alpha.6) (2023-12-29)
### Bug Fixes
- check if store value is valid ([765ea07](https://github.com/netease/tango/commit/765ea07cd5be6528e3b326e4ecb163647774d9e5))
- refactor VariableTree & Workspace ([#83](https://github.com/netease/tango/issues/83)) ([8c07821](https://github.com/netease/tango/commit/8c07821d93cea4dfc43f81ca948b845176821184))
# [1.0.0-alpha.5](https://github.com/netease/tango/compare/@music163/tango-designer@1.0.0-alpha.4...@music163/tango-designer@1.0.0-alpha.5) (2023-12-26)
### Bug Fixes
- list overflow in expSetter ([4d6b67e](https://github.com/netease/tango/commit/4d6b67eecd02b31f01d1bc896c6eeb83f6b34b35))
# [1.0.0-alpha.4](https://github.com/netease/tango/compare/@music163/tango-designer@1.0.0-alpha.3...@music163/tango-designer@1.0.0-alpha.4) (2023-12-25)
**Note:** Version bump only for package @music163/tango-designer

View File

@@ -1,6 +1,6 @@
{
"name": "@music163/tango-designer",
"version": "1.0.0-alpha.4",
"version": "1.0.0-alpha.6",
"description": "lowcode designer",
"keywords": [
"react"
@@ -33,12 +33,12 @@
"dependencies": {
"@ant-design/icons": "^4.8.0",
"@music163/request": "^0.1.2",
"@music163/tango-context": "^1.0.0-alpha.3",
"@music163/tango-core": "^1.0.0-alpha.3",
"@music163/tango-helpers": "^1.0.0-alpha.0",
"@music163/tango-sandbox": "^1.0.0-alpha.3",
"@music163/tango-setting-form": "^1.0.0-alpha.3",
"@music163/tango-ui": "^1.0.0-alpha.2",
"@music163/tango-context": "^1.0.0-alpha.5",
"@music163/tango-core": "^1.0.0-alpha.5",
"@music163/tango-helpers": "^1.0.0-alpha.1",
"@music163/tango-sandbox": "^1.0.0-alpha.5",
"@music163/tango-setting-form": "^1.0.0-alpha.5",
"@music163/tango-ui": "^1.0.0-alpha.3",
"antd": "^4.24.2",
"cash-dom": "^8.1.2",
"classnames": "^2.3.2",

View File

@@ -1,3 +1,4 @@
export * from './drag-box';
export * from './input-kv';
export * from './variable-tree';
export * from './variable-tree-modal';

View File

@@ -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>
);
}

View File

@@ -1,643 +0,0 @@
import React, { useMemo, useState } from 'react';
import {
Tree,
Button,
Form,
Input,
Space,
ModalProps,
Modal,
Radio,
Empty,
Popconfirm,
Tooltip,
Alert,
} 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%">
<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 ? (
<Box width="60%" position="sticky" top="0">
{node.help && (
<Alert
type="info"
message={`使用说明:${node.help}`}
closable
style={{ marginBottom: 12 }}
/>
)}
<Panel
shape="solid"
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
}
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>
</Box>
) : (
<Panel shape="solid" 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 <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={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 image={Empty.PRESENTED_IMAGE_SIMPLE} 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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -0,0 +1,360 @@
import React, { useCallback, useMemo, useState } from 'react';
import {
IVariableTreeNode,
filterTreeData,
isServiceVariablePath,
isStoreVariablePath,
noop,
parseServiceVariablePath,
} from '@music163/tango-helpers';
import { css, Box, Text } from 'coral-system';
import { Button, Popconfirm, Tooltip, Tree } from 'antd';
import {
CopyOutlined,
DeleteOutlined,
EyeOutlined,
FunctionOutlined,
PlusOutlined,
} from '@ant-design/icons';
import { CopyClipboard, Panel, Search } from '@music163/tango-ui';
import { AddStoreForm, AddStoreVariableForm } from './add-store';
import { AddServiceForm } from './add-service';
import {
NodeCommonDetail,
ValueDefine,
ValueDefineProps,
ValueDetail,
ValueDetailProps,
} from './value-detail';
import { ValuePreview } from './value-preview';
import { ServicePreview } from './service-preview';
const varTreeStyle = css`
overflow: auto;
position: relative;
.ant-tree {
font-family: Consolas, Menlo, Courier, monospace;
}
.ant-tree-node-content-wrapper.ant-tree-node-content-wrapper-normal {
width: calc(100% - 50px);
}
.ant-tree .ant-tree-treenode {
padding: 0;
}
.ant-tree.ant-tree-directory .ant-tree-treenode::before {
bottom: 0;
}
.ant-tree-indent-unit {
width: 12px;
}
`;
type SelectNodeCallback = (data: IVariableTreeNode) => void;
export interface VariableTreeProps {
defaultValueDetailMode?: ValueDetailProps['defaultMode'];
dataSource: IVariableTreeNode[];
appContext?: object;
serviceModules?: any[];
getPreviewValue?: (node: IVariableTreeNode) => unknown;
getServiceData?: (serviceKey: string) => object;
getServiceNames?: (moduleName: string) => string[];
getStoreNames?: () => string[];
onSelect?: SelectNodeCallback;
onAddStoreVariable?: (storeName: string, data: any) => void;
onAddStore?: (newStoreName: string) => void;
onAddService?: (data: object) => void;
onRemoveVariable?: (variableKey: string) => void;
onUpdateVariable?: ValueDefineProps['onSave'];
onUpdateService?: (data: object) => void;
onCopy?: (data: IVariableTreeNode) => void;
onView?: SelectNodeCallback;
height?: number | string;
showViewButton?: boolean;
}
export function VariableTree({
dataSource = [],
serviceModules = [],
appContext = {},
defaultValueDetailMode,
onSelect = noop,
onAddStoreVariable = noop,
onAddStore = noop,
onAddService = noop,
onRemoveVariable = noop,
onUpdateVariable = noop,
onUpdateService = noop,
onCopy = noop,
onView = noop,
getServiceData,
getServiceNames,
getStoreNames,
getPreviewValue = noop,
showViewButton,
...rest
}: VariableTreeProps) {
const [keyword, setKeyword] = useState('');
const [activeNode, setActiveNode] = useState<IVariableTreeNode>();
const [mode, setMode] = useState<
'detail' | 'storeVariableDetail' | 'serviceDetail' | 'addVariable' | 'addStore' | 'addService'
>();
const clear = useCallback(() => {
setActiveNode(null);
setMode(null);
}, []);
const selectNode = useCallback((node: IVariableTreeNode, callback?: SelectNodeCallback) => {
if (isStoreVariablePath(node.key)) {
setMode('storeVariableDetail');
} else if (isServiceVariablePath(node.key)) {
setMode('serviceDetail');
} else {
setMode('detail');
}
setActiveNode(node);
callback?.(node);
}, []);
const treeData = useMemo(() => {
const pattern = new RegExp(keyword, 'ig');
return keyword
? filterTreeData(dataSource, (leaf) => pattern.test(leaf.title), 'children', true)
: dataSource;
}, [keyword, dataSource]);
return (
<Box display="flex" columnGap="l" className="VariableTree" css={varTreeStyle} {...rest}>
<Box className="VariableList" width="40%">
<Box mb="m" position="sticky" top="0" bg="white" zIndex={2}>
<Search placeholder="请输入变量名" onChange={(val) => setKeyword(val?.trim())} />
</Box>
<Tree
blockNode
showIcon={false}
defaultExpandAll
treeData={treeData}
onSelect={(keys, detail) => {
selectNode(detail.node, onSelect);
}}
titleRender={(node) => {
const isLeaf = !node.children;
if (isLeaf) {
const showView = node.showViewButton ?? showViewButton;
return (
<Box display="flex" justifyContent="space-between" alignItems="center">
<Text flex="1" truncated>
{node.title}
{node.type === 'function' && (
<Text color="text3" ml="m">
<FunctionOutlined />
</Text>
)}
</Text>
<Box flex="0 0 72px" textAlign="right">
{node.showRemoveButton && (
<Popconfirm
title={`确认删除吗 ${node.title}?该操作会导致引用此模型的代码报错,请谨慎操作!`}
onConfirm={() => {
onRemoveVariable(node.key);
}}
>
<Button
type="text"
size="small"
icon={<DeleteOutlined />}
onClick={(e) => {
e.stopPropagation();
}}
/>
</Popconfirm>
)}
<CopyClipboard text={`tango.${node.key.replaceAll('.', '?.')}`}>
{({ copied, onClick }) => {
const label = copied ? '已复制' : '复制变量路径';
return (
<Tooltip title={label}>
<Button
type="text"
size="small"
icon={<CopyOutlined />}
onClick={(e) => {
e.stopPropagation();
onClick();
onCopy(node);
}}
/>
</Tooltip>
);
}}
</CopyClipboard>
{showView && (
<Tooltip title="查看变量详情">
<Button
type="text"
size="small"
icon={<EyeOutlined />}
onClick={(e) => {
e.stopPropagation();
selectNode(node, onView);
}}
/>
</Tooltip>
)}
</Box>
</Box>
);
}
return (
<Box display="flex" alignItems="center" justifyContent="space-between">
<Text mr="m">{node.title}</Text>
<Box>
{/^\$?stores$/.test(node.key) && (
<Tooltip title="新建数据模型">
<Button
type="text"
size="small"
icon={<PlusOutlined />}
onClick={(e) => {
e.stopPropagation();
setMode('addStore');
}}
/>
</Tooltip>
)}
{node.showAddButton && /^stores\.[a-zA-Z0-9]+$/.test(node.key) && (
<Tooltip title={`${node.title} 中添加变量`}>
<Button
type="text"
size="small"
icon={<PlusOutlined />}
onClick={(e) => {
e.stopPropagation();
setActiveNode(node);
setMode('addVariable');
}}
/>
</Tooltip>
)}
{/^services(\.[a-zA-Z0-9]+)?$/.test(node.key) && (
<Tooltip title={`${node.title} 中添加服务函数`}>
<Button
type="text"
size="small"
icon={<PlusOutlined />}
onClick={(e) => {
e.stopPropagation();
setActiveNode(node);
setMode('addService');
}}
/>
</Tooltip>
)}
</Box>
</Box>
);
}}
/>
</Box>
<Box className="VariableDetail" flex="1" position="sticky" top="0" overflow="auto">
{mode === 'detail' && <NodeCommonDetail data={activeNode} />}
{mode === 'storeVariableDetail' && (
<ValueDetail key={activeNode.key} defaultMode={defaultValueDetailMode}>
{(previewMode) =>
previewMode === 'runtime' ? (
<ValuePreview
value={getPreviewValue(activeNode)}
onCopy={(valuePath) => {
return ['tango', activeNode.key.replaceAll('.', '?.'), valuePath].join('.');
}}
/>
) : (
<ValueDefine data={activeNode} onSave={onUpdateVariable} />
)
}
</ValueDetail>
)}
{mode === 'serviceDetail' && (
<>
<Panel shape="solid" title="服务函数配置">
<AddServiceForm
key={activeNode.key}
serviceModules={serviceModules}
serviceNames={(function () {
const { moduleName } = parseServiceVariablePath(activeNode.key);
return getServiceNames?.(moduleName) || [];
})()}
initialValues={{
...getServiceData?.(activeNode.key),
}}
onCancel={clear}
onSubmit={(values) => {
onUpdateService(values);
clear();
}}
/>
</Panel>
<Panel shape="solid" title="服务函数预览" mt="l">
<ServicePreview
key={activeNode.key}
appContext={appContext}
functionKey={activeNode.key}
/>
</Panel>
</>
)}
{mode === 'addVariable' && (
<Panel shape="solid" title="添加变量">
<AddStoreVariableForm
parentNode={activeNode}
onSubmit={(storeName, data) => {
onAddStoreVariable(storeName, data);
clear();
}}
onCancel={() => {
clear();
}}
/>
</Panel>
)}
{mode === 'addStore' && (
<AddStoreForm
storeNames={getStoreNames?.() || []}
onSubmit={({ name }) => {
onAddStore(name);
clear();
}}
onCancel={() => {
clear();
}}
/>
)}
{mode === 'addService' && (
<Panel shape="solid" title="创建服务函数">
<AddServiceForm
key={activeNode.key}
serviceModules={serviceModules}
serviceNames={activeNode.children?.map((item) => item.title) || []}
initialValues={{
moduleName: activeNode.title,
}}
onCancel={() => {
clear();
}}
onSubmit={(values) => {
onAddService(values);
clear();
}}
/>
</Panel>
)}
</Box>
</Box>
);
}

View File

@@ -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>
);
}

View File

@@ -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 />;
}

View File

@@ -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}
/>
);
}

View File

@@ -1 +1,2 @@
export * from './dom';
export * from './template';

View File

@@ -0,0 +1,9 @@
const newStoreTemplate = `
import { defineStore } from '@music163/tango-boot';
export default defineStore({
});
`;
export const CODE_TEMPLATES = {
newStoreTemplate,
};

View File

@@ -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';

View File

@@ -6,13 +6,22 @@ import {
isWrappedByExpressionContainer,
value2expressionCode,
} from '@music163/tango-core';
import { getVariableContent, noop, useBoolean, getValue } from '@music163/tango-helpers';
import {
getVariableContent,
noop,
useBoolean,
getValue,
isStoreVariablePath,
IVariableTreeNode,
} from '@music163/tango-helpers';
import { CloseCircleFilled, ExpandAltOutlined } from '@ant-design/icons';
import { IconButton, Panel, InputCode } from '@music163/tango-ui';
import { FormItemComponentProps } from '@music163/tango-setting-form';
import { useWorkspace, useWorkspaceData } from '@music163/tango-context';
import { EditableVariableTree, IVariableTreeNode } from '../components';
import { VariableTree } from '../components';
import { useSandboxQuery } from '../context';
import { CODE_TEMPLATES } from '../helpers';
import { shapeServiceValues } from '../sidebar/datasource-panel/interface-config';
export const expressionValueValidate = (value: string) => {
if (isWrappedByExpressionContainer(value)) {
@@ -77,6 +86,7 @@ export function ExpressionSetter(props: ExpressionSetterProps) {
value: valueProp,
status,
allowClear = true,
newStoreTemplate,
} = props;
const [visible, { on, off }] = useBoolean();
const [inputValue, setInputValue] = useState(() => {
@@ -143,6 +153,7 @@ export function ExpressionSetter(props: ExpressionSetterProps) {
subTitle={modalTip}
placeholder={placeholder}
autoCompleteOptions={autoCompleteOptions}
newStoreTemplate={newStoreTemplate}
visible={visible}
value={inputValue}
onCancel={() => off()}
@@ -166,6 +177,10 @@ export interface ExpressionModalProps {
onOk?: (value: string) => void;
dataSource?: IVariableTreeNode[];
autoCompleteOptions?: string[];
/**
* 新建 store 的模板代码
*/
newStoreTemplate?: string;
}
export function ExpressionModal({
@@ -179,18 +194,19 @@ export function ExpressionModal({
value,
dataSource,
autoCompleteOptions,
newStoreTemplate = CODE_TEMPLATES.newStoreTemplate,
}: ExpressionModalProps) {
const [exp, setExp] = useState(value ?? defaultValue);
const [error, setError] = useState('');
const workspace = useWorkspace();
const onAction = useCallback(
(action: string, args: unknown[]) => {
workspace[action]?.(...args);
},
[workspace],
);
const sandbox = useSandboxQuery();
const { expressionVariables } = useWorkspaceData();
const serviceModules = Object.keys(workspace.serviceModules).map((key) => ({
label: key === 'index' ? '默认模块' : key,
value: key,
}));
const sandbox = useSandboxQuery();
const evaluateContext = sandbox.window;
const handleExpInputChange = (val: string) => {
@@ -231,10 +247,29 @@ export function ExpressionModal({
/>
{error ? <Text color="red"></Text> : null}
</Panel>
<Panel title="从变量列表中选中" shape="solid" borderTop="0">
<EditableVariableTree
<Panel
title="从变量列表中选中"
shape="solid"
borderTop="0"
overflow="hidden"
bodyProps={{ overflow: 'hidden' }}
>
<VariableTree
height={380}
showViewButton
dataSource={dataSource || expressionVariables}
appContext={sandbox?.window['tango']}
getStoreNames={() => Object.keys(workspace.storeModules)}
serviceModules={serviceModules}
getServiceData={(serviceKey) => {
const data = workspace.getServiceFunction(serviceKey);
return {
name: data.name,
moduleName: data.moduleName,
method: 'get',
...data.config,
};
}}
onSelect={(node) => {
if (!node.key) {
return;
@@ -250,17 +285,29 @@ export function ExpressionModal({
}
setExp(str);
}}
onAddVariable={(storeName, data) => {
onAction('addStoreState', [storeName, data.name, data.initialValue]);
onAddStoreVariable={(storeName, data) => {
workspace.addStoreState(storeName, data.name, data.initialValue);
}}
onDeleteVariable={(storeName, stateName) => {
onAction('removeStoreState', [storeName, stateName]);
onUpdateVariable={(variableKey, code) => {
workspace.updateStoreVariable(variableKey, code);
}}
onDeleteStore={(storeName) => {
onAction('removeStoreModule', [storeName]);
onAddStore={(storeName) => {
workspace.addStoreFile(storeName, newStoreTemplate);
}}
onSave={(code, node) => {
onAction('updateModuleCodeByVariablePath', [node.key, code]);
onRemoveVariable={(variablePath) => {
if (isStoreVariablePath(variablePath)) {
workspace.removeStoreVariable(variablePath);
} else {
workspace.removeServiceFunction(variablePath);
}
}}
onAddService={(data) => {
const { name, moduleName, ...payload } = shapeServiceValues(data);
workspace.addServiceFunction(name, payload, moduleName);
}}
onUpdateService={(data) => {
const { name, moduleName, ...payload } = shapeServiceValues(data);
workspace.updateServiceFunction(name, payload, moduleName);
}}
getPreviewValue={(node) => {
if (!node || !node.key) {
@@ -268,7 +315,7 @@ export function ExpressionModal({
}
if (node.type === 'function') {
return node.raw;
return;
}
return getValue(evaluateContext['tango'], node.key);

View File

@@ -1,12 +1,13 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { Box } from 'coral-system';
import { Input, Tooltip } from 'antd';
import { getValue, isFunction } from '@music163/tango-helpers';
import { getValue, isFunction, isStoreVariablePath } from '@music163/tango-helpers';
import { FormItemComponentProps } from '@music163/tango-setting-form';
import { MenuOutlined } from '@ant-design/icons';
import { useWorkspace, useWorkspaceData } from '@music163/tango-context';
import { EditableVariableTreeModal } from '../components';
import { VariableTreeModal } from '../components';
import { useSandboxQuery } from '../context';
import { CODE_TEMPLATES } from '../helpers';
function object2treeData(
val: any,
@@ -52,43 +53,40 @@ function traverseTreeData(val: any, callback: (val: any) => void) {
}
}
export function ModelSetter({ value, onChange }: FormItemComponentProps) {
export function ModelSetter({
value,
onChange,
newStoreTemplate = CODE_TEMPLATES.newStoreTemplate,
}: FormItemComponentProps) {
const [inputValue, setInputValue] = useState(value);
const { modelVariables } = useWorkspaceData();
const evaluateContext = useSandboxQuery().window || {};
const workspace = useWorkspace();
const onAction = useCallback(
(action: string, args: unknown[]) => {
workspace[action]?.(...args);
},
[workspace],
);
const definedVariables = useMemo(() => {
const map = new Map();
const list: string[] = [];
traverseTreeData(modelVariables, (node) => {
if (node.key.split('.').length > 1) {
map.set(node.key, node);
list.push(node.key);
}
});
return map;
return list;
}, [modelVariables]);
const variables = evaluateContext['tango']?.stores
? [
object2treeData(evaluateContext['tango']?.stores, 'stores', 0, 1, (keyPath, val) => {
if (keyPath.split('.').length === 2 && definedVariables.has(keyPath)) {
return {
showAddChildIcon: true,
showRemoveIcon: true,
};
}
const ret: any = {};
if (isFunction(val)) {
// 不可以同步给函数类型变量
return {
disabled: true,
type: 'function',
};
ret.disabled = true;
ret.type = 'function';
}
if (!definedVariables.includes(keyPath)) {
ret.showAddButton = false;
ret.showRemoveButton = false;
}
return ret;
}),
]
: modelVariables;
@@ -122,31 +120,35 @@ export function ModelSetter({ value, onChange }: FormItemComponentProps) {
onChange={onInputChange}
onBlur={onInputBlur}
suffix={
<EditableVariableTreeModal
<VariableTreeModal
title="同步到的变量"
trigger={
<Tooltip title="从模型列表选择" placement="topRight">
<MenuOutlined />
</Tooltip>
}
modes={['preview']}
dataSource={variables as any[]}
onSelect={(node) => {
const modelPath = node.key.split('.').slice(1).join('.');
onChange(modelPath);
}}
onAddVariable={(storeName, data) => {
onAction('addStoreState', [storeName, data.name, data.initialValue]);
onAddStoreVariable={(storeName, data) => {
workspace.addStoreState(storeName, data.name, data.initialValue);
}}
onDeleteVariable={(storeName, stateName) => {
onAction('removeStoreState', [storeName, stateName]);
onUpdateVariable={(variableKey, code) => {
workspace.updateStoreVariable(variableKey, code);
}}
onDeleteStore={(storeName) => {
onAction('removeStoreModule', [storeName]);
onAddStore={(storeName) => {
workspace.addStoreFile(storeName, newStoreTemplate);
}}
onSave={(code, node) => {
onAction('updateModuleCodeByVariablePath', [node.key, code]);
onRemoveVariable={(variablePath) => {
if (isStoreVariablePath(variablePath)) {
workspace.removeStoreVariable(variablePath);
} else {
workspace.removeServiceFunction(variablePath);
}
}}
getStoreNames={() => Object.keys(workspace.storeModules)}
getPreviewValue={(node) => {
if (!node || !node.key) {
return;

View File

@@ -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">

View File

@@ -1,492 +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, Form, Select, Input } from 'antd';
import { PlayCircleOutlined } from '@ant-design/icons';
import { InputCode, Panel, JsonView, Search } from '@music163/tango-ui';
import {
isNil,
getVariableContent,
isValidFunctionCode,
logger,
code2object,
filterTreeData,
} from '@music163/tango-helpers';
import { Box } from 'coral-system';
import { getVariableContent, parseServiceVariablePath } from '@music163/tango-helpers';
import { isWrappedByExpressionContainer } from '@music163/tango-core';
import { useSandboxQuery } from '../../context';
import { VariableTree } from '../../components';
import { useSandboxQuery } from '../../context';
/*
* 服务函数的操作类型
*/
enum ServiceFunctionOperationModeType {
ADD = 'add',
UPDATE = 'update',
DELETE = 'delete',
}
/**
* 服务函数 HTTP Type
* 云音乐网关只支持 get 和 post
*/
enum ServiceFunctionMethodType {
GET = 'GET',
// PUT = 'PUT',
POST = 'POST',
// PATCH = 'PATCH',
// DELETE = 'DELETE',
}
/**
* 将 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,
}));
const detailFormStyle = css`
.ant-form-item {
margin-bottom: 12px;
// 移除掉不必要的属性
export function shapeServiceValues(val: any) {
const shapeValues = { ...val };
delete shapeValues.type;
// 兼容旧版,如果 formatter 包裹了 {} 则删掉首尾
if (shapeValues.formatter && isWrappedByExpressionContainer(shapeValues.formatter)) {
shapeValues.formatter = getVariableContent(shapeValues.formatter);
}
`;
return shapeValues;
}
const DataSourceView = observer(({ onAdd, onUpdate, onDelete }: DataServiceViewProps) => {
const [serviceData, setServiceData] = useState<any>();
const [keyword, setKeyword] = useState<string>();
const workspace = useWorkspace();
const DataSourceView = observer(() => {
const sandbox = useSandboxQuery();
const workspace = useWorkspace();
const { serviceVariables } = useWorkspaceData();
const serviceModules = Object.keys(workspace.serviceModules).map((key) => ({
label: key === 'index' ? '默认模块' : key,
value: key,
}));
const serviceFunctions = serviceVariables.reduce((acc, cur) => {
acc = acc.concat(cur.children.map((item: any) => item.key));
return acc;
}, []);
const dataSource = useMemo(() => {
if (!keyword) {
return serviceVariables;
}
return filterTreeData(
serviceVariables,
(leaf) => leaf.title.includes(keyword),
'children',
true,
);
}, [serviceVariables, keyword]);
const isAddMode = serviceData && !serviceData.name;
return (
<Box className="ServiceFunctionList" display="flex" borderTopColor="line.normal">
<Box width="35%" overflow="auto" borderRight="solid" borderColor="line.normal">
<Panel
height="100%"
bodyProps={{
flex: 1,
}}
>
<Box
p="m"
display="flex"
flexDirection="column"
justifyContent="center"
alignItems="center"
rowGap="m"
>
<Button
block
onClick={() => {
setServiceData({});
}}
>
</Button>
<Search placeholder="搜索服务函数" onChange={setKeyword} />
</Box>
<VariableTree
dataSource={dataSource}
showDeleteIcon
onSelect={(item) => {
setServiceData({ key: item.key, ...workspace.getServiceFunction(item.key) });
}}
onRemove={(item) => {
workspace.removeServiceFunction(item.key);
onDelete?.(item);
}}
/>
</Panel>
</Box>
<Box width="65%" overflow="auto" css={detailFormStyle}>
{serviceData && (
<>
<Panel title={isAddMode ? '新建服务函数' : '服务函数详情'}>
<ServiceDetailForm
key={serviceData.key || 'createSF'}
serviceModules={serviceModules}
serviceKeys={serviceFunctions}
initialValues={
isAddMode
? {
moduleName: 'index',
}
: {
name: serviceData.name,
moduleName: serviceData.moduleName,
method: 'get',
...serviceData.config,
}
}
onCancel={() => {
setServiceData(undefined);
}}
onSubmit={(values, mode: ServiceFunctionOperationModeType) => {
function shapeServiceValues(val: any) {
const shapeValues = { ...val };
delete shapeValues.type;
// 兼容旧版,如果 formatter 包裹了 {} 则删掉首尾
if (
shapeValues.formatter &&
isWrappedByExpressionContainer(shapeValues.formatter)
) {
shapeValues.formatter = getVariableContent(shapeValues.formatter);
}
return shapeValues;
}
// 移除掉不必要的属性
const { moduleName, ...data } = shapeServiceValues(values);
if (mode === ServiceFunctionOperationModeType.ADD) {
workspace.addServiceFunction(data, moduleName);
onAdd && onAdd(values);
} else if (mode === ServiceFunctionOperationModeType.UPDATE) {
workspace.updateServiceFunction(data, moduleName);
onUpdate && onUpdate(values);
}
}}
/>
</Panel>
{serviceData?.key ? (
<ServiceFunctionPreview
key={serviceData.key}
appContext={sandbox?.window['tango']}
functionName={serviceData.key}
/>
) : null}
</>
)}
{!serviceData && (
<Box py="xxl">
<Empty description="请从左侧选择数据服务函数或新建数据服务函数" />
</Box>
)}
</Box>
<Box className="ServiceFunctionList" p="m">
<VariableTree
dataSource={serviceVariables}
appContext={sandbox?.window['tango']}
serviceModules={serviceModules}
onRemoveVariable={(variableKey) => {
workspace.removeServiceFunction(variableKey);
}}
onAddService={(data) => {
const { name, moduleName, ...payload } = shapeServiceValues(data);
workspace.addServiceFunction(name, payload, moduleName);
}}
onUpdateService={(data) => {
const { name, moduleName, ...payload } = shapeServiceValues(data);
workspace.updateServiceFunction(name, payload, moduleName);
}}
getServiceData={(serviceKey) => {
const data = workspace.getServiceFunction(serviceKey);
return {
name: data.name,
moduleName: data.moduleName,
method: 'get',
...data.config,
};
}}
/>
</Box>
);
});
interface ServiceDetailFormProps extends FormProps {
serviceKeys?: string[];
serviceModules?: any[];
onCancel?: () => void;
onSubmit?: (values: any, mode: ServiceFunctionOperationModeType) => void;
}
function ServiceDetailForm({
serviceKeys = [],
serviceModules = [],
onCancel,
onSubmit,
initialValues,
...formProps
}: ServiceDetailFormProps) {
const isModifyMode = !!initialValues?.name; // 是否为更新模式
const [disabled, setDisabled] = useState(isModifyMode);
const [form] = Form.useForm();
// 监听服务类型变化,默认是自定义
const formType = Form.useWatch('type', form);
// 新建的时候,类型变换重置部分字段
useEffect(() => {
if (!formType) return;
!isModifyMode &&
form.setFieldsValue({
url: undefined,
method: undefined,
formatter: undefined,
});
}, [formType, form, isModifyMode]);
return (
<Form
form={form}
labelCol={{ span: 6 }}
wrapperCol={{ span: 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,
},
]}
>
<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 = serviceKeys.includes(
['services', form.getFieldValue('moduleName'), value].join('.'),
);
return isExist
? Promise.reject(new Error('重复的方法名,请换一个!'))
: Promise.resolve();
},
},
]}
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>
)
}
>
<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={httpMethods} 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 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;

View File

@@ -1,55 +1,39 @@
import React from 'react';
import { Button, Space, Form, Input } from 'antd';
import { FileAddOutlined, QuestionCircleOutlined } from '@ant-design/icons';
import { Button } from 'antd';
import { FileAddOutlined } from '@ant-design/icons';
import { useBoolean } from '@music163/tango-helpers';
import { Panel, IconButton } from '@music163/tango-ui';
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,74 +41,21 @@ export const VariablePanel = observer(
}}
/>
) : (
<EditableVariableTree
modes={['add', 'define']}
defaultMode="define"
<VariableTree
defaultValueDetailMode="define"
dataSource={storeVariables}
onAddVariable={(storeName, data) => {
onAddStoreVariable={(storeName, data) => {
workspace.addStoreState(storeName, data.name, data.initialValue);
}}
onDeleteVariable={(storeName, stateName) => {
workspace.removeStoreState(storeName, stateName);
onRemoveVariable={(variableKey) => {
workspace.removeStoreVariable(variableKey);
}}
onDeleteStore={(storeName) => {
workspace.removeStoreModule(storeName);
onUpdateVariable={(variableKey, code) => {
workspace.updateStoreVariable(variableKey, code);
}}
onSave={(code, node) => {
workspace.updateModuleCodeByVariablePath(node.key, code);
}}
height="100%"
{...restProps}
/>
)}
</Panel>
);
},
);
interface AddStoreFormProps {
existNames?: string[];
onCancel?: () => void;
onSubmit?: (data: { name: string }) => void;
}
function AddStoreForm({ existNames = [], onCancel, onSubmit }: AddStoreFormProps) {
return (
<Panel title="添加模型" subTitle="模型可以用来组织一组变量" 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 = existNames.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>
);
}

View File

@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.0.0-alpha.1](https://github.com/netease/tango/compare/@music163/tango-helpers@1.0.0-alpha.0...@music163/tango-helpers@1.0.0-alpha.1) (2023-12-29)
### Bug Fixes
- refactor VariableTree & Workspace ([#83](https://github.com/netease/tango/issues/83)) ([8c07821](https://github.com/netease/tango/commit/8c07821d93cea4dfc43f81ca948b845176821184))
## [0.1.8](https://github.com/netease/tango/compare/@music163/tango-helpers@0.1.7...@music163/tango-helpers@0.1.8) (2023-11-23)
### Bug Fixes

View File

@@ -1,6 +1,6 @@
{
"name": "@music163/tango-helpers",
"version": "1.0.0-alpha.0",
"version": "1.0.0-alpha.1",
"description": "Shared types, helpers, and hooks of tango-apps",
"keywords": [
"shared",

View File

@@ -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);
}

View File

@@ -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,
};
}

View File

@@ -33,6 +33,52 @@ export type OptionType = {
relatedImports?: string[];
};
/**
* 变量树节点类型
*/
export interface IVariableTreeNode {
/**
* 唯一标识符
*/
key: string;
/**
* 标题
*/
title?: string;
/**
* 辅助提示信息
*/
help?: string;
/**
* 是否可选中
*/
selectable?: boolean;
/**
* 展示删除按钮
*/
showRemoveButton?: boolean;
/**
* 展示添加按钮
*/
showAddButton?: boolean;
/**
* 展示查看按钮
*/
showViewButton?: boolean;
/**
* 结点类型,用来展示图标
*/
type?: 'function' | 'property';
/**
* 定义的原始值
*/
raw?: any;
/**
* 子结点
*/
children?: IVariableTreeNode[];
}
/**
* 服务函数类型
*/

View File

@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [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

View File

@@ -1,6 +1,6 @@
{
"name": "@music163/tango-sandbox",
"version": "1.0.0-alpha.3",
"version": "1.0.0-alpha.5",
"description": "sandbox of tango apps",
"author": "wwsun <ww.sun@outlook.com>",
"homepage": "",
@@ -29,8 +29,8 @@
},
"dependencies": {
"@ant-design/icons": "^4.8.0",
"@music163/tango-core": "^1.0.0-alpha.3",
"@music163/tango-helpers": "^1.0.0-alpha.0",
"@music163/tango-core": "^1.0.0-alpha.5",
"@music163/tango-helpers": "^1.0.0-alpha.1",
"crypto-js": "^4.1.1",
"lodash.isequal": "4.5.0",
"react-frame-component": "^5.2.4"

View File

@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [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

View File

@@ -1,6 +1,6 @@
{
"name": "@music163/tango-setting-form",
"version": "1.0.0-alpha.3",
"version": "1.0.0-alpha.5",
"description": "setting form of tango-apps",
"author": "wwsun <ww.sun@outlook.com>",
"homepage": "",
@@ -28,9 +28,9 @@
},
"dependencies": {
"@ant-design/icons": "^4.8.0",
"@music163/tango-core": "^1.0.0-alpha.3",
"@music163/tango-helpers": "^1.0.0-alpha.0",
"@music163/tango-ui": "^1.0.0-alpha.2",
"@music163/tango-core": "^1.0.0-alpha.5",
"@music163/tango-helpers": "^1.0.0-alpha.1",
"@music163/tango-ui": "^1.0.0-alpha.3",
"antd": "^4.24.2",
"coral-system": "^1.0.5",
"mobx": "6.12.0",

View File

@@ -3,6 +3,10 @@
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.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

View File

@@ -1,6 +1,6 @@
{
"name": "@music163/tango-ui",
"version": "1.0.0-alpha.2",
"version": "1.0.0-alpha.3",
"description": "ui widgets of tango",
"keywords": [
"react",
@@ -38,7 +38,7 @@
"@codemirror/lang-javascript": "^6.2.1",
"@codemirror/lint": "^6.4.2",
"@codemirror/search": "^6.5.5",
"@music163/tango-helpers": "^1.0.0-alpha.0",
"@music163/tango-helpers": "^1.0.0-alpha.1",
"@uiw/react-codemirror": "^4.21.21",
"antd": "^4.24.2",
"coral-system": "^1.0.5",