Compare commits

...

17 Commits

Author SHA1 Message Date
wwsun c240c1824b chore(release): publish
- @music163/tango-context@0.2.2
 - @music163/tango-designer@0.5.0
 - @music163/tango-setting-form@0.2.5
 - @music163/tango-ui@0.1.6
2023-10-12 15:24:21 +08:00
Wells d63517ecb9 fix: refactor expSetter (#41)
* fix: refactor expSetter

* feat: support pass custom variable list

---------

Co-authored-by: wwsun <ww.sww@outlook.com>
2023-10-11 23:51:27 -05:00
Samson Yang 1a17ee1e94 docs: fix typo (#39)
修改错别字
2023-10-09 21:24:01 -05:00
864907600cc e1bab66819 feat: add umd resources config when modifying dependency (#36)
* feat: mark dependency when it's in both package.json and tango.config.json

* fix: remove mark for internal resources

* feat: add resource config when adding dependency

* feat: reuse add dependency form modal when editing dependency

* fix: tidy up

---------

Co-authored-by: ccloli <8115912+ccloli@users.noreply.github.com>
2023-10-09 05:58:05 -05:00
wwsun 89bcd45728 chore(release): publish
- @music163/tango-designer@0.4.1
 - @music163/tango-setting-form@0.2.4
 - @music163/tango-ui@0.1.5
2023-10-09 18:44:15 +08:00
wwsun e86a6d7cd9 docs: update 2023-10-09 18:41:21 +08:00
wwsun 46b54459ce fix: update tabs style 2023-09-25 19:38:04 +08:00
wwsun 85898dee19 fix: always switch to design mode after preview is on 2023-09-25 16:31:47 +08:00
wwsun e429610606 fix: update export entry 2023-09-25 16:01:04 +08:00
wwsun b6814d6484 chore: update script 2023-09-22 17:55:30 +08:00
wwsun 3d7b58f19a chore(release): publish
- @music163/tango-context@0.2.1
 - @music163/tango-core@0.2.1
 - @music163/tango-designer@0.4.0
 - @music163/tango-sandbox@0.1.6
 - @music163/tango-setting-form@0.2.3
2023-09-22 17:52:12 +08:00
wwsun b6a47acdde chore: add husky 2023-09-22 17:48:54 +08:00
wwsun 226d71c704 fix: remove useless api 2023-09-22 17:48:15 +08:00
wwsun 1c72ee6ac2 feat: add global widget map 2023-09-22 17:37:26 +08:00
wwsun 09a1e2135a fix: add listServiceFunctions 2023-09-22 16:42:38 +08:00
wwsun 7999f604fd chore(release): publish
- @music163/tango-context@0.2.0
 - @music163/tango-core@0.2.0
 - @music163/tango-designer@0.3.0
 - @music163/tango-sandbox@0.1.5
 - @music163/tango-setting-form@0.2.2
2023-09-21 13:59:44 +08:00
Wells a7df29c3de feat: support multiple service modules & refactor workspace
* feat: support multiple service modules

* style: add memo

* fix: refactor module

* fix: refactor workspace

---------

Co-authored-by: wwsun <ww.sww@outlook.com>
2023-09-21 13:57:04 +08:00
56 changed files with 1765 additions and 1450 deletions
+4
View File
@@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
yarn commitlint --edit "$1"
+4
View File
@@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
yarn lint-staged --allow-empty
+1 -1
View File
@@ -14,7 +14,7 @@ export default {
],
devServer: {
host: 'local.netease.com',
port: 7007,
port: 8008,
https: {
key: path.resolve(__dirname, 'local.netease.com-key.pem'),
cert: path.resolve(__dirname, 'local.netease.com.pem'),
+1 -1
View File
@@ -10,7 +10,7 @@
],
"scripts": {
"build": "COMPRESS=none umi build",
"start": "HTTPS=1 umi dev"
"start": "umi dev"
},
"dependencies": {
"@ant-design/icons": "^4.8.0",
@@ -1,6 +1,6 @@
import React from 'react';
import { Row, Switch } from 'antd';
import { observer } from '@music163/tango-designer';
import { observer } from '@music163/tango-context';
const OtherPanel = observer(({ autoRemove, setAutoRemove }: any) => {
const onChange = (checked: boolean) => {
+15 -1
View File
@@ -44,7 +44,7 @@ const tangoConfigJson = {
resources: ['https://unpkg.com/moment@{{version}}/moment.js'],
},
'@music163/tango-boot': {
version: '0.1.3',
version: '0.2.1',
library: 'TangoBoot',
type: 'baseDependency',
resources: ['https://unpkg.com/@music163/tango-boot@{{version}}/dist/boot.js'],
@@ -175,6 +175,7 @@ export default defineStore({
const serviceCode = `
import { defineServices } from '@music163/tango-boot';
import './sub';
export default defineServices({
get: {
@@ -201,6 +202,18 @@ export default defineServices({
});
`;
const subServiceCode = `
import { defineServices } from '@music163/tango-boot';
export default defineServices({
list: {
url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/anchor-list-normal',
},
}, {
namespace: 'sub',
});
`;
const lessCode = `
body {
font-size: 12px;
@@ -232,6 +245,7 @@ export const sampleFiles = [
{ 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() {}` },
];
+1 -1
View File
@@ -2,4 +2,4 @@
@import '~antd/dist/antd.less'; // 引入官方提供的 less 样式入口文件
@primary-color: #2f54eb;
@border-radius-base: 2px;
@border-radius-base: 2px;
+43 -11
View File
@@ -9,15 +9,21 @@ import {
Toolbar,
WorkspacePanel,
WorkspaceView,
ComponentsPanel,
CodeEditor,
Sandbox,
DndQuery,
} from '@music163/tango-designer';
import { createEngine, Workspace } from '@music163/tango-core';
import { Logo, ProjectDetail } from './share';
import { Logo, ProjectDetail, bootHelperVariables } from './share';
import { sampleFiles } from '../mock/project';
import './index.less';
import {
ApiOutlined,
AppstoreAddOutlined,
BuildOutlined,
ClusterOutlined,
FunctionOutlined,
} from '@ant-design/icons';
// 1. 实例化工作区
const workspace = new Workspace({
@@ -44,7 +50,14 @@ export default function App() {
const [menuLoading, setMenuLoading] = useState(true);
const [menuData, setMenuData] = useState(false);
return (
<Designer engine={engine} sandboxQuery={sandboxQuery}>
<Designer
engine={engine}
config={{
customActionVariables: bootHelperVariables,
customExpressionVariables: bootHelperVariables,
}}
sandboxQuery={sandboxQuery}
>
<DesignerPanel
logo={<Logo />}
description={<ProjectDetail />}
@@ -52,10 +65,11 @@ export default function App() {
<Box px="l">
<Toolbar>
<Toolbar.Item key="routeSwitch" placement="left" />
<Toolbar.Item key="preview" placement="left" />
<Toolbar.Item key="modeSwitch" placement="right" />
<Toolbar.Item key="togglePanel" placement="right" />
<Toolbar.Separator />
<Toolbar.Item key="extra" placement="right">
<Toolbar.Item placement="right">
<Space>
<Button type="primary"></Button>
</Space>
@@ -65,13 +79,31 @@ export default function App() {
}
>
<Sidebar>
<Sidebar.Item key="outline" />
<Sidebar.Item key="components">
<ComponentsPanel menuData={menuData as any} loading={menuLoading} />
</Sidebar.Item>
<Sidebar.Item key="variables" isFloat width={800} />
<Sidebar.Item key="dataSource" isFloat width={800} />
<Sidebar.Item key="dependency" isFloat width={800} />
<Sidebar.Item key="outline" label="结构" icon={<BuildOutlined />} />
<Sidebar.Item
key="components"
label="组件"
icon={<AppstoreAddOutlined />}
widgetProps={{
menuData: menuData as any,
loading: menuLoading,
}}
/>
<Sidebar.Item
key="variables"
label="变量"
icon={<FunctionOutlined />}
isFloat
width={800}
/>
<Sidebar.Item key="dataSource" label="接口" icon={<ApiOutlined />} isFloat width={800} />
<Sidebar.Item
key="dependency"
label="依赖"
icon={<ClusterOutlined />}
isFloat
width={800}
/>
</Sidebar>
<WorkspacePanel>
<WorkspaceView mode="design">
+31 -1
View File
@@ -6,6 +6,36 @@ import { registerSetter } from '@music163/tango-designer';
import type { ComponentPrototypeType } from '@music163/tango-helpers';
import { FooSetter } from '../components';
export const bootHelperVariables = [
{
key: '$helpers',
title: '工具函数',
children: [
{
title: 'setStoreValue',
key: '() => tango.setStoreValue("variableName", "variableValue")',
type: 'function',
},
{
title: 'getStoreValue',
key: '() => tango.getStoreValue("variableName")',
type: 'function',
},
{ 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: 'copyToClipboard',
key: '() => tango.copyToClipboard("hello")',
type: 'function',
},
],
},
];
// folder-name
// 物料列表定义
const bizToggleButtonPrototype: ComponentPrototypeType = {
@@ -133,7 +163,7 @@ export function ProjectDetail() {
return (
<Box display="flex" alignItems="center" columnGap="l">
<Box className="ProjectName" fontSize="18px" fontWeight="bold">
lc-online-test
community-test
</Box>
<Box className="BranchName" as="code" fontSize="14px">
<BranchesOutlined /> feature/list
+10 -1
View File
@@ -10,11 +10,20 @@
项目使用的 npm 包依赖信息,用于定义依赖的类型以及外置依赖资源。该配置项为一个 map,key 为对应的 npm 包名,value 为该依赖的配置信息。
- version 包的版本号,必选
- library UMD 包中的全局变量名,必选
- type 依赖的类型
- baseDependency 基础依赖包
- bizDependency 业务组件依赖包
- dependency 其他依赖包
- resources 外置依赖资源列表
- designerResources 在设计器视图载入的外置资源列表,用来需要区别加载时使用
- description 描述信息
依赖的类型 `type` 可以是 `baseDependency` 即项目的基础依赖,用于获取项目基础物料列表的信息;也可以是 `bizDependency` 即业务组件依赖,用于获取合并到物料面板业务组件菜单中的业务组件列表;也可以是 `depenency` 即项目的其他依赖,只用于提供 external 信息与版本信息等,不会和物料面板产生关系。
当配置了外置依赖资源 `resources` 与其对应的全局变量 `library` 时,当这些依赖被 import 时,相关资源不会被打包到 bundle 中,而是在运行时再去外部获取这些扩展依赖,其等效于之前的 `sandbox.externalResources` 与 `sandbox.externals` 配置项。此外,可以将 `resource` 中的版本号替换为 `{{version}}`,其会在平台上构建时替换为版本号。特殊的,可以定义 `designerResources` 用于适配平台的设计器视图。
```json
{
"react": {
+1 -1
View File
@@ -6,7 +6,7 @@ Tango 是一个用于快速构建低代码平台的低代码设计器框架,
## 引擎架构概览
Tango 低代码引擎在实现上进行了分层解,使得上层的低代码平台与底层的低代码引擎可以独立开发和维护,快速集成部署。另一方面,Tango 低代码引擎定义了一套开放的物料生态体系,开发者可以自由的贡献扩展组件配置能力的属性设置器,以及扩展低代码物料的二方三方业务组件。
Tango 低代码引擎在实现上进行了分层解,使得上层的低代码平台与底层的低代码引擎可以独立开发和维护,快速集成部署。另一方面,Tango 低代码引擎定义了一套开放的物料生态体系,开发者可以自由的贡献扩展组件配置能力的属性设置器,以及扩展低代码物料的二方三方业务组件。
如下图所示:
+7
View File
@@ -0,0 +1,7 @@
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'scope-case': [0],
'subject-case': [0],
},
};
+10 -3
View File
@@ -21,7 +21,8 @@
"ver": "lerna version --no-private --conventional-commits",
"release": "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"
"up": "yarn upgrade-interactive --latest",
"prepare": "husky install"
},
"devDependencies": {
"@babel/preset-env": "^7.19.3",
@@ -49,11 +50,11 @@
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"http-server": "^14.1.0",
"husky": "^8.0.1",
"husky": "^8.0.3",
"jest": "^29.1.1",
"jest-environment-jsdom": "^29.1.1",
"lerna": "^6.6.1",
"lint-staged": "^13.1.0",
"lint-staged": "^14.0.1",
"prettier": "^2.8.7",
"react": "^17.0.0",
"react-dom": "^17.0.0",
@@ -63,5 +64,11 @@
},
"resolutions": {
"@yarnpkg/parsers": "3.0.0-rc.48.1"
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"eslint --fix",
"prettier --write"
]
}
}
+16
View File
@@ -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.
## [0.2.2](https://github.com/netease/tango/compare/@music163/tango-context@0.2.1...@music163/tango-context@0.2.2) (2023-10-12)
### Bug Fixes
- refactor expSetter ([#41](https://github.com/netease/tango/issues/41)) ([d63517e](https://github.com/netease/tango/commit/d63517ecb936e4227e70c33e610664316625f4f4))
## [0.2.1](https://github.com/netease/tango/compare/@music163/tango-context@0.2.0...@music163/tango-context@0.2.1) (2023-09-22)
**Note:** Version bump only for package @music163/tango-context
# [0.2.0](https://github.com/netease/tango/compare/@music163/tango-context@0.1.4...@music163/tango-context@0.2.0) (2023-09-21)
### Features
- support multiple service modules & refactor workspace ([a7df29c](https://github.com/netease/tango/commit/a7df29c3debc56b187792d3e203b470e9d368ea5))
## [0.1.4](https://github.com/netease/tango/compare/@music163/tango-context@0.1.3...@music163/tango-context@0.1.4) (2023-09-18)
**Note:** Version bump only for package @music163/tango-context
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@music163/tango-context",
"version": "0.1.4",
"version": "0.2.2",
"description": "react context for tango-apps",
"keywords": [
"react",
@@ -31,7 +31,7 @@
"react": ">= 16.8"
},
"dependencies": {
"@music163/tango-core": "^0.1.4",
"@music163/tango-core": "^0.2.1",
"@music163/tango-helpers": "^0.1.3",
"mobx-react-lite": "4.0.2"
},
+46 -39
View File
@@ -1,11 +1,25 @@
import type { Engine } from '@music163/tango-core';
import { createContext } from '@music163/tango-helpers';
interface CustomVariableData {
key: string;
title: string;
children?: CustomVariableData[];
[key: string]: any;
}
export interface ITangoEngineContext {
/**
* 低代码引擎
*/
engine: Engine;
/**
* 自定义配置数据
*/
config: {
customActionVariables: CustomVariableData[];
customExpressionVariables: CustomVariableData[];
};
}
const [TangoEngineProvider, useTangoEngine] = createContext<ITangoEngineContext>({
@@ -22,38 +36,15 @@ export const useDesigner = () => {
return useTangoEngine()?.engine.designer;
};
const builtinHelpers = [
{
title: 'setStoreValue',
key: '() => tango.setStoreValue("variableName", "variableValue")',
type: 'function',
},
{
title: 'getStoreValue',
key: '() => tango.getStoreValue("variableName")',
type: 'function',
},
{ 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: 'copyToClipboard',
key: '() => tango.copyToClipboard("hello")',
type: 'function',
},
];
export const useWorkspaceData = () => {
const ctx = useTangoEngine();
const workspace = useWorkspace();
const modelVariables: any[] = []; // 绑定变量列表
const storeActionVariables: any[] = []; // 模型中的所有 actions
const storeVariables: any[] = []; // 模型中的所有变量
const serviceVariables: any[] = []; // 服务中的所有变量
workspace.listStoreModules?.().forEach((file) => {
Object.values(workspace.storeModules).forEach((file) => {
const prefix = `stores.${file.name}`;
const states = file.states.map((item) => ({
title: item.name,
@@ -91,11 +82,17 @@ export const useWorkspaceData = () => {
});
});
Object.keys(workspace.serviceModule?.serviceFunctions || {}).forEach((key) => {
Object.values(workspace.serviceModules).forEach((file) => {
const prefix = file.name !== 'index' ? `services.${file.name}` : 'services';
serviceVariables.push({
title: key,
key: `services.${key}`,
type: 'function',
title: file.name,
key: prefix,
selectable: false,
children: Object.keys(file.serviceFunctions || {}).map((key) => ({
title: key,
key: [prefix, key].join('.'),
type: 'function',
})),
});
});
@@ -105,19 +102,29 @@ export const useWorkspaceData = () => {
value: item.path,
}));
let actionVariables: CustomVariableData[] = [
buildVariableOptions('数据模型', '$stores', storeActionVariables),
buildVariableOptions('服务函数', '$services', serviceVariables),
];
if (ctx.config?.customActionVariables) {
actionVariables = actionVariables.concat(ctx.config?.customActionVariables);
}
let expressionVariables: CustomVariableData[] = [
buildVariableOptions('数据模型', '$stores', storeVariables),
buildVariableOptions('服务函数', '$services', serviceVariables),
];
if (ctx.config?.customExpressionVariables) {
expressionVariables = expressionVariables.concat(ctx.config?.customExpressionVariables);
}
return {
modelVariables: [buildVariableOptions('数据模型', 'stores', modelVariables)],
actionVariables: [
buildVariableOptions('数据模型', 'stores', storeActionVariables),
buildVariableOptions('服务函数', 'services', serviceVariables),
buildVariableOptions('工具函数', 'helpers', builtinHelpers),
],
actionVariables,
storeVariables,
expressionVariables: [
buildVariableOptions('数据模型', 'stores', storeVariables),
buildVariableOptions('服务函数', 'services', serviceVariables),
buildVariableOptions('工具函数', 'helpers', builtinHelpers),
],
serviceVariables,
expressionVariables,
routeOptions,
};
};
+12
View File
@@ -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.
## [0.2.1](https://github.com/netease/tango/compare/@music163/tango-core@0.2.0...@music163/tango-core@0.2.1) (2023-09-22)
### Bug Fixes
- add listServiceFunctions ([09a1e21](https://github.com/netease/tango/commit/09a1e2135a1f51b0a5f6c0a507ba42d2e6355c24))
# [0.2.0](https://github.com/netease/tango/compare/@music163/tango-core@0.1.4...@music163/tango-core@0.2.0) (2023-09-21)
### Features
- support multiple service modules & refactor workspace ([a7df29c](https://github.com/netease/tango/commit/a7df29c3debc56b187792d3e203b470e9d368ea5))
## [0.1.4](https://github.com/netease/tango/compare/@music163/tango-core@0.1.3...@music163/tango-core@0.1.4) (2023-09-18)
**Note:** Version bump only for package @music163/tango-core
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@music163/tango-core",
"version": "0.1.4",
"version": "0.2.1",
"description": "tango core",
"author": "wwsun <ww.sun@outlook.com>",
"homepage": "",
+9 -33
View File
@@ -11,14 +11,9 @@ import {
StringOrNumber,
Dict,
parseDndId,
upperCamelCase
upperCamelCase,
} from '@music163/tango-helpers';
import {
keyNode2value,
jsxAttributeValueNode2value,
node2value,
node2code,
} from './generate';
import { keyNode2value, jsxAttributeValueNode2value, node2value, node2code } from './generate';
import {
value2jsxAttributeValueNode,
value2jsxChildrenValueNode,
@@ -957,42 +952,22 @@ export function updateServiceConfigToServiceFile(
return ast;
}
function visitObjectProperties(
node: t.ObjectExpression,
visitor: (propName: string, node: t.ObjectProperty) => void,
) {
for (const propNode of node.properties) {
if (t.isObjectProperty(propNode)) {
const propName = keyNode2value(propNode.key);
visitor?.(propName as string, propNode);
}
}
}
export function updateBaseConfigToServiceFile(ast: t.File, configName: string, configValue: any) {
export function updateBaseConfigToServiceFile(ast: t.File, config: object) {
traverse(ast, {
CallExpression(path) {
const calleeName = keyNode2value(path.node.callee) as string;
if (isDefineService(calleeName)) {
switch (path.node.arguments.length) {
case 1:
path.node.arguments.push(value2node({ [configName]: configValue }));
// 没有 baseConfig,直接创建新的参数即可
path.node.arguments.push(value2node(config));
break;
case 2: {
// 已存在 baseConfig,需要进行参数的合并
const baseConfigNode = path.node.arguments[1];
if (t.isObjectExpression(baseConfigNode)) {
let found = false;
visitObjectProperties(baseConfigNode, (propName, propNode) => {
if (propName === configName) {
found = true;
propNode.value = value2node(configValue);
}
});
if (!found) {
baseConfigNode.properties.push(
t.objectProperty(t.identifier(configName), value2node(configValue)),
);
}
const baseConfig = node2value(baseConfigNode);
path.node.arguments[1] = value2node({ ...baseConfig, ...config });
}
break;
}
@@ -1020,6 +995,7 @@ export function traverseServiceFile(ast: t.File) {
if (path.node.arguments[1]) {
const config = node2value(path.node.arguments[1]);
baseConfig.encryptFetch = !!config.encryptFetch;
baseConfig.namespace = config.namespace || 'index';
}
}
path.stop();
+1 -1
View File
@@ -35,7 +35,7 @@ export function inferFileType(filename: string): FileType {
}
// FIXME: 暂时写死,后续要支持多 services 文件解析
if (/\/services\/index.js$/.test(filename)) {
if (/\/services\/.+\.js$/.test(filename)) {
return FileType.ServiceModule;
}
+4
View File
@@ -7,5 +7,9 @@ export * from './drag-source';
export * from './history';
export * from './file';
export * from './module';
export * from './route-module';
export * from './service-module';
export * from './store-module';
export * from './view-module';
export * from './node';
export * from './interfaces';
+21 -45
View File
@@ -9,11 +9,12 @@ import {
InsertChildPositionType,
ITangoConfigPackages,
IPageConfigData,
IProjectData,
IServiceFunctionPayload,
} from '../types';
import { TangoFile, TangoJsonFile } from './file';
import { TangoRouteModule, TangoServiceModule, TangoStoreModule } from './module';
import { TangoRouteModule } from './route-module';
import { TangoStoreModule } from './store-module';
import { TangoServiceModule } from './service-module';
export interface IViewFile {
readonly workspace: IWorkspace;
@@ -25,6 +26,9 @@ export interface IViewFile {
*/
importMap?: Dict<{ package: string; isDefault?: boolean }>;
listModals?: () => Array<{ label: string; value: string }>;
listForms?: () => Record<string, string[]>;
update: (code?: string, isFormatCode?: boolean, refreshWorkspace?: boolean) => void;
getNode: (targetNodeId: string) => IViewNode;
@@ -139,7 +143,8 @@ export interface IWorkspace {
tangoConfigJson: TangoJsonFile;
routeModule?: TangoRouteModule;
serviceModule?: TangoServiceModule;
storeModules?: Record<string, TangoStoreModule>;
serviceModules?: Record<string, TangoServiceModule>;
refresh: (names: string[]) => void;
ready: () => void;
@@ -150,11 +155,6 @@ export interface IWorkspace {
setComponentPrototypes: (prototypes: Record<string, ComponentPrototypeType>) => void;
getPrototype: (name: string | ComponentPrototypeType) => ComponentPrototypeType;
/**
* 获取项目数据
*/
getProjectData?: () => IProjectData;
/**
* 查询节点
* @param id 节点 ID
@@ -162,73 +162,55 @@ export interface IWorkspace {
* @returns 返回节点对象
*/
getNode: (id: string, module?: string) => IViewNode;
listModals?: () => Array<{ label: string; value: string }>;
listForms?: () => Record<string, string[]>;
getFile: (filename: string) => TangoFile;
listFiles: () => Record<string, string>;
addFile: (filename: string, code: string, fileType?: FileType) => void;
addFiles: (files: IFileConfig[]) => void;
updateFile: (filename: string, code: string, shouldFormatCode?: boolean) => void;
removeFile: (filename: string) => void;
renameFile: (oldFilename: string, newFilename: string) => void;
renameFolder: (oldFoldername: string, newFoldername: string) => void;
getFile: (filename: string) => TangoFile;
listFiles: () => Record<string, string>;
addViewPage: (name: string, code: string) => void;
removeSelectedNode: () => void;
cloneSelectedNode: () => void;
copySelectedNode: () => void;
pasteSelectedNode: () => void;
insertToSelectedNode: (childNameOrPrototype: string | ComponentPrototypeType) => void;
dropNode: () => void;
insertToNode: (
targetNodeId: string,
sourceNameOrPrototype: string | ComponentPrototypeType,
) => void;
replaceNode: (
targetNodeId: string,
sourceNameOrPrototype: string | ComponentPrototypeType,
) => void;
updateSelectedNodeAttributes: (
attributes: Record<string, any>,
relatedImports?: string[],
) => void;
addBlock?: (files: object, name: string) => void;
generateBlockFilesBySelectedNode?: () => Record<string, string>;
removeServiceFunction?: (serviceName: string) => void;
addServiceFunction?: (payload: IServiceFunctionPayload | IServiceFunctionPayload[]) => void;
updateServiceFunction?: (payload: any) => void;
updateServiceBaseConfig?: (name: string, value: any) => void;
listStoreModules?: () => TangoStoreModule[];
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;
addStoreModule?: (storeName: string, code: string) => void;
removeStoreModule?: (storeName: string) => void;
addStoreState?: (storeName: string, stateName: string, initValue: string) => void;
removeStoreState?: (storeName: string, stateName: string) => void;
updateModuleCodeByVariablePath?: (variablePath: string, code: string) => void;
// TODO: 是否需要,和 addFile 有啥区别 ???
addViewPage: (name: string, code: string) => void;
removeViewModule: (routePath: string) => void;
copyViewPage: (sourceRoutePath: string, targetPageData: IPageConfigData) => void;
updateRoute: (sourceRoutePath: string, targetPageData: IPageConfigData) => void;
@@ -258,14 +240,8 @@ export interface IWorkspace {
removeBizComp?: (name: string) => void;
get activeViewModule(): IViewFile;
// TODO: -> getStoreModules
get storeModules(): TangoStoreModule[];
// TODO: -> getPages
get pages(): any[];
// TODO: getBizComps
get bizComps(): string[];
// TODO: getBaseComps
get baseComps(): string[];
// TODO: getBlocks
get blocks(): any[];
}
+7 -645
View File
@@ -1,60 +1,17 @@
import * as t from '@babel/types';
import { action, computed, makeObservable, observable, toJS } from 'mobx';
import { clone, ComponentPrototypeType, Dict, isNil, logger } from '@music163/tango-helpers';
import {
code2ast,
ast2code,
traverseRouteFile,
traverseViewFile,
traverseServiceFile,
traverseStoreFile,
traverseStoreEntryFile,
addStoreToEntryFile,
updateServiceConfigToServiceFile,
removeJSXElement,
insertSiblingAfterJSXElement,
getModuleNameByFilename,
addRouteToRouteFile,
removeRouteFromRouteFile,
appendChildToJSXElement,
addImportDeclaration,
updateImportDeclaration,
deleteServiceConfigFromServiceFile,
replaceJSXElement,
formatCode,
getImportDeclarationPayloadByPrototype,
addStoreState,
removeUnusedImportSpecifiers,
insertSiblingBeforeJSXElement,
updateStoreState,
removeStoreState,
removeStoreToEntryFile,
replaceRootJSXElementChildren,
updateRouteToRouteFile,
updateBaseConfigToServiceFile,
IdGenerator,
updateJSXAttributes,
} from '../helpers';
import { TangoNode } from './node';
import { action, computed, makeObservable, observable } from 'mobx';
import { isNil } from '@music163/tango-helpers';
import { code2ast, ast2code, formatCode } from '../helpers';
import { TangoFile } from './file';
import {
IRouteData,
IFileConfig,
IClassPropertyNodeData,
IServiceFunctionPayload,
ITangoViewNodeData,
IStorePropertyData,
IImportDeclarationPayload,
InsertChildPositionType,
} from '../types';
import { IViewFile, IWorkspace } from './interfaces';
import { IFileConfig } from '../types';
import { IWorkspace } from './interfaces';
/**
* 模块实现规范
* JS 模块实现规范
* - ast 操纵类方法,统一返回 this,支持外层链式调用
* - observable state 统一用 _foo 格式,并提供 getter 方法
*/
class TangoModule extends TangoFile {
export class TangoModule extends TangoFile {
ast: t.File;
constructor(workspace: IWorkspace, props: IFileConfig, isSyncCode = true) {
@@ -132,598 +89,3 @@ export class TangoJsModule extends TangoModule {
});
}
}
/**
* 入口配置模块
*/
export class TangoStoreEntryModule extends TangoModule {
_stores: string[] = [];
get stores() {
return toJS(this._stores);
}
constructor(workspace: IWorkspace, props: IFileConfig) {
super(workspace, props, false);
this.update(props.code, true, false);
makeObservable(this, {
_stores: observable,
_code: observable,
_cleanCode: observable,
stores: computed,
code: computed,
cleanCode: computed,
update: action,
});
}
_analysisAst() {
this._stores = traverseStoreEntryFile(this.ast);
}
/**
* 新建模型
* @param name
*/
addStore(name: string) {
this.ast = addStoreToEntryFile(this.ast, name);
return this;
}
/**
* 删除模型
* @param name
*/
removeStore(name: string) {
this.ast = removeStoreToEntryFile(this.ast, name);
return this;
}
}
/**
* 路由配置模块
*/
export class TangoRouteModule extends TangoModule {
_routes: IRouteData[];
get routes() {
return toJS(this._routes);
}
constructor(workspace: IWorkspace, props: IFileConfig) {
super(workspace, props, false);
this.update(props.code, true, false);
makeObservable(this, {
_routes: observable,
_code: observable,
_cleanCode: observable,
routes: computed,
code: computed,
cleanCode: computed,
update: action,
});
}
/**
* 根据路由地址获取 route 对象
*/
getRouteByRoutePath(route: string) {
let record;
for (const item of this.routes) {
if (item.path === route) {
record = item;
break;
}
}
return record;
}
/**
* 添加一条新路由
* @param name
*/
addRoute(routePath: string, importFilePath: string) {
this.ast = addRouteToRouteFile(this.ast, routePath, importFilePath);
return this;
}
/**
* 更新页面路由
* @param oldRoutePath
* @param newRoutePath
* @returns
*/
updateRoute(oldRoutePath: string, newRoutePath: string) {
this.ast = updateRouteToRouteFile(this.ast, oldRoutePath, newRoutePath);
return this;
}
/**
* 删除一条路由
* @param route 路由地址
*/
removeRoute(route: string) {
if (route === '/') {
console.warn('index route should not be removed!');
return;
}
const record = this.getRouteByRoutePath(route);
this.ast = removeRouteFromRouteFile(this.ast, route, record.importPath);
return this;
}
_analysisAst() {
this._routes = traverseRouteFile(this.ast);
}
}
/**
* 视图模块
*/
export class TangoViewModule extends TangoModule implements IViewFile {
// 解析为树结构的 jsxNodes 数组
_nodesTree: ITangoViewNodeData[];
/**
* 通过导入组件名查找组件来自的包
*/
importMap: Dict<{ package: string; isDefault?: boolean }>;
/**
* 视图中依赖的 tango 变量,仅 stores 和 services
*/
variables: string[];
/**
* 节点列表
*/
private _nodes: Map<string, TangoNode>;
/**
* 导入的模块
*/
private _importedModules: Dict<IImportDeclarationPayload | IImportDeclarationPayload[]>;
/**
* ID 生成器
*/
private _idGenerator: IdGenerator;
get nodes() {
return this._nodes;
}
get nodesTree() {
return toJS(this._nodesTree);
}
get tree() {
return this.ast;
}
constructor(workspace: IWorkspace, props: IFileConfig) {
super(workspace, props, false);
this._nodes = new Map();
this._idGenerator = new IdGenerator({ prefix: props.filename });
this.update(props.code, true, false);
makeObservable(this, {
_nodesTree: observable,
_code: observable,
_cleanCode: observable,
code: computed,
cleanCode: computed,
update: action,
});
}
_syncByAst() {
// 空方法,逻辑合并到 this._analysisAst
}
_analysisAst() {
const {
ast: newAst,
cleanAst,
nodes,
importedModules,
variables,
} = traverseViewFile(this.ast, this._idGenerator);
this.ast = newAst;
this._code = ast2code(newAst);
this._cleanCode = ast2code(cleanAst);
this._importedModules = importedModules;
this.importMap = this.buildImportMap(importedModules);
this.variables = variables;
this._nodes.clear();
nodes.forEach((cur) => {
const node = new TangoNode({
...cur,
file: this,
});
this._nodes.set(cur.id, node);
});
this._nodesTree = nodeListToTreeData(nodes);
}
/**
* 基于组件的 prototype 信息更新导入信息
* @param prototype
* @param shouldUpdateCode
*/
updateImportSpecifiersByPrototype(sourcePrototype: string | ComponentPrototypeType) {
const prototype = this.workspace.getPrototype(sourcePrototype);
if (prototype) {
const importDeclaration = getImportDeclarationPayloadByPrototype(prototype, this.filename);
this.updateImportSpecifiers(importDeclaration);
}
return this;
}
/**
* 更新导入的变量(新版)
*/
updateImportSpecifiers(importDeclaration: IImportDeclarationPayload) {
const mods = this._importedModules[importDeclaration.sourcePath];
let ast;
// 如果模块已存在,需要去重
if (mods) {
const targetMod = Array.isArray(mods) ? mods[0] : mods;
const specifiers = Array.isArray(mods)
? mods.reduce((prev, cur) => prev.concat(cur.specifiers || []), [])
: mods.specifiers;
// 去掉已存在的导入声明
const newSpecifiers = importDeclaration.specifiers.filter(
(name) => !specifiers.includes(name),
);
ast = updateImportDeclaration(this.ast, {
...importDeclaration,
specifiers: newSpecifiers.concat(targetMod.specifiers),
});
} else {
ast = addImportDeclaration(this.ast, importDeclaration);
}
this.ast = ast;
return this;
}
/**
* 清除无效的导入声明
*/
removeUnusedImportSpecifiers() {
this.ast = removeUnusedImportSpecifiers(this.ast);
return this;
}
getNode(nodeId: string) {
return this._nodes.get(nodeId);
}
/**
* 删除节点
* @param nodeId
*/
removeNode(nodeId: string) {
this.ast = removeJSXElement(this.ast, nodeId);
return this;
}
/**
* 更新节点的属性
* @deprecated 使用 updateNodeAttributes 代替
*/
updateNodeAttribute(
nodeId: string,
attrName: string,
attrValue?: any,
relatedImports?: string[],
) {
return this.updateNodeAttributes(nodeId, { [attrName]: attrValue }, relatedImports);
}
updateNodeAttributes(nodeId: string, config: Record<string, any>, relatedImports?: string[]) {
if (relatedImports && relatedImports.length) {
// 导入依赖的组件
relatedImports.forEach((name: string) => {
const proto = this.workspace.getPrototype(name);
this.updateImportSpecifiersByPrototype(proto);
});
}
this.ast = updateJSXAttributes(this.ast, nodeId, config);
return this;
}
/**
* 插入子节点的最后面
* @param targetNodeId
* @param newNode
* @param sourceName
* @returns
*/
insertChild(
targetNodeId: string,
newNode: t.JSXElement,
position: InsertChildPositionType = 'last',
sourceName: string | ComponentPrototypeType,
) {
this.ast = appendChildToJSXElement(this.ast, targetNodeId, newNode, position);
if (sourceName) {
this.updateImportSpecifiersByPrototype(sourceName);
}
return this;
}
insertAfter(
targetNodeId: string,
newNode: t.JSXElement,
sourceName?: string | ComponentPrototypeType,
) {
this.ast = insertSiblingAfterJSXElement(this.ast, targetNodeId, newNode);
if (sourceName) {
this.updateImportSpecifiersByPrototype(sourceName);
}
return this;
}
insertBefore(
targetNodeId: string,
newNode: t.JSXElement,
sourceName?: string | ComponentPrototypeType,
) {
this.ast = insertSiblingBeforeJSXElement(this.ast, targetNodeId, newNode);
if (sourceName) {
this.updateImportSpecifiersByPrototype(sourceName);
}
return this;
}
/**
* 替换目标节点为新节点
* @param targetNodeId
* @param newNode
* @param sourcePrototype
*/
replaceNode(
targetNodeId: string,
newNode: t.JSXElement,
sourceName?: string | ComponentPrototypeType,
) {
this.ast = replaceJSXElement(this.ast, targetNodeId, newNode);
if (sourceName) {
this.updateImportSpecifiersByPrototype(sourceName);
}
return this;
}
/**
* 替换 jsx 跟结点的子元素
*/
replaceViewChildren(
childrenNodes: t.JSXElement[],
importDeclarations?: IImportDeclarationPayload[],
) {
if (childrenNodes.length) {
this.ast = replaceRootJSXElementChildren(this.ast, childrenNodes);
}
if (importDeclarations?.length) {
importDeclarations.forEach((item) => {
this.updateImportSpecifiers(item);
});
}
return this;
}
private buildImportMap(
importedModules: Dict<IImportDeclarationPayload | IImportDeclarationPayload[]>,
) {
const map = {};
Object.keys(importedModules).forEach((modName) => {
const mod = importedModules[modName];
(Array.isArray(mod) ? mod : [mod]).forEach((item) => {
if (item.defaultSpecifier) {
map[item.defaultSpecifier] = {
package: modName,
isDefault: true,
};
}
if (item.specifiers.length) {
item.specifiers.forEach((spe) => {
map[spe] = { package: modName };
});
}
});
});
return map;
}
}
/**
* 将节点列表转换为 tree data 嵌套数组
* @param list
*/
export function nodeListToTreeData(list: ITangoViewNodeData[]) {
const map: Record<string, ITangoViewNodeData> = {};
list.forEach((item) => {
// 如果不存在,则初始化
if (!map[item.id]) {
map[item.id] = {
...item,
children: [],
};
}
// 是否找到父节点,找到则塞进去
if (item.parentId && map[item.parentId]) {
map[item.parentId].children.push(map[item.id]);
}
});
// 保留根节点
const ret = Object.values(map).filter((item) => !item.parentId);
return ret;
}
/**
* 数据服务模块
*/
export class TangoServiceModule extends TangoModule {
/**
* 模块名
*/
name: string;
_serviceFunctions: Dict;
_baseConfig: Dict;
get serviceFunctions() {
return toJS(this._serviceFunctions);
}
get baseConfig() {
return toJS(this._baseConfig);
}
constructor(workspace: IWorkspace, props: IFileConfig) {
super(workspace, props, false);
this.name = getModuleNameByFilename(props.filename);
this.update(props.code, true, false);
makeObservable(this, {
_serviceFunctions: observable,
_baseConfig: observable,
_code: observable,
_cleanCode: observable,
serviceFunctions: computed,
baseConfig: computed,
cleanCode: computed,
code: computed,
update: action,
});
}
_analysisAst() {
const { services, baseConfig } = traverseServiceFile(this.ast);
this._serviceFunctions = services;
this._baseConfig = baseConfig;
}
addServiceFunction(payload: IServiceFunctionPayload) {
const { name, ...rest } = payload;
this.ast = updateServiceConfigToServiceFile(this.ast, { [name]: clone(rest, 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);
return this;
}
updateServiceFunction(payload: IServiceFunctionPayload) {
const { name, ...rest } = payload;
this.ast = updateServiceConfigToServiceFile(this.ast, { [name]: clone(rest, false) });
return this;
}
deleteServiceFunction(serviceFunctionName: string) {
try {
this.ast = deleteServiceConfigFromServiceFile(this.ast, serviceFunctionName);
} catch (e) {
logger.error(e);
}
return this;
}
/**
* 更新服务的基础配置
*/
updateBaseConfig(optionName: string, optionValue: any) {
this.ast = updateBaseConfigToServiceFile(this.ast, optionName, optionValue);
return this;
}
}
/**
* 状态模型模块
*/
export class TangoStoreModule extends TangoModule {
/**
* 模块名
*/
name: string;
namespace: string;
states: IStorePropertyData[];
actions: IStorePropertyData[];
constructor(workspace: IWorkspace, props: IFileConfig) {
super(workspace, props, false);
this.name = getModuleNameByFilename(props.filename);
this.update(props.code, true, false);
makeObservable(this, {
states: observable,
actions: observable,
_code: observable,
_cleanCode: observable,
cleanCode: computed,
code: computed,
update: action,
});
}
/**
* 添加状态属性
* @param stateName
* @param initValue
*/
addState(stateName: string, initValue: string) {
this.ast = addStoreState(this.ast, stateName, initValue);
return this;
}
/**
* 移除状态
*/
removeState(stateName: string) {
this.ast = removeStoreState(this.ast, stateName);
return this;
}
/**
* 更新状态代码
* @param stateName 状态名
* @param code 代码
*/
updateState(stateName: string, code: string) {
this.ast = updateStoreState(this.ast, stateName, code);
return this;
}
_analysisAst() {
const { namespace, states, actions } = traverseStoreFile(this.ast);
this.namespace = namespace || this.name;
this.states = states;
this.actions = actions;
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
import { JSXElement, SourceLocation } from '@babel/types';
import { cloneJSXElementWithoutTrackingData, getJSXElementAttributes } from '../helpers';
import { ITangoViewNodeData } from '../types';
import { TangoViewModule } from './module';
import { TangoViewModule } from './view-module';
import { IViewNode } from './interfaces';
type TangoNodeConstructorPropsType = ITangoViewNodeData & {
+88
View File
@@ -0,0 +1,88 @@
import { action, computed, makeObservable, observable, toJS } from 'mobx';
import {
traverseRouteFile,
addRouteToRouteFile,
removeRouteFromRouteFile,
updateRouteToRouteFile,
} from '../helpers';
import { IRouteData, IFileConfig } from '../types';
import { IWorkspace } from './interfaces';
import { TangoModule } from './module';
/**
* 路由配置模块
*/
export class TangoRouteModule extends TangoModule {
_routes: IRouteData[];
get routes() {
return toJS(this._routes);
}
constructor(workspace: IWorkspace, props: IFileConfig) {
super(workspace, props, false);
this.update(props.code, true, false);
makeObservable(this, {
_routes: observable,
_code: observable,
_cleanCode: observable,
routes: computed,
code: computed,
cleanCode: computed,
update: action,
});
}
/**
* 根据路由地址获取 route 对象
*/
getRouteByRoutePath(route: string) {
let record;
for (const item of this.routes) {
if (item.path === route) {
record = item;
break;
}
}
return record;
}
/**
* 添加一条新路由
* @param name
*/
addRoute(routePath: string, importFilePath: string) {
this.ast = addRouteToRouteFile(this.ast, routePath, importFilePath);
return this;
}
/**
* 更新页面路由
* @param oldRoutePath
* @param newRoutePath
* @returns
*/
updateRoute(oldRoutePath: string, newRoutePath: string) {
this.ast = updateRouteToRouteFile(this.ast, oldRoutePath, newRoutePath);
return this;
}
/**
* 删除一条路由
* @param route 路由地址
*/
removeRoute(route: string) {
if (route === '/') {
console.warn('index route should not be removed!');
return;
}
const record = this.getRouteByRoutePath(route);
this.ast = removeRouteFromRouteFile(this.ast, route, record.importPath);
return this;
}
_analysisAst() {
this._routes = traverseRouteFile(this.ast);
}
}
+107
View File
@@ -0,0 +1,107 @@
import { action, computed, makeObservable, observable, toJS } from 'mobx';
import { clone, Dict, logger } from '@music163/tango-helpers';
import {
traverseServiceFile,
updateServiceConfigToServiceFile,
getModuleNameByFilename,
deleteServiceConfigFromServiceFile,
updateBaseConfigToServiceFile,
} from '../helpers';
import { IFileConfig, IServiceFunctionPayload } from '../types';
import { IWorkspace } from './interfaces';
import { TangoModule } from './module';
/**
* 数据服务模块
*/
export class TangoServiceModule extends TangoModule {
/**
* 服务函数的模块名,默认为 index
*/
name: string;
/**
* 原始注册的服务函数
* @example { get: {}, list: {} }
*/
_serviceFunctions: Dict;
/**
* 原始的基础配置
*/
_baseConfig: Dict;
get serviceFunctions() {
return toJS(this._serviceFunctions);
}
get baseConfig() {
return toJS(this._baseConfig);
}
constructor(workspace: IWorkspace, props: IFileConfig) {
super(workspace, props, false);
this.name = getModuleNameByFilename(props.filename);
this.update(props.code, true, false);
makeObservable(this, {
_serviceFunctions: observable,
_baseConfig: observable,
_code: observable,
_cleanCode: observable,
serviceFunctions: computed,
baseConfig: computed,
cleanCode: computed,
code: computed,
update: action,
});
}
_analysisAst() {
const { services, baseConfig } = traverseServiceFile(this.ast);
this._serviceFunctions = services;
this._baseConfig = baseConfig;
if (baseConfig.namespace) {
this.name = baseConfig.namespace;
}
}
addServiceFunction(payload: IServiceFunctionPayload) {
const { name, ...rest } = payload;
this.ast = updateServiceConfigToServiceFile(this.ast, { [name]: clone(rest, 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);
return this;
}
updateServiceFunction(payload: IServiceFunctionPayload) {
const { name, ...rest } = payload;
this.ast = updateServiceConfigToServiceFile(this.ast, { [name]: clone(rest, false) });
return this;
}
deleteServiceFunction(serviceFunctionName: string) {
try {
this.ast = deleteServiceConfigFromServiceFile(this.ast, serviceFunctionName);
} catch (e) {
logger.error(e);
}
return this;
}
/**
* 更新服务的基础配置
*/
updateBaseConfig(data: object) {
this.ast = updateBaseConfigToServiceFile(this.ast, data);
return this;
}
}
+129
View File
@@ -0,0 +1,129 @@
import { action, computed, makeObservable, observable, toJS } from 'mobx';
import {
traverseStoreFile,
traverseStoreEntryFile,
addStoreToEntryFile,
getModuleNameByFilename,
addStoreState,
updateStoreState,
removeStoreState,
removeStoreToEntryFile,
} from '../helpers';
import { IFileConfig, IStorePropertyData } from '../types';
import { IWorkspace } from './interfaces';
import { TangoModule } from './module';
/**
* 入口配置模块
*/
export class TangoStoreEntryModule extends TangoModule {
_stores: string[] = [];
get stores() {
return toJS(this._stores);
}
constructor(workspace: IWorkspace, props: IFileConfig) {
super(workspace, props, false);
this.update(props.code, true, false);
makeObservable(this, {
_stores: observable,
_code: observable,
_cleanCode: observable,
stores: computed,
code: computed,
cleanCode: computed,
update: action,
});
}
_analysisAst() {
this._stores = traverseStoreEntryFile(this.ast);
}
/**
* 新建模型
* @param name
*/
addStore(name: string) {
this.ast = addStoreToEntryFile(this.ast, name);
return this;
}
/**
* 删除模型
* @param name
*/
removeStore(name: string) {
this.ast = removeStoreToEntryFile(this.ast, name);
return this;
}
}
/**
* 状态模型模块
*/
export class TangoStoreModule extends TangoModule {
/**
* 模块名
*/
name: string;
namespace: string;
states: IStorePropertyData[];
actions: IStorePropertyData[];
constructor(workspace: IWorkspace, props: IFileConfig) {
super(workspace, props, false);
this.name = getModuleNameByFilename(props.filename);
this.update(props.code, true, false);
makeObservable(this, {
states: observable,
actions: observable,
_code: observable,
_cleanCode: observable,
cleanCode: computed,
code: computed,
update: action,
});
}
/**
* 添加状态属性
* @param stateName
* @param initValue
*/
addState(stateName: string, initValue: string) {
this.ast = addStoreState(this.ast, stateName, initValue);
return this;
}
/**
* 移除状态
*/
removeState(stateName: string) {
this.ast = removeStoreState(this.ast, stateName);
return this;
}
/**
* 更新状态代码
* @param stateName 状态名
* @param code 代码
*/
updateState(stateName: string, code: string) {
this.ast = updateStoreState(this.ast, stateName, code);
return this;
}
_analysisAst() {
const { namespace, states, actions } = traverseStoreFile(this.ast);
this.namespace = namespace || this.name;
this.states = states;
this.actions = actions;
}
}
+382
View File
@@ -0,0 +1,382 @@
import * as t from '@babel/types';
import { action, computed, makeObservable, observable, toJS } from 'mobx';
import { ComponentPrototypeType, Dict } from '@music163/tango-helpers';
import {
ast2code,
traverseViewFile,
removeJSXElement,
insertSiblingAfterJSXElement,
appendChildToJSXElement,
addImportDeclaration,
updateImportDeclaration,
replaceJSXElement,
getImportDeclarationPayloadByPrototype,
removeUnusedImportSpecifiers,
insertSiblingBeforeJSXElement,
replaceRootJSXElementChildren,
IdGenerator,
updateJSXAttributes,
queryXFormItemFields,
} from '../helpers';
import { TangoNode } from './node';
import {
IFileConfig,
ITangoViewNodeData,
IImportDeclarationPayload,
InsertChildPositionType,
} from '../types';
import { IViewFile, IWorkspace } from './interfaces';
import { TangoModule } from './module';
/**
* 视图模块
*/
export class TangoViewModule extends TangoModule implements IViewFile {
// 解析为树结构的 jsxNodes 数组
_nodesTree: ITangoViewNodeData[];
/**
* 通过导入组件名查找组件来自的包
*/
importMap: Dict<{ package: string; isDefault?: boolean }>;
/**
* 视图中依赖的 tango 变量,仅 stores 和 services
*/
variables: string[];
/**
* 节点列表
*/
private _nodes: Map<string, TangoNode>;
/**
* 导入的模块
*/
private _importedModules: Dict<IImportDeclarationPayload | IImportDeclarationPayload[]>;
/**
* ID 生成器
*/
private _idGenerator: IdGenerator;
get nodes() {
return this._nodes;
}
get nodesTree() {
return toJS(this._nodesTree);
}
get tree() {
return this.ast;
}
constructor(workspace: IWorkspace, props: IFileConfig) {
super(workspace, props, false);
this._nodes = new Map();
this._idGenerator = new IdGenerator({ prefix: props.filename });
this.update(props.code, true, false);
makeObservable(this, {
_nodesTree: observable,
_code: observable,
_cleanCode: observable,
code: computed,
cleanCode: computed,
update: action,
});
}
_syncByAst() {
// 空方法,逻辑合并到 this._analysisAst
}
_analysisAst() {
const {
ast: newAst,
cleanAst,
nodes,
importedModules,
variables,
} = traverseViewFile(this.ast, this._idGenerator);
this.ast = newAst;
this._code = ast2code(newAst);
this._cleanCode = ast2code(cleanAst);
this._importedModules = importedModules;
this.importMap = this.buildImportMap(importedModules);
this.variables = variables;
this._nodes.clear();
nodes.forEach((cur) => {
const node = new TangoNode({
...cur,
file: this,
});
this._nodes.set(cur.id, node);
});
this._nodesTree = nodeListToTreeData(nodes);
}
/**
* 弹窗列表
*/
listModals() {
const modals: Array<{ label: string; value: string }> = [];
const activeViewNodes = this.nodes || new Map();
Array.from(activeViewNodes.values()).forEach((node) => {
if (['Modal', 'Drawer'].includes(node.component) && node.props.id) {
modals.push({
label: `${node.component}(${node.props.id})`,
value: node.props.id,
});
}
});
return modals;
}
/**
* 表单列表
*/
listForms() {
const forms: Record<string, string[]> = {};
const activeViewNodes = this.nodes;
Array.from(activeViewNodes.values()).forEach((node) => {
if (
['XAction', 'XColumnAction', 'XForm', 'XStepForm', 'XSearchForm', 'XFormList'].includes(
node.component,
)
) {
forms[node.id] = queryXFormItemFields(node.rawNode);
}
});
return forms;
}
/**
* 基于组件的 prototype 信息更新导入信息
* @param prototype
* @param shouldUpdateCode
*/
updateImportSpecifiersByPrototype(sourcePrototype: string | ComponentPrototypeType) {
const prototype = this.workspace.getPrototype(sourcePrototype);
if (prototype) {
const importDeclaration = getImportDeclarationPayloadByPrototype(prototype, this.filename);
this.updateImportSpecifiers(importDeclaration);
}
return this;
}
/**
* 更新导入的变量(新版)
*/
updateImportSpecifiers(importDeclaration: IImportDeclarationPayload) {
const mods = this._importedModules[importDeclaration.sourcePath];
let ast;
// 如果模块已存在,需要去重
if (mods) {
const targetMod = Array.isArray(mods) ? mods[0] : mods;
const specifiers = Array.isArray(mods)
? mods.reduce((prev, cur) => prev.concat(cur.specifiers || []), [])
: mods.specifiers;
// 去掉已存在的导入声明
const newSpecifiers = importDeclaration.specifiers.filter(
(name) => !specifiers.includes(name),
);
ast = updateImportDeclaration(this.ast, {
...importDeclaration,
specifiers: newSpecifiers.concat(targetMod.specifiers),
});
} else {
ast = addImportDeclaration(this.ast, importDeclaration);
}
this.ast = ast;
return this;
}
/**
* 清除无效的导入声明
*/
removeUnusedImportSpecifiers() {
this.ast = removeUnusedImportSpecifiers(this.ast);
return this;
}
getNode(nodeId: string) {
return this._nodes.get(nodeId);
}
/**
* 删除节点
* @param nodeId
*/
removeNode(nodeId: string) {
this.ast = removeJSXElement(this.ast, nodeId);
return this;
}
/**
* 更新节点的属性
* @deprecated 使用 updateNodeAttributes 代替
*/
updateNodeAttribute(
nodeId: string,
attrName: string,
attrValue?: any,
relatedImports?: string[],
) {
return this.updateNodeAttributes(nodeId, { [attrName]: attrValue }, relatedImports);
}
updateNodeAttributes(nodeId: string, config: Record<string, any>, relatedImports?: string[]) {
if (relatedImports && relatedImports.length) {
// 导入依赖的组件
relatedImports.forEach((name: string) => {
const proto = this.workspace.getPrototype(name);
this.updateImportSpecifiersByPrototype(proto);
});
}
this.ast = updateJSXAttributes(this.ast, nodeId, config);
return this;
}
/**
* 插入子节点的最后面
* @param targetNodeId
* @param newNode
* @param sourceName
* @returns
*/
insertChild(
targetNodeId: string,
newNode: t.JSXElement,
position: InsertChildPositionType = 'last',
sourceName: string | ComponentPrototypeType,
) {
this.ast = appendChildToJSXElement(this.ast, targetNodeId, newNode, position);
if (sourceName) {
this.updateImportSpecifiersByPrototype(sourceName);
}
return this;
}
insertAfter(
targetNodeId: string,
newNode: t.JSXElement,
sourceName?: string | ComponentPrototypeType,
) {
this.ast = insertSiblingAfterJSXElement(this.ast, targetNodeId, newNode);
if (sourceName) {
this.updateImportSpecifiersByPrototype(sourceName);
}
return this;
}
insertBefore(
targetNodeId: string,
newNode: t.JSXElement,
sourceName?: string | ComponentPrototypeType,
) {
this.ast = insertSiblingBeforeJSXElement(this.ast, targetNodeId, newNode);
if (sourceName) {
this.updateImportSpecifiersByPrototype(sourceName);
}
return this;
}
/**
* 替换目标节点为新节点
* @param targetNodeId
* @param newNode
* @param sourcePrototype
*/
replaceNode(
targetNodeId: string,
newNode: t.JSXElement,
sourceName?: string | ComponentPrototypeType,
) {
this.ast = replaceJSXElement(this.ast, targetNodeId, newNode);
if (sourceName) {
this.updateImportSpecifiersByPrototype(sourceName);
}
return this;
}
/**
* 替换 jsx 跟结点的子元素
*/
replaceViewChildren(
childrenNodes: t.JSXElement[],
importDeclarations?: IImportDeclarationPayload[],
) {
if (childrenNodes.length) {
this.ast = replaceRootJSXElementChildren(this.ast, childrenNodes);
}
if (importDeclarations?.length) {
importDeclarations.forEach((item) => {
this.updateImportSpecifiers(item);
});
}
return this;
}
private buildImportMap(
importedModules: Dict<IImportDeclarationPayload | IImportDeclarationPayload[]>,
) {
const map = {};
Object.keys(importedModules).forEach((modName) => {
const mod = importedModules[modName];
(Array.isArray(mod) ? mod : [mod]).forEach((item) => {
if (item.defaultSpecifier) {
map[item.defaultSpecifier] = {
package: modName,
isDefault: true,
};
}
if (item.specifiers.length) {
item.specifiers.forEach((spe) => {
map[spe] = { package: modName };
});
}
});
});
return map;
}
}
/**
* 将节点列表转换为 tree data 嵌套数组
* @param list
*/
function nodeListToTreeData(list: ITangoViewNodeData[]) {
const map: Record<string, ITangoViewNodeData> = {};
list.forEach((item) => {
// 如果不存在,则初始化
if (!map[item.id]) {
map[item.id] = {
...item,
children: [],
};
}
// 是否找到父节点,找到则塞进去
if (item.parentId && map[item.parentId]) {
map[item.parentId].children.push(map[item.id]);
}
});
// 保留根节点
const ret = Object.values(map).filter((item) => !item.parentId);
return ret;
}
+100 -157
View File
@@ -16,20 +16,12 @@ import {
getJSXElementChildrenNames,
namesToImportDeclarations,
getBlockNameByFilename,
queryXFormItemFields,
getPrivilegeCode,
} from '../helpers';
import { DropMethod } from './drop-target';
import { HistoryMessage, TangoHistory } from './history';
import { TangoNode } from './node';
import {
TangoStoreEntryModule,
TangoRouteModule,
TangoServiceModule,
TangoStoreModule,
TangoViewModule,
TangoJsModule,
} from './module';
import { TangoJsModule } from './module';
import { TangoFile, TangoJsonFile, TangoLessFile } from './file';
import { IWorkspace } from './interfaces';
import {
@@ -37,11 +29,14 @@ import {
FileType,
ITangoConfigPackages,
IPageConfigData,
IProjectData,
IServiceFunctionPayload,
} 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';
export interface IWorkspaceOptions {
/**
@@ -96,14 +91,10 @@ export class Workspace extends EventTarget implements IWorkspace {
/**
* 本地区块 { [blockName]: filePath }
* FIXME: 修正类型 Record<string, TangoBlockModule>
*/
localBlocks: Record<string, string>;
/**
* service 模块
*/
serviceModule: TangoServiceModule;
/**
* 路由配置模块
*/
@@ -114,6 +105,10 @@ export class Workspace extends EventTarget implements IWorkspace {
*/
storeEntryModule: TangoStoreEntryModule;
storeModules: Record<string, TangoStoreModule> = {};
serviceModules: Record<string, TangoServiceModule> = {};
/**
* package.json 文件
*/
@@ -161,19 +156,6 @@ export class Workspace extends EventTarget implements IWorkspace {
return this.files.get(this.activeViewFile) as TangoViewModule;
}
/**
* store 模块列表
*/
get storeModules() {
const mods: TangoStoreModule[] = [];
this.files.forEach((file) => {
if (file instanceof TangoStoreModule) {
mods.push(file);
}
});
return mods;
}
/**
* 获取页面列表
*/
@@ -270,36 +252,6 @@ export class Workspace extends EventTarget implements IWorkspace {
return name as ComponentPrototypeType;
}
getProjectData() {
const pages = this.pages.map((item) => {
const filename = this.getFilenameByRoutePath(item.path);
return {
title: item.name,
path: item.path,
filename,
includes: {
variables: (this.getFile(filename) as TangoViewModule).variables,
},
};
});
const stores = this.storeModules.reduce((acc, item) => {
acc[item.name] = { filename: item.filename };
return acc;
}, {});
const services = {
index: {
filename: this.serviceModule?.filename,
functions: Object.keys(this.serviceModule.serviceFunctions),
},
};
const data: IProjectData = {
pages,
stores,
services,
};
return data;
}
/**
* 设置当前路由
* @param routePath 路由路径
@@ -395,10 +347,11 @@ export class Workspace extends EventTarget implements IWorkspace {
break;
case FileType.ServiceModule:
module = new TangoServiceModule(this, props);
this.serviceModule = module; // 仅支持单个 service 文件
this.serviceModules[module.name] = module;
break;
case FileType.StoreModule:
module = new TangoStoreModule(this, props);
this.storeModules[module.name] = module;
break;
case FileType.BlockEntryModule: {
const blockName = getBlockNameByFilename(props.filename);
@@ -524,24 +477,6 @@ export class Workspace extends EventTarget implements IWorkspace {
return ret;
}
listStoreModules() {
return Array.from(this.files.values()).filter(
(file) => file instanceof TangoStoreModule,
) as TangoStoreModule[];
}
getStoreModuleByName(storeName: string) {
let mod;
const files = this.files.values();
for (const file of files) {
if (file instanceof TangoStoreModule && file.name === storeName) {
mod = file;
break;
}
}
return mod;
}
/**
* 删除视图模块
* @param route 路由名称
@@ -641,45 +576,6 @@ export class Workspace extends EventTarget implements IWorkspace {
}
}
/**
* TODO: 仅获取当前视图即可
* 获取当前视图的弹窗列表
* @returns
*/
listModals() {
const modals: Array<{ label: string; value: string }> = [];
const activeViewNodes = this.activeViewModule?.nodes || new Map();
Array.from(activeViewNodes.values()).forEach((node) => {
if (['Modal', 'Drawer'].includes(node.component) && node.props.id) {
modals.push({
label: `${node.component}(${node.props.id})`,
value: node.props.id,
});
}
});
return modals;
}
/**
* TODO: 获取当前视图的表单列表
*/
listForms() {
const forms: Record<string, string[]> = {};
const activeViewNodes = this.activeViewModule?.nodes;
Array.from(activeViewNodes.values()).forEach((node) => {
if (
['XAction', 'XColumnAction', 'XForm', 'XStepForm', 'XSearchForm', 'XFormList'].includes(
node.component,
)
) {
forms[node.id] = queryXFormItemFields(node.rawNode);
}
});
return forms;
}
/**
* 应用代码初始化完成
*/
@@ -696,34 +592,6 @@ export class Workspace extends EventTarget implements IWorkspace {
}
}
/**
* 添加区块文件
* @param files 文件
* @param name 区块文件名
*/
addBlock(files: Object, name: string) {
const blockFiles = {};
Object.keys(files).forEach((path) => {
if (!path.includes('/src')) return;
blockFiles[path.replace('/src/', '')] = files[path];
});
this.addFiles(
Object.keys(blockFiles).map((filepath) => ({
filename: `/src/blocks/${name}/${filepath}`,
code: blockFiles[filepath],
})),
);
const { dependencies } = JSON.parse(files['/package.json']);
Object.keys(dependencies).forEach((pkg) => {
this.packageJson?.setValue('dependencies', (deps) => {
if (!deps[pkg]) {
deps[pkg] = dependencies[pkg];
}
return deps;
});
});
}
/**
* 添加新的模型文件
* @param name 模型名
@@ -752,7 +620,7 @@ export class Workspace extends EventTarget implements IWorkspace {
* @param initValue
*/
addStoreState(storeName: string, stateName: string, initValue: string) {
this.getStoreModuleByName(storeName).addState(stateName, initValue).update();
this.storeModules[storeName]?.addState(stateName, initValue).update();
}
/**
@@ -761,7 +629,7 @@ export class Workspace extends EventTarget implements IWorkspace {
* @param stateName
*/
removeStoreState(storeName: string, stateName: string) {
this.getStoreModuleByName(storeName).removeState(stateName).update();
this.storeModules[storeName]?.removeState(stateName).update();
}
/**
@@ -772,26 +640,65 @@ export class Workspace extends EventTarget implements IWorkspace {
*/
updateModuleCodeByVariablePath(variablePath: string, code: string) {
if (/^stores\.\w+\.\w+$/.test(variablePath)) {
const [type, storeName, stateName] = variablePath.split('.');
this.getStoreModuleByName(storeName).updateState(stateName, code).update();
const [, storeName, stateName] = variablePath.split('.');
this.storeModules[storeName]?.updateState(stateName, code).update();
}
}
/**
* 获取服务函数的详情
* TODO: 不要 services 前缀
* @param serviceKey `services.list` 或 `services.sub.list`
* @returns
*/
getServiceFunction(serviceKey: string) {
const { name, moduleName } = this.parseServiceKey(serviceKey);
if (!name) {
return;
}
return {
name,
moduleName,
config: this.serviceModules[moduleName]?.serviceFunctions[name],
};
}
/**
* 获取服务函数的列表
*/
listServiceFunctions() {
const ret: Record<string, object> = {};
Object.values(this.serviceModules).forEach((module) => {
Object.keys(module.serviceFunctions).forEach((name) => {
const serviceKey =
module.baseConfig.namespace !== 'index'
? name
: [module.baseConfig.namespace, name].join('.');
ret[serviceKey] = module.serviceFunctions[name];
});
});
return ret;
}
/**
* 更新服务函数
*/
updateServiceFunction(payload: any) {
this.serviceModule.updateServiceFunction(payload).update();
updateServiceFunction(payload: IServiceFunctionPayload, moduleName = 'index') {
this.serviceModules[moduleName].updateServiceFunction(payload).update();
}
/**
* 新增服务函数,支持批量添加
*/
addServiceFunction(payload: IServiceFunctionPayload | IServiceFunctionPayload[]) {
addServiceFunction(
payload: IServiceFunctionPayload | IServiceFunctionPayload[],
moduleName = 'index',
) {
if (Array.isArray(payload)) {
this.serviceModule.addServiceFunctions(payload).update();
this.serviceModules[moduleName]?.addServiceFunctions(payload).update();
} else {
this.serviceModule.addServiceFunction(payload).update();
this.serviceModules[moduleName]?.addServiceFunction(payload).update();
}
}
@@ -799,15 +706,15 @@ export class Workspace extends EventTarget implements IWorkspace {
* 删除服务函数
* @param name
*/
removeServiceFunction(name: string) {
this.serviceModule.deleteServiceFunction(name).update();
removeServiceFunction(name: string, moduleName = 'index') {
this.serviceModules[moduleName]?.deleteServiceFunction(name).update();
}
/**
* 更新服务的基础配置
*/
updateServiceBaseConfig(name: string, value: any) {
this.serviceModule.updateBaseConfig(name, value).update();
updateServiceBaseConfig(config: object, moduleName = 'index') {
this.serviceModules[moduleName]?.updateBaseConfig(config).update();
}
/**
@@ -1295,4 +1202,40 @@ 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
View File
@@ -3,6 +3,39 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [0.5.0](https://github.com/netease/tango/compare/@music163/tango-designer@0.4.1...@music163/tango-designer@0.5.0) (2023-10-12)
### Bug Fixes
- refactor expSetter ([#41](https://github.com/netease/tango/issues/41)) ([d63517e](https://github.com/netease/tango/commit/d63517ecb936e4227e70c33e610664316625f4f4))
### Features
- add umd resources config when modifying dependency ([#36](https://github.com/netease/tango/issues/36)) ([e1bab66](https://github.com/netease/tango/commit/e1bab66819cf9a0bd7b87b72b2962498dfd51b2c))
## [0.4.1](https://github.com/netease/tango/compare/@music163/tango-designer@0.4.0...@music163/tango-designer@0.4.1) (2023-10-09)
### Bug Fixes
- always switch to design mode after preview is on ([85898de](https://github.com/netease/tango/commit/85898dee19b6031016679e33a4ce9edbd1089713))
- update export entry ([e429610](https://github.com/netease/tango/commit/e4296106066ad290ac838135fe6a57bc50ad2680))
# [0.4.0](https://github.com/netease/tango/compare/@music163/tango-designer@0.3.0...@music163/tango-designer@0.4.0) (2023-09-22)
### Bug Fixes
- remove useless api ([226d71c](https://github.com/netease/tango/commit/226d71c704f41a756ee777d6de4627a7eb7b4259))
### Features
- add global widget map ([1c72ee6](https://github.com/netease/tango/commit/1c72ee6ac287c1b6fb5eb3806a68b877f3749405))
# [0.3.0](https://github.com/netease/tango/compare/@music163/tango-designer@0.2.1...@music163/tango-designer@0.3.0) (2023-09-21)
### Features
- support multiple service modules & refactor workspace ([a7df29c](https://github.com/netease/tango/commit/a7df29c3debc56b187792d3e203b470e9d368ea5))
## [0.2.1](https://github.com/netease/tango/compare/@music163/tango-designer@0.2.0...@music163/tango-designer@0.2.1) (2023-09-18)
**Note:** Version bump only for package @music163/tango-designer
+6 -6
View File
@@ -1,6 +1,6 @@
{
"name": "@music163/tango-designer",
"version": "0.2.1",
"version": "0.5.0",
"description": "lowcode designer",
"keywords": [
"react"
@@ -33,12 +33,12 @@
"dependencies": {
"@ant-design/icons": "^4.8.0",
"@music163/request": "^0.1.0",
"@music163/tango-context": "^0.1.4",
"@music163/tango-core": "^0.1.4",
"@music163/tango-context": "^0.2.2",
"@music163/tango-core": "^0.2.1",
"@music163/tango-helpers": "^0.1.3",
"@music163/tango-sandbox": "^0.1.4",
"@music163/tango-setting-form": "^0.2.1",
"@music163/tango-ui": "^0.1.4",
"@music163/tango-sandbox": "^0.1.6",
"@music163/tango-setting-form": "^0.2.5",
"@music163/tango-ui": "^0.1.6",
"antd": "^4.24.2",
"cash-dom": "^8.1.2",
"classnames": "^2.3.2",
@@ -11,10 +11,15 @@ import {
Empty,
Popconfirm,
Tooltip,
Alert,
} from 'antd';
import { Box, Text, css, Link } from 'coral-system';
import { PlusOutlined, FunctionOutlined, DeleteOutlined } from '@ant-design/icons';
import { Box, Text, css } from 'coral-system';
import {
PlusOutlined,
FunctionOutlined,
DeleteOutlined,
CopyOutlined,
EyeOutlined,
} from '@ant-design/icons';
import {
filterTreeData,
isFunction,
@@ -96,10 +101,6 @@ export interface EditableVariableTreeProps {
* 选择列表项时的回调
*/
onSelect?: (data: IVariableTreeNode) => void;
/**
* 点击使用此变量按钮时的回调
*/
onUse?: (data: IVariableTreeNode) => void;
/**
* 保存结点定义的回调
*/
@@ -124,10 +125,6 @@ export interface EditableVariableTreeProps {
* 搜索框的后置结点
*/
searchAddonAfter?: React.ReactNode;
/**
* 是否展示预览值的提示信息
*/
showPreviewAlert?: boolean;
}
const previewOptions = [
@@ -145,11 +142,9 @@ export function EditableVariableTree({
onDeleteStore = noop,
getPreviewValue = noop,
onSelect = noop,
onUse = noop,
onSave = noop,
modes = ['add', 'preview', 'define'],
defaultMode = 'preview',
showPreviewAlert = true,
}: EditableVariableTreeProps) {
const [keyword, setKeyword] = useState('');
const [node, setNode] = useState<IVariableTreeNode>();
@@ -184,11 +179,16 @@ export function EditableVariableTree({
</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');
@@ -225,33 +225,15 @@ export function EditableVariableTree({
borderRadius="m"
position="sticky"
top="0"
bodyProps={{ px: 'l' }}
bodyProps={{ px: 'm' }}
>
{mode === 'preview' && (
<>
<Box mb="m" display={showPreviewAlert ? 'block' : 'none'}>
<Alert
message={
<Space>
<Text>
<Text as="strong"></Text>tango.{node.key}
</Text>
<Link onClick={() => onUse(node)}>使</Link>
<CopyClipboard text={`tango.${node.key.replaceAll('.', '?.')}`}>
{(copied) => <Link>{copied ? '已复制' : '复制变量路径'}</Link>}
</CopyClipboard>
</Space>
}
type="info"
/>
</Box>
<ValuePreview
value={getPreviewValue(node)}
onSelect={(valuePath) => {
return ['tango', node.key.replaceAll('.', '?.'), valuePath].join('.');
}}
/>
</>
<ValuePreview
value={getPreviewValue(node)}
onSelect={(valuePath) => {
return ['tango', node.key.replaceAll('.', '?.'), valuePath].join('.');
}}
/>
)}
{mode === 'define' && (
<NodeDefineForm
@@ -308,7 +290,7 @@ export function EditableVariableTreeModal({
{React.cloneElement(trigger, { onClick: on })}
<Modal
title={title}
visible={visible}
open={visible}
onCancel={off}
okButtonProps={{
disabled: !node,
@@ -345,6 +327,11 @@ const varTreeStyle = css`
.ant-tree-indent-unit {
width: 12px;
}
.anticon-function {
margin-left: 4px;
color: var(--tango-colors-text2);
}
`;
interface VariableTreeProps {
@@ -352,17 +339,26 @@ interface VariableTreeProps {
onSelect?: (data: IVariableTreeNode) => void;
onAdd?: (data: IVariableTreeNode) => void;
onRemove?: (data: IVariableTreeNode) => void;
onCopy?: (data: IVariableTreeNode) => void;
onView?: (data: IVariableTreeNode) => void;
showDeleteIcon?: boolean;
showViewIcon?: boolean;
}
function VariableTree({
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.DirectoryTree
<Tree
blockNode
showIcon={false}
defaultExpandAll
treeData={dataSource}
@@ -370,25 +366,18 @@ function VariableTree({
onSelect(detail.node);
}}
titleRender={(node) => {
if (node.showAddChildIcon || node.showRemoveIcon) {
const isLeaf = !node.children;
if (isLeaf) {
const isDeletable = node.showDeleteIcon ?? showDeleteIcon;
return (
<Box display="flex" justifyContent="space-between" alignItems="center" pr="l">
<Text>{node.title}</Text>
<Box display="flex" justifyContent="space-between" alignItems="center">
<Text>
{node.title}
{node.type === 'function' && <FunctionOutlined />}
</Text>
<Box>
{node.showAddChildIcon && (
<Tooltip title={`${node.title} 中添加变量`}>
<Button
type="text"
size="small"
icon={<PlusOutlined />}
onClick={(e) => {
e.stopPropagation();
onAdd(node);
}}
/>
</Tooltip>
)}
{node.showRemoveIcon && (
{isDeletable && (
<Popconfirm
title="确认删除吗?该操作会导致引用此模型的代码报错,请谨慎操作!"
onConfirm={() => onRemove(node)}
@@ -403,14 +392,61 @@ function VariableTree({
/>
</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>
<Box display="flex" alignItems="center" justifyContent="space-between">
<Text mr="m">{node.title}</Text>
{node.type === 'function' && <FunctionOutlined />}
<Box>
{node.showAddChildIcon && (
<Tooltip title={`${node.title} 中添加变量`}>
<Button
type="text"
size="small"
icon={<PlusOutlined />}
onClick={(e) => {
e.stopPropagation();
onAdd(node);
}}
/>
</Tooltip>
)}
</Box>
</Box>
);
}}
+2 -1
View File
@@ -27,6 +27,7 @@ export interface DesignerProps extends IDesignerContext, ITangoEngineContext {
export function Designer(props: DesignerProps) {
const {
engine,
config,
theme: themeProp,
sandboxQuery,
remoteServices = {},
@@ -42,7 +43,7 @@ export function Designer(props: DesignerProps) {
return (
<SystemProvider theme={theme} prefix="--tango">
<ConfigProvider locale={zhCN}>
<TangoEngineProvider value={{ engine }}>
<TangoEngineProvider value={{ engine, config }}>
<DesignerProvider value={{ sandboxQuery, remoteServices }}>{children}</DesignerProvider>
</TangoEngineProvider>
</ConfigProvider>
+1
View File
@@ -11,6 +11,7 @@ export * from './sandbox';
export * from './sidebar';
export * from './toolbar';
export * from './selection-menu';
export * from './widgets';
export { register as registerSetter } from '@music163/tango-setting-form';
+3 -5
View File
@@ -19,7 +19,6 @@ interface ISandboxEventHandlerConfig {
export type SandboxProps = Omit<CodeSandboxProps, 'files' | 'eventHandlers' | 'onMessage'> & {
isPreview?: boolean;
selectionTools?: SelectionToolsProps['actions'];
builtinSelectionMenuMap?: SelectionToolsProps['builtinActionMap'];
/**
* tangoConfigJson 处理器
*/
@@ -32,10 +31,10 @@ export type SandboxProps = Omit<CodeSandboxProps, 'files' | 'eventHandlers' | 'o
onLoad?: (config?: ISandboxEventHandlerConfig) => void;
};
export type CombinedSandboxRef = {
export interface CombinedSandboxRef {
designSandbox: CodeSandbox;
previewSandbox: CodeSandbox;
};
}
const LANDING_PAGE_PATH = '/__background_landing_page__';
@@ -281,7 +280,6 @@ export const Sandbox = observer(
({
isPreview: isPreviewProp,
selectionTools,
builtinSelectionMenuMap,
bundlerURL,
mode = 'combined',
...props
@@ -352,7 +350,7 @@ export const Sandbox = observer(
}}
/>
<Simulator>
<Viewport selectionTools={selectionTools} builtinActionMap={builtinSelectionMenuMap}>
<Viewport selectionTools={selectionTools}>
{mode === 'single' && (
<DesignSandbox
ref={sandboxRef}
@@ -43,7 +43,7 @@ export function EventSetter(props: EventSetterProps) {
const { actionVariables, routeOptions } = useWorkspaceData();
const workspace = useWorkspace();
const modalOptions = workspace.listModals() || [];
const modalOptions = workspace.activeViewModule.listModals() || [];
const handleChange = useCallback<FormItemComponentProps['onChange']>(
(nextValue: any, ...args) => {
@@ -1,6 +1,6 @@
import React, { useState, useEffect, useCallback } from 'react';
import { Box, css } from 'coral-system';
import { Alert, Modal, Tabs } from 'antd';
import { Modal } from 'antd';
import { value2code, isValidExpressionCode } from '@music163/tango-core';
import {
isVariableString,
@@ -9,12 +9,12 @@ import {
useBoolean,
getValue,
} from '@music163/tango-helpers';
import { CloseCircleFilled, ExpandAltOutlined, QuestionCircleOutlined } from '@ant-design/icons';
import { IconButton, Panel, InputCode, ChatInput } from '@music163/tango-ui';
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 { useRemoteServices, useSandboxQuery } from '../context';
import { useSandboxQuery } from '../context';
export const expressionValueValidate = (value: string) => {
if (isVariableString(value)) {
@@ -190,38 +190,24 @@ export function ExpressionModal({
[workspace],
);
const sandbox = useSandboxQuery();
const remoteServices = useRemoteServices();
const { expressionVariables } = useWorkspaceData();
const evaluateContext = sandbox.window;
const gptService = remoteServices?.GptService;
const handleExpInputChange = (val: string) => {
setExp(val?.trim());
};
return (
<Modal
title={title}
closable={false}
destroyOnClose
width="70%"
open={visible}
onCancel={onCancel}
onOk={() => {
onOk(exp);
}}
destroyOnClose
>
<Panel
title="绑定的表达式"
subTitle={subTitle}
extra={
<IconButton
icon={<QuestionCircleOutlined />}
tooltip="你可以直接编辑下面的表达式,表达式需要使用 { } 进行包裹,点击此图标可以查看详细文档。"
href="https://music-doc.st.netease.com/st/tango-docs/docs/guide/javascript/overview"
/>
}
shape="solid"
borderRadius="m"
>
<Panel title={`${title} 设置为引用变量或自定义表达式`} subTitle={subTitle} shape="solid">
<InputCode
shape="inset"
minHeight="56px"
@@ -233,91 +219,49 @@ export function ExpressionModal({
autoCompleteOptions={autoCompleteOptions}
/>
</Panel>
<Box mt="m" maxHeight={400} overflow="auto">
<Tabs type="card" size="small">
<Tabs.TabPane key="list" tab="从变量列表中选择">
<Box my="m">
<Alert
type="info"
message="你可以直接从下方的列表中选择已定义的变量"
banner
closable
/>
</Box>
<EditableVariableTree
// height={400}
dataSource={dataSource || expressionVariables}
onUse={(node) => {
let str;
if (/^(stores|services)\./.test(node.key)) {
str = `{tango.${node.key.replaceAll('.', '?.')}}`;
} else {
str = `{${node.key}}`;
}
setExp(str);
}}
onAddVariable={(storeName, data) => {
onAction('addStoreState', [storeName, data.name, data.initialValue]);
}}
onDeleteVariable={(storeName, stateName) => {
onAction('removeStoreState', [storeName, stateName]);
}}
onDeleteStore={(storeName) => {
onAction('removeStoreModule', [storeName]);
}}
onSave={(code, node) => {
onAction('updateModuleCodeByVariablePath', [node.key, code]);
}}
getPreviewValue={(node) => {
if (!node || !node.key) {
return;
}
<Panel title="从变量列表中选中" shape="solid" borderTop="0" maxHeight={400} overflow="auto">
<EditableVariableTree
dataSource={dataSource || expressionVariables}
onSelect={(node) => {
if (!node.key) {
return;
}
if (node.key.split('.').length < 2) {
return;
}
let str;
if (/^(stores|services)\./.test(node.key)) {
str = `{tango.${node.key.replaceAll('.', '?.')}}`;
} else {
str = `{${node.key}}`;
}
setExp(str);
}}
onAddVariable={(storeName, data) => {
onAction('addStoreState', [storeName, data.name, data.initialValue]);
}}
onDeleteVariable={(storeName, stateName) => {
onAction('removeStoreState', [storeName, stateName]);
}}
onDeleteStore={(storeName) => {
onAction('removeStoreModule', [storeName]);
}}
onSave={(code, node) => {
onAction('updateModuleCodeByVariablePath', [node.key, code]);
}}
getPreviewValue={(node) => {
if (!node || !node.key) {
return;
}
if (node.type === 'function') {
return node.raw;
}
if (node.type === 'function') {
return node.raw;
}
return getValue(evaluateContext['tango'], node.key);
}}
/>
</Tabs.TabPane>
<Tabs.TabPane key="ai" tab="使用 AI 助手生成" disabled={!gptService?.textCompletion}>
<ChatInput
placeholder="例如:实现一个函数,从给定的数组中过滤出name为bob的数据"
onGenerate={(text) =>
gptService
.textCompletion({
prompt: getExpressionCode(text),
model: 'gpt-3.5-turbo',
temperature: 0.5,
topP: 1,
presencePenalty: 0,
frequencyPenalty: 0,
mode: 'chat',
})
.then((res: any) => {
return res?.detail?.choices?.[0]?.text;
})
}
onApply={(preview) => {
setExp(`{${preview}}`);
}}
showCloseIcon={false}
/>
</Tabs.TabPane>
</Tabs>
</Box>
return getValue(evaluateContext['tango'], node.key);
}}
/>
</Panel>
</Modal>
);
}
function getExpressionCode(userInput: string) {
return `你是一个代码生成助手。你使用的编程语言是 JavaScript。不要解释代码,只返回代码块本身,代码必须包裹在 {} 内部。
标准的输出格式如下:
1. {this.a}
2. {() => {}}
3. {tango.stores.user.name}
${userInput}
`;
}
@@ -123,7 +123,6 @@ export function ModelSetter({ value, onChange }: FormItemComponentProps) {
onBlur={onInputBlur}
suffix={
<EditableVariableTreeModal
showPreviewAlert={false}
title="同步到的变量"
trigger={
<Tooltip title="从模型列表选择" placement="topRight">
@@ -1,9 +1,9 @@
import React, { useState, useEffect, useMemo } from 'react';
import { observer, useWorkspace } from '@music163/tango-context';
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, Menu, Panel, JsonView, Search } from '@music163/tango-ui';
import { Form, InputCode, Panel, JsonView, Search } from '@music163/tango-ui';
import {
isNil,
isVariableString,
@@ -11,8 +11,10 @@ import {
isValidFunctionCode,
logger,
code2object,
filterTreeData,
} from '@music163/tango-helpers';
import { useSandboxQuery } from '../../context';
import { VariableTree } from '../../components';
/*
* 服务函数的操作类型
@@ -39,7 +41,7 @@ enum ServiceFunctionMethodType {
* requestType
* headers: Content-Type
*/
enum ESFHTTRequestType {
enum DataServiceRequestType {
'application/json' = 'json',
'application/x-www-form-urlencoded' = 'x-www-form-urlencoded',
}
@@ -65,11 +67,11 @@ export const getApiDefaultName = (url: string) =>
// 方法名以数字开头,添加 api 前缀
.replace(/^\d/, (str) => `api${str}`);
type DataServiceViewProps = {
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) => ({
@@ -78,9 +80,9 @@ const httpMethods = Object.keys(ServiceFunctionMethodType).map((key) => ({
}));
// requestType 类型
const requestTypes = Object.keys(ESFHTTRequestType).map((key) => ({
const requestTypes = Object.keys(DataServiceRequestType).map((key) => ({
label: key,
value: ESFHTTRequestType[key],
value: DataServiceRequestType[key],
}));
const detailFormStyle = css`
@@ -90,31 +92,32 @@ const detailFormStyle = css`
`;
const DataSourceView = observer(({ onAdd, onUpdate, onDelete }: DataServiceViewProps) => {
const [serviceKey, setServiceKey] = useState<string>();
const [serviceData, setServiceData] = useState<any>();
const [keyword, setKeyword] = useState<string>();
const [visible, setVisible] = useState(null);
const workspace = useWorkspace();
const sandbox = useSandboxQuery();
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 serviceMap = workspace.serviceModule.serviceFunctions;
const serviceItems = useMemo(() => {
if (!serviceMap) {
return [];
}
let list = Object.keys(serviceMap).map((key) => ({
key,
label: key,
note: serviceMap[key].app || 'http',
deletable: true,
deletableConfirm: '确定删除此服务函数吗?',
}));
if (keyword) {
list = list.filter((item) => item.label.includes(keyword));
}
return list;
}, [serviceMap, keyword]);
const serviceData = serviceMap?.[serviceKey];
const isAddMode = !serviceKey;
const isAddMode = serviceData && !serviceData.name;
return (
<Box className="ServiceFunctionList" display="flex" borderTopColor="line.normal">
@@ -136,52 +139,48 @@ const DataSourceView = observer(({ onAdd, onUpdate, onDelete }: DataServiceViewP
<Button
block
onClick={() => {
setServiceKey(undefined);
setVisible('detail');
setServiceData({});
}}
>
</Button>
<Search placeholder="搜索服务函数" onChange={setKeyword} />
</Box>
<Menu
activeKey={serviceKey}
items={serviceItems}
onItemClick={(key) => {
setServiceKey(key);
setVisible('detail');
<VariableTree
dataSource={dataSource}
showDeleteIcon
onSelect={(item) => {
setServiceData({ key: item.key, ...workspace.getServiceFunction(item.key) });
}}
onDelete={(key) => {
const values = {
name: key,
...workspace.serviceModule.serviceFunctions[key],
};
workspace.removeServiceFunction(key);
onDelete && onDelete(values);
setVisible(null);
onRemove={(item) => {
workspace.removeServiceFunction(item.key);
onDelete?.(item);
}}
/>
</Panel>
</Box>
<Box width="65%" overflow="auto" css={detailFormStyle}>
{visible === 'detail' && (
{serviceData && (
<>
<Panel title={isAddMode ? '新建服务函数' : '服务函数详情'}>
<ServiceDetailForm
key={serviceKey || 'createSF'}
serviceKeys={Object.keys(serviceMap)}
key={serviceData.key || 'createSF'}
serviceModules={serviceModules}
serviceKeys={serviceFunctions}
initialValues={
isAddMode
? {}
? {
moduleName: 'index',
}
: {
name: serviceKey,
name: serviceData.name,
moduleName: serviceData.moduleName,
method: 'get',
...serviceData,
...serviceData.config,
}
}
onCancel={() => {
setServiceKey(undefined);
setVisible(null);
setServiceData(undefined);
}}
onSubmit={(values, mode: ServiceFunctionOperationModeType) => {
function shapeServiceValues(val: any) {
@@ -194,28 +193,27 @@ const DataSourceView = observer(({ onAdd, onUpdate, onDelete }: DataServiceViewP
return shapeValues;
}
// 移除掉不必要的属性
const data = shapeServiceValues(values);
const { moduleName, ...data } = shapeServiceValues(values);
if (mode === ServiceFunctionOperationModeType.ADD) {
workspace.addServiceFunction(data);
workspace.addServiceFunction(data, moduleName);
onAdd && onAdd(values);
} else if (mode === ServiceFunctionOperationModeType.UPDATE) {
workspace.updateServiceFunction(data);
workspace.updateServiceFunction(data, moduleName);
onUpdate && onUpdate(values);
}
setServiceKey(values.name);
}}
/>
</Panel>
{serviceKey ? (
{serviceData?.key ? (
<ServiceFunctionPreview
key={serviceKey}
key={serviceData.key}
appContext={sandbox?.window['tango']}
functionName={serviceKey}
functionName={serviceData.key}
/>
) : null}
</>
)}
{!visible && (
{!serviceData && (
<Box py="xxl">
<Empty description="请从左侧选择数据服务函数或新建数据服务函数" />
</Box>
@@ -227,12 +225,14 @@ const DataSourceView = observer(({ onAdd, onUpdate, onDelete }: DataServiceViewP
interface ServiceDetailFormProps extends FormProps {
serviceKeys?: string[];
serviceModules?: any[];
onCancel?: () => void;
onSubmit?: (values: any, mode: ServiceFunctionOperationModeType) => void;
}
function ServiceDetailForm({
serviceKeys = [],
serviceModules = [],
onCancel,
onSubmit,
initialValues,
@@ -274,6 +274,20 @@ function ServiceDetailForm({
}}
{...formProps}
>
<Form.Item
label="所属模块"
name="moduleName"
rules={[
{
required: true,
},
]}
component="select"
componentProps={{
options: serviceModules,
disabled: disabled || isModifyMode,
}}
/>
<Form.Item
label="方法名"
name="name"
@@ -282,7 +296,9 @@ function ServiceDetailForm({
{ pattern: /^[a-z]\w+$/, message: '请输入合法的方法名称' },
!isModifyMode && {
validator(_, value) {
const isExist = serviceKeys.includes(value);
const isExist = serviceKeys.includes(
['services', form.getFieldValue('moduleName'), value].join('.'),
);
return isExist
? Promise.reject(new Error('重复的方法名,请换一个!'))
: Promise.resolve();
+298 -149
View File
@@ -1,4 +1,4 @@
import React, { useMemo, useState } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import { observer, useWorkspace } from '@music163/tango-context';
import {
Button,
@@ -9,15 +9,17 @@ import {
Tooltip,
Popconfirm,
Radio,
Select,
message,
Tag,
Checkbox,
ModalProps,
} from 'antd';
import { FormListProps } from 'antd/lib/form';
import { Box, Text } from 'coral-system';
import semverLt from 'semver/functions/lt';
import semverValid from 'semver/functions/valid';
import { ConfigGroup, ConfigItem } from '@music163/tango-ui';
import { PlusOutlined, QuestionCircleOutlined } from '@ant-design/icons';
import { MinusCircleOutlined, PlusOutlined, QuestionCircleOutlined } from '@ant-design/icons';
import { useBoolean } from '@music163/tango-helpers';
import { isUndefined } from 'lodash-es';
import { Workspace } from '@music163/tango-core';
@@ -177,18 +179,9 @@ function RenderItem({
onUpgrade,
showTags = true,
}: RenderItemProps) {
const [version, setVersion] = useState<string>();
const [versionList, setVersionList] = useState<Array<{ label: string; value: string }>>([]);
const [open, { on, off }] = useBoolean(false);
const workspace = useWorkspace() as Workspace;
// const remoteServices = useRemoteServices();
// useEffect(() => {
// remoteServices?.DependencyService?.listPackageVersions({ packageName: record.name }).then(
// (data: any) => setVersionList((data || []).map((v: string) => ({ label: v, value: v }))),
// );
// }, [remoteServices, record]);
const basePackage = useMemo(() => {
if (type !== DependencyItemType. || !templateBaseDependencies) return undefined;
@@ -198,9 +191,29 @@ function RenderItem({
/**
* 处理依赖升级的逻辑
*/
function onDepUpgrade() {
function onDepUpgrade(values: Record<string, any>) {
// 根据是否有 packages 字段,判断是否已升级为新的规范
const hasUpgraded = !!workspace.tangoConfigJson?.getValue('packages');
const { version, umd, library, resources, designerResources } = values || {};
let packageConfig = record?.package;
if (umd === false && packageConfig) {
packageConfig = {
...packageConfig,
version,
library: undefined,
resources: undefined,
designerResources: undefined,
};
} else {
packageConfig = {
...packageConfig,
version,
library,
resources,
designerResources,
};
}
switch (type) {
case DependencyItemType.业务组件:
@@ -210,7 +223,7 @@ function RenderItem({
version,
hasUpgraded
? {
package: record?.package,
package: packageConfig,
}
: {},
);
@@ -225,7 +238,7 @@ function RenderItem({
basePackage?.version || 'latest',
hasUpgraded
? {
package: record?.package,
package: packageConfig,
}
: {},
);
@@ -295,7 +308,6 @@ function RenderItem({
}
function onCloseModal() {
setVersion(undefined);
off();
}
@@ -317,10 +329,8 @@ function RenderItem({
const { resources } = packageInfo || {};
const dependencies = workspace.packageJson?.getValue('dependencies');
const hasUmd = !!resources?.length;
const unsafeUmd =
hasUmd && resources.some((e: string) => /\/\/unpkg\.f[nt]\.netease\.com\//.test(e));
const packageJsonOnly = dependencies?.[name] && !packageInfo;
const tangoConfigJsonOnly = !dependencies?.[name] && packageInfo;
const packageJsonIncluded = !!dependencies?.[name];
const tangoConfigJsonIncluded = !!packageInfo;
return [
hasUmd && (
@@ -330,21 +340,14 @@ function RenderItem({
</Tag>
</Tooltip>
),
unsafeUmd && (
<Tooltip title="依赖的 UMD 资源来自自建 unpkg 服务,稳定性不可靠,建议修改 tango.config.json 更换为正式 cdn 资源地址">
<Tag key="unsafeUmd" color="orange">
CDN
</Tag>
packageJsonIncluded && (
<Tooltip title="依赖在 package.json 中存在">
<Tag key="packageJson">package</Tag>
</Tooltip>
),
packageJsonOnly && (
<Tooltip title="此依赖package.json 中存在">
<Tag key="packageJsonOnly">package.json</Tag>
</Tooltip>
),
tangoConfigJsonOnly && (
<Tooltip title="此依赖只在 tango.config.json 中存在">
<Tag key="packageJsonOnly">tango.config.json</Tag>
tangoConfigJsonIncluded && (
<Tooltip title="此依赖在 tango.config.json 中存在">
<Tag key="tangoConfigJson">tango.config</Tag>
</Tooltip>
),
].filter((e) => e);
@@ -356,7 +359,7 @@ function RenderItem({
type !== DependencyItemType.
? [
<a key="upgrade" onClick={on}>
</a>,
<Popconfirm onConfirm={onDepDelete} title={`确认删除依赖 ${record.name}`}>
<a key="delete"></a>
@@ -366,7 +369,7 @@ function RenderItem({
? [
<Popconfirm
title={`确认升级 ${record.name}${basePackage.version} 吗?`}
onConfirm={onDepUpgrade}
onConfirm={() => onDepUpgrade({ version: basePackage.version })}
>
<a key="upgrade"></a>
</Popconfirm>,
@@ -394,28 +397,12 @@ function RenderItem({
/>
}
/>
<Modal
<DependencyConfigModal
open={open}
title={`升级依赖:${record.name}`}
record={record}
onCancel={onCloseModal}
onOk={onDepUpgrade}
>
<Text mr="6px">
&nbsp;
<Tooltip title="只提供正式版本列表,Beta版本依赖需要手动升级">
<QuestionCircleOutlined />
</Tooltip>
</Text>
<Select
style={{ width: 160 }}
showSearch
defaultValue={record.version}
options={versionList || []}
onChange={(value: string) => setVersion(value)}
placeholder="请选择要升级的版本号"
optionFilterProp="value"
/>
</Modal>
onOk={(event, { values }) => onDepUpgrade(values)}
/>
</List.Item>
);
}
@@ -447,111 +434,114 @@ interface DependencyModalProps {
templateBaseDependencies: Record<string, any>;
onBizDependencyAdd: Function;
onBaseDependencyAdd: Function;
record?: Record<string, any>;
}
function AddDependencyModal({
templateBaseDependencies,
onBizDependencyAdd,
onBaseDependencyAdd,
}: DependencyModalProps) {
record,
...rest
}: DependencyModalProps & ModalProps) {
// 依赖类型
const [type, setType] = useState<DependencyItemType>(DependencyItemType.);
const [open, { on, off }] = useBoolean();
const [form] = Form.useForm();
const workspace = useWorkspace();
const sandbox = useSandboxQuery();
const hasUpgraded = !!workspace.tangoConfigJson?.getValue('packages');
const onFinishCallback = (name?: string) => {
name && message.success(`${name} 添加成功`);
// FIXME: 修复沙箱添加组件后 HMR 失效的 bug,添加完刷新
sandbox.reload();
off();
form.resetFields();
};
const onOk = () => {
form.validateFields().then((values) => {
const { name, version } = values;
// 根据是否有 packages 字段,判断是否已升级为新的规范
const hasUpgraded = !!workspace.tangoConfigJson?.getValue('packages');
const onOk: DependencyConfigModalProps['onOk'] = (event, { values }) => {
const { type, name, version, umd, library, resources, designerResources } = values;
switch (type) {
case DependencyItemType.: {
workspace.updateDependency(name, version);
break;
}
// case DependencyItemType.业务组件: {
// const targetBizInfo = (bizList as any)?.find(
// (biz: { packageName: string }) => biz.packageName === name,
// );
// onBizDependencyAdd &&
// onBizDependencyAdd({
// ...values,
// detail: targetBizInfo,
// });
// break;
// }
case DependencyItemType.: {
const basePackage = templateBaseDependencies[name];
// 添加 package.json
workspace.updateDependency(
name,
basePackage.version || 'latest',
hasUpgraded
? {
package: basePackage.package,
}
: {},
);
// 配置已经合并为新版,不需要走旧的逻辑
if (!hasUpgraded) {
// 更新 tango.config.json baseDependencies
workspace.tangoConfigJson.setValue('baseDependencies', (targetValue) => {
const bases = targetValue || [];
(bases as string[]).push(name);
return bases;
});
// 更新 tango.config.json externals
workspace.tangoConfigJson.setValue('sandbox.externals', (targetValue) => {
targetValue[name] = basePackage?.global;
return targetValue;
});
// 更新 tango.config.json externalResources
workspace.tangoConfigJson
.setValue('sandbox.externalResources', (targetValue) => {
basePackage.resources.forEach((resource: string) => {
(targetValue as string[]).push(
resource.replace('{latest_version}', basePackage.version),
);
});
return targetValue;
})
.update();
}
onBaseDependencyAdd && onBaseDependencyAdd(basePackage);
break;
}
default:
break;
switch (type) {
case DependencyItemType.: {
workspace.updateDependency(
name,
version,
hasUpgraded && umd
? {
package: {
type: 'dependency',
...record?.package,
version,
library,
resources,
designerResources,
},
}
: {},
);
break;
}
onFinishCallback(values?.name);
});
};
// case DependencyItemType.业务组件: {
// const targetBizInfo = (bizList as any)?.find(
// (biz: { packageName: string }) => biz.packageName === name,
// );
// onBizDependencyAdd &&
// onBizDependencyAdd({
// ...values,
// detail: targetBizInfo,
// });
// break;
// }
case DependencyItemType.: {
const basePackage = templateBaseDependencies[name];
// 添加 package.json
workspace.updateDependency(
name,
basePackage.version || 'latest',
hasUpgraded
? {
package: basePackage.package,
}
: {},
);
const onValuesChange = ({ type: depType }: { type: DependencyItemType; name: string }) => {
if (depType) {
// type 变更触发
form.resetFields(['name', 'version']);
setType(depType);
// 配置已经合并为新版,不需要走旧的逻辑
if (!hasUpgraded) {
// 更新 tango.config.json baseDependencies
workspace.tangoConfigJson.setValue('baseDependencies', (targetValue) => {
const bases = targetValue || [];
(bases as string[]).push(name);
return bases;
});
// 更新 tango.config.json externals
workspace.tangoConfigJson.setValue('sandbox.externals', (targetValue) => {
targetValue[name] = basePackage?.global;
return targetValue;
});
// 更新 tango.config.json externalResources
workspace.tangoConfigJson
.setValue('sandbox.externalResources', (targetValue) => {
basePackage.resources.forEach((resource: string) => {
(targetValue as string[]).push(
resource.replace('{latest_version}', basePackage.version),
);
});
return targetValue;
})
.update();
}
onBaseDependencyAdd && onBaseDependencyAdd(basePackage);
break;
}
default:
break;
}
onFinishCallback(values?.name);
};
const onCloseModal = () => {
function onCloseModal() {
off();
form.resetFields(['name', 'version']);
};
}
return (
<Box px="l" py="m">
@@ -559,31 +549,155 @@ function AddDependencyModal({
<PlusOutlined />
</Button>
<Modal
width={560}
<DependencyConfigModal
open={open}
title="添加依赖"
title={record ? `修改依赖:${record.name}` : '添加依赖'}
onOk={onOk}
onCancel={onCloseModal}
destroyOnClose
{...rest}
/>
</Box>
);
}
type DependencyConfigModalProps = Omit<ModalProps, 'onOk'> & {
record?: Record<string, any>;
onOk: (
event: React.MouseEvent<HTMLElement, MouseEvent>,
data: {
values: Record<string, any>;
hasUpgraded: boolean;
},
) => any;
};
function ResourceFormList(props: Omit<FormListProps, 'children'>) {
return (
<Form.List {...props}>
{(fields, { add, remove }, { errors }) => (
<>
{fields.map((field) => (
<Form.Item key={field.key}>
<Box display="flex" alignItems="center">
<Form.Item
label="资源地址"
{...field}
rules={[{ required: true, whitespace: true, type: 'url' }]}
noStyle
>
<Input
placeholder="资源地址中的版本号可用 {{version}} 替代"
style={{ width: '100%' }}
/>
</Form.Item>
<Box width="20px" fontSize="18px" ml="6px" textAlign="center" flex="none">
<a onClick={() => remove(field.name)}>
<MinusCircleOutlined />
</a>
</Box>
</Box>
</Form.Item>
))}
<Button type="dashed" onClick={() => add()} block>
</Button>
<Form.ErrorList errors={errors} />
</>
)}
</Form.List>
);
}
function DependencyConfigModal({
record,
open,
onOk,
onCancel,
...rest
}: DependencyConfigModalProps) {
// 依赖类型
const [type, setType] = useState<DependencyItemType>(DependencyItemType.);
const [form] = Form.useForm();
const workspace = useWorkspace();
// 根据是否有 packages 字段,判断是否已升级为新的规范
const hasUpgraded = !!workspace.tangoConfigJson?.getValue('packages');
// const [versionList, setVersionList] = useState<Array<{ label: string; value: string }>>([]);
// const remoteServices = useRemoteServices();
// useEffect(() => {
// remoteServices?.DependencyService?.listPackageVersions({ packageName: record.name }).then(
// (data: any) => setVersionList((data || []).map((v: string) => ({ label: v, value: v }))),
// );
// }, [remoteServices, record]);
useEffect(() => {
if (open && form) {
form.resetFields();
if (record) {
setType(record?.type);
form.setFieldsValue({
...record,
...record?.package,
umd: !!record?.package?.resources?.length,
});
}
}
}, [record, open, form]);
const onSubmit = (event: React.MouseEvent<HTMLElement, MouseEvent>) => {
form.validateFields().then((values) => {
if (onOk) {
onOk(event, {
values,
hasUpgraded,
});
}
});
};
const onValuesChange = ({ type: depType }: { type: DependencyItemType; name: string }) => {
if (depType) {
// type 变更触发
form.resetFields();
form.setFieldValue('type', depType);
setType(depType);
}
};
return (
<>
<Modal
width={560}
open={open}
title={record ? `修改依赖:${record.name}` : '添加依赖'}
onOk={onSubmit}
onCancel={onCancel}
destroyOnClose
{...rest}
>
<Form
form={form}
layout="vertical"
autoComplete="off"
initialValues={{ type: DependencyItemType. }}
initialValues={{ type }}
onValuesChange={onValuesChange}
>
<Form.Item label="依赖类型" name="type">
<Radio.Group value={type}>
{/* <Radio.Button value={DependencyItemType.业务组件}>业务组件</Radio.Button> */}
<Radio.Button value={DependencyItemType.}></Radio.Button>
<Radio.Button value={DependencyItemType.}></Radio.Button>
</Radio.Group>
</Form.Item>
<Form.Item label="npm包名" name="name" rules={[{ required: true }]}>
<Input placeholder="输入依赖名" />
</Form.Item>
{!record && (
<>
<Form.Item label="依赖类型" name="type" rules={[{ required: true }]}>
<Radio.Group>
{/* <Radio.Button value={DependencyItemType.业务组件}>业务组件</Radio.Button> */}
<Radio.Button value={DependencyItemType.}></Radio.Button>
<Radio.Button value={DependencyItemType.}></Radio.Button>
</Radio.Group>
</Form.Item>
<Form.Item label="npm 包名" name="name" rules={[{ required: true }]}>
<Input placeholder="输入依赖名" />
</Form.Item>
</>
)}
<Form.Item
label="版本号"
name="version"
@@ -591,8 +705,43 @@ function AddDependencyModal({
>
<Input placeholder="输入版本号" />
</Form.Item>
{hasUpgraded && (
<Form.Item name="umd" valuePropName="checked">
<Checkbox>使 UMD </Checkbox>
</Form.Item>
)}
<Form.Item noStyle shouldUpdate>
{() =>
form.getFieldValue('umd') && (
<>
<Form.Item label="全局变量名" name="library">
<Input placeholder="输入 UMD 资源挂载在全局变量下的变量名" />
</Form.Item>
<Form.Item label="资源地址" required>
<ResourceFormList
name="resources"
rules={[
{
validator: async (rule, value) => {
if (!value?.length) {
throw new Error('资源地址不能为空');
}
},
},
]}
/>
</Form.Item>
{form.getFieldValue('type') === DependencyItemType. && (
<Form.Item label="设计器资源地址">
<ResourceFormList name="designerResources" />
</Form.Item>
)}
</>
)
}
</Form.Item>
</Form>
</Modal>
</Box>
</>
);
}
+17 -54
View File
@@ -1,21 +1,11 @@
import React, { useMemo } from 'react';
import cx from 'classnames';
import { Box, Text, css, HTMLCoralProps } from 'coral-system';
import {
ClusterOutlined,
FunctionOutlined,
ApiOutlined,
AppstoreOutlined,
BuildOutlined,
} from '@ant-design/icons';
import { Badge } from 'antd';
import { ReactComponentProps } from '@music163/tango-helpers';
import { observer, useDesigner } from '@music163/tango-context';
import { ResizableBox } from './resizable-box';
import { OutlinePanel } from './outline-panel';
import { DependencyPanel } from './dependency-panel';
import { VariablePanel } from './variable-panel';
import { DataSourcePanel } from './datasource-panel';
import { getWidget } from '../widgets';
const sidebarStyle = css`
position: relative;
@@ -51,7 +41,6 @@ export interface SidebarProps extends ReactComponentProps {
* 底部附加内容
*/
footer?: React.ReactNode;
builtinPanelMap?: Record<string, SidebarPanelItemProps>;
}
export interface SidebarPanelItemProps
@@ -79,54 +68,21 @@ export interface SidebarPanelItemProps
* 面板宽度
*/
width?: number;
/**
* 如果 key 匹配到内置组件的话,传递给子节点的属性
*/
widgetProps?: object;
}
const INTERNAL_SIDEBAR_PANEL_MAP: Record<string, SidebarPanelItemProps> = {
components: {
label: '组件',
title: '组件列表',
icon: <AppstoreOutlined />,
},
outline: {
label: '结构',
title: '结构',
icon: <BuildOutlined />,
children: <OutlinePanel />,
},
dependency: {
label: '依赖',
title: '项目依赖',
icon: <ClusterOutlined />,
children: <DependencyPanel />,
},
variables: {
label: '变量',
title: '变量管理',
icon: <FunctionOutlined />,
children: <VariablePanel />,
width: 440,
},
dataSource: {
label: '接口',
title: '数据源与接口',
icon: <ApiOutlined />,
children: <DataSourcePanel />,
width: 600,
},
};
function BaseSidebarPanel({
panelWidth: defaultPanelWidth = 280,
builtinPanelMap = INTERNAL_SIDEBAR_PANEL_MAP,
footer,
children,
}: SidebarProps) {
function BaseSidebarPanel({ panelWidth: defaultPanelWidth = 280, footer, children }: SidebarProps) {
const items = useMemo(() => {
const ret: Record<React.Key, SidebarPanelItemProps> = {};
React.Children.forEach(children, (child) => {
if (child && React.isValidElement(child)) {
const widget = getWidget(['sidebar', child.key].join('.'));
const fallbackNode = widget ? React.createElement(widget, child.props.widgetProps) : null;
ret[child.key] = {
...builtinPanelMap[child.key],
children: fallbackNode,
...child.props,
};
}
@@ -203,7 +159,14 @@ function BaseSidebarPanel({
);
}
BaseSidebarPanel.Item = function ({ key, title, doc, isFloat, width }: SidebarPanelItemProps) {
BaseSidebarPanel.Item = function ({
key,
title,
doc,
isFloat,
width,
widgetProps,
}: SidebarPanelItemProps) {
return <></>;
};
+4 -17
View File
@@ -6,7 +6,7 @@ import { ISelectedItemData, isString, noop } from '@music163/tango-helpers';
import { observer, useDesigner, useWorkspace } from '@music163/tango-context';
import { IconButton } from '@music163/tango-ui';
import { getDragGhostElement } from '../helpers';
import { CopyNodeAction, DeleteNodeAction, ViewSourceAction } from '../selection-menu';
import { getWidget } from '../widgets';
/**
* 选择辅助工具的对齐方式
@@ -15,34 +15,21 @@ type SelectionHelperAlignType = 'top-right' | 'top-left' | 'inner-top-right';
const boundingOffset = 100; // 检测选中模块的工具栏是否溢出的偏移值
const internalSelectionToolsMap = {
source: <ViewSourceAction />,
copy: <CopyNodeAction />,
delete: <DeleteNodeAction />,
};
export const INTERNAL_SELECTION_TOOLS = ['source', 'create', 'copy', 'delete'];
export interface SelectionToolsProps {
/**
* 动作列表,内置的动作列表有 source-定位到源码, block-创建区块, copy-复制节点, delete-删除节点
*/
actions?: Array<string | React.ReactElement>;
builtinActionMap?: Record<string, React.ReactElement>;
}
export const SelectionTools = observer(
({
actions: actionsProp = INTERNAL_SELECTION_TOOLS,
builtinActionMap = internalSelectionToolsMap,
}: SelectionToolsProps) => {
({ actions: actionsProp = ['viewSource', 'copyNode', 'deleteNode'] }: SelectionToolsProps) => {
const workspace = useWorkspace();
const selectSource = workspace.selectSource;
const actions = actionsProp.map((item) => {
if (isString(item)) {
return builtinActionMap[item]
? React.cloneElement(builtinActionMap[item], { key: item })
: null;
const widget = getWidget(['selectionMenu', item].join('.'));
return widget ? React.createElement(widget, { key: item }) : null;
}
return item;
});
+2 -9
View File
@@ -10,21 +10,14 @@ import { DraggingMask } from './mask';
export interface ViewportProps extends HTMLCoralProps<'div'> {
selectionTools?: SelectionToolsProps['actions'];
builtinActionMap?: SelectionToolsProps['builtinActionMap'];
}
export function Viewport({
selectionTools,
builtinActionMap,
children,
className,
...rest
}: ViewportProps) {
export function Viewport({ selectionTools, children, className, ...rest }: ViewportProps) {
return (
<Box position="relative" height="100%" className={cx('Viewport', className)} {...rest}>
<ViewportBody>{children}</ViewportBody>
<div className="AuxTools">
<SelectionTools actions={selectionTools} builtinActionMap={builtinActionMap} />
<SelectionTools actions={selectionTools} />
<InsertionPrompt />
<DraggingMask />
</div>
+5 -1
View File
@@ -11,7 +11,11 @@ export const PreviewTool = observer(() => {
shape="ghost"
selected={designer.isPreview}
onClick={() => {
designer.toggleIsPreview(!designer.isPreview);
const nextIsPreview = !designer.isPreview;
designer.toggleIsPreview(nextIsPreview);
if (nextIsPreview) {
designer.setActiveView('design');
}
}}
tip="预览"
>
+16 -17
View File
@@ -1,26 +1,13 @@
import React, { useMemo } from 'react';
import { Box, Group } from 'coral-system';
import { ReactComponentProps } from '@music163/tango-helpers';
import { HistoryTool } from './history';
import { ModeSwitchTool } from './mode-switch';
import { PreviewTool } from './preview';
import { RouteSwitchTool } from './route-switch';
import { TogglePanelTool } from './toggle-panel';
import { getWidget } from '../widgets';
export interface ToolbarProps {
children?: React.ReactElement | React.ReactElement[];
builtinToolMap?: Record<string, React.ReactNode>;
}
const builtinToolsMap = {
history: <HistoryTool />,
modeSwitch: <ModeSwitchTool />,
preview: <PreviewTool />,
routeSwitch: <RouteSwitchTool />,
togglePanel: <TogglePanelTool />,
};
export function Toolbar({ children, builtinToolMap = builtinToolsMap }: ToolbarProps) {
export function Toolbar({ children }: ToolbarProps) {
const [leftTools, centerTools, rightTools] = useMemo(() => {
const left: React.ReactNode[] = [];
const center: React.ReactNode[] = [];
@@ -28,7 +15,15 @@ export function Toolbar({ children, builtinToolMap = builtinToolsMap }: ToolbarP
let prevPlacement: string;
React.Children.forEach(children, (child: React.ReactElement, index) => {
let node = child.props.children || builtinToolMap[child.key];
let fallbackNode;
if (child.key) {
const Widget = getWidget(['toolbar', child.key].join('.'));
if (Widget) {
fallbackNode = React.createElement(Widget, child.props.widgetProps);
}
}
let node = child.props.children ?? fallbackNode ?? null;
if (!node) {
node = child; // separator
}
@@ -75,9 +70,13 @@ export function Toolbar({ children, builtinToolMap = builtinToolsMap }: ToolbarP
export interface ToolbarItemProps extends ReactComponentProps {
placement?: 'left' | 'center' | 'right';
children?: React.ReactElement;
/**
* 如果 key 匹配到内置组件的话,传递给子节点的属性
*/
widgetProps?: object;
}
function ToolbarItem({ placement, children }: ToolbarItemProps) {
function ToolbarItem({ placement, widgetProps, children }: ToolbarItemProps) {
return <div>{children}</div>;
}
+44
View File
@@ -0,0 +1,44 @@
import { getValue, setValue } from '@music163/tango-helpers';
import { ModeSwitchTool, PreviewTool, RouteSwitchTool, TogglePanelTool } from './toolbar';
import {
ComponentsPanel,
DataSourcePanel,
DependencyPanel,
OutlinePanel,
VariablePanel,
} from './sidebar';
import { CopyNodeAction, DeleteNodeAction, ViewSourceAction } from './selection-menu';
const widgets = {};
export function registerWidget(key: string, component: React.ComponentType<any>) {
if (!/^(toolbar|sidebar|selectionMenu)\.[a-zA-z]+$/.test(key)) {
throw new Error(
`Invalid widget key(${key}), should start with toolbar, sidebar, or selection-menu`,
);
}
setValue(widgets, key, component);
}
export function getWidget(key: string) {
if (!key) {
return;
}
return getValue(widgets, key);
}
registerWidget('toolbar.modeSwitch', ModeSwitchTool);
registerWidget('toolbar.preview', PreviewTool);
registerWidget('toolbar.routeSwitch', RouteSwitchTool);
registerWidget('toolbar.togglePanel', TogglePanelTool);
registerWidget('sidebar.components', ComponentsPanel);
registerWidget('sidebar.outline', OutlinePanel);
registerWidget('sidebar.dependency', DependencyPanel);
registerWidget('sidebar.variables', VariablePanel);
registerWidget('sidebar.dataSource', DataSourcePanel);
registerWidget('selectionMenu.copyNode', CopyNodeAction);
registerWidget('selectionMenu.deleteNode', DeleteNodeAction);
registerWidget('selectionMenu.viewSource', ViewSourceAction);
+8
View File
@@ -3,6 +3,14 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.1.6](https://github.com/netease/tango/compare/@music163/tango-sandbox@0.1.5...@music163/tango-sandbox@0.1.6) (2023-09-22)
**Note:** Version bump only for package @music163/tango-sandbox
## [0.1.5](https://github.com/netease/tango/compare/@music163/tango-sandbox@0.1.4...@music163/tango-sandbox@0.1.5) (2023-09-21)
**Note:** Version bump only for package @music163/tango-sandbox
## [0.1.4](https://github.com/netease/tango/compare/@music163/tango-sandbox@0.1.3...@music163/tango-sandbox@0.1.4) (2023-09-18)
**Note:** Version bump only for package @music163/tango-sandbox
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@music163/tango-sandbox",
"version": "0.1.4",
"version": "0.1.6",
"description": "sandbox of tango apps",
"author": "wwsun <ww.sun@outlook.com>",
"homepage": "",
@@ -29,7 +29,7 @@
},
"dependencies": {
"@ant-design/icons": "^4.8.0",
"@music163/tango-core": "^0.1.4",
"@music163/tango-core": "^0.2.1",
"@music163/tango-helpers": "^0.1.3",
"crypto-js": "^4.1.1",
"lodash.isequal": "4.5.0",
+16
View File
@@ -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.
## [0.2.5](https://github.com/netease/tango/compare/@music163/tango-setting-form@0.2.4...@music163/tango-setting-form@0.2.5) (2023-10-12)
**Note:** Version bump only for package @music163/tango-setting-form
## [0.2.4](https://github.com/netease/tango/compare/@music163/tango-setting-form@0.2.3...@music163/tango-setting-form@0.2.4) (2023-10-09)
**Note:** Version bump only for package @music163/tango-setting-form
## [0.2.3](https://github.com/netease/tango/compare/@music163/tango-setting-form@0.2.2...@music163/tango-setting-form@0.2.3) (2023-09-22)
**Note:** Version bump only for package @music163/tango-setting-form
## [0.2.2](https://github.com/netease/tango/compare/@music163/tango-setting-form@0.2.1...@music163/tango-setting-form@0.2.2) (2023-09-21)
**Note:** Version bump only for package @music163/tango-setting-form
## [0.2.1](https://github.com/netease/tango/compare/@music163/tango-setting-form@0.2.0...@music163/tango-setting-form@0.2.1) (2023-09-18)
### Bug Fixes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@music163/tango-setting-form",
"version": "0.2.1",
"version": "0.2.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": "^0.1.4",
"@music163/tango-core": "^0.2.1",
"@music163/tango-helpers": "^0.1.3",
"@music163/tango-ui": "^0.1.4",
"@music163/tango-ui": "^0.1.6",
"antd": "^4.24.2",
"coral-system": "^1.0.5",
"mobx": "6.9.0",
+12
View File
@@ -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.
## [0.1.6](https://github.com/netease/tango/compare/@music163/tango-ui@0.1.5...@music163/tango-ui@0.1.6) (2023-10-12)
### Bug Fixes
- refactor expSetter ([#41](https://github.com/netease/tango/issues/41)) ([d63517e](https://github.com/netease/tango/commit/d63517ecb936e4227e70c33e610664316625f4f4))
## [0.1.5](https://github.com/netease/tango/compare/@music163/tango-ui@0.1.4...@music163/tango-ui@0.1.5) (2023-10-09)
### Bug Fixes
- update tabs style ([46b5445](https://github.com/netease/tango/commit/46b54459ceb23f9df52b9eb40dc935bc872ba1b4))
## [0.1.4](https://github.com/netease/tango/compare/@music163/tango-ui@0.1.3...@music163/tango-ui@0.1.4) (2023-09-18)
**Note:** Version bump only for package @music163/tango-ui
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@music163/tango-ui",
"version": "0.1.4",
"version": "0.1.6",
"description": "ui widgets of tango",
"keywords": [
"react",
+4 -2
View File
@@ -9,7 +9,9 @@ export interface CopyClipboardProps {
* 拷贝成功后到回调
*/
onCopy?: () => void;
children?: React.ReactNode | ((copied: boolean) => React.ReactElement);
children?:
| React.ReactNode
| ((data: { copied: boolean; onClick: Function }) => React.ReactElement);
}
export function CopyClipboard({ onCopy, text, children }: CopyClipboardProps) {
@@ -37,7 +39,7 @@ export function CopyClipboard({ onCopy, text, children }: CopyClipboardProps) {
};
if (typeof children === 'function') {
return React.cloneElement(children(copied), {
return React.cloneElement(children({ copied, onClick }), {
onClick,
});
}
+1 -1
View File
@@ -18,7 +18,7 @@ export function Tabs({
const tabBarStyle = useMemo(() => {
let ret = centered ? centerTabBarStyle : defaultTabBarStyle;
if (isTabBarSticky) {
ret = { ...ret, position: 'sticky', top: tabBarStickyOffset, zIndex: 1, background: '#fff' };
ret = { ...ret, position: 'sticky', top: tabBarStickyOffset, zIndex: 2, background: '#fff' };
}
return ret;
}, [centered, isTabBarSticky, tabBarStickyOffset]);
+4 -3
View File
@@ -7,11 +7,12 @@
"resolveJsonModule": true,
"strictFunctionTypes": true,
"paths": {
"@music163/tango-core": ["packages/core/src/index.ts"],
"@music163/tango-setting-form": ["packages/setting-form/src/index.ts"],
"@music163/tango-helpers": ["packages/helpers/src/index.ts"],
"@music163/tango": ["packages/designer/src/index.ts"],
"@music163/tango-core": ["packages/core/src/index.ts"],
"@music163/tango-context": ["packages/context/src/index.ts"],
"@music163/tango-setting-form": ["packages/setting-form/src/index.ts"],
"@music163/tango-ui": ["packages/ui/src/index.ts"],
"@music163/tango": ["packages/designer/src/index.ts"]
}
}
}
+6 -6
View File
@@ -11185,9 +11185,9 @@ humanize-ms@^1.2.1:
dependencies:
ms "^2.0.0"
husky@^8.0.1:
husky@^8.0.3:
version "8.0.3"
resolved "https://registry.npmmirror.com/husky/-/husky-8.0.3.tgz"
resolved "https://registry.npmmirror.com/husky/-/husky-8.0.3.tgz#4936d7212e46d1dea28fef29bb3a108872cd9184"
integrity sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg==
iconv-lite@0.4.24, iconv-lite@^0.4.24:
@@ -13043,10 +13043,10 @@ lines-and-columns@~2.0.3:
resolved "https://registry.npmmirror.com/lines-and-columns/-/lines-and-columns-2.0.3.tgz"
integrity sha512-cNOjgCnLB+FnvWWtyRTzmB3POJ+cXxTA81LoW7u8JdmhfXzriropYwpjShnz1QLLWsQwY7nIxoDmcPTwphDK9w==
lint-staged@^13.1.0:
version "13.3.0"
resolved "https://registry.npmmirror.com/lint-staged/-/lint-staged-13.3.0.tgz"
integrity sha512-mPRtrYnipYYv1FEE134ufbWpeggNTo+O/UPzngoaKzbzHAthvR55am+8GfHTnqNRQVRRrYQLGW9ZyUoD7DsBHQ==
lint-staged@^14.0.1:
version "14.0.1"
resolved "https://registry.npmmirror.com/lint-staged/-/lint-staged-14.0.1.tgz#57dfa3013a3d60762d9af5d9c83bdb51291a6232"
integrity sha512-Mw0cL6HXnHN1ag0mN/Dg4g6sr8uf8sn98w2Oc1ECtFto9tvRF7nkXGJRbx8gPlHyoR0pLyBr2lQHbWwmUHe1Sw==
dependencies:
chalk "5.3.0"
commander "11.0.0"