mirror of
https://github.com/NetEase/tango.git
synced 2026-08-31 20:06:59 +08:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7999f604fd | |||
| a7df29c3de | |||
| 308db9cac8 | |||
| a1dbf785a5 | |||
| 813eb83ebb | |||
| 994db5db55 | |||
| 15542d9eb2 | |||
| 4d809e9afd | |||
| b786de2f1a | |||
| 8d396ff134 | |||
| 7e9ab12503 | |||
| 443ae589f6 | |||
| 12de10b0bc | |||
| 19d0a4523b | |||
| b80f5e4c10 | |||
| f9e6537dd6 | |||
| 5ecf444fc3 |
@@ -6,6 +6,8 @@
|
||||
"@typescript-eslint/no-unused-vars": "warn",
|
||||
"import/no-cycle": "off",
|
||||
"no-nested-ternary": "off",
|
||||
"no-useless-return":"off"
|
||||
"no-useless-return":"off",
|
||||
"no-param-reassign": "off",
|
||||
"prefer-destructuring": "off"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ const packageJson = {
|
||||
name: 'demo',
|
||||
private: true,
|
||||
dependencies: {
|
||||
'@music163/antd': '0.1.5',
|
||||
'@music163/antd': '0.1.6',
|
||||
'@music163/tango-boot': '0.1.3',
|
||||
react: '17.0.2',
|
||||
'react-dom': '17.0.2',
|
||||
@@ -11,7 +11,7 @@ const packageJson = {
|
||||
},
|
||||
};
|
||||
|
||||
const tangoJson = {
|
||||
const tangoConfigJson = {
|
||||
packages: {
|
||||
react: {
|
||||
version: '17.0.2',
|
||||
@@ -44,7 +44,7 @@ const tangoJson = {
|
||||
resources: ['https://unpkg.com/moment@{{version}}/moment.js'],
|
||||
},
|
||||
'@music163/tango-boot': {
|
||||
version: '0.1.3',
|
||||
version: '0.2.0',
|
||||
library: 'TangoBoot',
|
||||
type: 'baseDependency',
|
||||
resources: ['https://unpkg.com/@music163/tango-boot@{{version}}/dist/boot.js'],
|
||||
@@ -52,7 +52,7 @@ const tangoJson = {
|
||||
description: '云音乐低代码运行时框架',
|
||||
},
|
||||
'@music163/antd': {
|
||||
version: '0.1.5',
|
||||
version: '0.1.7',
|
||||
library: 'TangoAntd',
|
||||
type: 'baseDependency',
|
||||
resources: [
|
||||
@@ -148,7 +148,7 @@ import {
|
||||
class App extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<Page title="页面标题">
|
||||
<Page title={tango.stores.app.title}>
|
||||
<Section>
|
||||
</Section>
|
||||
<Section>
|
||||
@@ -167,7 +167,7 @@ import { defineStore } from '@music163/tango-boot';
|
||||
|
||||
export default defineStore({
|
||||
|
||||
title: 'hello world',
|
||||
title: 'Page Title',
|
||||
|
||||
array: [1, 2, 3],
|
||||
}, 'app');
|
||||
@@ -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;
|
||||
@@ -221,7 +234,7 @@ p {
|
||||
export const sampleFiles = [
|
||||
{ filename: '/package.json', code: JSON.stringify(packageJson) },
|
||||
{ filename: '/appJson.json', code: JSON.stringify(appJson) },
|
||||
{ filename: '/tango.config.json', code: JSON.stringify(tangoJson) },
|
||||
{ filename: '/tango.config.json', code: JSON.stringify(tangoConfigJson) },
|
||||
{ filename: '/README.md', code: '# readme' },
|
||||
{ filename: '/src/index.less', code: lessCode },
|
||||
{ filename: '/src/style.css', code: cssCode },
|
||||
@@ -232,6 +245,8 @@ 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() {}` },
|
||||
];
|
||||
|
||||
export const genDefaultPage = (index: number) => ({
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Box } from 'coral-system';
|
||||
import { Button, Space } from 'antd';
|
||||
import {
|
||||
Designer,
|
||||
DesignerPanel,
|
||||
@@ -6,8 +8,8 @@ import {
|
||||
Sidebar,
|
||||
Toolbar,
|
||||
WorkspacePanel,
|
||||
WorkspaceView,
|
||||
ComponentsPanel,
|
||||
ViewPanel,
|
||||
CodeEditor,
|
||||
Sandbox,
|
||||
DndQuery,
|
||||
@@ -16,8 +18,6 @@ import { createEngine, Workspace } from '@music163/tango-core';
|
||||
import { Logo, ProjectDetail } from './share';
|
||||
import { sampleFiles } from '../mock/project';
|
||||
import './index.less';
|
||||
import { Box } from 'coral-system';
|
||||
import { Button, Space } from 'antd';
|
||||
|
||||
// 1. 实例化工作区
|
||||
const workspace = new Workspace({
|
||||
@@ -74,7 +74,7 @@ export default function App() {
|
||||
<Sidebar.Item key="dependency" isFloat width={800} />
|
||||
</Sidebar>
|
||||
<WorkspacePanel>
|
||||
<ViewPanel mode="design">
|
||||
<WorkspaceView mode="design">
|
||||
<Sandbox
|
||||
onMessage={(e) => {
|
||||
if (e.type === 'done') {
|
||||
@@ -91,10 +91,10 @@ export default function App() {
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</ViewPanel>
|
||||
<ViewPanel mode="code">
|
||||
</WorkspaceView>
|
||||
<WorkspaceView mode="code">
|
||||
<CodeEditor />
|
||||
</ViewPanel>
|
||||
</WorkspaceView>
|
||||
</WorkspacePanel>
|
||||
<SettingPanel />
|
||||
</DesignerPanel>
|
||||
|
||||
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 297 KiB After Width: | Height: | Size: 279 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 254 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 61 KiB |
@@ -6,7 +6,7 @@ tags: [netease, tango, open-source]
|
||||
|
||||
## 📝 Introduction
|
||||
|
||||
Tango is a low-code designer framework for quickly building low-code platforms. With Tango, you only need a few lines of code to build a basic low-code platform frontend system. The Tango low-code designer directly reads the source code of the frontend project, executes and renders the frontend view based on the source code, and provides users with low-code visual building capabilities. User's building operations will be translated into modifications to the source code. With low-code tools or platforms built using Tango, it is possible to achieve the effect of "source code in, source code out" and seamlessly integrate with the existing R&D system within the enterprise.
|
||||
Tango is a low-code builder framework for quickly building low-code platforms or tools. With Tango, you only need a few lines of code to build a basic low-code builder. The Tango low-code builder reads the source code of the frontend project directly, executes and renders the frontend view based on the source code, and provides users with low-code visual building capabilities. User's building operations will be translated into modifications to the source code. With low-code tools or platforms built using Tango, it is possible to achieve the effect of "source code in, source code out" and seamlessly integrate with the existing development flow within the enterprise.
|
||||
|
||||
### ✨ Features
|
||||
|
||||
@@ -20,11 +20,11 @@ Tango is a low-code designer framework for quickly building low-code platforms.
|
||||
|
||||
Due to the fact that the engine kernel is completely based on the source code-driven implementation, Tango low-code engine can achieve the visualization building capability of source code input and source code output, without providing any private intermediate products. If the company already has a complete R&D system (code hosting, building, deployment, CDN), Tango low-code engine can be directly used to integrate with existing services to build a low-code development platform.
|
||||
|
||||
<img src="https://p5.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/13140534982/ee2e/f42c/cc9a/184e2918a011b57d46e6c64a2722fa44.png" />
|
||||

|
||||
|
||||
### 📄 Code in, Code out
|
||||
|
||||
由于引擎内核完全基于源代码驱动实现,Tango 低代码引擎能够实现源代码进,源代码出的可视化搭建能力,不提供任何私有的中间产物。如果公司内部已经有了一套完善的研发体系(代码托管、构建、部署、CDN),那么可以直接使用 Tango 低代码引擎与现有的服务集成构建低代码开发平台。
|
||||
Due to the engine kernel being completely driven by source code, the Tango low-code engine can achieve the visual construction capability of source code in and source code out, without providing any private intermediate products. If the company already has a complete development system (code hosting, building, deployment, CDN), then it can directly use the Tango low-code engine to integrate with existing services to build a low-code development platform.
|
||||
|
||||

|
||||
|
||||
@@ -32,23 +32,24 @@ Due to the fact that the engine kernel is completely based on the source code-dr
|
||||
|
||||
Compared to the low-code building solution based on private schema, Tango low-code engine has the following advantages:
|
||||
|
||||
| Comparison Items | Schema-base low-code solution | Tango (Code-based) |
|
||||
| ---------------- | ----------------------------------------- | ------------------------------------------- |
|
||||
| Applicable Scenarios | Targeted vertical building scenarios, such as forms, marketing pages, etc. | 🔥 Applicable to application building scenarios centered on source code |
|
||||
| Language Capabilities | Relies on private protocols for extension, inflexible, and difficult to align with programming language capabilities | 🔥 Based directly on JavaScript language, can use all language features, no extensibility issues |
|
||||
| Development Capability | LowCode | 🔥 LowCode + ProCode |
|
||||
| Source Code Export | Centered on schema, one-way code output, irreversible | 🔥 Source code-centered, two-way transcoding |
|
||||
| Custom Dependencies | Need to be encapsulated according to private protocol extension, high customization cost | 🔥 Existing components can be seamlessly and low-cost integrated |
|
||||
| Integrated R&D Facilities | High customization cost, additional customization required | 🔥 Low-cost integration, can directly reuse existing deployment and publishing capabilities |
|
||||
| Comparison Items | Schema-base low-code solution | Tango (Code-based) |
|
||||
| ------------------------- | -------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
|
||||
| Applicable Scenarios | Targeted vertical building scenarios, such as forms, marketing pages, etc. | 🔥 Applicable to application building scenarios centered on source code |
|
||||
| Language Capabilities | Relies on private protocols for extension, inflexible, and difficult to align with programming language capabilities | 🔥 Based directly on JavaScript language, can use all language features, no extensibility issues |
|
||||
| Development Capability | LowCode | 🔥 LowCode + ProCode |
|
||||
| Source Code Export | Centered on schema, one-way code output, irreversible | 🔥 Source code-centered, two-way transcoding |
|
||||
| Custom Dependencies | Need to be encapsulated according to private protocol extension, high customization cost | 🔥 Existing components can be seamlessly and low-cost integrated |
|
||||
| Integrated R&D Facilities | High customization cost, additional customization required | 🔥 Low-cost integration, can directly reuse existing deployment and publishing capabilities |
|
||||
|
||||
The specific technical architecture is shown in the following figure:
|
||||
|
||||
## 📐 Architecture
|
||||
|
||||
The Tango low-code engine has been layered and decoupled in implementation, enabling independent development and maintenance of the upper-level low-code platform and the lower-level low-code engine, allowing for fast integration and deployment. In addition, Tango's low-code engine defines an open material ecosystem, allowing developers to freely contribute to extension components and configure property setters that enhance low-code materials.
|
||||
|
||||
The specific technical architecture is shown in the following diagram.
|
||||
|
||||

|
||||

|
||||
|
||||
## ⏰ Milestones
|
||||
|
||||
@@ -58,6 +59,7 @@ Tango low-code engine is the core component of NetEase Cloud Music's internal lo
|
||||
2. We plan to release the 1.0 Beta version of the low-code engine on September 30th this year. This version will refactor the core implementation for community scenarios, remove some compatibility code we have internally in Cloud Music, and optimize the implementation of the core.
|
||||
3. We plan to release the 1.0 RC version of the low-code engine on October 30th this year. This version will ensure that the core API is basically stable and no longer has BREAKING CHANGE. At the same time, we will provide comprehensive and detailed development guides, deployment documents, and demonstration applications.
|
||||
4. The official version will be released before the end of Q4 2023, and we will further improve our open source community operation mechanism.
|
||||
|
||||

|
||||
|
||||
## 🤝 Community
|
||||
@@ -74,3 +76,5 @@ Welcome everyone to join our community and participate in the open source constr
|
||||
Thanks to the Netease Cloud Music Public Technical Team, the Front-End Team, the Live Broadcasting Technical Team, and all the colleagues who participated in the Tango project.
|
||||
|
||||
Thanks to the Sandpack project provided by CodeSandbox, which provides powerful browser-based code building and execution capabilities for Tango.
|
||||
|
||||
Tanks to the Babel project, which provides powerful javascript ast parsing and traversing capabilities for Tango.
|
||||
|
||||
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 259 KiB |
@@ -25,8 +25,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@docusaurus/module-type-aliases": "2.4.1",
|
||||
"@tsconfig/docusaurus": "^1.0.5",
|
||||
"typescript": "^4.7.4"
|
||||
"@tsconfig/docusaurus": "^1.0.5"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
|
||||
+1
-7
@@ -53,19 +53,13 @@
|
||||
"jest": "^29.1.1",
|
||||
"jest-environment-jsdom": "^29.1.1",
|
||||
"lerna": "^6.6.1",
|
||||
"less": "^4.1.2",
|
||||
"less-loader": "^7.3.0",
|
||||
"lint-staged": "^13.1.0",
|
||||
"mini-css-extract-plugin": "^1.6.2",
|
||||
"prettier": "^2.8.7",
|
||||
"progress-bar-webpack-plugin": "^2.1.0",
|
||||
"react": "^17.0.0",
|
||||
"react-dom": "^17.0.0",
|
||||
"styled-components": "^5.3.6",
|
||||
"typedoc": "^0.25.0",
|
||||
"typescript": "^5.0.4",
|
||||
"webpack": "^4.46.0",
|
||||
"webpack-cli": "^4.9.2"
|
||||
"typescript": "^5.0.4"
|
||||
},
|
||||
"resolutions": {
|
||||
"@yarnpkg/parsers": "3.0.0-rc.48.1"
|
||||
|
||||
@@ -3,6 +3,20 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [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
|
||||
|
||||
## [0.1.3](https://github.com/netease/tango/compare/@music163/tango-context@0.1.2...@music163/tango-context@0.1.3) (2023-09-13)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
## 0.1.2 (2023-09-06)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-context",
|
||||
"version": "0.1.2",
|
||||
"version": "0.2.0",
|
||||
"description": "react context for tango-apps",
|
||||
"keywords": [
|
||||
"react",
|
||||
@@ -28,11 +28,11 @@
|
||||
"prepublishOnly": "yarn build"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">= 16.8.0"
|
||||
"react": ">= 16.8"
|
||||
},
|
||||
"dependencies": {
|
||||
"@music163/tango-core": "^0.1.2",
|
||||
"@music163/tango-helpers": "^0.1.2",
|
||||
"@music163/tango-core": "^0.2.0",
|
||||
"@music163/tango-helpers": "^0.1.3",
|
||||
"mobx-react-lite": "4.0.2"
|
||||
},
|
||||
"publishConfig": {
|
||||
|
||||
@@ -53,7 +53,7 @@ export const useWorkspaceData = () => {
|
||||
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 +91,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',
|
||||
})),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -113,6 +119,7 @@ export const useWorkspaceData = () => {
|
||||
buildVariableOptions('工具函数', 'helpers', builtinHelpers),
|
||||
],
|
||||
storeVariables,
|
||||
serviceVariables,
|
||||
expressionVariables: [
|
||||
buildVariableOptions('数据模型', 'stores', storeVariables),
|
||||
buildVariableOptions('服务函数', 'services', serviceVariables),
|
||||
|
||||
@@ -3,6 +3,25 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [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
|
||||
|
||||
## [0.1.3](https://github.com/netease/tango/compare/@music163/tango-core@0.1.2...@music163/tango-core@0.1.3) (2023-09-13)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **core:** optimize find jsxElement parent node way ([#22](https://github.com/netease/tango/issues/22)) ([19d0a45](https://github.com/netease/tango/commit/19d0a4523bb31ea9286714737a0f4f1883e1c801))
|
||||
- refactor setting formItem ([b786de2](https://github.com/netease/tango/commit/b786de2f1a0e4e9141eb09fce696e45df633b232))
|
||||
- refactor types ([15542d9](https://github.com/netease/tango/commit/15542d9eb2f8959597b81cae457091ee71710c83))
|
||||
- update sidebar ([4d809e9](https://github.com/netease/tango/commit/4d809e9afd0d6d525850708722736847b510638e))
|
||||
|
||||
## 0.1.2 (2023-09-06)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-core",
|
||||
"version": "0.1.2",
|
||||
"version": "0.2.0",
|
||||
"description": "tango core",
|
||||
"author": "wwsun <ww.sun@outlook.com>",
|
||||
"homepage": "",
|
||||
@@ -28,12 +28,15 @@
|
||||
"@babel/parser": "^7.22.15",
|
||||
"@babel/traverse": "^7.22.15",
|
||||
"@babel/types": "^7.22.15",
|
||||
"@music163/tango-helpers": "^0.1.2",
|
||||
"@music163/tango-helpers": "^0.1.3",
|
||||
"@types/babel__generator": "^7.6.4",
|
||||
"@types/babel__traverse": "^7.18.3",
|
||||
"mobx": "6.9.0",
|
||||
"path-browserify": "^1.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"mobx": "6.9.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
|
||||
@@ -11,16 +11,9 @@ import {
|
||||
StringOrNumber,
|
||||
Dict,
|
||||
parseDndId,
|
||||
upperCamelCase
|
||||
upperCamelCase,
|
||||
} from '@music163/tango-helpers';
|
||||
import {
|
||||
keyNode2value,
|
||||
jsxAttributeValueNode2value,
|
||||
node2value,
|
||||
ast2code,
|
||||
expression2code,
|
||||
node2code,
|
||||
} from './generate';
|
||||
import { keyNode2value, jsxAttributeValueNode2value, node2value, node2code } from './generate';
|
||||
import {
|
||||
value2jsxAttributeValueNode,
|
||||
value2jsxChildrenValueNode,
|
||||
@@ -32,12 +25,12 @@ import {
|
||||
import { isValidComponentName } from '../string';
|
||||
import { isDefineService, isDefineStore } from '../assert';
|
||||
import type {
|
||||
RouteDataType,
|
||||
StorePropertyType,
|
||||
ClassPropertyNodeType,
|
||||
TangoViewNodeDataType,
|
||||
ImportDeclarationPayloadType,
|
||||
ServiceFunctionPayloadType,
|
||||
IRouteData,
|
||||
IStorePropertyData,
|
||||
IClassPropertyNodeData,
|
||||
ITangoViewNodeData,
|
||||
IImportDeclarationPayload,
|
||||
IServiceFunctionPayload,
|
||||
InsertChildPositionType,
|
||||
} from '../../types';
|
||||
import { IdGenerator } from '../id-generator';
|
||||
@@ -459,7 +452,7 @@ function getLastExportDeclarationIndex(node: t.Program) {
|
||||
* @param importedSourcePath
|
||||
* @returns
|
||||
*/
|
||||
export function makeImportDeclaration(importedModule: ImportDeclarationPayloadType) {
|
||||
export function makeImportDeclaration(importedModule: IImportDeclarationPayload) {
|
||||
const specifierNodes =
|
||||
importedModule.specifiers?.map((localName) =>
|
||||
t.importSpecifier(t.identifier(localName), t.identifier(localName)),
|
||||
@@ -474,7 +467,7 @@ export function makeImportDeclaration(importedModule: ImportDeclarationPayloadTy
|
||||
return t.importDeclaration(newSpecifierNodes, t.stringLiteral(importedModule.sourcePath));
|
||||
}
|
||||
|
||||
function getImportDeclarationData(node: t.ImportDeclaration): ImportDeclarationPayloadType {
|
||||
function getImportDeclarationData(node: t.ImportDeclaration): IImportDeclarationPayload {
|
||||
const sourcePath = node2value(node.source);
|
||||
let defaultSpecifier;
|
||||
const specifiers: string[] = [];
|
||||
@@ -499,7 +492,7 @@ function getImportDeclarationData(node: t.ImportDeclaration): ImportDeclarationP
|
||||
* @param importedSourcePath
|
||||
* @returns
|
||||
*/
|
||||
export function addImportDeclaration(ast: t.File, importedModule: ImportDeclarationPayloadType) {
|
||||
export function addImportDeclaration(ast: t.File, importedModule: IImportDeclarationPayload) {
|
||||
traverse(ast, {
|
||||
Program(path) {
|
||||
const lastIndex = getLastImportDeclarationIndex(path.node);
|
||||
@@ -518,7 +511,7 @@ export function addImportDeclaration(ast: t.File, importedModule: ImportDeclarat
|
||||
* @param importedSourcePath
|
||||
* @returns
|
||||
*/
|
||||
export function updateImportDeclaration(ast: t.File, importedModule: ImportDeclarationPayloadType) {
|
||||
export function updateImportDeclaration(ast: t.File, importedModule: IImportDeclarationPayload) {
|
||||
traverse(ast, {
|
||||
ImportDeclaration(path) {
|
||||
const currentSourcePath = node2value(path.node.source);
|
||||
@@ -656,7 +649,7 @@ export function removeRouteFromRouteFile(ast: t.File, routePath: string, importP
|
||||
}
|
||||
|
||||
export function traverseRouteFile(ast: t.File) {
|
||||
const routes: RouteDataType[] = [];
|
||||
const routes: IRouteData[] = [];
|
||||
const importMap: Dict<string> = {};
|
||||
|
||||
traverse(ast, {
|
||||
@@ -819,8 +812,8 @@ export function updateStoreState(ast: t.File, stateName: string, code: string) {
|
||||
|
||||
export function traverseStoreFile(ast: t.File) {
|
||||
let namespace: string;
|
||||
const actions: StorePropertyType[] = [];
|
||||
const states: StorePropertyType[] = [];
|
||||
const actions: IStorePropertyData[] = [];
|
||||
const states: IStorePropertyData[] = [];
|
||||
|
||||
traverse(ast, {
|
||||
CallExpression(path) {
|
||||
@@ -909,7 +902,7 @@ export function deleteServiceConfigFromServiceFile(ast: t.File, serviceFunctionN
|
||||
* @param payload
|
||||
* @returns
|
||||
*/
|
||||
export function serviceConfig2Node(payload: ServiceFunctionPayloadType) {
|
||||
export function serviceConfig2Node(payload: IServiceFunctionPayload) {
|
||||
return object2node(payload, (value, key) => {
|
||||
if (key === 'formatter' && value) {
|
||||
return code2expression(value);
|
||||
@@ -920,7 +913,7 @@ export function serviceConfig2Node(payload: ServiceFunctionPayloadType) {
|
||||
|
||||
export function updateServiceConfigToServiceFile(
|
||||
ast: t.File,
|
||||
config: { [key: string]: ServiceFunctionPayloadType },
|
||||
config: { [key: string]: IServiceFunctionPayload },
|
||||
) {
|
||||
traverse(ast, {
|
||||
CallExpression(path) {
|
||||
@@ -959,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;
|
||||
}
|
||||
@@ -1022,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();
|
||||
@@ -1133,11 +1107,8 @@ export function cloneJSXElementWithoutTrackingData(node: t.JSXElement) {
|
||||
}
|
||||
|
||||
export function traverseViewFile(ast: t.File, idGenerator: IdGenerator) {
|
||||
const importedModules: Dict<ImportDeclarationPayloadType | ImportDeclarationPayloadType[]> = {};
|
||||
const classProperties: Dict<ClassPropertyNodeType> = {};
|
||||
let stateProperties: string[] = [];
|
||||
let stateCode = '';
|
||||
const nodes: Array<TangoViewNodeDataType<t.JSXElement>> = [];
|
||||
const importedModules: Dict<IImportDeclarationPayload | IImportDeclarationPayload[]> = {};
|
||||
const nodes: Array<ITangoViewNodeData<t.JSXElement>> = [];
|
||||
const cloneAst = t.cloneNode(ast, true, true);
|
||||
const cleanAst = removeTrackingDataFromViewAst(cloneAst);
|
||||
const variables: string[] = []; // 使用的 tango 变量
|
||||
@@ -1166,30 +1137,6 @@ export function traverseViewFile(ast: t.File, idGenerator: IdGenerator) {
|
||||
}
|
||||
},
|
||||
|
||||
ClassProperty(path) {
|
||||
const propName = keyNode2value(path.node.key) as string;
|
||||
if (!propName) {
|
||||
return;
|
||||
}
|
||||
|
||||
const reference = `this.${propName}`;
|
||||
if (['defaultProps', 'propTypes', 'constructor'].includes(propName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (propName === 'state') {
|
||||
stateProperties = Object.keys(node2value(path.node.value));
|
||||
stateCode = ast2code(path.node);
|
||||
} else {
|
||||
// TODO: 这里需要完善下,并不是所有的都是 arrowFunction
|
||||
classProperties[reference] = {
|
||||
reference,
|
||||
propertyName: propName,
|
||||
propertyBody: expression2code(path.node.value),
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
JSXElement(path) {
|
||||
const attributes = getJSXElementAttributes(path.node);
|
||||
|
||||
@@ -1216,9 +1163,10 @@ export function traverseViewFile(ast: t.File, idGenerator: IdGenerator) {
|
||||
|
||||
// parentId 用于追溯上下游关系
|
||||
let parentId;
|
||||
const parentNode = path.parentPath.node;
|
||||
if (t.isJSXElement(parentNode)) {
|
||||
const parentAttributes = getJSXElementAttributes(parentNode);
|
||||
const parentNode = path.findParent((p) => p.isJSXElement());
|
||||
|
||||
if (t.isJSXElement(parentNode?.node)) {
|
||||
const parentAttributes = getJSXElementAttributes(parentNode.node);
|
||||
parentId = parentAttributes[SLOT.dnd];
|
||||
}
|
||||
|
||||
@@ -1235,9 +1183,6 @@ export function traverseViewFile(ast: t.File, idGenerator: IdGenerator) {
|
||||
ast,
|
||||
cleanAst,
|
||||
nodes,
|
||||
stateCode,
|
||||
classProperties,
|
||||
stateProperties,
|
||||
importedModules,
|
||||
variables,
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ImportDeclarationPayloadType } from '../types';
|
||||
import { IImportDeclarationPayload } from '../types';
|
||||
|
||||
/**
|
||||
* 导入列表解析为导入声明对象
|
||||
@@ -20,7 +20,7 @@ export function namesToImportDeclarations(
|
||||
return Object.keys(map).map((sourcePath) => ({
|
||||
sourcePath,
|
||||
...map[sourcePath],
|
||||
})) as ImportDeclarationPayloadType[];
|
||||
})) as IImportDeclarationPayload[];
|
||||
}
|
||||
|
||||
function updateMod(
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
uuid,
|
||||
} from '@music163/tango-helpers';
|
||||
import { getRelativePath, isFilepath } from './string';
|
||||
import type { ImportDeclarationPayloadType } from '../types';
|
||||
import type { IImportDeclarationPayload } from '../types';
|
||||
import { code2expression } from './ast';
|
||||
|
||||
/**
|
||||
@@ -17,7 +17,7 @@ import { code2expression } from './ast';
|
||||
export function getImportDeclarationPayloadByPrototype(
|
||||
prototype: ComponentPrototypeType,
|
||||
relativeFilepath?: string,
|
||||
): ImportDeclarationPayloadType {
|
||||
): IImportDeclarationPayload {
|
||||
let defaultSpecifier;
|
||||
let specifiers;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export * from './models';
|
||||
export * from './factory';
|
||||
export * from './types';
|
||||
export * from './helpers';
|
||||
export * from './types';
|
||||
@@ -1,8 +1,8 @@
|
||||
import { action, computed, makeObservable, observable, toJS } from 'mobx';
|
||||
import { getValue, isNil, logger, setValue } from '@music163/tango-helpers';
|
||||
import type { FileType, ModulePropsType } from '../types';
|
||||
import { formatCode } from '../helpers';
|
||||
import type { FileType, IFileConfig } from '../types';
|
||||
import { IWorkspace } from './interfaces';
|
||||
import { formatCode } from '../helpers';
|
||||
|
||||
/**
|
||||
* 普通文件,不进行 AST 解析
|
||||
@@ -35,7 +35,7 @@ export class TangoFile {
|
||||
return this._cleanCode;
|
||||
}
|
||||
|
||||
constructor(workspace: IWorkspace, props: ModulePropsType, isSyncCode = true) {
|
||||
constructor(workspace: IWorkspace, props: IFileConfig, isSyncCode = true) {
|
||||
this.workspace = workspace;
|
||||
this.filename = props.filename;
|
||||
this.type = props.type;
|
||||
@@ -60,7 +60,7 @@ export class TangoFile {
|
||||
}
|
||||
|
||||
export class TangoLessFile extends TangoFile {
|
||||
constructor(workspace: IWorkspace, props: ModulePropsType) {
|
||||
constructor(workspace: IWorkspace, props: IFileConfig) {
|
||||
super(workspace, props, false);
|
||||
this.update(props.code);
|
||||
makeObservable(this, {
|
||||
@@ -87,7 +87,7 @@ export class TangoJsonFile extends TangoFile {
|
||||
return toJS(this._object);
|
||||
}
|
||||
|
||||
constructor(workspace: IWorkspace, props: ModulePropsType) {
|
||||
constructor(workspace: IWorkspace, props: IFileConfig) {
|
||||
super(workspace, props, false);
|
||||
this.update(props.code);
|
||||
makeObservable(this, {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -3,17 +3,18 @@ import { TangoHistory } from './history';
|
||||
import { SelectSource } from './select-source';
|
||||
import { DragSource } from './drag-source';
|
||||
import {
|
||||
FileItemType,
|
||||
IFileConfig,
|
||||
FileType,
|
||||
ImportDeclarationPayloadType,
|
||||
IImportDeclarationPayload,
|
||||
InsertChildPositionType,
|
||||
PackageConfigType,
|
||||
PageConfigType,
|
||||
ProjectDataType,
|
||||
ServiceFunctionPayloadType,
|
||||
} from '../types/types';
|
||||
ITangoConfigPackages,
|
||||
IPageConfigData,
|
||||
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;
|
||||
@@ -38,17 +42,41 @@ export interface IViewFile {
|
||||
sourcePrototype?: string | ComponentPrototypeType,
|
||||
) => IViewFile;
|
||||
|
||||
insertAfter: (targetNodeId: string, newNode: any, sourcePrototype?: string | ComponentPrototypeType) => IViewFile;
|
||||
insertAfter: (
|
||||
targetNodeId: string,
|
||||
newNode: any,
|
||||
sourcePrototype?: string | ComponentPrototypeType,
|
||||
) => IViewFile;
|
||||
|
||||
insertBefore: (targetNodeId: string, newNode: any, sourcePrototype?: string | ComponentPrototypeType) => IViewFile;
|
||||
insertBefore: (
|
||||
targetNodeId: string,
|
||||
newNode: any,
|
||||
sourcePrototype?: string | ComponentPrototypeType,
|
||||
) => IViewFile;
|
||||
|
||||
replaceNode: (targetNodeId: string, newNode: any, sourcePrototype?: string | ComponentPrototypeType) => IViewFile;
|
||||
replaceNode: (
|
||||
targetNodeId: string,
|
||||
newNode: any,
|
||||
sourcePrototype?: string | ComponentPrototypeType,
|
||||
) => IViewFile;
|
||||
|
||||
replaceViewChildren: (rawNodes: any[], importDeclarations?: ImportDeclarationPayloadType[]) => IViewFile;
|
||||
replaceViewChildren: (
|
||||
rawNodes: any[],
|
||||
importDeclarations?: IImportDeclarationPayload[],
|
||||
) => IViewFile;
|
||||
|
||||
updateNodeAttribute: (nodeId: string, attrName: string, attrValue?: any, relatedImports?: string[]) => IViewFile;
|
||||
updateNodeAttribute: (
|
||||
nodeId: string,
|
||||
attrName: string,
|
||||
attrValue?: any,
|
||||
relatedImports?: string[],
|
||||
) => IViewFile;
|
||||
|
||||
updateNodeAttributes: (nodeId: string, config: Record<string, any>, relatedImports?: string[]) => IViewFile;
|
||||
updateNodeAttributes: (
|
||||
nodeId: string,
|
||||
config: Record<string, any>,
|
||||
relatedImports?: string[],
|
||||
) => IViewFile;
|
||||
|
||||
get code(): string;
|
||||
get nodes(): Map<string, IViewNode>;
|
||||
@@ -115,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;
|
||||
@@ -126,11 +155,6 @@ export interface IWorkspace {
|
||||
setComponentPrototypes: (prototypes: Record<string, ComponentPrototypeType>) => void;
|
||||
getPrototype: (name: string | ComponentPrototypeType) => ComponentPrototypeType;
|
||||
|
||||
/**
|
||||
* 获取项目数据
|
||||
*/
|
||||
getProjectData?: () => ProjectDataType;
|
||||
|
||||
/**
|
||||
* 查询节点
|
||||
* @param id 节点 ID
|
||||
@@ -138,66 +162,57 @@ 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: FileItemType[]) => void;
|
||||
addFiles: (files: IFileConfig[]) => void;
|
||||
updateFile: (filename: string, code: string, shouldFormatCode?: boolean) => void;
|
||||
removeFile: (filename: string) => void;
|
||||
renameFile: (oldFilename: string, newFilename: string) => void;
|
||||
getFile: (filename: string) => TangoFile;
|
||||
listFiles: () => Record<string, string>;
|
||||
|
||||
addViewPage: (name: string, code: string) => void;
|
||||
renameFolder: (oldFoldername: string, newFoldername: 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;
|
||||
|
||||
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: ServiceFunctionPayloadType | ServiceFunctionPayloadType[]) => void;
|
||||
|
||||
updateServiceFunction?: (payload: any) => void;
|
||||
|
||||
updateServiceBaseConfig?: (name: string, value: any) => void;
|
||||
|
||||
listStoreModules?: () => TangoStoreModule[];
|
||||
getServiceFunction?: (serviceKey: 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;
|
||||
|
||||
copyViewPage: (sourceRoutePath: string, targetPageData: PageConfigType) => void;
|
||||
|
||||
updateRoute: (sourceRoutePath: string, targetPageData: PageConfigType) => void;
|
||||
updateRoute: (sourceRoutePath: string, targetPageData: IPageConfigData) => void;
|
||||
|
||||
listDependencies?: () => any;
|
||||
|
||||
@@ -205,7 +220,7 @@ export interface IWorkspace {
|
||||
name: string,
|
||||
version: string,
|
||||
options?: {
|
||||
package?: PackageConfigType;
|
||||
package?: ITangoConfigPackages;
|
||||
[x: string]: any;
|
||||
},
|
||||
) => void;
|
||||
@@ -216,7 +231,7 @@ export interface IWorkspace {
|
||||
name: string,
|
||||
version: string,
|
||||
options?: {
|
||||
package?: PackageConfigType;
|
||||
package?: ITangoConfigPackages;
|
||||
[x: string]: any;
|
||||
},
|
||||
) => void;
|
||||
@@ -224,14 +239,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[];
|
||||
}
|
||||
|
||||
@@ -1,63 +1,20 @@
|
||||
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 {
|
||||
RouteDataType,
|
||||
ModulePropsType,
|
||||
ClassPropertyNodeType,
|
||||
ServiceFunctionPayloadType,
|
||||
TangoViewNodeDataType,
|
||||
StorePropertyType,
|
||||
ImportDeclarationPayloadType,
|
||||
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: ModulePropsType, isSyncCode = true) {
|
||||
constructor(workspace: IWorkspace, props: IFileConfig, isSyncCode = true) {
|
||||
super(workspace, props, isSyncCode);
|
||||
}
|
||||
|
||||
@@ -119,7 +76,7 @@ class TangoModule extends TangoFile {
|
||||
* 普通 JS 文件
|
||||
*/
|
||||
export class TangoJsModule extends TangoModule {
|
||||
constructor(workspace: IWorkspace, props: ModulePropsType) {
|
||||
constructor(workspace: IWorkspace, props: IFileConfig) {
|
||||
super(workspace, props, false);
|
||||
this.update(props.code, false, false);
|
||||
|
||||
@@ -132,628 +89,3 @@ export class TangoJsModule extends TangoModule {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 入口配置模块
|
||||
*/
|
||||
export class TangoStoreEntryModule extends TangoModule {
|
||||
_stores: string[] = [];
|
||||
|
||||
get stores() {
|
||||
return toJS(this._stores);
|
||||
}
|
||||
|
||||
constructor(workspace: IWorkspace, props: ModulePropsType) {
|
||||
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: RouteDataType[];
|
||||
|
||||
get routes() {
|
||||
return toJS(this._routes);
|
||||
}
|
||||
|
||||
constructor(workspace: IWorkspace, props: ModulePropsType) {
|
||||
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: TangoViewNodeDataType[];
|
||||
/**
|
||||
* 通过导入组件名查找组件来自的包
|
||||
*/
|
||||
importMap: Dict<{ package: string; isDefault?: boolean }>;
|
||||
|
||||
/**
|
||||
* 视图中依赖的 tango 变量,仅 stores 和 services
|
||||
*/
|
||||
variables: string[];
|
||||
|
||||
/**
|
||||
* 节点列表
|
||||
*/
|
||||
private _nodes: Map<string, TangoNode>;
|
||||
/**
|
||||
* 导入的模块
|
||||
*/
|
||||
private _importedModules: Dict<ImportDeclarationPayloadType | ImportDeclarationPayloadType[]>;
|
||||
/**
|
||||
* 类属性
|
||||
* @deprecated
|
||||
*/
|
||||
private _classProperties: Dict<ClassPropertyNodeType>;
|
||||
/**
|
||||
* 状态属性
|
||||
*/
|
||||
private _stateProperties: string[];
|
||||
/**
|
||||
* 状态代码
|
||||
*/
|
||||
private _stateCode: string;
|
||||
/**
|
||||
* ID 生成器
|
||||
*/
|
||||
private _idGenerator: IdGenerator;
|
||||
|
||||
get classProperties() {
|
||||
return this._classProperties;
|
||||
}
|
||||
|
||||
get stateProperties() {
|
||||
return this._stateProperties;
|
||||
}
|
||||
|
||||
get nodes() {
|
||||
return this._nodes;
|
||||
}
|
||||
|
||||
get nodesTree() {
|
||||
return toJS(this._nodesTree);
|
||||
}
|
||||
|
||||
get stateCode() {
|
||||
return this._stateCode;
|
||||
}
|
||||
|
||||
get tree() {
|
||||
return this.ast;
|
||||
}
|
||||
|
||||
constructor(workspace: IWorkspace, props: ModulePropsType) {
|
||||
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,
|
||||
stateCode,
|
||||
stateProperties,
|
||||
classProperties,
|
||||
importedModules,
|
||||
variables,
|
||||
} = traverseViewFile(this.ast, this._idGenerator);
|
||||
this.ast = newAst;
|
||||
|
||||
this._code = ast2code(newAst);
|
||||
this._cleanCode = ast2code(cleanAst);
|
||||
|
||||
this._stateCode = stateCode;
|
||||
|
||||
this._stateProperties = stateProperties;
|
||||
this._classProperties = classProperties;
|
||||
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: ImportDeclarationPayloadType) {
|
||||
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?: ImportDeclarationPayloadType[],
|
||||
) {
|
||||
if (childrenNodes.length) {
|
||||
this.ast = replaceRootJSXElementChildren(this.ast, childrenNodes);
|
||||
}
|
||||
|
||||
if (importDeclarations?.length) {
|
||||
importDeclarations.forEach((item) => {
|
||||
this.updateImportSpecifiers(item);
|
||||
});
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
private buildImportMap(
|
||||
importedModules: Dict<ImportDeclarationPayloadType | ImportDeclarationPayloadType[]>,
|
||||
) {
|
||||
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: TangoViewNodeDataType[]) {
|
||||
const map: Record<string, TangoViewNodeDataType> = {};
|
||||
|
||||
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: ModulePropsType) {
|
||||
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: ServiceFunctionPayloadType) {
|
||||
const { name, ...rest } = payload;
|
||||
this.ast = updateServiceConfigToServiceFile(this.ast, { [name]: clone(rest, false) });
|
||||
return this;
|
||||
}
|
||||
|
||||
addServiceFunctions(payloads: ServiceFunctionPayloadType[]) {
|
||||
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: ServiceFunctionPayloadType) {
|
||||
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: StorePropertyType[];
|
||||
|
||||
actions: StorePropertyType[];
|
||||
|
||||
constructor(workspace: IWorkspace, props: ModulePropsType) {
|
||||
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,10 +1,10 @@
|
||||
import { JSXElement, SourceLocation } from '@babel/types';
|
||||
import { cloneJSXElementWithoutTrackingData, getJSXElementAttributes } from '../helpers';
|
||||
import { TangoViewNodeDataType } from '../types';
|
||||
import { TangoViewModule } from './module';
|
||||
import { ITangoViewNodeData } from '../types';
|
||||
import { TangoViewModule } from './view-module';
|
||||
import { IViewNode } from './interfaces';
|
||||
|
||||
type TangoNodeConstructorPropsType = TangoViewNodeDataType & {
|
||||
type TangoNodeConstructorPropsType = ITangoViewNodeData & {
|
||||
file: TangoViewModule;
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
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 {
|
||||
/**
|
||||
* 模块名
|
||||
*/
|
||||
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;
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -16,34 +16,29 @@ 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 {
|
||||
FileItemType,
|
||||
FileType,
|
||||
PackageConfigType,
|
||||
PageConfigType,
|
||||
ProjectDataType,
|
||||
ServiceFunctionPayloadType,
|
||||
} from '../types';
|
||||
import { IWorkspace } from './interfaces';
|
||||
import {
|
||||
IFileConfig,
|
||||
FileType,
|
||||
ITangoConfigPackages,
|
||||
IPageConfigData,
|
||||
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 type WorkspaceOptionsType = {
|
||||
export interface IWorkspaceOptions {
|
||||
/**
|
||||
* 入口文件
|
||||
*/
|
||||
@@ -51,7 +46,7 @@ export type WorkspaceOptionsType = {
|
||||
/**
|
||||
* 初始化文件列表
|
||||
*/
|
||||
files?: FileItemType[];
|
||||
files?: IFileConfig[];
|
||||
/**
|
||||
* 默认的激活的路由
|
||||
*/
|
||||
@@ -60,7 +55,7 @@ export type WorkspaceOptionsType = {
|
||||
* 组件描述列表
|
||||
*/
|
||||
prototypes?: Record<string, ComponentPrototypeType>;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 工作区
|
||||
@@ -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,26 +156,13 @@ 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取页面列表
|
||||
*/
|
||||
get pages() {
|
||||
const appJsonPages = this.appJson?.getValue('pages');
|
||||
const pagesMap = array2object(appJsonPages || [], (item) => item.path);
|
||||
const ret: PageConfigType[] = [];
|
||||
const ret: IPageConfigData[] = [];
|
||||
this.routeModule?.routes.forEach((item) => {
|
||||
const data = pagesMap[item.path];
|
||||
if (item.path !== '*') {
|
||||
@@ -225,7 +207,7 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
return Object.keys(this.localBlocks);
|
||||
}
|
||||
|
||||
constructor(options?: WorkspaceOptionsType) {
|
||||
constructor(options?: IWorkspaceOptions) {
|
||||
super();
|
||||
this.history = new TangoHistory(this);
|
||||
this.selectSource = new SelectSource(this);
|
||||
@@ -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: ProjectDataType = {
|
||||
pages,
|
||||
stores,
|
||||
services,
|
||||
};
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置当前路由
|
||||
* @param routePath 路由路径
|
||||
@@ -353,7 +305,7 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
* 添加一组文件到工作区
|
||||
* @param files
|
||||
*/
|
||||
addFiles(files: FileItemType[] = []) {
|
||||
addFiles(files: IFileConfig[] = []) {
|
||||
files.forEach((file) => {
|
||||
this.addFile(file.filename, file.code, file.type);
|
||||
});
|
||||
@@ -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);
|
||||
@@ -483,8 +436,24 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
*/
|
||||
renameFile(oldFilename: string, newFilename: string) {
|
||||
const file = this.files.get(oldFilename);
|
||||
this.removeFile(oldFilename);
|
||||
this.addFile(newFilename, file.code);
|
||||
if (file) {
|
||||
this.removeFile(oldFilename);
|
||||
this.addFile(newFilename, file.code);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重命名文件夹
|
||||
* @param oldFoldername 旧文件夹名
|
||||
* @param newFoldername 新文件夹名
|
||||
*/
|
||||
renameFolder(oldFoldername: string, newFoldername: string) {
|
||||
Array.from(this.files.keys()).forEach((key) => {
|
||||
if (key.startsWith(`${oldFoldername}/`)) {
|
||||
const newKey = key.replace(oldFoldername, newFoldername);
|
||||
this.renameFile(key, newKey);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -508,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 路由名称
|
||||
@@ -540,7 +491,7 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
// remove appJson page
|
||||
this.appJson
|
||||
?.setValue('pages', (pages) => {
|
||||
return (pages as PageConfigType[]).filter((page) => page.path !== routePath);
|
||||
return (pages as IPageConfigData[]).filter((page) => page.path !== routePath);
|
||||
})
|
||||
.update();
|
||||
this.removeFile(filename);
|
||||
@@ -552,7 +503,7 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
* @param route 视图名
|
||||
* @param code 视图代码
|
||||
*/
|
||||
addViewPage(routeConfig: string | PageConfigType, code: string) {
|
||||
addViewPage(routeConfig: string | IPageConfigData, code: string) {
|
||||
const config =
|
||||
typeof routeConfig === 'string'
|
||||
? {
|
||||
@@ -569,11 +520,11 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
/**
|
||||
* 添加新的路由
|
||||
*/
|
||||
addRoute(routeData: PageConfigType, importFilePath: string) {
|
||||
addRoute(routeData: IPageConfigData, importFilePath: string) {
|
||||
this.routeModule?.addRoute(routeData.path, importFilePath).update();
|
||||
this.appJson
|
||||
?.setValue('pages', (pages) => {
|
||||
(pages as PageConfigType[]).push({
|
||||
(pages as IPageConfigData[]).push({
|
||||
name: routeData.name || routeData.path,
|
||||
path: routeData.path,
|
||||
privilegeCode: getPrivilegeCode(this.packageJson?.getValue('name'), routeData.path),
|
||||
@@ -588,13 +539,13 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
* @param sourceRoutePath
|
||||
* @param targetPageData
|
||||
*/
|
||||
updateRoute(sourceRoutePath: string, targetPageData: PageConfigType) {
|
||||
updateRoute(sourceRoutePath: string, targetPageData: IPageConfigData) {
|
||||
if (sourceRoutePath !== targetPageData.path) {
|
||||
this.routeModule?.updateRoute(sourceRoutePath, targetPageData.path).update();
|
||||
}
|
||||
this.appJson
|
||||
?.setValue('pages', (pages) => {
|
||||
for (const page of pages as PageConfigType[]) {
|
||||
for (const page of pages as IPageConfigData[]) {
|
||||
if (page.path === sourceRoutePath) {
|
||||
page.path = targetPageData.path;
|
||||
page.name = targetPageData.name;
|
||||
@@ -611,7 +562,7 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
* @param sourceRoute
|
||||
* @param targetRouteConfig
|
||||
*/
|
||||
copyViewPage(sourceRoutePath: string, targetPageData: PageConfigType) {
|
||||
copyViewPage(sourceRoutePath: string, targetPageData: IPageConfigData) {
|
||||
const sourceFilePath = this.getRealViewFilePath(this.getFilenameByRoutePath(sourceRoutePath));
|
||||
const targetFilePath = getFilepath(targetPageData.path, '/src/pages');
|
||||
this.copyFiles(sourceFilePath, targetFilePath);
|
||||
@@ -625,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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用代码初始化完成
|
||||
*/
|
||||
@@ -680,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 模型名
|
||||
@@ -736,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();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -745,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();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -756,26 +640,47 @@ 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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取服务函数的详情
|
||||
* @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],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新服务函数
|
||||
*/
|
||||
updateServiceFunction(payload: any) {
|
||||
this.serviceModule.updateServiceFunction(payload).update();
|
||||
updateServiceFunction(payload: IServiceFunctionPayload, moduleName = 'index') {
|
||||
this.serviceModules[moduleName].updateServiceFunction(payload).update();
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增服务函数,支持批量添加
|
||||
*/
|
||||
addServiceFunction(payload: ServiceFunctionPayloadType | ServiceFunctionPayloadType[]) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -783,15 +688,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();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -811,7 +716,7 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
name: string,
|
||||
version: string,
|
||||
options?: {
|
||||
package?: PackageConfigType;
|
||||
package?: ITangoConfigPackages;
|
||||
[x: string]: any;
|
||||
},
|
||||
) {
|
||||
@@ -906,7 +811,7 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
name: string,
|
||||
version: string,
|
||||
options?: {
|
||||
package?: PackageConfigType;
|
||||
package?: ITangoConfigPackages;
|
||||
[x: string]: any;
|
||||
},
|
||||
) {
|
||||
@@ -1279,4 +1184,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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ export enum FileType {
|
||||
Scss = 'scss',
|
||||
}
|
||||
|
||||
export type FileItemType = {
|
||||
export interface IFileConfig {
|
||||
/**
|
||||
* 文件名
|
||||
*/
|
||||
@@ -40,14 +40,12 @@ export type FileItemType = {
|
||||
* 文件类型
|
||||
*/
|
||||
type?: FileType;
|
||||
};
|
||||
|
||||
export type ModulePropsType = FileItemType;
|
||||
}
|
||||
|
||||
/**
|
||||
* 视图节点数据类型
|
||||
*/
|
||||
export type TangoViewNodeDataType<T = JSXElement> = {
|
||||
export interface ITangoViewNodeData<T = JSXElement> {
|
||||
/**
|
||||
* 节点 ID
|
||||
*/
|
||||
@@ -71,39 +69,39 @@ export type TangoViewNodeDataType<T = JSXElement> = {
|
||||
/**
|
||||
* 子节点列表
|
||||
*/
|
||||
children?: Array<TangoViewNodeDataType<T>>;
|
||||
};
|
||||
children?: Array<ITangoViewNodeData<T>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 模块导入的参数类型
|
||||
*/
|
||||
export type ImportDeclarationPayloadType = {
|
||||
export interface IImportDeclarationPayload {
|
||||
defaultSpecifier?: string;
|
||||
specifiers?: string[];
|
||||
sourcePath: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type ClassPropertyNodeType = {
|
||||
export interface IClassPropertyNodeData {
|
||||
reference: string;
|
||||
propertyName: string;
|
||||
propertyBody: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 服务函数参数类型
|
||||
*/
|
||||
export type ServiceFunctionPayloadType = {
|
||||
export interface IServiceFunctionPayload {
|
||||
[key: string]: any;
|
||||
/**
|
||||
* 服务函数名
|
||||
*/
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Store 属性类型
|
||||
*/
|
||||
export type StorePropertyType = {
|
||||
export interface IStorePropertyData {
|
||||
/**
|
||||
* 属性名
|
||||
*/
|
||||
@@ -130,50 +128,12 @@ export type StorePropertyType = {
|
||||
| 'type'
|
||||
| 'variable'
|
||||
| 'object';
|
||||
};
|
||||
|
||||
/*
|
||||
* 服务函数的操作类型
|
||||
*/
|
||||
export enum ESFOperationType {
|
||||
ADD = 'add',
|
||||
UPDATE = 'update',
|
||||
DELETE = 'delete',
|
||||
}
|
||||
|
||||
/**
|
||||
* 服务函数数据源类型
|
||||
*/
|
||||
export enum ESFDataSourceType {
|
||||
OvermindX = 'ox',
|
||||
Custom = 'custom',
|
||||
}
|
||||
|
||||
/**
|
||||
* 服务函数 HTTP Type
|
||||
* 云音乐网关只支持 get 和 post
|
||||
*/
|
||||
export enum ESFHTTPMethodType {
|
||||
GET = 'GET',
|
||||
// PUT = 'PUT',
|
||||
POST = 'POST',
|
||||
// PATCH = 'PATCH',
|
||||
// DELETE = 'DELETE',
|
||||
}
|
||||
|
||||
/**
|
||||
* requestType
|
||||
* headers: Content-Type
|
||||
*/
|
||||
export enum ESFHTTRequestType {
|
||||
'application/json' = 'json',
|
||||
'application/x-www-form-urlencoded' = 'x-www-form-urlencoded',
|
||||
}
|
||||
|
||||
/**
|
||||
* 路由解析数据
|
||||
*/
|
||||
export type RouteDataType = {
|
||||
export interface IRouteData {
|
||||
/**
|
||||
* 路由
|
||||
*/
|
||||
@@ -187,12 +147,12 @@ export type RouteDataType = {
|
||||
*/
|
||||
importPath?: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 页面配置数据
|
||||
*/
|
||||
export type PageConfigType = {
|
||||
export interface IPageConfigData {
|
||||
/**
|
||||
* 路由
|
||||
*/
|
||||
@@ -209,12 +169,12 @@ export type PageConfigType = {
|
||||
* PMS 权限码
|
||||
*/
|
||||
privilegeCode?: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 项目数据
|
||||
*/
|
||||
export type ProjectDataType = {
|
||||
export interface IProjectData {
|
||||
/**
|
||||
* 页面列表
|
||||
*/
|
||||
@@ -241,14 +201,14 @@ export type ProjectDataType = {
|
||||
functions: string[];
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export type InsertChildPositionType = 'first' | 'last';
|
||||
|
||||
/**
|
||||
* tango.config.json 中 packages 定义
|
||||
*/
|
||||
export type PackageConfigType = {
|
||||
export interface ITangoConfigPackages {
|
||||
/**
|
||||
* 依赖类型
|
||||
*/
|
||||
@@ -273,4 +233,4 @@ export type PackageConfigType = {
|
||||
* 设计态 umd 资源
|
||||
*/
|
||||
designerResources?: string[];
|
||||
};
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from './types';
|
||||
@@ -3,6 +3,37 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
# [0.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
|
||||
|
||||
# [0.2.0](https://github.com/netease/tango/compare/@music163/tango-designer@0.1.3...@music163/tango-designer@0.2.0) (2023-09-13)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- nodeTree defaultExpandAll rerender bug ([#24](https://github.com/netease/tango/issues/24)) ([12de10b](https://github.com/netease/tango/commit/12de10b0bc89a27d80b1b3bbb4d4b5aed4b73b35))
|
||||
- refactor setting formItem ([b786de2](https://github.com/netease/tango/commit/b786de2f1a0e4e9141eb09fce696e45df633b232))
|
||||
- refactor types ([15542d9](https://github.com/netease/tango/commit/15542d9eb2f8959597b81cae457091ee71710c83))
|
||||
- remove some useless setters ([7e9ab12](https://github.com/netease/tango/commit/7e9ab12503ed33c1e6acb8a1fa5fd89fc82d35fd))
|
||||
- update exports ([443ae58](https://github.com/netease/tango/commit/443ae589f67255f3815b587fd8f928c2b8742e9a))
|
||||
- update sidebar ([4d809e9](https://github.com/netease/tango/commit/4d809e9afd0d6d525850708722736847b510638e))
|
||||
|
||||
### Features
|
||||
|
||||
- refactor setting-form ([8d396ff](https://github.com/netease/tango/commit/8d396ff13459beeb57b6b3c48f7e8fe1765041ae))
|
||||
|
||||
## [0.1.3](https://github.com/netease/tango/compare/@music163/tango-designer@0.1.2...@music163/tango-designer@0.1.3) (2023-09-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- add WorkspaceView ([5ecf444](https://github.com/netease/tango/commit/5ecf444fc3563a1bf8e533136e93c1a1355d39dc))
|
||||
|
||||
## 0.1.2 (2023-09-06)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-designer",
|
||||
"version": "0.1.2",
|
||||
"version": "0.3.0",
|
||||
"description": "lowcode designer",
|
||||
"keywords": [
|
||||
"react"
|
||||
@@ -27,28 +27,34 @@
|
||||
"prepublishOnly": "yarn build"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">= 16.8.0",
|
||||
"styled-components": "5"
|
||||
"react": ">= 16.8",
|
||||
"styled-components": ">= 4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^4.8.0",
|
||||
"@music163/request": "^0.1.0",
|
||||
"@music163/tango-context": "^0.1.2",
|
||||
"@music163/tango-core": "^0.1.2",
|
||||
"@music163/tango-helpers": "^0.1.2",
|
||||
"@music163/tango-sandbox": "^0.1.2",
|
||||
"@music163/tango-setting-form": "^0.1.2",
|
||||
"@music163/tango-ui": "^0.1.2",
|
||||
"@music163/tango-context": "^0.2.0",
|
||||
"@music163/tango-core": "^0.2.0",
|
||||
"@music163/tango-helpers": "^0.1.3",
|
||||
"@music163/tango-sandbox": "^0.1.5",
|
||||
"@music163/tango-setting-form": "^0.2.2",
|
||||
"@music163/tango-ui": "^0.1.4",
|
||||
"antd": "^4.24.2",
|
||||
"cash-dom": "^8.1.2",
|
||||
"classnames": "^2.3.2",
|
||||
"color": "^4.2.3",
|
||||
"coral-system": "^1.0.5",
|
||||
"cssjson": "^2.1.3",
|
||||
"date-fns": "^2.29.2",
|
||||
"lodash-es": "^4.17.21",
|
||||
"moment": "^2.29.4",
|
||||
"react-color": "^2.19.3",
|
||||
"react-resizable": "^3.0.5",
|
||||
"semver": "^7.3.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/color": "^3.0.4",
|
||||
"@types/react-color": "^3.0.6",
|
||||
"@types/react-resizable": "^3.0.4"
|
||||
},
|
||||
"publishConfig": {
|
||||
|
||||
+60
-8
@@ -1,9 +1,7 @@
|
||||
import React, { useState, useRef, useEffect, memo } from 'react';
|
||||
import { Input } from 'antd';
|
||||
import { css } from 'styled-components';
|
||||
import { Box } from 'coral-system';
|
||||
import { css, Box } from 'coral-system';
|
||||
import { DeleteOutlined, PlusOutlined } from '@ant-design/icons';
|
||||
import { formatValue, compareValue } from './util';
|
||||
|
||||
const rowCss = css`
|
||||
display: flex;
|
||||
@@ -99,25 +97,25 @@ const OneInputKV = (props: any) => {
|
||||
!newData.length && setValidate(false);
|
||||
};
|
||||
|
||||
const handleInput = (name: string, index: number, value: string) => {
|
||||
const handleInput = (name: string, index: number, val: string) => {
|
||||
const newData = [...data];
|
||||
|
||||
if (!newData[index]) return;
|
||||
|
||||
// 更新值
|
||||
newData[index][name] = value;
|
||||
newData[index][name] = val;
|
||||
|
||||
_updateData(newData);
|
||||
};
|
||||
|
||||
const _updateData = (values: Array<Record<string, string>>) => {
|
||||
const omit = ({ index, ...rest }: any) => rest;
|
||||
let value = formatValue({ data: values, format });
|
||||
let nextValue = formatValue({ data: values, format });
|
||||
|
||||
Array.isArray(value) && (value = value.map((item) => omit(item)));
|
||||
Array.isArray(nextValue) && (nextValue = nextValue.map((item) => omit(item)));
|
||||
|
||||
setData(values);
|
||||
onChange?.(value);
|
||||
onChange?.(nextValue);
|
||||
};
|
||||
|
||||
// render
|
||||
@@ -166,3 +164,57 @@ const OneInputKV = (props: any) => {
|
||||
};
|
||||
|
||||
export const InputKV = memo(OneInputKV, (prev, next) => prev.value === next.value);
|
||||
|
||||
interface IFormatValueConfig {
|
||||
format: 'simple' | 'original';
|
||||
data?: Record<string, string> | Array<Record<string, string>>;
|
||||
}
|
||||
|
||||
const formatData = (data?: Array<Record<string, string>>) => {
|
||||
const result: Record<string, string> = {};
|
||||
|
||||
// 遍历数据
|
||||
data?.forEach((item) => {
|
||||
item.key && !(item.key in result) && (result[item.key] = item.value);
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// 格式化value
|
||||
const formatValue = ({ format, data }: IFormatValueConfig) => {
|
||||
if (format === 'original') {
|
||||
if (!data) return [{ index: 0 }];
|
||||
|
||||
const list = Array.isArray(data)
|
||||
? data
|
||||
: Object.keys(data).map((key) => ({ key, value: data[key] }));
|
||||
|
||||
return list.map((item, idx) => ({ ...item, index: idx - list.length - 1 }));
|
||||
} else {
|
||||
if (!data) return {};
|
||||
|
||||
return Array.isArray(data) ? formatData(data) : data;
|
||||
}
|
||||
};
|
||||
|
||||
const compareValue = (origin: Array<any>, data: Array<any>) => {
|
||||
const resolve = (list: Array<any>) => {
|
||||
let keyStr = '';
|
||||
let valueStr = '';
|
||||
|
||||
list.forEach((item) => {
|
||||
const { key, value } = item;
|
||||
|
||||
keyStr += key || '$';
|
||||
valueStr += value || '$';
|
||||
});
|
||||
|
||||
return { keyStr, valueStr };
|
||||
};
|
||||
|
||||
const oldRes = resolve(origin);
|
||||
const newRes = resolve(data);
|
||||
|
||||
return oldRes.keyStr === newRes.keyStr && oldRes.valueStr === newRes.valueStr;
|
||||
};
|
||||
+35
-30
@@ -33,7 +33,7 @@ import {
|
||||
} from '@music163/tango-ui';
|
||||
import { isValidExpressionCode } from '@music163/tango-core';
|
||||
|
||||
export type VariableTreeNodeType = {
|
||||
export interface IVariableTreeNode {
|
||||
/**
|
||||
* 唯一标识符
|
||||
*/
|
||||
@@ -65,9 +65,9 @@ export type VariableTreeNodeType = {
|
||||
/**
|
||||
* 子结点
|
||||
*/
|
||||
children?: VariableTreeNodeType[];
|
||||
children?: IVariableTreeNode[];
|
||||
[key: string]: any;
|
||||
};
|
||||
}
|
||||
|
||||
type VariableModelType = 'preview' | 'define' | 'add';
|
||||
|
||||
@@ -91,15 +91,15 @@ export interface EditableVariableTreeProps {
|
||||
/**
|
||||
* 预览值取值函数
|
||||
*/
|
||||
getPreviewValue?: (node: VariableTreeNodeType) => unknown;
|
||||
getPreviewValue?: (node: IVariableTreeNode) => unknown;
|
||||
/**
|
||||
* 选择列表项时的回调
|
||||
*/
|
||||
onSelect?: (data: VariableTreeNodeType) => void;
|
||||
onSelect?: (data: IVariableTreeNode) => void;
|
||||
/**
|
||||
* 点击使用此变量按钮时的回调
|
||||
*/
|
||||
onUse?: (data: VariableTreeNodeType) => void;
|
||||
onUse?: (data: IVariableTreeNode) => void;
|
||||
/**
|
||||
* 保存结点定义的回调
|
||||
*/
|
||||
@@ -152,7 +152,7 @@ export function EditableVariableTree({
|
||||
showPreviewAlert = true,
|
||||
}: EditableVariableTreeProps) {
|
||||
const [keyword, setKeyword] = useState('');
|
||||
const [node, setNode] = useState<VariableTreeNodeType>();
|
||||
const [node, setNode] = useState<IVariableTreeNode>();
|
||||
const [mode, setMode] = useState<EditableVariableTreeProps['defaultMode']>(defaultMode);
|
||||
const hasPanelSwitch = modes.includes('define') && modes.includes('preview');
|
||||
|
||||
@@ -184,17 +184,17 @@ export function EditableVariableTree({
|
||||
</Box>
|
||||
<VariableTree
|
||||
dataSource={treeData}
|
||||
onSelect={(node) => {
|
||||
setNode(node);
|
||||
onSelect={(item) => {
|
||||
setNode(item);
|
||||
mode === 'add' && setMode(defaultMode);
|
||||
onSelect(node);
|
||||
onSelect(item);
|
||||
}}
|
||||
onAdd={(node) => {
|
||||
setNode(node);
|
||||
onAdd={(item) => {
|
||||
setNode(item);
|
||||
setMode('add');
|
||||
}}
|
||||
onRemove={(node) => {
|
||||
const [type, storeName] = node.key.split('.');
|
||||
onRemove={(item) => {
|
||||
const [type, storeName] = item.key.split('.');
|
||||
setNode(null);
|
||||
onDeleteStore(storeName);
|
||||
}}
|
||||
@@ -257,8 +257,8 @@ export function EditableVariableTree({
|
||||
<NodeDefineForm
|
||||
node={node}
|
||||
onSave={onSave}
|
||||
onDelete={(node) => {
|
||||
const [type, storeName, stateName] = node.key.split('.');
|
||||
onDelete={(item) => {
|
||||
const [type, storeName, stateName] = item.key.split('.');
|
||||
onDeleteVariable(storeName, stateName);
|
||||
setNode(null);
|
||||
}}
|
||||
@@ -301,7 +301,7 @@ export function EditableVariableTreeModal({
|
||||
onSelect = noop,
|
||||
...rest
|
||||
}: EditableVariableTreeModalProps) {
|
||||
const [node, setNode] = useState<VariableTreeNodeType>();
|
||||
const [node, setNode] = useState<IVariableTreeNode>();
|
||||
const [visible, { on, off }] = useBoolean(false);
|
||||
return (
|
||||
<Box>
|
||||
@@ -348,17 +348,19 @@ const varTreeStyle = css`
|
||||
`;
|
||||
|
||||
interface VariableTreeProps {
|
||||
dataSource?: VariableTreeNodeType[];
|
||||
onSelect?: (data: VariableTreeNodeType) => void;
|
||||
onAdd?: (data: VariableTreeNodeType) => void;
|
||||
onRemove?: (data: VariableTreeNodeType) => void;
|
||||
dataSource?: IVariableTreeNode[];
|
||||
onSelect?: (data: IVariableTreeNode) => void;
|
||||
onAdd?: (data: IVariableTreeNode) => void;
|
||||
onRemove?: (data: IVariableTreeNode) => void;
|
||||
deletable?: boolean;
|
||||
}
|
||||
|
||||
function VariableTree({
|
||||
export function VariableTree({
|
||||
dataSource = [],
|
||||
onSelect = noop,
|
||||
onAdd = noop,
|
||||
onRemove = noop,
|
||||
deletable = false,
|
||||
}: VariableTreeProps) {
|
||||
return (
|
||||
<Box className="VariableTree" css={varTreeStyle}>
|
||||
@@ -370,12 +372,15 @@ function VariableTree({
|
||||
onSelect(detail.node);
|
||||
}}
|
||||
titleRender={(node) => {
|
||||
if (node.showAddChildIcon || node.showRemoveIcon) {
|
||||
// 只有叶子结点支持删除
|
||||
const showRemoveIcon = (!node.children && node.showDeleteIcon) ?? deletable;
|
||||
const showAddIcon = node.showAddChildIcon ?? false;
|
||||
if (showRemoveIcon || showAddIcon) {
|
||||
return (
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center" pr="l">
|
||||
<Text>{node.title}</Text>
|
||||
<Box>
|
||||
{node.showAddChildIcon && (
|
||||
{showAddIcon && (
|
||||
<Tooltip title={`向 ${node.title} 中添加变量`}>
|
||||
<Button
|
||||
type="text"
|
||||
@@ -388,7 +393,7 @@ function VariableTree({
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
{node.showRemoveIcon && (
|
||||
{showRemoveIcon && (
|
||||
<Popconfirm
|
||||
title="确认删除吗?该操作会导致引用此模型的代码报错,请谨慎操作!"
|
||||
onConfirm={() => onRemove(node)}
|
||||
@@ -450,9 +455,9 @@ export function ValuePreview({ value, onSelect }: NodePreviewProps) {
|
||||
}
|
||||
|
||||
interface NodeDefineProps {
|
||||
node: VariableTreeNodeType;
|
||||
onSave?: (code: string, node: VariableTreeNodeType) => void;
|
||||
onDelete?: (node: VariableTreeNodeType) => void;
|
||||
node: IVariableTreeNode;
|
||||
onSave?: (code: string, node: IVariableTreeNode) => void;
|
||||
onDelete?: (node: IVariableTreeNode) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -470,7 +475,7 @@ function NodeDefineForm({ node, onSave = noop, onDelete = noop }: NodeDefineProp
|
||||
<InputCode
|
||||
shape="inset"
|
||||
value={node.raw}
|
||||
onChange={(value) => setValue(value)}
|
||||
onChange={(nextValue) => setValue(nextValue)}
|
||||
editable={editable}
|
||||
onBlur={() => {
|
||||
if (!isValidExpressionCode(value)) {
|
||||
@@ -527,7 +532,7 @@ function NodeDefineForm({ node, onSave = noop, onDelete = noop }: NodeDefineProp
|
||||
}
|
||||
|
||||
interface AddNodeFormProps {
|
||||
parentNode: VariableTreeNodeType;
|
||||
parentNode: IVariableTreeNode;
|
||||
onCancel?: React.MouseEventHandler<HTMLButtonElement>;
|
||||
onSubmit?: (storeName: string, data: { name: string; initialValue: string }) => void;
|
||||
}
|
||||
@@ -1,10 +1,7 @@
|
||||
import { createContext, TangoRemoteServicesType } from '@music163/tango-helpers';
|
||||
import { createContext } from '@music163/tango-helpers';
|
||||
import type { DndQuery } from './dnd';
|
||||
|
||||
/**
|
||||
* TODO: 是不是直接合并到 Context 中
|
||||
*/
|
||||
export interface IDesignerContext {
|
||||
export interface IDesignerContext<S = any> {
|
||||
/**
|
||||
* 沙箱查询实例
|
||||
*/
|
||||
@@ -12,7 +9,7 @@ export interface IDesignerContext {
|
||||
/**
|
||||
* 远程服务
|
||||
*/
|
||||
remoteServices?: TangoRemoteServicesType;
|
||||
remoteServices?: Record<string, S>;
|
||||
}
|
||||
|
||||
const [DesignerProvider, useDesigner] = createContext<IDesignerContext>({
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { SystemProvider } from 'coral-system';
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { SystemProvider, extendTheme } from 'coral-system';
|
||||
import { ConfigProvider } from 'antd';
|
||||
import zhCN from 'antd/lib/locale/zh_CN';
|
||||
import { TangoEngineProvider, ITangoEngineContext } from '@music163/tango-context';
|
||||
import { createFromIconfontCN } from '@ant-design/icons';
|
||||
import { DesignerProvider, IDesignerContext } from './context';
|
||||
import { theme as baseTheme } from './theme';
|
||||
import { createFromIconfontCN } from '@ant-design/icons';
|
||||
import zhCN from 'antd/lib/locale/zh_CN';
|
||||
|
||||
export interface DesignerProps extends IDesignerContext, ITangoEngineContext {
|
||||
/**
|
||||
@@ -33,7 +33,7 @@ export function Designer(props: DesignerProps) {
|
||||
iconfontScriptUrl = '//at.alicdn.com/t/c/font_2891794_lzc7rtwuzf.js',
|
||||
children,
|
||||
} = props;
|
||||
const theme = themeProp ?? baseTheme;
|
||||
const theme = useMemo(() => extendTheme(themeProp, baseTheme), [themeProp]);
|
||||
useEffect(() => {
|
||||
createFromIconfontCN({
|
||||
scriptUrl: iconfontScriptUrl,
|
||||
|
||||
@@ -69,14 +69,6 @@ export const CodeEditor = observer(
|
||||
[workspace, autoRemoveUnusedImports, activeFile, files],
|
||||
);
|
||||
|
||||
const handleRenameFile = useCallback(
|
||||
(oldFilename: string, newFilename: string) => {
|
||||
workspace.renameFile(oldFilename, newFilename);
|
||||
workspace.setActiveFile(newFilename);
|
||||
},
|
||||
[workspace],
|
||||
);
|
||||
|
||||
const handleFileChange = useCallback(
|
||||
(type: string, info: any) => {
|
||||
switch (type) {
|
||||
@@ -88,7 +80,12 @@ export const CodeEditor = observer(
|
||||
workspace.removeFile(info.path);
|
||||
break;
|
||||
case 'renameFile':
|
||||
workspace.renameFile(info.path, info.newpath);
|
||||
workspace.setActiveFile(info.newpath);
|
||||
break;
|
||||
case 'renameFolder':
|
||||
workspace.renameFolder(info.path, info.newpath);
|
||||
break;
|
||||
case 'addFolder':
|
||||
default:
|
||||
break;
|
||||
@@ -114,7 +111,6 @@ export const CodeEditor = observer(
|
||||
}}
|
||||
ideConfig={ideConfig}
|
||||
onFileSave={fileSave}
|
||||
onRenameFile={handleRenameFile}
|
||||
onPathChange={handlePathChange}
|
||||
onFileChange={handleFileChange}
|
||||
defaultPath={activeFile}
|
||||
|
||||
@@ -3,14 +3,15 @@ export * from './designer';
|
||||
export * from './designer-panel';
|
||||
export * from './workspace-panel';
|
||||
export * from './setting-panel';
|
||||
export * from './view-panel';
|
||||
export * from './workspace-view';
|
||||
export * from './simulator';
|
||||
export * from './dnd';
|
||||
export * from './editor';
|
||||
export * from './sandbox';
|
||||
export * from './sidebar';
|
||||
export * from './toolbar';
|
||||
export * from './dnd';
|
||||
export * from './selection-menu';
|
||||
|
||||
export { INTERNAL_SELECTION_TOOLS } from './simulator/selection';
|
||||
|
||||
export { useWorkspace, useDesigner, observer } from '@music163/tango-context';
|
||||
export { register as registerSetter } from '@music163/tango-setting-form';
|
||||
|
||||
export type { FormItemComponentProps, IFormItemCreateOptions } from '@music163/tango-setting-form';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useImperativeHandle, ForwardedRef, useRef, useState, useEffect } from 'react';
|
||||
import { Box } from 'coral-system';
|
||||
import { CodeSandbox, CodeSandboxProps } from '@music163/tango-sandbox';
|
||||
import { getValue, logger, pick, setValue } from '@music163/tango-helpers';
|
||||
import { getValue, isFunction, logger, pick, setValue } from '@music163/tango-helpers';
|
||||
import { observer, useWorkspace, useDesigner } from '@music163/tango-context';
|
||||
import { Simulator, Viewport } from '../simulator';
|
||||
import { useSandboxQuery } from '../context';
|
||||
@@ -9,22 +9,27 @@ import { DndQuery, useDnd } from '../dnd';
|
||||
import { Navigator } from './navigator';
|
||||
import { SelectionToolsProps } from '../simulator/selection';
|
||||
|
||||
type SandboxEventHandlerConfig = {
|
||||
interface ISandboxEventHandlerConfig {
|
||||
sandboxQuery?: DndQuery;
|
||||
sandboxType?: 'design' | 'preview';
|
||||
isActive: boolean;
|
||||
[x: string]: any;
|
||||
};
|
||||
}
|
||||
|
||||
export type SandboxProps = Omit<CodeSandboxProps, 'files' | 'eventHandlers' | 'onMessage'> & {
|
||||
isPreview?: boolean;
|
||||
selectionTools?: SelectionToolsProps['actions'];
|
||||
builtinSelectionMenuMap?: SelectionToolsProps['builtinActionMap'];
|
||||
/**
|
||||
* tangoConfigJson 处理器
|
||||
*/
|
||||
configFormatter?: IMergeTangoConfigJsonConfig['formatter'];
|
||||
sandboxType?: 'design' | 'preview';
|
||||
mode?: 'single' | 'combined';
|
||||
injectScript?: string;
|
||||
onViewChange?: (data: any, config?: SandboxEventHandlerConfig) => void;
|
||||
onMessage?: (data: any, config?: SandboxEventHandlerConfig) => void;
|
||||
onLoad?: (config?: SandboxEventHandlerConfig) => void;
|
||||
onViewChange?: (data: any, config?: ISandboxEventHandlerConfig) => void;
|
||||
onMessage?: (data: any, config?: ISandboxEventHandlerConfig) => void;
|
||||
onLoad?: (config?: ISandboxEventHandlerConfig) => void;
|
||||
};
|
||||
|
||||
export type CombinedSandboxRef = {
|
||||
@@ -36,6 +41,7 @@ const LANDING_PAGE_PATH = '/__background_landing_page__';
|
||||
|
||||
function useSandbox({
|
||||
isPreview: isPreviewProp,
|
||||
configFormatter,
|
||||
onViewChange,
|
||||
onMessage: onMessageProp,
|
||||
onLoad: onLoadProp,
|
||||
@@ -69,12 +75,12 @@ function useSandbox({
|
||||
let files = Array.from(workspace.files.keys()).reduce((prev, filename) => {
|
||||
let code = workspace.getFile(filename).code;
|
||||
if (filename === '/tango.config.json') {
|
||||
code = mergeTangoConfigJson(code, isPreview, { injectScript });
|
||||
code = mergeTangoConfigJson(code, { isPreview, injectScript, formatter: configFormatter });
|
||||
}
|
||||
prev[filename] = { code };
|
||||
return prev;
|
||||
}, {});
|
||||
files = normalizeFiles(files, workspace.entry);
|
||||
files = fixSandboxFiles(files, workspace.entry);
|
||||
|
||||
const onMessage = (data: any) => onMessageProp && onMessageProp(data, getSandboxConfig());
|
||||
const onLoad = () => onLoadProp && onLoadProp(getSandboxConfig());
|
||||
@@ -187,7 +193,7 @@ export const CombinedSandbox = observer(
|
||||
const activeSandbox = useRef<string>();
|
||||
const [startRoute, setStartRoute] = useState(workspace.activeRoute);
|
||||
|
||||
const onViewChange = (data: any, config: SandboxEventHandlerConfig) => {
|
||||
const onViewChange = (data: any, config: ISandboxEventHandlerConfig) => {
|
||||
if (config.isActive) {
|
||||
const curPath = data?.pathname + data?.search;
|
||||
const isSandboxChanged = config.sandboxType !== activeSandbox.current;
|
||||
@@ -275,6 +281,7 @@ export const Sandbox = observer(
|
||||
({
|
||||
isPreview: isPreviewProp,
|
||||
selectionTools,
|
||||
builtinSelectionMenuMap,
|
||||
bundlerURL,
|
||||
mode = 'combined',
|
||||
...props
|
||||
@@ -294,7 +301,7 @@ export const Sandbox = observer(
|
||||
: combinedSandboxRef.current?.designSandbox;
|
||||
}
|
||||
|
||||
const onViewChange = (data: any, { isActive }: SandboxEventHandlerConfig) => {
|
||||
const onViewChange = (data: any, { isActive }: ISandboxEventHandlerConfig) => {
|
||||
if (isActive) {
|
||||
navigatorRef.current.changeRelativeUrl(data?.pathname + data?.search);
|
||||
}
|
||||
@@ -345,7 +352,7 @@ export const Sandbox = observer(
|
||||
}}
|
||||
/>
|
||||
<Simulator>
|
||||
<Viewport selectionTools={selectionTools}>
|
||||
<Viewport selectionTools={selectionTools} builtinActionMap={builtinSelectionMenuMap}>
|
||||
{mode === 'single' && (
|
||||
<DesignSandbox
|
||||
ref={sandboxRef}
|
||||
@@ -378,7 +385,7 @@ export const Sandbox = observer(
|
||||
);
|
||||
|
||||
// 兼容 tango.config.json,转成 sandbox.config.json
|
||||
function normalizeFiles(files: object, entry = '/src/index.js') {
|
||||
function fixSandboxFiles(files: Record<string, { code: string }>, entry = '/src/index.js') {
|
||||
if (files['/tango.config.json']) {
|
||||
const tangConfigJsonStr = files['/tango.config.json'].code;
|
||||
const tangConfigJson = JSON.parse(tangConfigJsonStr);
|
||||
@@ -386,6 +393,7 @@ function normalizeFiles(files: object, entry = '/src/index.js') {
|
||||
code: JSON.stringify(tangConfigJson.sandbox, null, 2),
|
||||
};
|
||||
}
|
||||
|
||||
if (!files['/index.html']) {
|
||||
files['/index.html'] = {
|
||||
code: `
|
||||
@@ -405,28 +413,30 @@ function normalizeFiles(files: object, entry = '/src/index.js') {
|
||||
`,
|
||||
};
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
function mergeTangoConfigJson(code: string, isPreview: boolean, config?: { [x: string]: any }) {
|
||||
interface IMergeTangoConfigJsonConfig {
|
||||
isPreview?: boolean;
|
||||
injectScript?: string;
|
||||
formatter?: (json: object) => object;
|
||||
}
|
||||
|
||||
function mergeTangoConfigJson(
|
||||
code: string,
|
||||
{ isPreview, injectScript, formatter }: IMergeTangoConfigJsonConfig = {},
|
||||
) {
|
||||
let json;
|
||||
try {
|
||||
json = JSON.parse(code);
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
logger.error('Json parse failed!', err);
|
||||
return code;
|
||||
}
|
||||
|
||||
const ox = getValue(json, 'dataSource.ox');
|
||||
const userJs = getValue(json, 'sandbox.evaluateJavaScript') || '';
|
||||
let mergedUserJs = userJs;
|
||||
const { injectScript } = config || {};
|
||||
|
||||
if (ox) {
|
||||
// TIP: 自动拼装 __tango_ox__ 注入到沙箱中
|
||||
mergedUserJs = `window.__tango_ox__=${JSON.stringify(ox)};${mergedUserJs}`;
|
||||
}
|
||||
if (injectScript) {
|
||||
mergedUserJs = `${mergedUserJs};${injectScript}`;
|
||||
}
|
||||
@@ -435,15 +445,6 @@ function mergeTangoConfigJson(code: string, isPreview: boolean, config?: { [x: s
|
||||
setValue(json, 'sandbox.evaluateJavaScript', mergedUserJs);
|
||||
}
|
||||
|
||||
const i18n = getValue(json, 'i18n');
|
||||
if (i18n) {
|
||||
// TIP: 合并 i18n 配置到沙箱配置中
|
||||
setValue(json, 'sandbox.i18n', {
|
||||
id: i18n.appId,
|
||||
preModule: i18n.preModule,
|
||||
});
|
||||
}
|
||||
|
||||
// 合并 packages 内的信息至 sandbox
|
||||
const packages = getValue(json, 'packages');
|
||||
const externals = getValue(json, 'sandbox.externals') || {};
|
||||
@@ -490,6 +491,10 @@ function mergeTangoConfigJson(code: string, isPreview: boolean, config?: { [x: s
|
||||
setValue(json, 'sandbox.externals', externals);
|
||||
setValue(json, 'sandbox.externalResources', [...new Set(externalResources)]);
|
||||
|
||||
if (isFunction(formatter)) {
|
||||
json = formatter?.(json);
|
||||
}
|
||||
|
||||
return JSON.stringify(json);
|
||||
}
|
||||
|
||||
|
||||
+6
-2
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Select } from 'antd';
|
||||
import { FormItemComponentProps } from '../form-item';
|
||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||
import { ListSetter, NewOptionFormFieldType } from './list-setter';
|
||||
|
||||
const defaultNewOptionValue = {
|
||||
@@ -28,7 +28,11 @@ const optionFormFields: NewOptionFormFieldType[] = [
|
||||
{ label: 'key', name: 'key', required: true },
|
||||
{ label: '文本', name: 'label', required: true },
|
||||
{ label: '外观', name: 'shape', component: <Select placeholder="请选择" options={shapes} /> },
|
||||
{ label: '按钮类型', name: 'buttonType', component: <Select placeholder="请选择" options={buttonTypes} /> },
|
||||
{
|
||||
label: '按钮类型',
|
||||
name: 'buttonType',
|
||||
component: <Select placeholder="请选择" options={buttonTypes} />,
|
||||
},
|
||||
{ label: '类型', name: 'actionType', component: <Select placeholder="请选择" options={types} /> },
|
||||
{ label: '确认气泡内容', name: 'confirmContent' },
|
||||
];
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Switch } from 'antd';
|
||||
import { FormItemComponentProps } from '../form-item';
|
||||
import { FormItemComponentProps } from '@music163/tango-setting-form/src/form-item';
|
||||
|
||||
export function BoolSetter({ value, onChange, ...props }: FormItemComponentProps<boolean>) {
|
||||
return <Switch checked={value} onChange={(val) => onChange?.(val)} {...props} />;
|
||||
+9
-9
@@ -2,21 +2,13 @@ import React from 'react';
|
||||
import { Radio, RadioProps, Tooltip } from 'antd';
|
||||
import { IconFont } from '@music163/tango-ui';
|
||||
import type { OptionType } from '@music163/tango-helpers';
|
||||
import { FormItemComponentProps } from '../form-item';
|
||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||
|
||||
interface ChoiceSetterProps {
|
||||
mode?: 'text' | 'icon';
|
||||
options?: OptionType[];
|
||||
}
|
||||
|
||||
function RadioButton({ tip, ...rest }: RadioProps & { tip?: string }) {
|
||||
let ret = <Radio.Button {...rest} />;
|
||||
if (tip) {
|
||||
ret = <Tooltip title={tip}>{ret}</Tooltip>;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
export function ChoiceSetter({
|
||||
options,
|
||||
mode = 'text',
|
||||
@@ -45,3 +37,11 @@ export function ChoiceSetter({
|
||||
</Radio.Group>
|
||||
);
|
||||
}
|
||||
|
||||
function RadioButton({ tip, ...rest }: RadioProps & { tip?: string }) {
|
||||
let ret = <Radio.Button {...rest} />;
|
||||
if (tip) {
|
||||
ret = <Tooltip title={tip}>{ret}</Tooltip>;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
+2
-2
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import { Switch } from 'antd';
|
||||
import { Box } from 'coral-system';
|
||||
import { FieldStringOutlined } from '@ant-design/icons';
|
||||
import { FormItemComponentProps } from '../form-item';
|
||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||
import { ListSetter, NewOptionFormFieldType } from './list-setter';
|
||||
|
||||
const defaultNewOptionValue = {
|
||||
@@ -37,7 +37,7 @@ const renderSetterItem = (item: any) => {
|
||||
export function ColumnSetter(props: FormItemComponentProps<any[]>) {
|
||||
return (
|
||||
<ListSetter
|
||||
getListItemKey={(item) => item.key || item.dataIndex}
|
||||
getListItemKey={(item: any) => item.key || item.dataIndex}
|
||||
addBtnText="添加新列"
|
||||
listItemFormFields={optionFormFields}
|
||||
newItemDefaultValues={defaultNewOptionValue}
|
||||
+1
-1
@@ -3,8 +3,8 @@ import { Box } from 'coral-system';
|
||||
// @ts-ignore
|
||||
import { toJSON } from 'cssjson';
|
||||
import { InputNumber, Space } from 'antd';
|
||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||
import { SliderSetter } from './number-setter';
|
||||
import { FormItemComponentProps } from '../form-item';
|
||||
import {
|
||||
BgSetter,
|
||||
BorderSetter,
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import moment from 'moment';
|
||||
import { DatePicker, TimePicker } from 'antd';
|
||||
import { FormItemComponentProps } from '../form-item';
|
||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||
|
||||
function toMoment(value: any, format: string) {
|
||||
let ret: moment.Moment;
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { FormItemComponentProps } from '../form-item';
|
||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||
import { InputKV } from '../components';
|
||||
|
||||
export function EnumSetter(props: FormItemComponentProps<string>) {
|
||||
+8
-7
@@ -1,11 +1,10 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { css } from 'styled-components';
|
||||
import { Box } from 'coral-system';
|
||||
import { css, Box } from 'coral-system';
|
||||
import { AutoComplete } from 'antd';
|
||||
import { ActionSelect } from '@music163/tango-ui';
|
||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||
import { useWorkspace, useWorkspaceData } from '@music163/tango-context';
|
||||
import { ExpressionModal, getInputValue } from './expression-setter';
|
||||
import { FormItemComponentProps } from '../form-item';
|
||||
import { useFormVariable } from '../context';
|
||||
|
||||
enum EventAction {
|
||||
NoAction = 'noAction',
|
||||
@@ -42,7 +41,9 @@ export function EventSetter(props: EventSetterProps) {
|
||||
const [temp, setTemp] = useState(''); // 二级暂存值
|
||||
const [expModalVisible, setExpModalVisible] = useState(false); // 弹窗是否显示
|
||||
|
||||
const { actionVariables, routeOptions, modalOptions } = useFormVariable();
|
||||
const { actionVariables, routeOptions } = useWorkspaceData();
|
||||
const workspace = useWorkspace();
|
||||
const modalOptions = workspace.activeViewModule.listModals() || [];
|
||||
|
||||
const handleChange = useCallback<FormItemComponentProps['onChange']>(
|
||||
(nextValue: any, ...args) => {
|
||||
@@ -80,9 +81,9 @@ export function EventSetter(props: EventSetterProps) {
|
||||
defaultValue={inputValue}
|
||||
visible={expModalVisible}
|
||||
onCancel={() => setExpModalVisible(false)}
|
||||
onOk={(value) => {
|
||||
onOk={(nextValue) => {
|
||||
setExpModalVisible(false);
|
||||
handleChange(value);
|
||||
handleChange(nextValue);
|
||||
}}
|
||||
dataSource={actionVariables}
|
||||
/>
|
||||
@@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
import { SingleMonacoEditor } from '@music163/tango-ui';
|
||||
import { Box } from 'coral-system';
|
||||
import { FormItemComponentProps } from '@music163/tango-setting-form/src/form-item';
|
||||
|
||||
export function ExpressionSetter({ value, onChange }: FormItemComponentProps) {
|
||||
return (
|
||||
<Box height="120px" border="solid" borderColor="line.normal" borderRadius="s">
|
||||
<SingleMonacoEditor
|
||||
defaultValue={value}
|
||||
onBlur={(newValue) => {
|
||||
if (newValue !== value) {
|
||||
onChange(`{${newValue}}`);
|
||||
}
|
||||
}}
|
||||
language="javascript"
|
||||
options={{
|
||||
lineNumbers: 'off',
|
||||
minimap: {
|
||||
enabled: false,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
+30
-13
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { Box, css } from 'coral-system';
|
||||
import { Alert, Modal, Tabs } from 'antd';
|
||||
import { value2code, isValidExpressionCode } from '@music163/tango-core';
|
||||
@@ -11,9 +11,10 @@ import {
|
||||
} from '@music163/tango-helpers';
|
||||
import { CloseCircleFilled, ExpandAltOutlined, QuestionCircleOutlined } from '@ant-design/icons';
|
||||
import { IconButton, Panel, InputCode, ChatInput } from '@music163/tango-ui';
|
||||
import { FormItemComponentProps } from '../form-item';
|
||||
import { EditableVariableTree, VariableTreeNodeType } from '../components';
|
||||
import { useFormVariable } from '../context';
|
||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||
import { useWorkspace, useWorkspaceData } from '@music163/tango-context';
|
||||
import { EditableVariableTree, IVariableTreeNode } from '../components';
|
||||
import { useRemoteServices, useSandboxQuery } from '../context';
|
||||
|
||||
export const expressionValueValidate = (value: string) => {
|
||||
if (isVariableString(value)) {
|
||||
@@ -91,7 +92,8 @@ export function ExpressionSetter(props: ExpressionSetterProps) {
|
||||
const [inputValue, setInputValue] = useState(() => {
|
||||
return getInputValue(valueProp);
|
||||
});
|
||||
const { evaluateContext } = useFormVariable();
|
||||
const sandbox = useSandboxQuery();
|
||||
const evaluateContext = sandbox.window;
|
||||
|
||||
// when receive new value, sync state
|
||||
useEffect(() => {
|
||||
@@ -122,7 +124,11 @@ export function ExpressionSetter(props: ExpressionSetterProps) {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<IconButton tooltip="打开表达式变量选择面板" icon={<ExpandAltOutlined />} onClick={on} />
|
||||
<IconButton
|
||||
tooltip="打开表达式变量选择面板"
|
||||
icon={<ExpandAltOutlined />}
|
||||
onClick={on}
|
||||
/>
|
||||
</Box>
|
||||
}
|
||||
value={inputValue}
|
||||
@@ -160,7 +166,7 @@ export interface ExpressionModalProps {
|
||||
defaultValue?: string;
|
||||
onCancel?: () => void;
|
||||
onOk?: (value: string) => void;
|
||||
dataSource?: VariableTreeNodeType[];
|
||||
dataSource?: IVariableTreeNode[];
|
||||
autoCompleteOptions?: string[];
|
||||
}
|
||||
|
||||
@@ -176,8 +182,19 @@ export function ExpressionModal({
|
||||
autoCompleteOptions,
|
||||
}: ExpressionModalProps) {
|
||||
const [exp, setExp] = useState(defaultValue);
|
||||
const { expressionVariables, evaluateContext, onAction, remoteServices } = useFormVariable();
|
||||
const workspace = useWorkspace();
|
||||
const onAction = useCallback(
|
||||
(action: string, args: unknown[]) => {
|
||||
workspace[action]?.(...args);
|
||||
},
|
||||
[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());
|
||||
};
|
||||
@@ -231,13 +248,13 @@ export function ExpressionModal({
|
||||
// height={400}
|
||||
dataSource={dataSource || expressionVariables}
|
||||
onUse={(node) => {
|
||||
let exp;
|
||||
let str;
|
||||
if (/^(stores|services)\./.test(node.key)) {
|
||||
exp = `{tango.${node.key.replaceAll('.', '?.')}}`;
|
||||
str = `{tango.${node.key.replaceAll('.', '?.')}}`;
|
||||
} else {
|
||||
exp = `{${node.key}}`;
|
||||
str = `{${node.key}}`;
|
||||
}
|
||||
setExp(exp);
|
||||
setExp(str);
|
||||
}}
|
||||
onAddVariable={(storeName, data) => {
|
||||
onAction('addStoreState', [storeName, data.name, data.initialValue]);
|
||||
@@ -278,7 +295,7 @@ export function ExpressionModal({
|
||||
frequencyPenalty: 0,
|
||||
mode: 'chat',
|
||||
})
|
||||
.then((res) => {
|
||||
.then((res: any) => {
|
||||
return res?.detail?.choices?.[0]?.text;
|
||||
})
|
||||
}
|
||||
+95
-140
@@ -1,18 +1,19 @@
|
||||
import { register, IFormItemCreateOptions } from '@music163/tango-setting-form';
|
||||
import { ActionListSetter } from './action-list-setter';
|
||||
import { EnumSetter } from './enum-setter';
|
||||
import { TextAreaSetter, TextSetter } from './text-setter';
|
||||
import { PickerSetter } from './picker-setter';
|
||||
import { ChoiceSetter } from './choice-setter';
|
||||
import { FormItemCreateOptionsType } from '../form-item';
|
||||
import { ListSetter } from './list-setter';
|
||||
import { IconSetter } from './icon-setter';
|
||||
import { ColumnSetter } from './column-setter';
|
||||
import { NumberSetter, SliderSetter } from './number-setter';
|
||||
import { ExpressionSetter, expressionValueValidate, jsonValueValidate } from './expression-setter';
|
||||
import { OptionSetter } from './option-setter';
|
||||
import { DateSetter, DateRangeSetter, TimeSetter, TimeRangeSetter } from './date-setter';
|
||||
import { JsxSetter } from './jsx-setter';
|
||||
import { CssSetter } from './css-setter';
|
||||
import { DateRangeSetter, DateSetter, TimeRangeSetter } from './date-setter';
|
||||
import { EnumSetter } from './enum-setter';
|
||||
import { EventSetter } from './event-setter';
|
||||
import { ExpressionSetter } from './expression-setter';
|
||||
import { JSONSetter } from './json-setter';
|
||||
import { JsxSetter } from './jsx-setter';
|
||||
import { ListSetter } from './list-setter';
|
||||
import { ModelSetter } from './model-setter';
|
||||
import { OptionSetter } from './option-setter';
|
||||
import { PickerSetter } from './picker-setter';
|
||||
import { RenderSetter, TableCellSetter, TableExpandableSetter } from './render-props-setter';
|
||||
import { RouterSetter } from './router-setter';
|
||||
import {
|
||||
SpacingSetter,
|
||||
ColorSetter,
|
||||
@@ -24,131 +25,119 @@ import {
|
||||
FlexAlignItemsSetter,
|
||||
FlexDirectionSetter,
|
||||
} from './style-setter';
|
||||
import { CssSetter } from './css-setter';
|
||||
import { ListenerSetter } from './listener-setter';
|
||||
import { RuleSetter } from './rule-setter';
|
||||
import { ModelSetter } from './model-setter';
|
||||
import {
|
||||
RenderSetter,
|
||||
TableCellSetter,
|
||||
TableExpandableSetter,
|
||||
XToolbarSetter,
|
||||
} from './render-props-setter';
|
||||
import { ImageSetter } from './image-setter';
|
||||
import { RouterSetter } from './router-setter';
|
||||
import { BoolSetter } from './bool-setter';
|
||||
import { ChoiceSetter } from './choice-setter';
|
||||
import { NumberSetter, SliderSetter } from './number-setter';
|
||||
import { TextAreaSetter, TextSetter } from './text-setter';
|
||||
|
||||
export const INTERNAL_SETTERS: FormItemCreateOptionsType[] = [
|
||||
{
|
||||
name: 'actionListSetter',
|
||||
component: ActionListSetter,
|
||||
},
|
||||
{
|
||||
name: 'jsxSetter',
|
||||
component: JsxSetter,
|
||||
},
|
||||
{
|
||||
name: 'modelSetter',
|
||||
component: ModelSetter,
|
||||
disableVariableSetter: true,
|
||||
validate(value: string) {
|
||||
if (value.split('.').length < 2) {
|
||||
return '请输入合法的模型变量路径,如:modelName.variableName';
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'eventSetter',
|
||||
alias: 'actionSetter',
|
||||
component: EventSetter,
|
||||
disableVariableSetter: true,
|
||||
},
|
||||
const setters: IFormItemCreateOptions[] = [
|
||||
{
|
||||
name: 'boolSetter',
|
||||
component: BoolSetter,
|
||||
},
|
||||
{
|
||||
name: 'expressionSetter',
|
||||
alias: 'expSetter',
|
||||
name: 'choiceSetter',
|
||||
component: ChoiceSetter,
|
||||
},
|
||||
{
|
||||
name: 'expSetter',
|
||||
alias: ['expressionSetter'],
|
||||
component: ExpressionSetter,
|
||||
disableVariableSetter: true,
|
||||
validate: expressionValueValidate,
|
||||
},
|
||||
{
|
||||
name: 'jsonSetter',
|
||||
component: ExpressionSetter,
|
||||
disableVariableSetter: true,
|
||||
validate: jsonValueValidate,
|
||||
},
|
||||
{
|
||||
name: 'dateSetter',
|
||||
component: DateSetter,
|
||||
},
|
||||
{
|
||||
name: 'enumSetter',
|
||||
component: EnumSetter,
|
||||
},
|
||||
{
|
||||
name: 'dateRangeSetter',
|
||||
component: DateRangeSetter,
|
||||
},
|
||||
{
|
||||
name: 'timeSetter',
|
||||
component: TimeSetter,
|
||||
},
|
||||
{
|
||||
name: 'timeRangeSetter',
|
||||
component: TimeRangeSetter,
|
||||
name: 'numberSetter',
|
||||
component: NumberSetter,
|
||||
},
|
||||
{
|
||||
name: 'textSetter',
|
||||
component: TextSetter,
|
||||
},
|
||||
{ name: 'textAreaSetter', component: TextAreaSetter },
|
||||
{
|
||||
name: 'numberSetter',
|
||||
component: NumberSetter,
|
||||
},
|
||||
{
|
||||
name: 'sliderSetter',
|
||||
component: SliderSetter,
|
||||
},
|
||||
{
|
||||
name: 'pickerSetter',
|
||||
alias: 'selectSetter',
|
||||
component: PickerSetter,
|
||||
name: 'actionListSetter',
|
||||
component: ActionListSetter,
|
||||
},
|
||||
{
|
||||
name: 'choiceSetter',
|
||||
component: ChoiceSetter,
|
||||
name: 'columnSetter',
|
||||
component: ColumnSetter,
|
||||
},
|
||||
{
|
||||
name: 'iconSetter',
|
||||
component: IconSetter,
|
||||
name: 'cssSetter',
|
||||
component: CssSetter,
|
||||
},
|
||||
{
|
||||
name: 'dateSetter',
|
||||
component: DateSetter,
|
||||
},
|
||||
{
|
||||
name: 'dateRangeSetter',
|
||||
component: DateRangeSetter,
|
||||
},
|
||||
{
|
||||
name: 'timeSetter',
|
||||
component: DateRangeSetter,
|
||||
},
|
||||
{
|
||||
name: 'timeRangeSetter',
|
||||
component: TimeRangeSetter,
|
||||
},
|
||||
{
|
||||
name: 'enumSetter',
|
||||
component: EnumSetter,
|
||||
},
|
||||
{
|
||||
name: 'eventSetter',
|
||||
alias: ['actionSetter', 'functionSetter', 'callbackSetter'],
|
||||
component: EventSetter,
|
||||
},
|
||||
{
|
||||
name: 'expressionSetter',
|
||||
alias: ['expSetter'],
|
||||
component: ExpressionSetter,
|
||||
},
|
||||
{
|
||||
name: 'jsonSetter',
|
||||
component: JSONSetter,
|
||||
},
|
||||
{
|
||||
name: 'jsxSetter',
|
||||
component: JsxSetter,
|
||||
},
|
||||
{
|
||||
name: 'listSetter',
|
||||
component: ListSetter as any,
|
||||
},
|
||||
{
|
||||
name: 'modelSetter',
|
||||
component: ModelSetter,
|
||||
},
|
||||
{
|
||||
name: 'optionSetter',
|
||||
component: OptionSetter,
|
||||
},
|
||||
// {
|
||||
// name: 'jsonSetter',
|
||||
// alias: 'styleSetter',
|
||||
// component: JSONSetter,
|
||||
// },
|
||||
{
|
||||
name: 'listSetter',
|
||||
component: ListSetter as any,
|
||||
name: 'pickerSetter',
|
||||
component: PickerSetter,
|
||||
},
|
||||
// {
|
||||
// name: 'stateSetter',
|
||||
// component: StateSetter,
|
||||
// disableVariableSetter: true,
|
||||
// },
|
||||
{
|
||||
name: 'cssSetter',
|
||||
component: CssSetter,
|
||||
type: 'subForm',
|
||||
defaultCollapsed: false,
|
||||
name: 'renderPropsSetter',
|
||||
component: RenderSetter,
|
||||
},
|
||||
{
|
||||
name: 'tableCellSetter',
|
||||
component: TableCellSetter,
|
||||
},
|
||||
{
|
||||
name: 'tableExpandableSetter',
|
||||
component: TableExpandableSetter,
|
||||
},
|
||||
{
|
||||
name: 'routerSetter',
|
||||
component: RouterSetter,
|
||||
},
|
||||
{
|
||||
name: 'displaySetter',
|
||||
@@ -177,40 +166,6 @@ export const INTERNAL_SETTERS: FormItemCreateOptionsType[] = [
|
||||
},
|
||||
{ name: 'bgSetter', component: BgSetter },
|
||||
{ name: 'borderSetter', component: BorderSetter },
|
||||
{
|
||||
name: 'columnSetter',
|
||||
component: ColumnSetter,
|
||||
},
|
||||
{
|
||||
name: 'listenerSetter',
|
||||
component: ListenerSetter,
|
||||
},
|
||||
{
|
||||
name: 'ruleSetter',
|
||||
component: RuleSetter,
|
||||
},
|
||||
{
|
||||
name: 'renderPropsSetter',
|
||||
component: RenderSetter,
|
||||
},
|
||||
{
|
||||
name: 'tableCellSetter',
|
||||
component: TableCellSetter,
|
||||
},
|
||||
{
|
||||
name: 'tableExpandableSetter',
|
||||
component: TableExpandableSetter,
|
||||
},
|
||||
{
|
||||
name: 'xtoolbarSetter',
|
||||
component: XToolbarSetter,
|
||||
},
|
||||
{
|
||||
name: 'imageSetter',
|
||||
component: ImageSetter,
|
||||
},
|
||||
{
|
||||
name: 'routerSetter',
|
||||
component: RouterSetter,
|
||||
},
|
||||
];
|
||||
|
||||
setters.forEach(register);
|
||||
+3
-4
@@ -1,11 +1,10 @@
|
||||
import React from 'react';
|
||||
import { SingleMonacoEditor } from '@music163/tango-ui';
|
||||
import { Box } from 'coral-system';
|
||||
import { FormItemComponentProps } from '../form-item';
|
||||
import { SingleMonacoEditor } from '@music163/tango-ui';
|
||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||
|
||||
/**
|
||||
* 废弃掉,直接使用 ExpressionSetter 代替
|
||||
* @deprecated
|
||||
* JSON Setter
|
||||
*/
|
||||
export function JSONSetter({ value, onChange }: FormItemComponentProps) {
|
||||
return (
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { isPlainString } from '@music163/tango-helpers';
|
||||
import { ActionSelect } from '@music163/tango-ui';
|
||||
import { FormItemComponentProps } from '../form-item';
|
||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||
|
||||
const options = [
|
||||
{ label: '取消自定义', value: '' },
|
||||
+4
-5
@@ -1,10 +1,9 @@
|
||||
import React, { useState } from 'react';
|
||||
import { css, Box } from 'coral-system';
|
||||
import { Form, Input, Button, Popover } from 'antd';
|
||||
import { DeleteOutlined, EditOutlined, HolderOutlined } from '@ant-design/icons';
|
||||
import { Box } from 'coral-system';
|
||||
import { FormItemComponentProps } from '../form-item';
|
||||
import { css } from 'styled-components';
|
||||
import { DragBox } from '../components/index';
|
||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||
import { DragBox } from '../components';
|
||||
|
||||
function copyWithoutUndefined(source: Record<string, any>) {
|
||||
const ret = {};
|
||||
@@ -254,7 +253,7 @@ function NewOptionForm({ fields = [], initialValues = {}, onSubmit }: NewOptionF
|
||||
);
|
||||
}
|
||||
|
||||
type ListSetterProps = FormItemComponentProps<any[]> & {
|
||||
interface ListSetterProps extends FormItemComponentProps<any[]> {
|
||||
listItemFormFields: ListSetterItemProps['formFields'];
|
||||
newItemDefaultValues?: AddListItemButtonProps['formInitialValues'];
|
||||
getNewItemDefaultValues?: AddListItemButtonProps['getFormInitialValues'];
|
||||
+13
-4
@@ -2,10 +2,11 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Box } from 'coral-system';
|
||||
import { Input, Tooltip } from 'antd';
|
||||
import { getValue, isFunction } from '@music163/tango-helpers';
|
||||
import { useFormVariable } from '../context';
|
||||
import { FormItemComponentProps } from '../form-item';
|
||||
import { EditableVariableTreeModal } from '../components';
|
||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||
import { MenuOutlined } from '@ant-design/icons';
|
||||
import { useWorkspace, useWorkspaceData } from '@music163/tango-context';
|
||||
import { EditableVariableTreeModal } from '../components';
|
||||
import { useSandboxQuery } from '../context';
|
||||
|
||||
function object2treeData(
|
||||
val: any,
|
||||
@@ -53,7 +54,15 @@ function traverseTreeData(val: any, callback: (val: any) => void) {
|
||||
|
||||
export function ModelSetter({ value, onChange }: FormItemComponentProps) {
|
||||
const [inputValue, setInputValue] = useState(value);
|
||||
const { onAction, modelVariables, evaluateContext = {} } = useFormVariable();
|
||||
const { modelVariables } = useWorkspaceData();
|
||||
const evaluateContext = useSandboxQuery().window || {};
|
||||
const workspace = useWorkspace();
|
||||
const onAction = useCallback(
|
||||
(action: string, args: unknown[]) => {
|
||||
workspace[action]?.(...args);
|
||||
},
|
||||
[workspace],
|
||||
);
|
||||
const definedVariables = useMemo(() => {
|
||||
const map = new Map();
|
||||
traverseTreeData(modelVariables, (node) => {
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { InputNumber, InputNumberProps, Slider } from 'antd';
|
||||
import { FormItemComponentProps } from '../form-item';
|
||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||
|
||||
const style = {
|
||||
width: '100%',
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { uuid } from '@music163/tango-helpers';
|
||||
import React from 'react';
|
||||
import { FormItemComponentProps } from '../form-item';
|
||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||
import { ListSetter, NewOptionFormFieldType } from './list-setter';
|
||||
|
||||
const optionFormFields: NewOptionFormFieldType[] = [
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Select } from 'antd';
|
||||
import { FormItemComponentProps } from '../form-item';
|
||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||
import { ListStore, OptionType } from '@music163/tango-helpers';
|
||||
|
||||
const style = {
|
||||
+5
-19
@@ -1,18 +1,18 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { ActionSelect } from '@music163/tango-ui';
|
||||
import { FormItemComponentProps } from '../form-item';
|
||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||
|
||||
type RenderOptionType = {
|
||||
interface IRenderOption {
|
||||
label: string;
|
||||
value: string;
|
||||
render?: string;
|
||||
relatedImports?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface RenderSetterProps {
|
||||
text?: string;
|
||||
options?: RenderOptionType[];
|
||||
fallbackOption?: RenderOptionType;
|
||||
options?: IRenderOption[];
|
||||
fallbackOption?: IRenderOption;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -97,16 +97,6 @@ const tableExpandableOptions: RenderSetterProps['options'] = [
|
||||
{ label: '取消可展开行', value: '' },
|
||||
];
|
||||
|
||||
const xtoolbarOptions: RenderSetterProps['options'] = [
|
||||
{
|
||||
label: '配置左侧区域',
|
||||
value: 'Box',
|
||||
render: getRender('<Box display="flex"></Box>'),
|
||||
relatedImports: ['Box'],
|
||||
},
|
||||
{ label: '取消左侧区域', value: undefined },
|
||||
];
|
||||
|
||||
export function TableCellSetter(props: FormItemComponentProps) {
|
||||
return <RenderSetter options={tableCellOptions} {...props} />;
|
||||
}
|
||||
@@ -114,7 +104,3 @@ export function TableCellSetter(props: FormItemComponentProps) {
|
||||
export function TableExpandableSetter(props: FormItemComponentProps) {
|
||||
return <RenderSetter options={tableExpandableOptions} text="配置表格可展开行" {...props} />;
|
||||
}
|
||||
|
||||
export function XToolbarSetter(props: FormItemComponentProps) {
|
||||
return <RenderSetter options={xtoolbarOptions} text="配置工具栏左侧区域" {...props} />;
|
||||
}
|
||||
+3
-3
@@ -2,8 +2,8 @@ import React, { useState } from 'react';
|
||||
import { Box } from 'coral-system';
|
||||
import { Input, Radio } from 'antd';
|
||||
import { isValidUrl } from '@music163/tango-helpers';
|
||||
import { useFormVariable } from '../context';
|
||||
import { FormItemComponentProps } from '../form-item';
|
||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||
import { useWorkspaceData } from '@music163/tango-context';
|
||||
import { PickerSetter } from './picker-setter';
|
||||
|
||||
export function RouterSetter(props: FormItemComponentProps) {
|
||||
@@ -11,7 +11,7 @@ export function RouterSetter(props: FormItemComponentProps) {
|
||||
return isValidUrl(props.value) ? 'input' : 'select';
|
||||
});
|
||||
const [input, setInput] = useState<string>(props.value);
|
||||
const { routeOptions } = useFormVariable();
|
||||
const { routeOptions } = useWorkspaceData();
|
||||
return (
|
||||
<Box>
|
||||
<Box mb="m">
|
||||
+10
-9
@@ -5,9 +5,10 @@ import { SketchPicker } from 'react-color';
|
||||
import Color from 'color';
|
||||
import { BgColorsOutlined, FileImageOutlined } from '@ant-design/icons';
|
||||
import { SingleMonacoEditor, IconFont } from '@music163/tango-ui';
|
||||
import { FormItemComponentProps } from '../form-item';
|
||||
import { ImageSetter } from './image-setter';
|
||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||
import { ChoiceSetter } from './choice-setter';
|
||||
import { TextSetter } from './text-setter';
|
||||
// import { ImageSetter } from './image-setter';
|
||||
|
||||
function getRawCssValue(value: string) {
|
||||
if (value && value.startsWith('{css`')) {
|
||||
@@ -175,8 +176,8 @@ export function ColorSetter(props: FormItemComponentProps<string>) {
|
||||
const colorPanel = (
|
||||
<SketchPicker
|
||||
color={color}
|
||||
onChangeComplete={(color: any) => {
|
||||
const nextValue = color.rgb.a === 1 ? color.hex : rgbaObjectToString(color.rgb);
|
||||
onChangeComplete={(newColor: any) => {
|
||||
const nextValue = newColor.rgb.a === 1 ? newColor.hex : rgbaObjectToString(newColor.rgb);
|
||||
if (typeof onChange === 'function') {
|
||||
onChange(nextValue);
|
||||
}
|
||||
@@ -238,7 +239,7 @@ export function BgSetter({ value, onChange }: FormItemComponentProps<string>) {
|
||||
<Box mt="m">
|
||||
{mode === 'color' && <ColorSetter value={value} onChange={onChange} />}
|
||||
{mode === 'image' && (
|
||||
<ImageSetter
|
||||
<TextSetter
|
||||
value={getImageUrl(value)}
|
||||
onChange={(imgUrl) => {
|
||||
onChange(imgUrl ? `url(${imgUrl})` : undefined);
|
||||
@@ -353,19 +354,19 @@ export function BorderSetter({ value, onChange }: FormItemComponentProps<string>
|
||||
const [width, setWidth] = useState<number>(Number(getPxValue(valueParts[0])) || 0);
|
||||
const [color, setColor] = useState<string>(valueParts[2] || '#000');
|
||||
|
||||
const handleChange = (key: string, value: any) => {
|
||||
const handleChange = (key: string, val: any) => {
|
||||
let ret: any[];
|
||||
switch (key) {
|
||||
case 'style': {
|
||||
ret = [width, value, color];
|
||||
ret = [width, val, color];
|
||||
break;
|
||||
}
|
||||
case 'width': {
|
||||
ret = [`${value}px`, style, color];
|
||||
ret = [`${val}px`, style, color];
|
||||
break;
|
||||
}
|
||||
case 'color': {
|
||||
ret = [width, style, value];
|
||||
ret = [width, style, val];
|
||||
break;
|
||||
}
|
||||
default:
|
||||
+3
-3
@@ -1,13 +1,13 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Input, InputProps } from 'antd';
|
||||
import { FormItemComponentProps } from '../form-item';
|
||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||
|
||||
const noop = () => {};
|
||||
|
||||
type TextSetterProps = {
|
||||
interface TextSetterProps extends Omit<InputProps, 'onChange' | 'value'> {
|
||||
value?: string;
|
||||
onChange?: (value: string) => void;
|
||||
} & Omit<InputProps, 'onChange' | 'value'>;
|
||||
}
|
||||
|
||||
export function TextSetter({
|
||||
value: valueProp,
|
||||
@@ -1,10 +1,10 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import React from 'react';
|
||||
import { Box } from 'coral-system';
|
||||
import { SettingForm, FormModel, SettingFormProps } from '@music163/tango-setting-form';
|
||||
import { Panel } from '@music163/tango-ui';
|
||||
import { clone } from '@music163/tango-helpers';
|
||||
import { observer, useDesigner, useWorkspace, useWorkspaceData } from '@music163/tango-context';
|
||||
import { useSandboxQuery } from './context';
|
||||
import { observer, useDesigner, useWorkspace } from '@music163/tango-context';
|
||||
import './setters';
|
||||
|
||||
export interface SettingPanelProps extends SettingFormProps {
|
||||
title?: React.ReactNode;
|
||||
@@ -15,17 +15,8 @@ const headerProps = {
|
||||
};
|
||||
|
||||
export const SettingPanel = observer(({ title = '设置面板', ...props }: SettingPanelProps) => {
|
||||
const sandbox = useSandboxQuery();
|
||||
const workspace = useWorkspace();
|
||||
const designer = useDesigner();
|
||||
const { modelVariables, actionVariables, expressionVariables, routeOptions } = useWorkspaceData();
|
||||
|
||||
const onAction = useCallback(
|
||||
(action: string, args: unknown[]) => {
|
||||
workspace[action]?.(...args);
|
||||
},
|
||||
[workspace],
|
||||
);
|
||||
|
||||
if (!designer.showRightPanel) {
|
||||
return <Box className="SettingPanel" />;
|
||||
@@ -74,7 +65,10 @@ export const SettingPanel = observer(({ title = '设置面板', ...props }: Sett
|
||||
});
|
||||
} else if (!field.error) {
|
||||
// 针对 Form.Item 变化的情况
|
||||
workspace.updateSelectedNodeAttributes({ [firstName]: realValue }, field.detail?.relatedImports);
|
||||
workspace.updateSelectedNodeAttributes(
|
||||
{ [firstName]: realValue },
|
||||
field.detail?.relatedImports,
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -99,13 +93,6 @@ export const SettingPanel = observer(({ title = '设置面板', ...props }: Sett
|
||||
key={workspace.selectSource.first.id}
|
||||
model={formModel}
|
||||
prototype={prototype}
|
||||
modelVariables={modelVariables}
|
||||
actionVariables={actionVariables}
|
||||
expressionVariables={expressionVariables}
|
||||
modalOptions={workspace.listModals() || []}
|
||||
routeOptions={routeOptions}
|
||||
onAction={onAction}
|
||||
evaluateContext={sandbox.window}
|
||||
{...props}
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import { observer, useWorkspace } from '@music163/tango-context';
|
||||
import { Box, Link, css } from 'coral-system';
|
||||
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,14 +11,40 @@ import {
|
||||
isValidFunctionCode,
|
||||
logger,
|
||||
code2object,
|
||||
filterTreeData,
|
||||
} from '@music163/tango-helpers';
|
||||
import {
|
||||
ESFDataSourceType,
|
||||
ESFOperationType,
|
||||
ESFHTTPMethodType,
|
||||
ESFHTTRequestType,
|
||||
} from '@music163/tango-core';
|
||||
import { useSandboxQuery } from '../../context';
|
||||
import { VariableTree } from '../../components';
|
||||
|
||||
/*
|
||||
* 服务函数的操作类型
|
||||
*/
|
||||
enum ServiceFunctionOperationModeType {
|
||||
ADD = 'add',
|
||||
UPDATE = 'update',
|
||||
DELETE = 'delete',
|
||||
}
|
||||
|
||||
/**
|
||||
* 服务函数 HTTP Type
|
||||
* 云音乐网关只支持 get 和 post
|
||||
*/
|
||||
enum ServiceFunctionMethodType {
|
||||
GET = 'GET',
|
||||
// PUT = 'PUT',
|
||||
POST = 'POST',
|
||||
// PATCH = 'PATCH',
|
||||
// DELETE = 'DELETE',
|
||||
}
|
||||
|
||||
/**
|
||||
* requestType
|
||||
* headers: Content-Type
|
||||
*/
|
||||
enum DataServiceRequestType {
|
||||
'application/json' = 'json',
|
||||
'application/x-www-form-urlencoded' = 'x-www-form-urlencoded',
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 api 路径转换为默认的驼峰方法名
|
||||
@@ -41,42 +67,22 @@ 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;
|
||||
};
|
||||
|
||||
// 判断是不是一个完整的 url 请求
|
||||
const VALID_REQUEST_URL_REG = /((http|https):)?\/\/([\w.]+\/?)\S*/;
|
||||
|
||||
/**
|
||||
* 根据 url 类型推断方法类型
|
||||
*/
|
||||
export function inferServiceType(url?: string) {
|
||||
// 删除的时候,也会走一下这个逻辑,兼容
|
||||
if (!url) return ESFDataSourceType.Custom;
|
||||
// 如果是完整的,是 custom
|
||||
if (VALID_REQUEST_URL_REG.test(url)) return ESFDataSourceType.Custom;
|
||||
// 如果是 / 开头的,认为是 ox 的
|
||||
if (url.startsWith('/')) return ESFDataSourceType.OvermindX;
|
||||
/**
|
||||
* 这里预留出数据平台 fastX 的
|
||||
*/
|
||||
// 默认是 ox 的
|
||||
return ESFDataSourceType.OvermindX;
|
||||
}
|
||||
|
||||
// http 方法类型
|
||||
const httpMethods = Object.keys(ESFHTTPMethodType).map((key) => ({
|
||||
const httpMethods = Object.keys(ServiceFunctionMethodType).map((key) => ({
|
||||
label: key,
|
||||
value: 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`
|
||||
@@ -86,40 +92,35 @@ 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="DataSourceView"
|
||||
display="flex"
|
||||
borderTopColor="line.normal"
|
||||
height="100%"
|
||||
overflow="hidden"
|
||||
>
|
||||
<Box className="ServiceFunctionList" display="flex" borderTopColor="line.normal">
|
||||
<Box width="35%" overflow="auto" borderRight="solid" borderColor="line.normal">
|
||||
<Panel
|
||||
height="100%"
|
||||
@@ -138,55 +139,50 @@ 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}
|
||||
deletable
|
||||
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
|
||||
? { type: ESFDataSourceType.OvermindX }
|
||||
? {
|
||||
moduleName: 'index',
|
||||
}
|
||||
: {
|
||||
name: serviceKey,
|
||||
name: serviceData.name,
|
||||
moduleName: serviceData.moduleName,
|
||||
method: 'get',
|
||||
type: inferServiceType(serviceData?.url),
|
||||
...serviceData,
|
||||
...serviceData.config,
|
||||
}
|
||||
}
|
||||
onCancel={() => {
|
||||
setServiceKey(undefined);
|
||||
setVisible(null);
|
||||
setServiceData(undefined);
|
||||
}}
|
||||
onSubmit={(values, mode: ESFOperationType) => {
|
||||
onSubmit={(values, mode: ServiceFunctionOperationModeType) => {
|
||||
function shapeServiceValues(val: any) {
|
||||
const shapeValues = { ...val };
|
||||
delete shapeValues.type;
|
||||
@@ -197,28 +193,27 @@ const DataSourceView = observer(({ onAdd, onUpdate, onDelete }: DataServiceViewP
|
||||
return shapeValues;
|
||||
}
|
||||
// 移除掉不必要的属性
|
||||
const data = shapeServiceValues(values);
|
||||
if (mode === ESFOperationType.ADD) {
|
||||
workspace.addServiceFunction(data);
|
||||
const { moduleName, ...data } = shapeServiceValues(values);
|
||||
if (mode === ServiceFunctionOperationModeType.ADD) {
|
||||
workspace.addServiceFunction(data, moduleName);
|
||||
onAdd && onAdd(values);
|
||||
} else if (mode === ESFOperationType.UPDATE) {
|
||||
workspace.updateServiceFunction(data);
|
||||
} else if (mode === ServiceFunctionOperationModeType.UPDATE) {
|
||||
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>
|
||||
@@ -230,12 +225,14 @@ const DataSourceView = observer(({ onAdd, onUpdate, onDelete }: DataServiceViewP
|
||||
|
||||
interface ServiceDetailFormProps extends FormProps {
|
||||
serviceKeys?: string[];
|
||||
serviceModules?: any[];
|
||||
onCancel?: () => void;
|
||||
onSubmit?: (values: any, mode: ESFOperationType) => void;
|
||||
onSubmit?: (values: any, mode: ServiceFunctionOperationModeType) => void;
|
||||
}
|
||||
|
||||
function ServiceDetailForm({
|
||||
serviceKeys = [],
|
||||
serviceModules = [],
|
||||
onCancel,
|
||||
onSubmit,
|
||||
initialValues,
|
||||
@@ -267,11 +264,30 @@ function ServiceDetailForm({
|
||||
layout="horizontal"
|
||||
initialValues={initialValues}
|
||||
onFinish={(values) => {
|
||||
onSubmit(values, isModifyMode ? ESFOperationType.UPDATE : ESFOperationType.ADD);
|
||||
onSubmit(
|
||||
values,
|
||||
isModifyMode
|
||||
? ServiceFunctionOperationModeType.UPDATE
|
||||
: ServiceFunctionOperationModeType.ADD,
|
||||
);
|
||||
setDisabled(true);
|
||||
}}
|
||||
{...formProps}
|
||||
>
|
||||
<Form.Item
|
||||
label="所属模块"
|
||||
name="moduleName"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
},
|
||||
]}
|
||||
component="select"
|
||||
componentProps={{
|
||||
options: serviceModules,
|
||||
disabled: disabled || isModifyMode,
|
||||
}}
|
||||
/>
|
||||
<Form.Item
|
||||
label="方法名"
|
||||
name="name"
|
||||
@@ -280,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();
|
||||
@@ -345,18 +363,7 @@ function ServiceDetailForm({
|
||||
<Form.Item
|
||||
label="格式化响应"
|
||||
name="formatter"
|
||||
tooltip={
|
||||
<Box>
|
||||
提供格式化函数,对结果进行格式化。请参考
|
||||
<Link
|
||||
href="https://music-doc.st.netease.com/st/tango-docs/docs/guide/basic/services#%E6%8E%A5%E5%8F%A3%E8%BF%94%E5%9B%9E%E5%80%BC%E8%A7%84%E8%8C%83"
|
||||
target="_blank"
|
||||
>
|
||||
云音乐接口规范
|
||||
</Link>
|
||||
。
|
||||
</Box>
|
||||
}
|
||||
tooltip="提供格式化函数,对结果进行格式化"
|
||||
validateTrigger="onBlur"
|
||||
rules={[
|
||||
{
|
||||
|
||||
@@ -372,7 +372,7 @@ function RenderItem({
|
||||
</Popconfirm>,
|
||||
]
|
||||
: [
|
||||
<Text color={'rgba(0, 0, 0, 0.45)'}>
|
||||
<Text key="latest" color={'rgba(0, 0, 0, 0.45)'}>
|
||||
{!isUndefined(baseNeedUpgrade) && '已是最新'}
|
||||
</Text>,
|
||||
]
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
export * from './components-panel';
|
||||
export * from './datasource-panel';
|
||||
export * from './outline-panel';
|
||||
export * from './components-panel';
|
||||
export * from './dependency-panel';
|
||||
export * from './history-panel';
|
||||
export * from './variable-panel';
|
||||
export * from './sidebar';
|
||||
@@ -4,7 +4,7 @@ import { Box, css } from 'coral-system';
|
||||
import { IconFont } from '@music163/tango-ui';
|
||||
import { EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons';
|
||||
import { observer, useWorkspace } from '@music163/tango-context';
|
||||
import { DropMethod, TangoViewNodeDataType } from '@music163/tango-core';
|
||||
import { DropMethod, ITangoViewNodeData } from '@music163/tango-core';
|
||||
import { parseDndId } from '@music163/tango-helpers';
|
||||
import { useSandboxQuery } from '../../context';
|
||||
import { buildQueryBySlotId } from '../../helpers';
|
||||
@@ -40,7 +40,18 @@ const filedNames = {
|
||||
children: 'children',
|
||||
};
|
||||
|
||||
const OutlineTreeNode: React.FC<any> = observer(({ node }: { node: TangoViewNodeDataType }) => {
|
||||
const getNodeKeys = (data: ITangoViewNodeData[]) => {
|
||||
const ids: string[] = [];
|
||||
data?.forEach((node) => {
|
||||
ids.push(node.id);
|
||||
if (node.children) {
|
||||
ids.push(...getNodeKeys(node.children));
|
||||
}
|
||||
});
|
||||
return ids;
|
||||
};
|
||||
|
||||
const OutlineTreeNode: React.FC<any> = observer(({ node }: { node: ITangoViewNodeData }) => {
|
||||
const workspace = useWorkspace();
|
||||
const sandboxQuery = useSandboxQuery();
|
||||
const [visible, setVisible] = useState(true);
|
||||
@@ -87,7 +98,7 @@ export const ComponentsTree = observer(() => {
|
||||
workspace.selectSource.selected.map((item) => item.id),
|
||||
);
|
||||
const file = workspace.activeViewModule;
|
||||
const nodesTree = file.nodesTree as TangoViewNodeDataType[];
|
||||
const nodesTree = file.nodesTree as ITangoViewNodeData[];
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedKeys(workspace.selectSource.selected.map((item) => item.id));
|
||||
@@ -124,7 +135,7 @@ export const ComponentsTree = observer(() => {
|
||||
}}
|
||||
blockNode
|
||||
autoExpandParent
|
||||
defaultExpandAll
|
||||
expandedKeys={getNodeKeys(nodesTree)}
|
||||
draggable
|
||||
onDragStart={(data) => {
|
||||
const prototype = workspace.componentPrototypes.get(data.node.component);
|
||||
|
||||
@@ -51,6 +51,7 @@ export interface SidebarProps extends ReactComponentProps {
|
||||
* 底部附加内容
|
||||
*/
|
||||
footer?: React.ReactNode;
|
||||
builtinPanelMap?: Record<string, SidebarPanelItemProps>;
|
||||
}
|
||||
|
||||
export interface SidebarPanelItemProps
|
||||
@@ -114,13 +115,18 @@ const INTERNAL_SIDEBAR_PANEL_MAP: Record<string, SidebarPanelItemProps> = {
|
||||
},
|
||||
};
|
||||
|
||||
function BaseSidebarPanel({ panelWidth: defaultPanelWidth = 280, footer, children }: SidebarProps) {
|
||||
function BaseSidebarPanel({
|
||||
panelWidth: defaultPanelWidth = 280,
|
||||
builtinPanelMap = INTERNAL_SIDEBAR_PANEL_MAP,
|
||||
footer,
|
||||
children,
|
||||
}: SidebarProps) {
|
||||
const items = useMemo(() => {
|
||||
const ret: Record<React.Key, SidebarPanelItemProps> = {};
|
||||
React.Children.forEach(children, (child) => {
|
||||
if (child && React.isValidElement(child)) {
|
||||
ret[child.key] = {
|
||||
...INTERNAL_SIDEBAR_PANEL_MAP[child.key],
|
||||
...builtinPanelMap[child.key],
|
||||
...child.props,
|
||||
};
|
||||
}
|
||||
@@ -272,11 +278,10 @@ function SidebarPanelBarItem({
|
||||
const expandPanelStyle = css`
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--tango-colors-white);
|
||||
border-left: 1px solid var(--tango-colors-line-normal);
|
||||
position: relative;
|
||||
overflow-y: auto;
|
||||
|
||||
&.isFloat {
|
||||
position: absolute;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React from 'react';
|
||||
import { Button, Space } from 'antd';
|
||||
import { EditableVariableTree, EditableVariableTreeProps } from '@music163/tango-setting-form';
|
||||
import { useBoolean } from '@music163/tango-helpers';
|
||||
import { Panel, Form, IconButton } from '@music163/tango-ui';
|
||||
import { FileAddOutlined, QuestionCircleOutlined } from '@ant-design/icons';
|
||||
import { observer, useWorkspace, useWorkspaceData } from '@music163/tango-context';
|
||||
import { EditableVariableTree, EditableVariableTreeProps } from '../components';
|
||||
|
||||
export interface VariablePanelProps extends EditableVariableTreeProps {
|
||||
wrapperHeight?: number | string;
|
||||
|
||||
@@ -15,7 +15,7 @@ type SelectionHelperAlignType = 'top-right' | 'top-left' | 'inner-top-right';
|
||||
|
||||
const boundingOffset = 100; // 检测选中模块的工具栏是否溢出的偏移值
|
||||
|
||||
const internalSelectionTools = {
|
||||
const internalSelectionToolsMap = {
|
||||
source: <ViewSourceAction />,
|
||||
copy: <CopyNodeAction />,
|
||||
delete: <DeleteNodeAction />,
|
||||
@@ -28,16 +28,20 @@ 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 }: SelectionToolsProps) => {
|
||||
({
|
||||
actions: actionsProp = INTERNAL_SELECTION_TOOLS,
|
||||
builtinActionMap = internalSelectionToolsMap,
|
||||
}: SelectionToolsProps) => {
|
||||
const workspace = useWorkspace();
|
||||
const selectSource = workspace.selectSource;
|
||||
const actions = actionsProp.map((item) => {
|
||||
if (isString(item)) {
|
||||
return internalSelectionTools[item]
|
||||
? React.cloneElement(internalSelectionTools[item], { key: item })
|
||||
return builtinActionMap[item]
|
||||
? React.cloneElement(builtinActionMap[item], { key: item })
|
||||
: null;
|
||||
}
|
||||
return item;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Box, HTMLCoralProps } from 'coral-system';
|
||||
import { css } from 'styled-components';
|
||||
import { css, Box, HTMLCoralProps } from 'coral-system';
|
||||
import { observer, useDesigner } from '@music163/tango-context';
|
||||
|
||||
export interface SimulatorProps {
|
||||
|
||||
@@ -10,14 +10,21 @@ import { DraggingMask } from './mask';
|
||||
|
||||
export interface ViewportProps extends HTMLCoralProps<'div'> {
|
||||
selectionTools?: SelectionToolsProps['actions'];
|
||||
builtinActionMap?: SelectionToolsProps['builtinActionMap'];
|
||||
}
|
||||
|
||||
export function Viewport({ selectionTools, children, className, ...rest }: ViewportProps) {
|
||||
export function Viewport({
|
||||
selectionTools,
|
||||
builtinActionMap,
|
||||
children,
|
||||
className,
|
||||
...rest
|
||||
}: ViewportProps) {
|
||||
return (
|
||||
<Box position="relative" height="100%" className={cx('Viewport', className)} {...rest}>
|
||||
<ViewportBody>{children}</ViewportBody>
|
||||
<div className="AuxTools">
|
||||
<SelectionTools actions={selectionTools} />
|
||||
<SelectionTools actions={selectionTools} builtinActionMap={builtinActionMap} />
|
||||
<InsertionPrompt />
|
||||
<DraggingMask />
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Button, Popconfirm, Modal, ModalProps } from 'antd';
|
||||
import { Box, css, Group, Text } from 'coral-system';
|
||||
import { FileOutlined, DeleteOutlined, CopyOutlined, SettingOutlined } from '@ant-design/icons';
|
||||
import { array2object, callAll } from '@music163/tango-helpers';
|
||||
import { PageConfigType } from '@music163/tango-core';
|
||||
import { IPageConfigData } from '@music163/tango-core';
|
||||
import { Form, ToggleButton } from '@music163/tango-ui';
|
||||
import { observer, useWorkspace } from '@music163/tango-context';
|
||||
|
||||
@@ -108,12 +108,12 @@ const pageListStyle = css`
|
||||
|
||||
interface PageListProps {
|
||||
activeKey?: string;
|
||||
items?: PageConfigType[];
|
||||
items?: IPageConfigData[];
|
||||
onAction?: (key: 'delete' | 'copy' | 'update', e: React.MouseEvent) => void;
|
||||
onSelect?: (key: string) => void;
|
||||
onRemove?: (key: string) => void;
|
||||
onCopy?: (sourcePath: string, newPageData: PageConfigType) => void;
|
||||
onUpdate?: (sourcePath: string, newPageData: PageConfigType) => void;
|
||||
onCopy?: (sourcePath: string, newPageData: IPageConfigData) => void;
|
||||
onUpdate?: (sourcePath: string, newPageData: IPageConfigData) => void;
|
||||
}
|
||||
|
||||
// 表单编辑模式
|
||||
@@ -128,7 +128,7 @@ function PageList({
|
||||
onCopy,
|
||||
onAction,
|
||||
}: PageListProps) {
|
||||
const [current, setCurrent] = useState<PageConfigType>(null);
|
||||
const [current, setCurrent] = useState<IPageConfigData>(null);
|
||||
const [type, setType] = useState<FormEditType>();
|
||||
return (
|
||||
<Box as="ul" bg="white" boxShadow="lowDown" borderRadius="m" css={pageListStyle}>
|
||||
@@ -219,10 +219,10 @@ function PageList({
|
||||
interface CopyViewModalFormProps {
|
||||
title?: string;
|
||||
visible?: boolean;
|
||||
onOk?: (sourceRoute: string, targetRouteConfig: PageConfigType) => void;
|
||||
onOk?: (sourceRoute: string, targetRouteConfig: IPageConfigData) => void;
|
||||
onCancel?: ModalProps['onCancel'];
|
||||
current?: PageConfigType;
|
||||
items?: PageConfigType[];
|
||||
current?: IPageConfigData;
|
||||
items?: IPageConfigData[];
|
||||
editType?: FormEditType;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,11 @@ import { PreviewTool } from './preview';
|
||||
import { RouteSwitchTool } from './route-switch';
|
||||
import { TogglePanelTool } from './toggle-panel';
|
||||
|
||||
export interface ToolbarProps {
|
||||
children?: React.ReactElement | React.ReactElement[];
|
||||
builtinToolMap?: Record<string, React.ReactNode>;
|
||||
}
|
||||
|
||||
const builtinToolsMap = {
|
||||
history: <HistoryTool />,
|
||||
modeSwitch: <ModeSwitchTool />,
|
||||
@@ -15,11 +20,7 @@ const builtinToolsMap = {
|
||||
togglePanel: <TogglePanelTool />,
|
||||
};
|
||||
|
||||
interface ToolbarProps {
|
||||
children?: React.ReactElement | React.ReactElement[];
|
||||
}
|
||||
|
||||
export function Toolbar({ children }: ToolbarProps) {
|
||||
export function Toolbar({ children, builtinToolMap = builtinToolsMap }: ToolbarProps) {
|
||||
const [leftTools, centerTools, rightTools] = useMemo(() => {
|
||||
const left: React.ReactNode[] = [];
|
||||
const center: React.ReactNode[] = [];
|
||||
@@ -27,7 +28,7 @@ export function Toolbar({ children }: ToolbarProps) {
|
||||
|
||||
let prevPlacement: string;
|
||||
React.Children.forEach(children, (child: React.ReactElement, index) => {
|
||||
let node = child.props.children || builtinToolsMap[child.key];
|
||||
let node = child.props.children || builtinToolMap[child.key];
|
||||
if (!node) {
|
||||
node = child; // separator
|
||||
}
|
||||
@@ -71,7 +72,7 @@ export function Toolbar({ children }: ToolbarProps) {
|
||||
);
|
||||
}
|
||||
|
||||
interface ToolbarItemProps extends ReactComponentProps {
|
||||
export interface ToolbarItemProps extends ReactComponentProps {
|
||||
placement?: 'left' | 'center' | 'right';
|
||||
children?: React.ReactElement;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Box } from 'coral-system';
|
||||
import { observer, useDesigner } from '@music163/tango-context';
|
||||
import { DesignerViewType } from '@music163/tango-core';
|
||||
|
||||
export interface ViewPanelProps {
|
||||
export interface WorkspaceViewProps {
|
||||
/**
|
||||
* 视图面板模式,对应 Workspace 的模式
|
||||
*/
|
||||
@@ -11,7 +11,7 @@ export interface ViewPanelProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const ViewPanel = observer((props: ViewPanelProps) => {
|
||||
export const WorkspaceView = observer((props: WorkspaceViewProps) => {
|
||||
const { mode = 'design', children } = props;
|
||||
const designer = useDesigner();
|
||||
const display = mode !== designer.activeView ? 'none' : 'block';
|
||||
@@ -3,6 +3,10 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [0.1.3](https://github.com/netease/tango/compare/@music163/tango-helpers@0.1.2...@music163/tango-helpers@0.1.3) (2023-09-18)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-helpers
|
||||
|
||||
## 0.1.2 (2023-09-06)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-helpers",
|
||||
"version": "0.1.2",
|
||||
"version": "0.1.3",
|
||||
"description": "Shared types, helpers, and hooks of tango-apps",
|
||||
"keywords": [
|
||||
"shared",
|
||||
@@ -31,7 +31,7 @@
|
||||
"prepublishOnly": "yarn build"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">= 16.8.0"
|
||||
"react": ">= 16.8"
|
||||
},
|
||||
"dependencies": {
|
||||
"hoist-non-react-statics": "^3.3.2",
|
||||
|
||||
@@ -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.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
|
||||
|
||||
## [0.1.3](https://github.com/netease/tango/compare/@music163/tango-sandbox@0.1.2...@music163/tango-sandbox@0.1.3) (2023-09-13)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-sandbox
|
||||
|
||||
## 0.1.2 (2023-09-06)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-sandbox",
|
||||
"version": "0.1.2",
|
||||
"version": "0.1.5",
|
||||
"description": "sandbox of tango apps",
|
||||
"author": "wwsun <ww.sun@outlook.com>",
|
||||
"homepage": "",
|
||||
@@ -24,13 +24,13 @@
|
||||
"prepublishOnly": "yarn build"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">= 16.8.0",
|
||||
"react-dom": ">= 16.8.0"
|
||||
"react": ">= 16.8",
|
||||
"styled-components": ">= 4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^4.8.0",
|
||||
"@music163/tango-core": "^0.1.2",
|
||||
"@music163/tango-helpers": "^0.1.2",
|
||||
"@music163/tango-core": "^0.2.0",
|
||||
"@music163/tango-helpers": "^0.1.3",
|
||||
"crypto-js": "^4.1.1",
|
||||
"lodash.isequal": "4.5.0",
|
||||
"react-frame-component": "^5.2.4"
|
||||
|
||||
@@ -3,6 +3,33 @@
|
||||
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-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
|
||||
|
||||
- remove useless codes ([813eb83](https://github.com/netease/tango/commit/813eb83ebb9ced891636f2653f25b6281386c18d))
|
||||
|
||||
# [0.2.0](https://github.com/netease/tango/compare/@music163/tango-setting-form@0.1.3...@music163/tango-setting-form@0.2.0) (2023-09-13)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- refactor setting formItem ([b786de2](https://github.com/netease/tango/commit/b786de2f1a0e4e9141eb09fce696e45df633b232))
|
||||
- remove some useless setters ([7e9ab12](https://github.com/netease/tango/commit/7e9ab12503ed33c1e6acb8a1fa5fd89fc82d35fd))
|
||||
|
||||
### Features
|
||||
|
||||
- refactor setting-form ([8d396ff](https://github.com/netease/tango/commit/8d396ff13459beeb57b6b3c48f7e8fe1765041ae))
|
||||
|
||||
## [0.1.3](https://github.com/netease/tango/compare/@music163/tango-setting-form@0.1.2...@music163/tango-setting-form@0.1.3) (2023-09-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- fix lint errors ([f9e6537](https://github.com/netease/tango/commit/f9e6537dd6569f4196b2799d51dbaab9838fd7f2))
|
||||
|
||||
## 0.1.2 (2023-09-06)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-setting-form
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-setting-form",
|
||||
"version": "0.1.2",
|
||||
"version": "0.2.2",
|
||||
"description": "setting form of tango-apps",
|
||||
"author": "wwsun <ww.sun@outlook.com>",
|
||||
"homepage": "",
|
||||
@@ -24,25 +24,17 @@
|
||||
"prepublishOnly": "yarn build"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">= 16.8.0"
|
||||
"react": ">= 16.8"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^4.8.0",
|
||||
"@music163/tango-core": "^0.1.2",
|
||||
"@music163/tango-helpers": "^0.1.2",
|
||||
"@music163/tango-ui": "^0.1.2",
|
||||
"@music163/tango-core": "^0.2.0",
|
||||
"@music163/tango-helpers": "^0.1.3",
|
||||
"@music163/tango-ui": "^0.1.4",
|
||||
"antd": "^4.24.2",
|
||||
"color": "^4.2.3",
|
||||
"coral-system": "^1.0.5",
|
||||
"cssjson": "^2.1.3",
|
||||
"mobx": "6.9.0",
|
||||
"mobx-react-lite": "4.0.2",
|
||||
"moment": "^2.29.3",
|
||||
"react-color": "^2.19.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/color": "^3.0.3",
|
||||
"@types/react-color": "^3.0.6"
|
||||
"mobx-react-lite": "4.0.2"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
interface FormatProps {
|
||||
format: 'simple' | 'original';
|
||||
data?: Record<string, string> | Array<Record<string, string>>;
|
||||
}
|
||||
|
||||
const formatData = (data?: Array<Record<string, string>>) => {
|
||||
const result: Record<string, string> = {};
|
||||
|
||||
// 遍历数据
|
||||
data?.forEach((item) => {
|
||||
item.key && !(item.key in result) && (result[item.key] = item.value);
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// 格式化value
|
||||
export const formatValue = ({ format, data }: FormatProps) => {
|
||||
if (format === 'original') {
|
||||
if (!data) return [{ index: 0 }];
|
||||
|
||||
const list = Array.isArray(data) ? data : Object.keys(data).map((key) => ({ key, value: data[key] }));
|
||||
|
||||
return list.map((item, idx) => ({ ...item, index: idx - list.length - 1 }));
|
||||
} else {
|
||||
if (!data) return {};
|
||||
|
||||
return Array.isArray(data) ? formatData(data) : data;
|
||||
}
|
||||
};
|
||||
|
||||
export const compareValue = (origin: Array<any>, data: Array<any>) => {
|
||||
const resolve = (list: Array<any>) => {
|
||||
let keyStr = '';
|
||||
let valueStr = '';
|
||||
|
||||
list.forEach((item) => {
|
||||
const { key, value } = item;
|
||||
|
||||
keyStr += key || '$';
|
||||
valueStr += value || '$';
|
||||
});
|
||||
|
||||
return { keyStr, valueStr };
|
||||
};
|
||||
|
||||
const oldRes = resolve(origin);
|
||||
const newRes = resolve(data);
|
||||
|
||||
return oldRes.keyStr === newRes.keyStr && oldRes.valueStr === newRes.valueStr;
|
||||
};
|
||||
@@ -1,4 +1,3 @@
|
||||
import { TangoRemoteServicesType } from '@music163/tango-helpers';
|
||||
import { createContext, useContext } from 'react';
|
||||
import { IFormModel } from './form-model';
|
||||
|
||||
@@ -11,54 +10,11 @@ export const useFormModel = () => {
|
||||
return useContext(FormModelContext);
|
||||
};
|
||||
|
||||
export type VariableNode = {
|
||||
label?: string;
|
||||
value?: string;
|
||||
type?: string;
|
||||
children?: VariableNode[];
|
||||
};
|
||||
|
||||
export interface FormVariableContextType {
|
||||
/**
|
||||
* 是否允许表单项切换到表达式设置器
|
||||
*/
|
||||
disableSwitchExpressionSetter?: boolean;
|
||||
/**
|
||||
* 探测输入上下文的对象
|
||||
*/
|
||||
evaluateContext?: object;
|
||||
/**
|
||||
* 表单动作发生时的回调
|
||||
*/
|
||||
onAction?: (fn: string, args: unknown[]) => void;
|
||||
/**
|
||||
* 状态变量列表
|
||||
*/
|
||||
modelVariables?: any[];
|
||||
/**
|
||||
* 动作变量列表
|
||||
*/
|
||||
actionVariables?: any[];
|
||||
/**
|
||||
* 表达式变量列表(含状态变量和动作变量)
|
||||
*/
|
||||
expressionVariables?: any[];
|
||||
/**
|
||||
* 路由选项
|
||||
*/
|
||||
routeOptions?: any[];
|
||||
/**
|
||||
* 可选弹窗列表
|
||||
*/
|
||||
modalOptions?: any[];
|
||||
/**
|
||||
* 关联的表单域列表
|
||||
*/
|
||||
formFieldsOptions?: any[];
|
||||
/**
|
||||
* 远程服务
|
||||
*/
|
||||
remoteServices?: TangoRemoteServicesType;
|
||||
}
|
||||
|
||||
const FormVariableContext = createContext<FormVariableContextType>(null);
|
||||
|
||||
@@ -11,10 +11,8 @@ import {
|
||||
import { IconFont, ToggleButton } from '@music163/tango-ui';
|
||||
import { QuestionCircleOutlined } from '@ant-design/icons';
|
||||
import { InputProps, Tooltip } from 'antd';
|
||||
import { INTERNAL_SETTERS } from './setters';
|
||||
import { ExpressionSetter } from './setters/expression-setter';
|
||||
import { useFormModel, useFormVariable } from './context';
|
||||
import { FormControl, FormControlProps } from './form-ui';
|
||||
import { FormControl } from './form-ui';
|
||||
|
||||
export interface FormItemProps extends ComponentPropType {
|
||||
extra?: React.ReactNode;
|
||||
@@ -29,15 +27,15 @@ export interface FormItemComponentProps<T = any> {
|
||||
[prop: string]: any;
|
||||
}
|
||||
|
||||
export interface FormItemCreateOptionsType {
|
||||
export interface IFormItemCreateOptions {
|
||||
/**
|
||||
* 设置器调用名
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* 别名
|
||||
* 别名列表
|
||||
*/
|
||||
alias?: string;
|
||||
alias?: string[];
|
||||
/**
|
||||
* 渲染的组件
|
||||
*/
|
||||
@@ -50,39 +48,18 @@ export interface FormItemCreateOptionsType {
|
||||
* 是否禁用变量设置器
|
||||
*/
|
||||
disableVariableSetter?: boolean;
|
||||
/**
|
||||
* 表单项类型
|
||||
*/
|
||||
type?: FormControlProps['type'];
|
||||
/**
|
||||
* 子表单是否默认折叠
|
||||
*/
|
||||
defaultCollapsed?: boolean;
|
||||
/**
|
||||
* 默认的表单值校验逻辑
|
||||
*/
|
||||
validate?: (value: string) => string | Promise<any>;
|
||||
}
|
||||
|
||||
function normalizeCreateOptions(
|
||||
options: FormItemCreateOptionsType,
|
||||
): Required<Omit<FormItemCreateOptionsType, 'component' | 'alias'>> {
|
||||
const render = options.render ?? ((props: any) => React.createElement(options.component, props));
|
||||
return {
|
||||
name: options.name,
|
||||
render,
|
||||
disableVariableSetter: options.disableVariableSetter ?? false,
|
||||
type: options.type || 'formItem',
|
||||
defaultCollapsed: options.defaultCollapsed ?? true,
|
||||
validate: options.validate,
|
||||
};
|
||||
}
|
||||
|
||||
const defaultGetSetterProps = () => ({});
|
||||
const defaultGetVisible = () => true;
|
||||
|
||||
export function createFormItem(options: FormItemCreateOptionsType) {
|
||||
const _options = normalizeCreateOptions(options);
|
||||
export function createFormItem(options: IFormItemCreateOptions) {
|
||||
const renderSetter =
|
||||
options.render ?? ((props: any) => React.createElement(options.component, props));
|
||||
|
||||
function FormItem({
|
||||
name,
|
||||
@@ -95,7 +72,7 @@ export function createFormItem(options: FormItemCreateOptionsType) {
|
||||
setterProps,
|
||||
defaultValue,
|
||||
options: setterOptions,
|
||||
disableVariableSetter: disableVariableSetterProp,
|
||||
disableVariableSetter: disableVariableSetterProp = options.disableVariableSetter,
|
||||
getVisible: getVisibleProp,
|
||||
getSetterProps: getSetterPropsProp,
|
||||
extra,
|
||||
@@ -104,16 +81,15 @@ export function createFormItem(options: FormItemCreateOptionsType) {
|
||||
const model = useFormModel();
|
||||
const field = model.getField(name);
|
||||
const value = toJS(field.value ?? defaultValue);
|
||||
const disableVariableSetter =
|
||||
disableSwitchExpressionSetter || disableVariableSetterProp || _options.disableVariableSetter;
|
||||
const disableVariableSetter = disableSwitchExpressionSetter ?? disableVariableSetterProp;
|
||||
const [isVariable, { toggle: toggleIsVariable }] = useBoolean(
|
||||
() => !disableVariableSetter && isVariableString(value),
|
||||
);
|
||||
|
||||
const setterName = isVariable ? 'expressionSetter' : setter;
|
||||
const validate = SETTERS_VALIDATE_DICT[setterName];
|
||||
|
||||
field.setConfig({
|
||||
validate,
|
||||
validate: options.validate,
|
||||
});
|
||||
|
||||
const baseComponentProps = clone(
|
||||
@@ -129,6 +105,8 @@ export function createFormItem(options: FormItemCreateOptionsType) {
|
||||
) as FormItemComponentProps;
|
||||
|
||||
let expProps = {};
|
||||
|
||||
// FIXME: 重新考虑这段代码的位置
|
||||
if (['expressionSetter', 'actionSetter', 'eventSetter'].includes(setter) || isVariable) {
|
||||
expProps = {
|
||||
modalTitle: title,
|
||||
@@ -138,11 +116,13 @@ export function createFormItem(options: FormItemCreateOptionsType) {
|
||||
}
|
||||
|
||||
const getSetterProps = getSetterPropsProp || defaultGetSetterProps;
|
||||
// 从注册表中获取 expSetter
|
||||
const ExpressionSetter = REGISTERED_FORM_ITEM_MAP['expressionSetter']?.config?.component;
|
||||
|
||||
const setterNode = isVariable ? (
|
||||
<ExpressionSetter {...expProps} {...baseComponentProps} />
|
||||
) : (
|
||||
_options.render({
|
||||
renderSetter({
|
||||
...expProps,
|
||||
...baseComponentProps,
|
||||
...setterProps, // setterProps 优先级大于快捷属性
|
||||
@@ -155,8 +135,6 @@ export function createFormItem(options: FormItemCreateOptionsType) {
|
||||
return (
|
||||
<FormControl
|
||||
visible={getVisible(model)}
|
||||
type={_options.type}
|
||||
defaultCollapsed={_options.defaultCollapsed}
|
||||
label={title}
|
||||
note={name}
|
||||
tip={tip}
|
||||
@@ -189,28 +167,25 @@ export function createFormItem(options: FormItemCreateOptionsType) {
|
||||
);
|
||||
}
|
||||
|
||||
FormItem.displayName = `FormItem_${_options.name}`;
|
||||
FormItem.displayName = `FormItem_${options.name}`;
|
||||
FormItem.config = options;
|
||||
|
||||
return observer(FormItem);
|
||||
}
|
||||
|
||||
// setter 查找表
|
||||
const SETTERS_DICT: Record<string, React.FunctionComponent<any>> = {};
|
||||
// setter 校验函数查找表
|
||||
const SETTERS_VALIDATE_DICT: Record<string, FormItemCreateOptionsType['validate']> = {};
|
||||
// 已注册的 setter 查找表
|
||||
const REGISTERED_FORM_ITEM_MAP: Record<string, ReturnType<typeof createFormItem>> = {};
|
||||
|
||||
INTERNAL_SETTERS.forEach((item) => {
|
||||
SETTERS_DICT[item.name] = createFormItem(item);
|
||||
if (item.alias) {
|
||||
SETTERS_DICT[item.alias] = SETTERS_DICT[item.name];
|
||||
}
|
||||
if (item.validate) {
|
||||
SETTERS_VALIDATE_DICT[item.name] = item.validate;
|
||||
if (item.alias) {
|
||||
SETTERS_VALIDATE_DICT[item.alias] = SETTERS_VALIDATE_DICT[item.name];
|
||||
}
|
||||
}
|
||||
});
|
||||
/**
|
||||
* Setter 注册
|
||||
* @param config 注册选项
|
||||
*/
|
||||
export function register(config: IFormItemCreateOptions) {
|
||||
const names = [config.name, ...(config.alias ?? [])];
|
||||
names.forEach((name) => {
|
||||
REGISTERED_FORM_ITEM_MAP[name] = createFormItem(config);
|
||||
});
|
||||
}
|
||||
|
||||
const iconStyle = {
|
||||
color: 'red',
|
||||
@@ -218,9 +193,9 @@ const iconStyle = {
|
||||
|
||||
export function SettingFormItem(props: FormItemProps) {
|
||||
const { setter } = props;
|
||||
const Comp = SETTERS_DICT[setter];
|
||||
const Comp = REGISTERED_FORM_ITEM_MAP[setter];
|
||||
if (Comp == null) {
|
||||
const Fallback = SETTERS_DICT.expressionSetter;
|
||||
const Fallback = REGISTERED_FORM_ITEM_MAP.expressionSetter;
|
||||
return (
|
||||
<Fallback
|
||||
{...props}
|
||||
@@ -234,14 +209,3 @@ export function SettingFormItem(props: FormItemProps) {
|
||||
}
|
||||
return React.createElement(Comp, props);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter 注册
|
||||
* @param options
|
||||
*/
|
||||
export function register(options: FormItemCreateOptionsType) {
|
||||
if (SETTERS_DICT[options.name]) {
|
||||
throw Error(`Duplicate setter name: <${options.name}>`);
|
||||
}
|
||||
SETTERS_DICT[options.name] = createFormItem(options);
|
||||
}
|
||||
|
||||
@@ -1,73 +1,40 @@
|
||||
import React, { useState } from 'react';
|
||||
import { css } from 'styled-components';
|
||||
import { css, Box, HTMLCoralProps, Text } from 'coral-system';
|
||||
import { Checkbox, Tooltip } from 'antd';
|
||||
import { Box, HTMLCoralProps, Text } from 'coral-system';
|
||||
import { CollapsePanel } from '@music163/tango-ui';
|
||||
import { isNil, isString } from '@music163/tango-helpers';
|
||||
|
||||
export interface FormControlProps extends Omit<FormLabelProps, 'type'> {
|
||||
/**
|
||||
* @deprecated 废弃 subForm 使用 FormObject 代替
|
||||
*/
|
||||
type?: 'formItem' | 'subForm';
|
||||
/**
|
||||
* 默认是否折叠,仅适用于 subForm 类型
|
||||
*/
|
||||
defaultCollapsed?: boolean;
|
||||
visible: boolean;
|
||||
extra?: React.ReactNode;
|
||||
error?: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function FormControl(props: FormControlProps) {
|
||||
const {
|
||||
type,
|
||||
defaultCollapsed = true,
|
||||
visible,
|
||||
label,
|
||||
note,
|
||||
tip,
|
||||
docs,
|
||||
extra,
|
||||
error,
|
||||
children,
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
if (type === 'formItem') {
|
||||
return (
|
||||
<Box className="FormControl" display={visible ? 'block' : 'none'} {...rest}>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center" mb="s">
|
||||
<FormLabel label={label} note={note} tip={tip} docs={docs} />
|
||||
{extra}
|
||||
</Box>
|
||||
<Box>{children}</Box>
|
||||
{!!error && (
|
||||
<Box mt="m" color="red">
|
||||
{error}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// else type === 'subForm'
|
||||
export function FormControl({
|
||||
visible,
|
||||
label,
|
||||
note,
|
||||
tip,
|
||||
docs,
|
||||
extra,
|
||||
error,
|
||||
children,
|
||||
...rest
|
||||
}: FormControlProps) {
|
||||
return (
|
||||
<CollapsePanel
|
||||
title={<FormLabel label={label} note={note} tip={tip} docs={docs} />}
|
||||
extra={
|
||||
<Box flex="1" textAlign="right">
|
||||
{extra}
|
||||
<Box className="FormControl" display={visible ? 'block' : 'none'} {...rest}>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center" mb="s">
|
||||
<FormLabel label={label} note={note} tip={tip} docs={docs} />
|
||||
{extra}
|
||||
</Box>
|
||||
<Box>{children}</Box>
|
||||
{!!error && (
|
||||
<Box mt="m" color="red">
|
||||
{error}
|
||||
</Box>
|
||||
}
|
||||
defaultCollapsed={defaultCollapsed}
|
||||
borderTop="solid"
|
||||
borderTopColor="line.normal"
|
||||
>
|
||||
<Box bg="#f9f9f9">{children}</Box>
|
||||
<Box color="red">{error}</Box>
|
||||
</CollapsePanel>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
} from '@music163/tango-helpers';
|
||||
import { Search, Tabs } from '@music163/tango-ui';
|
||||
import { SettingFormItem, register } from './form-item';
|
||||
import { FormModelProvider, FormVariableProvider, FormVariableContextType } from './context';
|
||||
import { FormModelProvider, FormVariableProvider } from './context';
|
||||
import { FormModel, FormModelOptionsType } from './form-model';
|
||||
import { SettingFormObject } from './form-object';
|
||||
import { isValidNestProps } from './helpers';
|
||||
@@ -53,19 +53,19 @@ const formStyle = css`
|
||||
}
|
||||
`;
|
||||
|
||||
type GroupOptionType = {
|
||||
interface IFormTabsGroupOption {
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
}
|
||||
|
||||
const internalGroups: GroupOptionType[] = [
|
||||
const internalGroups: IFormTabsGroupOption[] = [
|
||||
{ label: '常用', value: 'basic' },
|
||||
{ label: '事件', value: 'event' },
|
||||
{ label: '样式', value: 'style' },
|
||||
{ label: '高级', value: 'advanced' },
|
||||
];
|
||||
|
||||
export interface SettingFormProps extends FormVariableContextType {
|
||||
export interface SettingFormProps {
|
||||
/**
|
||||
* 表单状态管理模型实例
|
||||
*/
|
||||
@@ -85,24 +85,19 @@ export interface SettingFormProps extends FormVariableContextType {
|
||||
/**
|
||||
* 选项分组信息
|
||||
*/
|
||||
groupOptions?: GroupOptionType[];
|
||||
groupOptions?: IFormTabsGroupOption[];
|
||||
/**
|
||||
* 是否允许表单项切换到表达式设置器
|
||||
*/
|
||||
disableSwitchExpressionSetter?: boolean;
|
||||
}
|
||||
|
||||
export function SettingForm({
|
||||
prototype,
|
||||
disableSwitchExpressionSetter,
|
||||
modelVariables = [],
|
||||
actionVariables = [],
|
||||
expressionVariables = [],
|
||||
routeOptions = [],
|
||||
modalOptions = [],
|
||||
formFieldsOptions = [],
|
||||
evaluateContext,
|
||||
model: modelProp,
|
||||
defaultValue,
|
||||
onChange = noop,
|
||||
onAction = noop,
|
||||
remoteServices,
|
||||
groupOptions = internalGroups,
|
||||
}: SettingFormProps) {
|
||||
const [keyword, setKeyword] = useState('');
|
||||
@@ -155,21 +150,8 @@ export function SettingForm({
|
||||
}
|
||||
}, [modelProp]);
|
||||
|
||||
const formVariable = {
|
||||
disableSwitchExpressionSetter,
|
||||
onAction,
|
||||
modelVariables,
|
||||
actionVariables,
|
||||
expressionVariables,
|
||||
routeOptions,
|
||||
modalOptions,
|
||||
formFieldsOptions,
|
||||
evaluateContext,
|
||||
remoteServices,
|
||||
};
|
||||
|
||||
return (
|
||||
<FormVariableProvider value={formVariable}>
|
||||
<FormVariableProvider value={{ disableSwitchExpressionSetter }}>
|
||||
<FormModelProvider value={model}>
|
||||
<Box className="SettingForm" mb="xl" css={formStyle}>
|
||||
<Box className="SettingFormHeader" position="sticky" top="0" bg="white" zIndex="2">
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import { register } from './form-item';
|
||||
import { ExpressionSetter } from './setter';
|
||||
|
||||
// 预注册基础 Setter
|
||||
register({
|
||||
name: 'expressionSetter',
|
||||
component: ExpressionSetter,
|
||||
});
|
||||
|
||||
export * from './form';
|
||||
export * from './form-item';
|
||||
export * from './form-object';
|
||||
export * from './form-model';
|
||||
export * from './context';
|
||||
|
||||
export { EditableVariableTree } from './components';
|
||||
export type { EditableVariableTreeProps, VariableTreeNodeType } from './components';
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import React from 'react';
|
||||
import { FormItemComponentProps } from './form-item';
|
||||
|
||||
// FIXME: 内置 ExpressionSetter,默认全部 fallback 到此 setter
|
||||
export function ExpressionSetter(props: FormItemComponentProps<string>) {
|
||||
return <div>exp setter</div>;
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
import React from 'react';
|
||||
import { css } from 'styled-components';
|
||||
import { Box } from 'coral-system';
|
||||
import { Select } from 'antd';
|
||||
// import { IconPro } from '@ant-design/icons';
|
||||
import { FormItemComponentProps } from '../form-item';
|
||||
|
||||
const acStyle = css`
|
||||
.ant-select {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
const ALL_ICON_NAMES: string[] = [];
|
||||
|
||||
/**
|
||||
* Icon 组件设置器
|
||||
* onChange 返回 `{<Icon type="iconName" />}`
|
||||
*/
|
||||
export function IconSetter({ onChange, ...rest }: FormItemComponentProps) {
|
||||
const handleChange = (val: string) => {
|
||||
onChange &&
|
||||
onChange(val, {
|
||||
relatedImports: ['Icon'],
|
||||
});
|
||||
};
|
||||
return (
|
||||
<Box css={acStyle}>
|
||||
<Select showSearch allowClear placeholder="请选择图标" onChange={handleChange} {...rest}>
|
||||
{ALL_ICON_NAMES.map((icon) => {
|
||||
const key = `{<Icon type="${icon}" />}`;
|
||||
return (
|
||||
<Select.Option key={key}>
|
||||
<Box display="inline-block" mr="m">
|
||||
{/* <IconPro type={icon as any} size="14px" /> */}
|
||||
</Box>
|
||||
{icon}
|
||||
</Select.Option>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Button, Empty, Modal, Pagination, Radio } from 'antd';
|
||||
import { Box, css, Grid, GridItem, Text } from 'coral-system';
|
||||
import { isFunction, useBoolean } from '@music163/tango-helpers';
|
||||
import { FormItemComponentProps } from '../form-item';
|
||||
import { useFormVariable } from '../context';
|
||||
|
||||
const wrapperStyle = css`
|
||||
img {
|
||||
max-width: 100%;
|
||||
max-height: 150px;
|
||||
}
|
||||
`;
|
||||
|
||||
export function ImageSetter({ value, onChange }: FormItemComponentProps) {
|
||||
const [visible, { on, off }] = useBoolean();
|
||||
const label = value ? '更新图片' : '上传图片';
|
||||
const { remoteServices } = useFormVariable();
|
||||
return (
|
||||
<Box css={wrapperStyle}>
|
||||
<Box border="solid" borderColor="line.normal" textAlign="center" mb="m">
|
||||
{value ? <img src={value} alt="preview image" /> : null}
|
||||
</Box>
|
||||
<Button block onClick={on}>
|
||||
{label}
|
||||
</Button>
|
||||
<Modal title={label} visible={visible} onCancel={off} width="60%">
|
||||
<ImageCenter
|
||||
imageService={remoteServices?.ImageService}
|
||||
onSelect={(url) => {
|
||||
onChange(url);
|
||||
off();
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const categories = [
|
||||
{ label: '我的素材', value: 'listMy' },
|
||||
{ label: '我的收藏', value: 'listFav' },
|
||||
{ label: '公共素材', value: 'listPub' },
|
||||
];
|
||||
|
||||
interface ImageCenterProps {
|
||||
onSelect?: ImageListProps['onSelect'];
|
||||
imageService?: Record<string, (...args: any[]) => Promise<any>>;
|
||||
}
|
||||
|
||||
const pageSize = 20;
|
||||
|
||||
function ImageCenter({ imageService, onSelect }: ImageCenterProps) {
|
||||
const [cate, setCate] = useState('listMy');
|
||||
const [page, setPage] = useState(1);
|
||||
const [data, setData] = useState<any>({});
|
||||
useEffect(() => {
|
||||
imageService?.[cate]?.({ limit: pageSize, offset: (page - 1) * pageSize }).then((res: any) => {
|
||||
setData(res || {});
|
||||
});
|
||||
}, [imageService, cate, page]);
|
||||
return (
|
||||
<Box>
|
||||
<Box display="flex" justifyContent="space-between">
|
||||
<Radio.Group
|
||||
value={cate}
|
||||
onChange={(e) => {
|
||||
setCate(e.target.value);
|
||||
}}
|
||||
options={categories}
|
||||
optionType="button"
|
||||
buttonStyle="solid"
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
href="https://music-fn.hz.netease.com/s/music-deer-web/user/image/myupload"
|
||||
target="_blank"
|
||||
>
|
||||
我要上传
|
||||
</Button>
|
||||
</Box>
|
||||
{data.list?.length ? (
|
||||
<ImageList dataSource={data.list} onSelect={onSelect} />
|
||||
) : (
|
||||
<Empty description="没有数据或数据服务请求出错" />
|
||||
)}
|
||||
{data.count > pageSize ? (
|
||||
<Pagination current={page} total={data.count} pageSize={pageSize} onChange={setPage} />
|
||||
) : null}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const imageListWrapper = css`
|
||||
img {
|
||||
max-width: 100%;
|
||||
max-height: 120px;
|
||||
}
|
||||
|
||||
button {
|
||||
border-color: transparent;
|
||||
outline: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--tango-colors-brand);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
interface ImageListProps {
|
||||
dataSource?: any[];
|
||||
onSelect?: (url: string) => void;
|
||||
}
|
||||
|
||||
function ImageList({ dataSource = [], onSelect }: ImageListProps) {
|
||||
return (
|
||||
<Grid my="m" columns={4} gap="12px" height="400px" overflow="auto" css={imageListWrapper}>
|
||||
{dataSource.map((item) => (
|
||||
<GridItem
|
||||
key={item.id}
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
bg="gray.20"
|
||||
>
|
||||
<Box
|
||||
as="button"
|
||||
display="inline-flex"
|
||||
flexDirection="column"
|
||||
alignItems="center"
|
||||
gap="m"
|
||||
bg="transparent"
|
||||
onClick={() => {
|
||||
isFunction(onSelect) && onSelect(item.url);
|
||||
}}
|
||||
>
|
||||
<img src={item.url} />
|
||||
<Text fontSize="12px" color="text.note">
|
||||
{item.name}
|
||||
</Text>
|
||||
</Box>
|
||||
</GridItem>
|
||||
))}
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
@@ -1,203 +0,0 @@
|
||||
import React, { FunctionComponent, useState } from 'react';
|
||||
import { Box } from 'coral-system';
|
||||
import { Select, Input, Tabs, Button } from 'antd';
|
||||
import { FieldStringOutlined } from '@ant-design/icons';
|
||||
import { SingleMonacoEditor } from '@music163/tango-ui';
|
||||
import { FormItemComponentProps } from '../form-item';
|
||||
import { useFormVariable } from '../context';
|
||||
import { ListSetter } from './list-setter';
|
||||
|
||||
const { TabPane } = Tabs;
|
||||
|
||||
// TODO: 需考虑 option选项联动 & 组件属性联动等面板
|
||||
const CustomComponent: FunctionComponent<any> = ({ value = {}, onChange }) => {
|
||||
// FIXME: 这里之前判断逻辑有问题,导致每次 value 变更都执行 parseExpression
|
||||
const [tabIndex, setTabIndex] = useState(typeof value === 'string' ? '2' : '1');
|
||||
|
||||
const triggerChange = (changedValue: any) => {
|
||||
onChange({
|
||||
...value,
|
||||
...changedValue,
|
||||
});
|
||||
};
|
||||
|
||||
const customListenerDefaultValue = `/* {(valid, field, form) => {
|
||||
form.fieldMap.input1.setValue("联动改变值");
|
||||
}}*/`;
|
||||
return (
|
||||
<Tabs type="card" activeKey={tabIndex} onChange={setTabIndex}>
|
||||
<TabPane tab="常用配置" key="1">
|
||||
<Box display="flex" flexDirection="column" gap="20px">
|
||||
<Select
|
||||
value={value?.status}
|
||||
onChange={(v) => {
|
||||
triggerChange({
|
||||
status: v,
|
||||
});
|
||||
}}
|
||||
placeholder="UI状态"
|
||||
options={[
|
||||
{
|
||||
label: '编辑',
|
||||
value: 'edit',
|
||||
},
|
||||
{
|
||||
label: '禁用',
|
||||
value: 'disabled',
|
||||
},
|
||||
{
|
||||
label: '隐藏',
|
||||
value: 'hidden',
|
||||
},
|
||||
{
|
||||
label: '预览',
|
||||
value: 'preview',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Select
|
||||
value={value?.ui?.required}
|
||||
onChange={(v) => {
|
||||
triggerChange({
|
||||
ui: {
|
||||
...value?.ui,
|
||||
required: v,
|
||||
},
|
||||
});
|
||||
}}
|
||||
placeholder="必填"
|
||||
options={[
|
||||
{
|
||||
label: '必填',
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
label: '不必填',
|
||||
value: false,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Input
|
||||
placeholder="设置label标题"
|
||||
value={value?.ui?.label}
|
||||
onChange={(e) => {
|
||||
triggerChange({
|
||||
ui: {
|
||||
...value?.ui,
|
||||
label: e.target.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
placeholder="设置字段值"
|
||||
value={value?.value}
|
||||
onChange={(e) => {
|
||||
triggerChange({
|
||||
value: e.target.value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</TabPane>
|
||||
<TabPane tab="自定义配置" key="2">
|
||||
<SingleMonacoEditor
|
||||
defaultValue={customListenerDefaultValue}
|
||||
value={
|
||||
Object.prototype.toString.call(value) === '[object Object]'
|
||||
? customListenerDefaultValue
|
||||
: value
|
||||
}
|
||||
onChange={(v) => onChange(v.trim())}
|
||||
hasBorder
|
||||
height="150px"
|
||||
options={{
|
||||
lineNumbers: 'off',
|
||||
fontSize: 12,
|
||||
wordWrap: 'on',
|
||||
minimap: {
|
||||
enabled: false,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
);
|
||||
};
|
||||
|
||||
const renderSetterItem = (item: any) => {
|
||||
return (
|
||||
<>
|
||||
<FieldStringOutlined />
|
||||
<Box display="inline-block" ml="m">
|
||||
{item.title}
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* XFormItem响应器
|
||||
*/
|
||||
export const ListenerSetter = (props: FormItemComponentProps<any[]>) => {
|
||||
const { formFieldsOptions } = useFormVariable();
|
||||
|
||||
return (
|
||||
<ListSetter
|
||||
getListItemKey={(item) => item.key || item.dataIndex}
|
||||
addBtnText="配置响应器"
|
||||
listItemFormFields={[
|
||||
{
|
||||
label: '来源字段',
|
||||
name: 'watch',
|
||||
required: true,
|
||||
component: <Select mode="tags" options={formFieldsOptions} />,
|
||||
extra: <span>请选择依赖的字段。</span>,
|
||||
},
|
||||
{
|
||||
label: '触发条件',
|
||||
name: 'condition',
|
||||
required: true,
|
||||
component: (
|
||||
<SingleMonacoEditor
|
||||
hasBorder
|
||||
height="50px"
|
||||
defaultValue="// input1.value === '123'"
|
||||
options={{
|
||||
lineNumbers: 'off',
|
||||
fontSize: 12,
|
||||
wordWrap: 'on',
|
||||
minimap: {
|
||||
enabled: false,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
),
|
||||
extra: <span>请输入一个布尔表达式,满足条件时即触发响应。</span>,
|
||||
},
|
||||
{
|
||||
label: '响应行为',
|
||||
name: 'set',
|
||||
required: true,
|
||||
component: <CustomComponent />,
|
||||
width: '400px',
|
||||
extra: (
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() =>
|
||||
window.open(
|
||||
'https://music-cms.hz.netease.com/xform-docs/docs/tutorial-basics/listeners#set-%E4%B8%BA-function',
|
||||
)
|
||||
}
|
||||
>
|
||||
文档地址
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
]}
|
||||
newItemDefaultValues={[]}
|
||||
renderItem={renderSetterItem}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,175 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Box } from 'coral-system';
|
||||
import { AutoComplete, Button, Select, Switch } from 'antd';
|
||||
import { FieldStringOutlined } from '@ant-design/icons';
|
||||
import { SingleMonacoEditor } from '@music163/tango-ui';
|
||||
import { FormItemComponentProps } from '../form-item';
|
||||
import { ListSetter, NewOptionFormFieldType } from './list-setter';
|
||||
|
||||
// 常用正则
|
||||
const enumPattern = [
|
||||
{
|
||||
label: '纯数字',
|
||||
value: '^[0-9]*$',
|
||||
},
|
||||
{
|
||||
label: '纯汉字',
|
||||
value: '^[\u4e00-\u9fa5]{0,}$',
|
||||
},
|
||||
{
|
||||
label: '手机号',
|
||||
value: '^[1][3-8][0-9]{9}$',
|
||||
},
|
||||
{
|
||||
label: 'IP地址',
|
||||
value: 'd+.d+.d+.d+',
|
||||
},
|
||||
{
|
||||
label: '身份证号',
|
||||
value: '^d{15}|d{18}$',
|
||||
},
|
||||
{
|
||||
label: '邮政编码',
|
||||
value: '[1-9]d{5}(?!d)',
|
||||
},
|
||||
{
|
||||
label: '图片URL',
|
||||
value: '(https?:[^:<>"]*/)([^:<>"]*)(.((png!thumbnail)|(png)|(jpg)|(webp)|(gif)))',
|
||||
},
|
||||
];
|
||||
|
||||
// async-validator type
|
||||
// https://github.com/yiminghe/async-validator#type
|
||||
const type = [
|
||||
'string', // // Must be of type string. This is the default type.
|
||||
// 'number', Input TextArea出来的value都是string / InputNumber是number。不在async-validator做数字类型的校验
|
||||
'boolean', // Must be of type boolean.
|
||||
'regexp', // Must be an instance of RegExp or a string that does not generate an exception when creating a new RegExp.
|
||||
'integer', // Must be of type number and an integer.
|
||||
'float', // Must be of type number and a floating point number.
|
||||
'array', // Must be an array as determined by Array.isArray.
|
||||
'object', // Must be of type object and not Array.isArray.
|
||||
'date', // Value // Must be valid as determined by Date
|
||||
'url', // Must be of type url.
|
||||
'email', // Must be of type email.
|
||||
];
|
||||
|
||||
const trigger = ['change', 'submit'];
|
||||
|
||||
const status = ['error', 'warning'];
|
||||
|
||||
/**
|
||||
* 正则输入框
|
||||
*/
|
||||
const PatternInput: React.FunctionComponent<any> = (props) => {
|
||||
return (
|
||||
<>
|
||||
<AutoComplete options={enumPattern} onSelect={props.onChange} {...props} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 必填选择
|
||||
*/
|
||||
const RequiredInput: React.FunctionComponent<any> = (props) => {
|
||||
return (
|
||||
<>
|
||||
<Switch onChange={props.onChange} checked={props.value} {...props} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const optionFormFields: NewOptionFormFieldType[] = [
|
||||
{
|
||||
label: '字段类型',
|
||||
name: 'type',
|
||||
required: true,
|
||||
component: <Select options={type.map((t) => ({ label: t, value: t }))} />,
|
||||
},
|
||||
{
|
||||
label: '是否必填',
|
||||
name: 'required',
|
||||
component: <RequiredInput />,
|
||||
},
|
||||
{
|
||||
label: '触发方式',
|
||||
name: 'trigger',
|
||||
component: <Select options={trigger.map((t) => ({ label: t, value: t }))} />,
|
||||
},
|
||||
{
|
||||
label: '触发状态',
|
||||
name: 'status',
|
||||
component: <Select options={status.map((t) => ({ label: t, value: t }))} />,
|
||||
},
|
||||
{
|
||||
label: '错误提示语',
|
||||
name: 'message',
|
||||
extra: '自定义错误提示',
|
||||
},
|
||||
{
|
||||
label: '正则表达式',
|
||||
name: 'pattern',
|
||||
extra: '复杂场景,可以自定义正则。',
|
||||
component: <PatternInput placeholder="例如: ^[0-9]*$" />,
|
||||
},
|
||||
{
|
||||
label: '自定义校验',
|
||||
name: 'validator',
|
||||
width: '400px',
|
||||
extra: (
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() => window.open('https://github.com/yiminghe/async-validator#validator')}
|
||||
>
|
||||
文档地址
|
||||
</Button>
|
||||
),
|
||||
component: (
|
||||
<SingleMonacoEditor
|
||||
hasBorder
|
||||
height="108px"
|
||||
defaultValue="/** {(rule, value, callback) => { return value === 'test';}}*/"
|
||||
options={{
|
||||
lineNumbers: 'off',
|
||||
fontSize: 12,
|
||||
wordWrap: 'on',
|
||||
minimap: {
|
||||
enabled: false,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const renderSetterItem = (item: any) => {
|
||||
return (
|
||||
<>
|
||||
<FieldStringOutlined />
|
||||
<Box display="inline-block" ml="m">
|
||||
{item.title}
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 校验属性配置
|
||||
*/
|
||||
export function RuleSetter(props: FormItemComponentProps<any[]>) {
|
||||
return (
|
||||
<ListSetter
|
||||
getListItemKey={(item) => item.key || item.dataIndex}
|
||||
addBtnText="配置校验器"
|
||||
listItemFormFields={optionFormFields}
|
||||
newItemDefaultValues={{
|
||||
type: 'string',
|
||||
trigger: 'change',
|
||||
status: 'error',
|
||||
}}
|
||||
renderItem={renderSetterItem}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
-1
@@ -1 +0,0 @@
|
||||
declare module 'cssjson';
|
||||
@@ -3,6 +3,16 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [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
|
||||
|
||||
## [0.1.3](https://github.com/netease/tango/compare/@music163/tango-ui@0.1.2...@music163/tango-ui@0.1.3) (2023-09-13)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- update exports ([443ae58](https://github.com/netease/tango/commit/443ae589f67255f3815b587fd8f928c2b8742e9a))
|
||||
|
||||
## 0.1.2 (2023-09-06)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-ui
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-ui",
|
||||
"version": "0.1.2",
|
||||
"version": "0.1.4",
|
||||
"description": "ui widgets of tango",
|
||||
"keywords": [
|
||||
"react",
|
||||
@@ -29,20 +29,21 @@
|
||||
"prepublishOnly": "yarn build"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">= 16.8.0"
|
||||
"react": ">= 16.8",
|
||||
"styled-components": ">= 4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^4.8.0",
|
||||
"@codemirror/autocomplete": "^6.7.1",
|
||||
"@codemirror/lang-javascript": "^6.1.2",
|
||||
"@codemirror/search": "^6.2.3",
|
||||
"@music163/tango-helpers": "^0.1.2",
|
||||
"@music163/tango-helpers": "^0.1.3",
|
||||
"@uiw/react-codemirror": "^4.19.3",
|
||||
"antd": "^4.24.2",
|
||||
"antd-easy-form": "^1.1.3",
|
||||
"coral-system": "^1.0.5",
|
||||
"react-json-view": "^1.21.3",
|
||||
"react-monaco-editor-lite": "^1.2.0"
|
||||
"react-monaco-editor-lite": "^1.2.3"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
import { css } from 'styled-components';
|
||||
import { Box } from 'coral-system';
|
||||
import { css, Box } from 'coral-system';
|
||||
import { FolderOpenOutlined, FolderOutlined, FileOutlined } from '@ant-design/icons';
|
||||
|
||||
export interface Props {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user