mirror of
https://github.com/NetEase/tango.git
synced 2026-08-31 09:30:28 +08:00
Compare commits
88 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a86e0e049c | |||
| c4e1ed67f5 | |||
| 179de0cc52 | |||
| 6de54884b5 | |||
| 55bf417ddf | |||
| ca56edd9ae | |||
| 65149f1326 | |||
| 939466648d | |||
| 8fe57333a0 | |||
| 269e304d68 | |||
| 8c80255f81 | |||
| 359aa0b148 | |||
| 51ca4a72ce | |||
| b83d965955 | |||
| a1eea02f94 | |||
| 0d0479e3e5 | |||
| 67d602a3ec | |||
| 8dd289cd7d | |||
| 82bc5db4d8 | |||
| 85c053b3db | |||
| 32f5667cae | |||
| 1d0fb4f274 | |||
| 0011cdca0f | |||
| 0f3f0afdfa | |||
| b53a9f7eef | |||
| 5336c2ec28 | |||
| cdbd2351a2 | |||
| 1f03977f7d | |||
| a2cc418a73 | |||
| 239c0b0a28 | |||
| ae04850bb2 | |||
| ca740c83ae | |||
| 00bf52a411 | |||
| c1306e24e0 | |||
| 28040fc006 | |||
| 3f3981bb9d | |||
| 253ceca969 | |||
| a0ed57ff4f | |||
| f83c6bc0f6 | |||
| bb92210594 | |||
| 163ecedff3 | |||
| 22a2e9bdd1 | |||
| 696f82bd26 | |||
| a0484e8f64 | |||
| cf31ef19f4 | |||
| fe31246483 | |||
| 9daf3872e7 | |||
| f17ccbb7f6 | |||
| f854f75b8a | |||
| df3021f75e | |||
| 8bf53a76f8 | |||
| 791fbb162a | |||
| 704277b8f0 | |||
| 1fc22a4535 | |||
| ffaa276b5c | |||
| 56d0877507 | |||
| 897fe0a5a1 | |||
| b52bbad547 | |||
| f7c837a8a9 | |||
| f64d178ab8 | |||
| f2dcc942ed | |||
| ac48a8880e | |||
| 0f1711868d | |||
| 06cd3b3a18 | |||
| 2664613ab2 | |||
| e39a913a87 | |||
| 52864b5400 | |||
| 27166ec949 | |||
| 1a8d15c9da | |||
| 43b99de01c | |||
| 1c1ad9e5eb | |||
| 776837079a | |||
| 14375284e4 | |||
| cb6aabf6ce | |||
| 3f42516816 | |||
| 93608d1037 | |||
| 41a61cce4e | |||
| 47364e4219 | |||
| 9ed6a7d1a4 | |||
| 6f2c8f239a | |||
| 6215ed96f2 | |||
| 190aa1d8ee | |||
| bceb7a5c9c | |||
| d64042922b | |||
| 9ada3fecb3 | |||
| 07febb385f | |||
| ddbda0e6fb | |||
| 9f7441c134 |
@@ -9,3 +9,4 @@ coverage/
|
||||
.umi-production/
|
||||
.log
|
||||
.eslintcache
|
||||
*.pem
|
||||
|
||||
@@ -31,23 +31,17 @@ You can view the detailed usage guide through the following links:
|
||||
- Out-of-the-box front-end low-code designer, providing flexible and easy-to-use designer React components
|
||||
- Developed using TypeScript, providing complete type definition files
|
||||
|
||||
## 🎯 Milestone
|
||||
## 💡 Examples
|
||||
|
||||
```mermaid
|
||||
gantt
|
||||
dateFormat YYYY-MM-DD
|
||||
title Tango 1.0 Milestones
|
||||
excludes weekends
|
||||
%% (`excludes` accepts specific dates in YYYY-MM-DD format, days of the week ("sunday") or "weekends", but not the word "weekdays".)
|
||||
You can use tango to build lowcode builders, for example:
|
||||
|
||||
section Builder Release
|
||||
1.0 Alpha : m1, 2023-10-01, 2024-01-31
|
||||
1.0 RC : m2, 2024-02-01, 2024-04-30
|
||||
1.0 Stable : m3, 2024-04-30, 2024-12-31
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> Alpha versions will continue to optimize some implementation protocols and continuously integrate new features from the internal version of NetEase; RC versions will provide a stable parsing protocol and implementation; The 1.0 official version is planned to be released in Q2 2024. After the release of the 1.0 official version, we will continue to iterate on versions and gradually incorporate core capabilities such as process design, component library, and AIGC from the internal version.
|
||||
| Preview | Description |
|
||||
| ---------------------------------------------------------------------------------------------- | ----------------------- |
|
||||
|  | Internal App Builder |
|
||||
|  | Dashboard App Builder |
|
||||
|  | Mobile App Builder |
|
||||
|  | ReactNative App Builder |
|
||||
|  | Email Builder |
|
||||
|
||||
## 🌐 Compatibility
|
||||
|
||||
@@ -76,6 +70,23 @@ yarn
|
||||
yarn start
|
||||
```
|
||||
|
||||
### Local Https certificate
|
||||
|
||||
If you need to use https in the local development environment, you can use the following command to generate a certificate:
|
||||
|
||||
```bash
|
||||
brew install mkcert
|
||||
|
||||
# install the local certificate
|
||||
mkcert -install
|
||||
|
||||
# enter the playground app directory
|
||||
cd apps/playground
|
||||
|
||||
# generate a certificate
|
||||
mkcert local.netease.com # or change to your own domain name
|
||||
```
|
||||
|
||||
## 💬 Community
|
||||
|
||||
Join NetEase Tango Community to share your ideas, suggestions, or questions and connect with other users and contributors.
|
||||
|
||||
+30
-19
@@ -26,28 +26,22 @@
|
||||
## ✨ 特性
|
||||
|
||||
- 经历网易云音乐内网生产环境的实际检验,可灵活集成应用于低代码平台,本地开发工具等
|
||||
- 基于源码 AST 驱动,无私有 DSL 和协议
|
||||
- 提供实时出码能力,支持源码进,源码出
|
||||
- 开箱即用的前端低代码设计器,提供灵活易用的设计器 React 组件
|
||||
- 使用 TypeScript 开发,提供完整的类型定义文件
|
||||
- 提供基于源码 AST 驱动的低代码引擎,无私有 DSL 和协议
|
||||
- 提供实时出码能力,支持源码进,源码出,适配多种源码开发场景
|
||||
- 提供开箱即用的前端低代码设计器,提供灵活易用的设计器 React 组件
|
||||
- 使用 TypeScript 开发,提供完整的类型定义文件和完善的文档支持
|
||||
|
||||
## 🎯 里程碑
|
||||
## 💡 例子
|
||||
|
||||
```mermaid
|
||||
gantt
|
||||
dateFormat YYYY-MM-DD
|
||||
title Tango 1.0 Milestones
|
||||
excludes weekends
|
||||
%% (`excludes` accepts specific dates in YYYY-MM-DD format, days of the week ("sunday") or "weekends", but not the word "weekdays".)
|
||||
你可以使用 Tango 快速构建多种类型的基于源码的低代码搭建工具,例如:
|
||||
|
||||
section Builder Release
|
||||
1.0 Alpha : m1, 2023-10-01, 2024-01-31
|
||||
1.0 RC : m2, 2024-02-01, 2024-04-30
|
||||
1.0 Stable : m3, 2024-04-30, 2024-12-31
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> Alpha 版本会持续对一些实现协议进行优化,并持续融合网易内网版本中的新增特性;RC 版本将会提供基本稳定的解析协议和实现;1.0 正式版本计划在 2024 年Q2发布。1.0 正式版发布后我们将会持续进行版本迭代,并逐步融入内网版本中的流程设计、组件库、AIGC 等核心基础能力。
|
||||
| 预览图 | 说明 |
|
||||
| ---------------------------------------------------------------------------------------------- | -------------------- |
|
||||
|  | 中后台系统搭建 |
|
||||
|  | 仪表盘应用搭建 |
|
||||
|  | H5活动页面搭建 |
|
||||
|  | ReactNative 应用搭建 |
|
||||
|  | 营销类邮件搭建 |
|
||||
|
||||
## 🌐 兼容环境
|
||||
|
||||
@@ -76,6 +70,23 @@ yarn
|
||||
yarn start
|
||||
```
|
||||
|
||||
### 本地 https 证书
|
||||
|
||||
如果需要在本地开发环境中使用 https,可以使用以下命令生成证书:
|
||||
|
||||
```bash
|
||||
brew install mkcert
|
||||
|
||||
# 将 mkcert 添加到本地根 CA,仅在本地生效
|
||||
mkcert -install
|
||||
|
||||
# 进入 playground 应用目录
|
||||
cd apps/playground
|
||||
|
||||
# 为网站生成一个由 mkcert 签名的证书
|
||||
mkcert local.netease.com
|
||||
```
|
||||
|
||||
## 💬 社区讨论
|
||||
|
||||
参与 NetEase Tango 的社区,以分享您的想法、建议或问题,并与其他用户和贡献者建立联系。
|
||||
|
||||
@@ -7,6 +7,7 @@ const resolvePackageIndex = (relativeEntry: string) =>
|
||||
export default defineConfig({
|
||||
routes: [
|
||||
{ path: '/', component: 'index' },
|
||||
{ path: '/mail', component: 'mail' },
|
||||
{ path: '/docs', component: 'docs' },
|
||||
],
|
||||
npmClient: 'yarn',
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC4sAqDE3nhtSWD
|
||||
YqV4mlglnzIs2AioZr2uxfj/OKrpE1ZML/dflJyc5NGUpOZWV+W1vRc70wWVNFxB
|
||||
Si8nrYrrseoID7Ehxxe6+J1m/JsEbhZWHwV3M1iGW7JD7VhujaDpSLg67wMTMpSW
|
||||
3BUS+0NT+MNfCisp+u/ls4fXPCTgXiWcjMULm95oLlTeYwurPHfxwn9f9hh85I5I
|
||||
K3dI7w8MsQN7EFNrJz3UfrNLpmARMtSUBMwBYqrCitoUxRT0q7KYCru7kHxGPB6A
|
||||
SQrFpSpOWT79vp8GBSp9g3axhRsK+aEgzQ0QGrFPHBwBP/kWgmdB1URc/K7STSC9
|
||||
xyMNZOM7AgMBAAECggEATbyDYDKVbaRymr+tyHqmuYctdmSkGEXOdz8LFFoEzj/+
|
||||
ZekWpVuHJB7H/FoAPb2XiYyCuAKVafz1C+IuwPOx8o0bZbM9KmuDmIZGsm+GzDGO
|
||||
I0fBZC/vhfYYrbC8NSRV43thWCT8VVtH4cvW7vtllnWxvlIoYu4lhbiwZ68AEpO1
|
||||
8PlJsOhnagfkFpG8LehSDs9if5L+Ig0geQEcUY3pGUM9LKYsRoxFv6Vh8wQku1XD
|
||||
YOxLvLWDPqJ75m816t50RARe3e6eDJ+ukzDCoDjmLP8uhf1sk1FcL5aOgUNU3VOi
|
||||
P6sIH5uzuD0MA3FXx2mItYLOc44r4uaFJZiRUMEXGQKBgQDCjUYblE4rh0nKFkqx
|
||||
AhgfcRgpgLbP42OwFtD0u20RXdZAEJDRDQ43NdOgjFp2B50k75LBPIoFXYUZdzKj
|
||||
l7S2FH5O9Cc6hEgZGI4hCiYd+nQVI1mxv820YnzZJN4oooxyL+B5RlnW5cDT7LJN
|
||||
11Q5PRewkM+XpfbE3lKUjTNcDwKBgQDzBS+j8T9GJSzV2xgA+mpGHe1RjDq6yQtk
|
||||
Rl5GWpflW4Nr3/58BbmCWNA8WP0NOWutJhrkltwLCqJC8OZX/WHnmcmq2k+UAfQQ
|
||||
z98p+xcMWOTcl5XIbveuhYoZb2udt9WVJLcpRVxVhEWcBrtHsrpScZXsKYf/TaZ1
|
||||
JtCjPU5KFQKBgGXj/UqmYkYzxXZ8NEP998pHvcLGsXew/F2VpXv2yQlmXrYQtvd1
|
||||
YfOSUjJsL1hPZoKYogBoB1UGBJYwsimxYyhVFU2eWwkvjF7wWEd/fDUJHVCQwgHw
|
||||
lPga+I303BDmCK4o2uRI7vY4P8P92+gelPKhR03mmYIvnky+rvsz9CkLAoGAVerl
|
||||
du6Z0jDecxUsnVvrKrL17jjHorXlYyRFvFXXEe2SvxbIIAzeEylXJZ7IiF5epS6t
|
||||
n1d+oCC4UTZeGYmpjXofhFn+fHNcWH1FhtAQy/q5nwuwltY0luz7cBamU3Jk/n+m
|
||||
id7N0CYdfhYbLDQSD4e822ureyV2zkBUzwGjpikCgYEAoCVbhkxVrQHRmxLC2jDx
|
||||
pM94W1ij9TqblfGg3OMpPXjpTdlknatWjBw7JkoyxYiYJq5Gi2p4QV6HHUvS1osj
|
||||
BB+4A99/PD9A0ICoJglz8SXlGe/895zwHwpKT9f0zjizQrEd2KCPf75URa+t2FGY
|
||||
B2GkXsbChRWVrey5+uMzUD8=
|
||||
-----END PRIVATE KEY-----
|
||||
@@ -1,25 +0,0 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIEQjCCAqqgAwIBAgIRAIeLJ2jwt4MSKycHFmFXL6YwDQYJKoZIhvcNAQELBQAw
|
||||
fTEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMSkwJwYDVQQLDCB3d3N1
|
||||
bkBzdW4tbWJwLmxvY2FsIChTVU4gV2Vpd2VpKTEwMC4GA1UEAwwnbWtjZXJ0IHd3
|
||||
c3VuQHN1bi1tYnAubG9jYWwgKFNVTiBXZWl3ZWkpMB4XDTIyMDcyODAxNTIxMFoX
|
||||
DTI0MTAyODAxNTIxMFowVDEnMCUGA1UEChMebWtjZXJ0IGRldmVsb3BtZW50IGNl
|
||||
cnRpZmljYXRlMSkwJwYDVQQLDCB3d3N1bkBzdW4tbWJwLmxvY2FsIChTVU4gV2Vp
|
||||
d2VpKTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALiwCoMTeeG1JYNi
|
||||
pXiaWCWfMizYCKhmva7F+P84qukTVkwv91+UnJzk0ZSk5lZX5bW9FzvTBZU0XEFK
|
||||
Lyetiuux6ggPsSHHF7r4nWb8mwRuFlYfBXczWIZbskPtWG6NoOlIuDrvAxMylJbc
|
||||
FRL7Q1P4w18KKyn67+Wzh9c8JOBeJZyMxQub3mguVN5jC6s8d/HCf1/2GHzkjkgr
|
||||
d0jvDwyxA3sQU2snPdR+s0umYBEy1JQEzAFiqsKK2hTFFPSrspgKu7uQfEY8HoBJ
|
||||
CsWlKk5ZPv2+nwYFKn2DdrGFGwr5oSDNDRAasU8cHAE/+RaCZ0HVRFz8rtJNIL3H
|
||||
Iw1k4zsCAwEAAaNmMGQwDgYDVR0PAQH/BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUF
|
||||
BwMBMB8GA1UdIwQYMBaAFDILZR+SW32Ug0HkAKrH1KT2a1ttMBwGA1UdEQQVMBOC
|
||||
EWxvY2FsLm5ldGVhc2UuY29tMA0GCSqGSIb3DQEBCwUAA4IBgQCEdeV2Likl4MUt
|
||||
wWeN/X3tAR3L7mkl7e4f0CQ4v2lOs8cekOP7p3ZFk5nSyVbxda3usPB0OtbVTibw
|
||||
qZ1n/TMRb5epHroXIMleqngP82zFZNg+WVyHm/ZAml8gVD0VZjYM1Tb+U9+SrgGw
|
||||
lvZndbuUXa3h/d0irPH90KsdEeAR1SqwzAzvLO2JgvBuimHRceQWcTiiGWDZOyii
|
||||
tux4U+pEzh0Dia40uFozFAoYZPDUpZ+gsiQ3MR3HOrAu2OR0a+CfelDXZNLR3C4z
|
||||
cBKr53ajAbHstnpxKJh9IfeKph44PrbDo6HMe1XPprl7idoEesX3Ma3SBgm4lmId
|
||||
cfEOUWNIYOgtOT5bflXNNyzXOgYVjpLnjwg4EOASO/XIjhvqdjIyCu7HJMIcZ9oo
|
||||
Aob2exeluv5LVwic1UJQDl2McDBIgJ8R32oEN4skCKIBSbWlxWyjnxL5I5WkJF4y
|
||||
vlSx8bKAjVr8map0C3rWISuZdgheJkF96qjwhVSxOv++p6srq6M=
|
||||
-----END CERTIFICATE-----
|
||||
@@ -4,7 +4,7 @@
|
||||
"private": true,
|
||||
"author": "Wells <ww.sun@outlook.com>",
|
||||
"scripts": {
|
||||
"dev": "HOST=local.netease.com PORT=8001 umi dev",
|
||||
"dev": "cross-env HOST=local.netease.com PORT=8001 umi dev",
|
||||
"build": "umi build",
|
||||
"postinstall": "umi setup",
|
||||
"setup": "umi setup",
|
||||
@@ -12,13 +12,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^4.8.0",
|
||||
"@music163/antd": "^0.2.4",
|
||||
"antd": "^4.24.2",
|
||||
"coral-system": "^1.0.5",
|
||||
"umi": "^4.0.89"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.0.33",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"typescript": "^5.0.3"
|
||||
"umi": "^4.2.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import React from 'react';
|
||||
import { Box, Group } from 'coral-system';
|
||||
import { Box, Group, Text } from 'coral-system';
|
||||
import { Avatar, Space, Switch } from 'antd';
|
||||
import { BranchesOutlined, MenuOutlined, QuestionCircleOutlined } from '@ant-design/icons';
|
||||
import { registerSetter } from '@music163/tango-designer';
|
||||
import type { ComponentPrototypeType, IVariableTreeNode } from '@music163/tango-helpers';
|
||||
import type { IVariableTreeNode } from '@music163/tango-helpers';
|
||||
import { FooSetter } from '../components';
|
||||
import { ToggleButton } from '@music163/tango-ui';
|
||||
|
||||
export * from './mock-files';
|
||||
|
||||
@@ -37,113 +38,6 @@ export const bootHelperVariables: IVariableTreeNode[] = [
|
||||
},
|
||||
];
|
||||
|
||||
// folder-name
|
||||
// 物料列表定义
|
||||
const bizToggleButtonPrototype: ComponentPrototypeType = {
|
||||
name: 'CtPcToggleButton',
|
||||
exportType: 'defaultExport',
|
||||
title: '示例业务组件',
|
||||
icon: 'icon-tupian',
|
||||
type: 'element',
|
||||
docs: 'https://redstone.fn.netease.com/mt/fe-comp/w8bq8px7n5/toggle-button',
|
||||
hasChildren: false,
|
||||
props: [
|
||||
{
|
||||
name: 'checked',
|
||||
title: '是否选中',
|
||||
setter: 'boolSetter',
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
name: 'children',
|
||||
title: '文本',
|
||||
setter: 'textSetter',
|
||||
initValue: '按钮',
|
||||
},
|
||||
],
|
||||
package: '@music/ct-pc-toggle-button',
|
||||
};
|
||||
|
||||
const sampleBlockCode = `
|
||||
<Section>
|
||||
<Result
|
||||
status="success"
|
||||
title="Successfully Purchased Cloud Server ECS!"
|
||||
subTitle="Order number: 2017182818828182881 Cloud server configuration takes 1-5 minutes, please wait."
|
||||
extra={[
|
||||
<Button type="primary" key="console">
|
||||
Go Console
|
||||
</Button>,
|
||||
<Button key="buy">Buy Again</Button>,
|
||||
]}
|
||||
/>
|
||||
</Section>
|
||||
`;
|
||||
|
||||
const SnippetSuccessResult: ComponentPrototypeType = {
|
||||
name: 'SnippetSuccessResult',
|
||||
title: '成功结果',
|
||||
icon: 'icon-tupian',
|
||||
type: 'snippet',
|
||||
package: '@music163/antd',
|
||||
initChildren: sampleBlockCode,
|
||||
relatedImports: ['Section', 'Result', 'Button'],
|
||||
};
|
||||
|
||||
const Snippet2ColumnLayout: ComponentPrototypeType = {
|
||||
name: 'Snippet2ColumnLayout',
|
||||
title: '两列布局',
|
||||
icon: 'icon-columns',
|
||||
type: 'snippet',
|
||||
package: '@music163/antd',
|
||||
initChildren: `
|
||||
<Columns columns={12}>
|
||||
<Column colSpan={6}></Column>
|
||||
<Column colSpan={6}></Column>
|
||||
</Columns>
|
||||
`,
|
||||
relatedImports: ['Columns', 'Column'],
|
||||
};
|
||||
|
||||
const Snippet3ColumnLayout: ComponentPrototypeType = {
|
||||
name: 'Snippet3ColumnLayout',
|
||||
title: '三列布局',
|
||||
icon: 'icon-column3',
|
||||
type: 'snippet',
|
||||
package: '@music163/antd',
|
||||
initChildren: `
|
||||
<Columns columns={12}>
|
||||
<Column colSpan={4}></Column>
|
||||
<Column colSpan={4}></Column>
|
||||
<Column colSpan={4}></Column>
|
||||
</Columns>
|
||||
`,
|
||||
relatedImports: ['Columns', 'Column'],
|
||||
};
|
||||
|
||||
const SnippetButtonGroup: ComponentPrototypeType = {
|
||||
name: 'SnippetButtonGroup',
|
||||
title: '按钮组',
|
||||
icon: 'icon-anniuzu',
|
||||
type: 'snippet',
|
||||
package: '@music163/antd',
|
||||
initChildren: `
|
||||
<Space>
|
||||
<Button type="primary">按钮1</Button>
|
||||
<Button>按钮2</Button>
|
||||
</Space>
|
||||
`,
|
||||
relatedImports: ['Space', 'Button'],
|
||||
};
|
||||
|
||||
export const prototypes = {
|
||||
CtPcToggleButton: bizToggleButtonPrototype,
|
||||
SnippetSuccessResult,
|
||||
Snippet2ColumnLayout,
|
||||
Snippet3ColumnLayout,
|
||||
SnippetButtonGroup,
|
||||
};
|
||||
|
||||
// 注册自定义 setter
|
||||
registerSetter({
|
||||
name: 'fooSetter',
|
||||
@@ -153,8 +47,17 @@ registerSetter({
|
||||
// 平台 Logo
|
||||
export function Logo() {
|
||||
return (
|
||||
<Box width="50px" display="flex" alignItems="center" justifyContent="center" fontSize="20px">
|
||||
<MenuOutlined />
|
||||
<Box width="50px" display="flex" alignItems="center" justifyContent="center" fontSize="14px">
|
||||
<ToggleButton
|
||||
shape="text"
|
||||
items={[
|
||||
{ label: '回到首页', key: 'home' },
|
||||
{ label: '文档', key: 'doc' },
|
||||
{ label: '设置', key: 'setting' },
|
||||
]}
|
||||
>
|
||||
<MenuOutlined />
|
||||
</ToggleButton>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -163,12 +66,12 @@ export function Logo() {
|
||||
export function ProjectDetail() {
|
||||
return (
|
||||
<Box display="flex" alignItems="center" columnGap="l">
|
||||
<Box className="ProjectName" fontSize="18px" fontWeight="bold">
|
||||
<Text className="ProjectName" fontSize="18px" fontWeight="bold" truncated>
|
||||
community-test
|
||||
</Box>
|
||||
<Box className="BranchName" as="code" fontSize="14px">
|
||||
</Text>
|
||||
<Text className="BranchName" as="code" fontSize="14px" truncated>
|
||||
<BranchesOutlined /> feature/list
|
||||
</Box>
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
const packageJson = {
|
||||
name: 'demo',
|
||||
private: true,
|
||||
dependencies: {
|
||||
'@music163/tango-mail': '0.2.4',
|
||||
'@music163/tango-boot': '0.2.5',
|
||||
react: '17.0.2',
|
||||
'react-dom': '17.0.2',
|
||||
'prop-types': '15.7.2',
|
||||
tslib: '2.5.0',
|
||||
},
|
||||
};
|
||||
|
||||
const tangoConfigJson = {
|
||||
designerConfig: {
|
||||
autoGenerateComponentId: true,
|
||||
},
|
||||
packages: {
|
||||
react: {
|
||||
version: '17.0.2',
|
||||
library: 'React',
|
||||
type: 'dependency',
|
||||
resources: ['https://unpkg.com/react@{{version}}/umd/react.development.js'],
|
||||
},
|
||||
'react-dom': {
|
||||
version: '17.0.2',
|
||||
library: 'ReactDOM',
|
||||
type: 'dependency',
|
||||
resources: ['https://unpkg.com/react-dom@{{version}}/umd/react-dom.development.js'],
|
||||
},
|
||||
'react-is': {
|
||||
version: '16.13.1',
|
||||
library: 'ReactIs',
|
||||
type: 'dependency',
|
||||
resources: ['https://unpkg.com/react-is@{{version}}/umd/react-is.production.min.js'],
|
||||
},
|
||||
'@music163/tango-boot': {
|
||||
description: '云音乐低代码运行时框架',
|
||||
version: '0.2.5',
|
||||
library: 'TangoBoot',
|
||||
type: 'baseDependency',
|
||||
resources: ['https://unpkg.com/@music163/tango-boot@{{version}}/dist/boot.js'],
|
||||
// resources: ['http://localhost:9001/boot.js'],
|
||||
},
|
||||
'@music163/tango-mail': {
|
||||
description: 'TangoMail 基础物料',
|
||||
version: '0.2.4',
|
||||
library: 'TangoMail',
|
||||
type: 'baseDependency',
|
||||
resources: ['https://unpkg.com/@music163/tango-mail@{{version}}/dist/index.js'],
|
||||
designerResources: ['https://unpkg.com/@music163/tango-mail@{{version}}/dist/designer.js'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const routesCode = `
|
||||
import Mail from "./pages/mail";
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
exact: true,
|
||||
component: Mail,
|
||||
},
|
||||
];
|
||||
|
||||
export default routes;
|
||||
`;
|
||||
|
||||
const entryCode = `
|
||||
import { runApp } from '@music163/tango-boot';
|
||||
import routes from './routes';
|
||||
|
||||
runApp({
|
||||
boot: {
|
||||
mountElement: document.querySelector('#root'),
|
||||
qiankun: false,
|
||||
},
|
||||
|
||||
router: {
|
||||
type: 'browser',
|
||||
config: routes,
|
||||
},
|
||||
});
|
||||
`;
|
||||
|
||||
const viewHomeCode = `
|
||||
import React from 'react';
|
||||
import {
|
||||
Body,
|
||||
Button,
|
||||
Container,
|
||||
Head,
|
||||
Hr,
|
||||
Html,
|
||||
Preview,
|
||||
Section,
|
||||
Text,
|
||||
} from '@music163/tango-mail';
|
||||
|
||||
const WelcomeEmail = () => (
|
||||
<Html>
|
||||
<Head />
|
||||
<Preview>The sales intelligence platform that helps you uncover qualified leads.</Preview>
|
||||
<Body style={main}>
|
||||
<Container style={container}>
|
||||
<Text style={paragraph}>Hi wells,</Text>
|
||||
<Text style={paragraph}>
|
||||
Welcome to Koala, the sales intelligence platform that helps you uncover qualified leads
|
||||
and close deals faster.
|
||||
</Text>
|
||||
<Section style={btnContainer}>
|
||||
<Button style={button} href="https://getkoala.com">
|
||||
Get started
|
||||
</Button>
|
||||
</Section>
|
||||
<Text style={paragraph}>
|
||||
Best,
|
||||
The Koala team
|
||||
</Text>
|
||||
<Hr style={hr} />
|
||||
<Text style={footer}>470 Noor Ave STE B #1148, South San Francisco, CA 94080</Text>
|
||||
</Container>
|
||||
</Body>
|
||||
</Html>
|
||||
);
|
||||
|
||||
const main = {
|
||||
backgroundColor: '#ffffff',
|
||||
fontFamily:
|
||||
'-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif',
|
||||
};
|
||||
|
||||
const container = {
|
||||
margin: '0 auto',
|
||||
padding: '20px 0 48px',
|
||||
};
|
||||
|
||||
const paragraph = {
|
||||
fontSize: '16px',
|
||||
lineHeight: '26px',
|
||||
};
|
||||
|
||||
const btnContainer = {
|
||||
textAlign: 'center',
|
||||
};
|
||||
|
||||
const button = {
|
||||
backgroundColor: '#5F51E8',
|
||||
borderRadius: '3px',
|
||||
color: '#fff',
|
||||
fontSize: '16px',
|
||||
textDecoration: 'none',
|
||||
textAlign: 'center',
|
||||
display: 'block',
|
||||
padding: '12px',
|
||||
};
|
||||
|
||||
const hr = {
|
||||
borderColor: '#cccccc',
|
||||
margin: '20px 0',
|
||||
};
|
||||
|
||||
const footer = {
|
||||
color: '#8898aa',
|
||||
fontSize: '12px',
|
||||
};
|
||||
|
||||
export default WelcomeEmail;
|
||||
`;
|
||||
|
||||
export const mailFiles = [
|
||||
{ filename: '/package.json', code: JSON.stringify(packageJson) },
|
||||
{ filename: '/tango.config.json', code: JSON.stringify(tangoConfigJson) },
|
||||
{ filename: '/README.md', code: '# readme' },
|
||||
{ filename: '/src/index.js', code: entryCode },
|
||||
{ filename: '/src/pages/mail.js', code: viewHomeCode },
|
||||
{ filename: '/src/routes.js', code: routesCode },
|
||||
];
|
||||
@@ -2,8 +2,8 @@ const packageJson = {
|
||||
name: 'demo',
|
||||
private: true,
|
||||
dependencies: {
|
||||
'@music163/antd': '0.2.1',
|
||||
'@music163/tango-boot': '0.2.5',
|
||||
'@music163/antd': '0.2.5',
|
||||
'@music163/tango-boot': '0.3.5',
|
||||
react: '17.0.2',
|
||||
'react-dom': '17.0.2',
|
||||
'prop-types': '15.7.2',
|
||||
@@ -48,7 +48,7 @@ const tangoConfigJson = {
|
||||
},
|
||||
'@music163/tango-boot': {
|
||||
description: '云音乐低代码运行时框架',
|
||||
version: '0.2.5',
|
||||
version: '0.3.5',
|
||||
library: 'TangoBoot',
|
||||
type: 'baseDependency',
|
||||
resources: ['https://unpkg.com/@music163/tango-boot@{{version}}/dist/boot.js'],
|
||||
@@ -56,11 +56,12 @@ const tangoConfigJson = {
|
||||
},
|
||||
'@music163/antd': {
|
||||
description: '云音乐低代码中后台应用基础物料',
|
||||
version: '0.2.1',
|
||||
version: '0.2.6',
|
||||
library: 'TangoAntd',
|
||||
type: 'baseDependency',
|
||||
resources: [
|
||||
'https://unpkg.com/@music163/antd@{{version}}/dist/index.js',
|
||||
// 'http://localhost:9002/designer.js',
|
||||
'https://unpkg.com/antd@4.24.13/dist/antd.css',
|
||||
],
|
||||
designerResources: [
|
||||
@@ -85,6 +86,7 @@ export function registerComponentPrototype(proto) {
|
||||
|
||||
const routesCode = `
|
||||
import Index from "./pages/list";
|
||||
import Detail from "./pages/detail";
|
||||
|
||||
const routes = [
|
||||
{
|
||||
@@ -92,6 +94,11 @@ const routes = [
|
||||
exact: true,
|
||||
component: Index
|
||||
},
|
||||
{
|
||||
path: '/detail',
|
||||
exact: true,
|
||||
component: Detail
|
||||
},
|
||||
];
|
||||
|
||||
export default routes;
|
||||
@@ -146,31 +153,126 @@ import { definePage } from "@music163/tango-boot";
|
||||
import {
|
||||
Page,
|
||||
Section,
|
||||
Box,
|
||||
Button,
|
||||
Input,
|
||||
FormilyForm,
|
||||
FormilyFormItem,
|
||||
Table,
|
||||
} from "@music163/antd";
|
||||
import { Space } from '@music163/antd';
|
||||
import { LocalButton } from '../components';
|
||||
import { Space } from "@music163/antd";
|
||||
import { LocalButton } from "../components";
|
||||
import { OutButton } from "../components";
|
||||
|
||||
class App extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<Page title={tango.stores.app.title}>
|
||||
<Section tid="section1" title="Section Title">
|
||||
your input: <Input tid="input1" defaultValue="hello" />
|
||||
copy input: <Input value={tango.page.input1?.value} />
|
||||
</Section>
|
||||
<Section tid="section2">
|
||||
<Space tid="space1">
|
||||
<LocalButton />
|
||||
<Button tid="button1">button</Button>
|
||||
<Page title={tango.stores.app.title} subTitle={<><Button>hello</Button></>}>
|
||||
<Section tid="section0">
|
||||
<Box></Box>
|
||||
</Section>
|
||||
<Section tid="section1" title="Section Title">
|
||||
your input: <Input tid="input1" defaultValue="hello" />
|
||||
copy input: <Input value={tango.page.input1?.value} />
|
||||
<Table
|
||||
columns={[
|
||||
{ title: "姓名", dataIndex: "name", key: "name" },
|
||||
{ title: "年龄", dataIndex: "age", key: "age" },
|
||||
{ title: "住址", dataIndex: "address", key: "address" },
|
||||
]}
|
||||
tid="table1"
|
||||
/>
|
||||
</Section>
|
||||
<Section tid="section2">
|
||||
<Space tid="space1">
|
||||
<LocalButton />
|
||||
<OutButton />
|
||||
<Button tid="button1">button</Button>
|
||||
</Space>
|
||||
</Section>
|
||||
<Section title="区块标题" tid="section3">
|
||||
<FormilyForm tid="formilyForm1">
|
||||
<FormilyFormItem name="input1" component="Input" label="表单项" />
|
||||
<FormilyFormItem name="select1" component="Select" label="表单项" />
|
||||
</FormilyForm>
|
||||
</Section>
|
||||
<Section title="原生 DOM" tid="section4">
|
||||
<h1 style={{ ...{ color: "red" }, fontSize: 64 }}>
|
||||
hello world
|
||||
</h1>
|
||||
<div
|
||||
style={{
|
||||
border: "1px solid #ccc",
|
||||
borderRadius: "5px",
|
||||
backgroundColor: "#f4f4f4",
|
||||
}}
|
||||
>
|
||||
<form onSubmit={tango.stores.app.submitForm} style={{
|
||||
padding: "10px",
|
||||
}}>
|
||||
<div>
|
||||
<label>Username:</label>
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
name="username"
|
||||
style={{ margin: "5px" }}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label>Password:</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
style={{ margin: "5px" }}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label>Role:</label>
|
||||
<select id="role" name="role" style={{ margin: "5px" }}>
|
||||
<option value="admin">Admin</option>
|
||||
<option value="user">User</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
type="submit"
|
||||
style={{
|
||||
marginTop: '5px',
|
||||
padding: "3px 10px",
|
||||
backgroundColor: "#007bff",
|
||||
color: "#fff",
|
||||
border: "none",
|
||||
borderRadius: "5px",
|
||||
}}
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Section>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
}
|
||||
export default definePage(App);
|
||||
`;
|
||||
|
||||
export const emptyPageCode = `
|
||||
import React from "react";
|
||||
import { definePage } from "@music163/tango-boot";
|
||||
import {
|
||||
Page,
|
||||
Section,
|
||||
} from "@music163/antd";
|
||||
|
||||
function App() {
|
||||
return (<Page title="Detail Page">
|
||||
<Section></Section>
|
||||
</Page>)
|
||||
}
|
||||
|
||||
export default definePage(App);
|
||||
`;
|
||||
@@ -194,6 +296,14 @@ registerComponentPrototype({
|
||||
});
|
||||
`;
|
||||
|
||||
const outButtonCode = `
|
||||
import React from 'react';
|
||||
|
||||
export default function OutButton(props) {
|
||||
return <button {...props}>Out button (from other file)</button>
|
||||
}
|
||||
`;
|
||||
|
||||
const componentsInputCode = `
|
||||
import React from 'react';
|
||||
import { registerComponentPrototype } from '../utils';
|
||||
@@ -216,44 +326,28 @@ registerComponentPrototype({
|
||||
const componentsEntryCode = `
|
||||
export { default as LocalButton } from './button';
|
||||
export { default as LocalInput } from './input';
|
||||
export { default as OutButton } from './out-button';
|
||||
`;
|
||||
|
||||
const storeApp = `
|
||||
import { defineStore } from '@music163/tango-boot';
|
||||
|
||||
export default defineStore({
|
||||
|
||||
title: 'Page Title',
|
||||
|
||||
array: [1, 2, 3],
|
||||
|
||||
test: function() {
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
console.log('test');
|
||||
},
|
||||
|
||||
submitForm: function(event) {
|
||||
event.preventDefault();
|
||||
const formData = new FormData(event.target);
|
||||
const username = formData.get("username");
|
||||
const password = formData.get("password");
|
||||
const role = formData.get("role");
|
||||
console.log("Submitted Data:", { username, password, role });
|
||||
}
|
||||
}, 'app');
|
||||
`;
|
||||
@@ -331,7 +425,9 @@ export const sampleFiles = [
|
||||
{ filename: '/src/style.css', code: cssCode },
|
||||
{ filename: '/src/index.js', code: entryCode },
|
||||
{ filename: '/src/pages/list.js', code: viewHomeCode },
|
||||
{ filename: '/src/pages/detail.js', code: emptyPageCode },
|
||||
{ filename: '/src/components/button.js', code: componentsButtonCode },
|
||||
{ filename: '/src/components/out-button.js', code: outButtonCode },
|
||||
{ filename: '/src/components/input.js', code: componentsInputCode },
|
||||
{ filename: '/src/components/index.js', code: componentsEntryCode },
|
||||
{ filename: '/src/routes.js', code: routesCode },
|
||||
|
||||
@@ -0,0 +1,249 @@
|
||||
import * as basePrototypes from '@music163/antd/prototypes';
|
||||
import type { IComponentPrototype, Dict } from '@music163/tango-helpers';
|
||||
|
||||
const sampleBlockCode = `
|
||||
<Section>
|
||||
<Result
|
||||
status="success"
|
||||
title="Successfully Purchased Cloud Server ECS!"
|
||||
subTitle="Order number: 2017182818828182881 Cloud server configuration takes 1-5 minutes, please wait."
|
||||
extra={[
|
||||
<Button type="primary" key="console">
|
||||
Go Console
|
||||
</Button>,
|
||||
<Button key="buy">Buy Again</Button>,
|
||||
]}
|
||||
/>
|
||||
</Section>
|
||||
`;
|
||||
|
||||
const SnippetSuccessResult: IComponentPrototype = {
|
||||
name: 'SnippetSuccessResult',
|
||||
title: '成功结果',
|
||||
icon: 'icon-tupian',
|
||||
type: 'snippet',
|
||||
package: '@music163/antd',
|
||||
initChildren: sampleBlockCode,
|
||||
relatedImports: ['Section', 'Result', 'Button'],
|
||||
};
|
||||
|
||||
const Snippet2ColumnLayout: IComponentPrototype = {
|
||||
name: 'Snippet2ColumnLayout',
|
||||
title: '两列布局',
|
||||
icon: 'icon-columns',
|
||||
type: 'snippet',
|
||||
package: '@music163/antd',
|
||||
initChildren: `
|
||||
<Columns columns={12}>
|
||||
<Column colSpan={6}></Column>
|
||||
<Column colSpan={6}></Column>
|
||||
</Columns>
|
||||
`,
|
||||
relatedImports: ['Columns', 'Column'],
|
||||
};
|
||||
|
||||
const Snippet3ColumnLayout: IComponentPrototype = {
|
||||
name: 'Snippet3ColumnLayout',
|
||||
title: '三列布局',
|
||||
icon: 'icon-column-3',
|
||||
type: 'snippet',
|
||||
package: '@music163/antd',
|
||||
// FIXME: Column 组件 需要更新为 defineComponent
|
||||
initChildren: `
|
||||
<Columns columns={12}>
|
||||
<Column colSpan={4}>
|
||||
</Column>
|
||||
<Column colSpan={4}>
|
||||
</Column>
|
||||
<Column colSpan={4}>
|
||||
</Column>
|
||||
</Columns>
|
||||
`,
|
||||
relatedImports: ['Columns', 'Column'],
|
||||
};
|
||||
|
||||
const SnippetButtonGroup: IComponentPrototype = {
|
||||
name: 'SnippetButtonGroup',
|
||||
title: '按钮组',
|
||||
icon: 'icon-anniuzu',
|
||||
type: 'snippet',
|
||||
package: '@music163/antd',
|
||||
initChildren: `
|
||||
<Space>
|
||||
<Button type="primary">按钮1</Button>
|
||||
<Button>按钮2</Button>
|
||||
</Space>
|
||||
`,
|
||||
relatedImports: ['Space', 'Button'],
|
||||
};
|
||||
|
||||
// hack some prototypes
|
||||
basePrototypes['Section'].siblingNames = [
|
||||
'SnippetButtonGroup',
|
||||
'Snippet2ColumnLayout',
|
||||
'Snippet3ColumnLayout',
|
||||
'SnippetSuccessResult',
|
||||
];
|
||||
|
||||
export const nativeDomPrototypes = () => {
|
||||
const doms = [
|
||||
'div',
|
||||
'span',
|
||||
'h1',
|
||||
'h2',
|
||||
'p',
|
||||
'a',
|
||||
'img',
|
||||
'ul',
|
||||
'ol',
|
||||
'li',
|
||||
'input',
|
||||
'button',
|
||||
'form',
|
||||
'table',
|
||||
'tr',
|
||||
'td',
|
||||
'header',
|
||||
'footer',
|
||||
'nav',
|
||||
'section',
|
||||
'article',
|
||||
'aside',
|
||||
'main',
|
||||
'video',
|
||||
'audio',
|
||||
'label',
|
||||
'select',
|
||||
'option',
|
||||
'textarea',
|
||||
'iframe',
|
||||
];
|
||||
const componentPrototypes: Dict<IComponentPrototype> = doms.reduce(
|
||||
(acc: Dict<IComponentPrototype>, tag) => {
|
||||
acc[tag] = {
|
||||
name: tag,
|
||||
title: tag,
|
||||
hasChildren: true,
|
||||
package: '',
|
||||
type: 'element',
|
||||
props: [
|
||||
{
|
||||
name: 'style',
|
||||
title: '样式',
|
||||
group: 'style',
|
||||
setter: 'expressionSetter',
|
||||
setterProps: {
|
||||
expressionType: 'cssObject',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'className',
|
||||
title: '类名',
|
||||
setter: 'textSetter',
|
||||
},
|
||||
{
|
||||
name: 'id',
|
||||
title: 'ID',
|
||||
setter: 'textSetter',
|
||||
},
|
||||
{
|
||||
name: 'onClick',
|
||||
title: '点击事件',
|
||||
setter: 'actionSetter',
|
||||
group: 'event',
|
||||
},
|
||||
],
|
||||
};
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
return componentPrototypes;
|
||||
};
|
||||
|
||||
// iconfont: https://www.iconfont.cn/manage/index?manage_type=myprojects&projectId=2891794
|
||||
const prototypes: Dict<IComponentPrototype> = {
|
||||
...nativeDomPrototypes(),
|
||||
...(basePrototypes as any),
|
||||
SnippetSuccessResult,
|
||||
Snippet2ColumnLayout,
|
||||
Snippet3ColumnLayout,
|
||||
SnippetButtonGroup,
|
||||
Box: {
|
||||
name: 'Box',
|
||||
title: '盒子',
|
||||
icon: 'icon-mianban',
|
||||
type: 'container',
|
||||
package: '@music163/antd',
|
||||
hasChildren: true,
|
||||
siblingNames: ['Box'],
|
||||
props: [
|
||||
{
|
||||
name: 'aaa',
|
||||
title: 'aaa',
|
||||
setter: 'textSetter',
|
||||
},
|
||||
{
|
||||
name: 'bbb',
|
||||
title: 'bbb',
|
||||
setter: 'textSetter',
|
||||
deprecated: true,
|
||||
},
|
||||
{
|
||||
name: 'ccc',
|
||||
title: 'ccc',
|
||||
setter: 'textSetter',
|
||||
deprecated: true,
|
||||
},
|
||||
{
|
||||
name: 'd',
|
||||
title: 'd',
|
||||
setter: 'textSetter',
|
||||
},
|
||||
{
|
||||
name: 'onClick',
|
||||
title: '点击事件',
|
||||
setter: 'eventSetter',
|
||||
template: '(e) => {\n {{content}}\n}',
|
||||
tip: '回调参数说明:e 为事件对象',
|
||||
},
|
||||
],
|
||||
},
|
||||
Columns: {
|
||||
name: 'Columns',
|
||||
type: 'container',
|
||||
icon: 'icon-column-4',
|
||||
package: '@music163/antd',
|
||||
hasChildren: true,
|
||||
childrenNames: ['Column'],
|
||||
},
|
||||
Column: {
|
||||
name: 'Column',
|
||||
type: 'container',
|
||||
icon: 'icon-juxing',
|
||||
package: '@music163/antd',
|
||||
hasChildren: true,
|
||||
siblingNames: ['Column'],
|
||||
},
|
||||
Text: {
|
||||
name: 'Text',
|
||||
type: 'element',
|
||||
icon: 'icon-wenzi',
|
||||
package: '@music163/antd',
|
||||
initChildren: '文本内容',
|
||||
},
|
||||
Placeholder: {
|
||||
name: 'Placeholder',
|
||||
type: 'element',
|
||||
package: '@music163/antd',
|
||||
},
|
||||
ButtonGroup: {
|
||||
name: 'ButtonGroup',
|
||||
type: 'element',
|
||||
package: '@music163/antd',
|
||||
hasChildren: true,
|
||||
childrenNames: ['Button'],
|
||||
},
|
||||
};
|
||||
|
||||
export default prototypes;
|
||||
@@ -3,9 +3,5 @@ import { Outlet } from 'umi';
|
||||
import './index.less';
|
||||
|
||||
export default function Layout() {
|
||||
return (
|
||||
<div>
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
return <Outlet />;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Box } from 'coral-system';
|
||||
import { Button, Space } from 'antd';
|
||||
import { Button, Form, Input, Modal, Space } from 'antd';
|
||||
import {
|
||||
Designer,
|
||||
DesignerPanel,
|
||||
@@ -15,25 +14,68 @@ import {
|
||||
themeLight,
|
||||
} from '@music163/tango-designer';
|
||||
import { createEngine, Workspace } from '@music163/tango-core';
|
||||
import { Logo, ProjectDetail, bootHelperVariables, sampleFiles } from '../helpers';
|
||||
import prototypes from '../helpers/prototypes';
|
||||
import { Logo, ProjectDetail, bootHelperVariables, emptyPageCode, sampleFiles } from '../helpers';
|
||||
import {
|
||||
ApiOutlined,
|
||||
AppstoreAddOutlined,
|
||||
BuildOutlined,
|
||||
ClusterOutlined,
|
||||
FunctionOutlined,
|
||||
PlusOutlined,
|
||||
createFromIconfontCN,
|
||||
} from '@ant-design/icons';
|
||||
import { Action, PackageOutlined } from '@music163/tango-ui';
|
||||
import { useState } from 'react';
|
||||
|
||||
const menuData = {
|
||||
common: [
|
||||
{
|
||||
title: '常用',
|
||||
items: [
|
||||
'Button',
|
||||
'Section',
|
||||
'Columns',
|
||||
'Column',
|
||||
'Box',
|
||||
'Text',
|
||||
'Space',
|
||||
'Typography',
|
||||
'Title',
|
||||
'Paragraph',
|
||||
'Table',
|
||||
'Each',
|
||||
],
|
||||
},
|
||||
{
|
||||
title: '输入',
|
||||
items: ['Input', 'InputNumber', 'Select'],
|
||||
},
|
||||
{
|
||||
title: 'Formily表单',
|
||||
items: ['FormilyForm', 'FormilyFormItem', 'FormilySubmit', 'FormilyReset'],
|
||||
},
|
||||
{
|
||||
title: '数据展示',
|
||||
items: ['Comment'],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// 1. 实例化工作区
|
||||
const workspace = new Workspace({
|
||||
entry: '/src/index.js',
|
||||
files: sampleFiles,
|
||||
prototypes,
|
||||
});
|
||||
|
||||
// inject workspace to window for debug
|
||||
(window as any).__workspace__ = workspace;
|
||||
|
||||
// 2. 引擎初始化
|
||||
const engine = createEngine({
|
||||
workspace,
|
||||
menuData,
|
||||
defaultActiveView: 'design', // dual code design
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
@@ -46,15 +88,15 @@ const sandboxQuery = new DndQuery({
|
||||
|
||||
// 4. 图标库初始化(物料面板和组件树使用了 iconfont 里的图标)
|
||||
createFromIconfontCN({
|
||||
scriptUrl: '//at.alicdn.com/t/c/font_2891794_lzc7rtwuzf.js',
|
||||
scriptUrl: '//at.alicdn.com/t/c/font_2891794_151xsllxqd7.js',
|
||||
});
|
||||
|
||||
/**
|
||||
* 5. 平台初始化,访问 https://local.netease.com:6006/
|
||||
*/
|
||||
export default function App() {
|
||||
const [menuLoading, setMenuLoading] = useState(true);
|
||||
const [menuData, setMenuData] = useState(false);
|
||||
const [showNewPageModal, setShowNewPageModal] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
return (
|
||||
<Designer
|
||||
theme={themeLight}
|
||||
@@ -71,11 +113,19 @@ export default function App() {
|
||||
actions={
|
||||
<Box px="l">
|
||||
<Toolbar>
|
||||
<Toolbar.Item key="routeSwitch" placement="left" />
|
||||
<Toolbar.Item key="history" placement="left" />
|
||||
<Toolbar.Item key="preview" placement="left" />
|
||||
<Toolbar.Item key="routeSwitch" placement="left" activeViews={['design']} />
|
||||
<Toolbar.Item key="addPage" placement="left" activeViews={['design']}>
|
||||
<Action
|
||||
tooltip="添加页面"
|
||||
shape="outline"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => setShowNewPageModal(true)}
|
||||
/>
|
||||
</Toolbar.Item>
|
||||
<Toolbar.Item key="history" placement="left" activeViews={['design']} />
|
||||
<Toolbar.Item key="preview" placement="left" activeViews={['design']} />
|
||||
<Toolbar.Item key="togglePanel" placement="right" activeViews={['design']} />
|
||||
<Toolbar.Item key="modeSwitch" placement="right" />
|
||||
<Toolbar.Item key="togglePanel" placement="right" />
|
||||
<Toolbar.Separator />
|
||||
<Toolbar.Item placement="right">
|
||||
<Space>
|
||||
@@ -83,6 +133,30 @@ export default function App() {
|
||||
</Space>
|
||||
</Toolbar.Item>
|
||||
</Toolbar>
|
||||
<Modal
|
||||
title="添加新页面"
|
||||
open={showNewPageModal}
|
||||
onCancel={() => setShowNewPageModal(false)}
|
||||
footer={null}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={(values) => {
|
||||
workspace.addViewFile(values.name, emptyPageCode);
|
||||
setShowNewPageModal(false);
|
||||
}}
|
||||
layout="vertical"
|
||||
>
|
||||
<Form.Item label="文件名" name="name" required rules={[{ required: true }]}>
|
||||
<Input placeholder="请输入文件名" />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit">
|
||||
提交
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
@@ -92,8 +166,7 @@ export default function App() {
|
||||
label="组件"
|
||||
icon={<AppstoreAddOutlined />}
|
||||
widgetProps={{
|
||||
menuData: menuData as any,
|
||||
loading: menuLoading,
|
||||
menuData,
|
||||
}}
|
||||
/>
|
||||
<Sidebar.Item key="outline" label="结构" icon={<BuildOutlined />} />
|
||||
@@ -108,37 +181,36 @@ export default function App() {
|
||||
<Sidebar.Item
|
||||
key="dependency"
|
||||
label="依赖"
|
||||
icon={<ClusterOutlined />}
|
||||
icon={<PackageOutlined />}
|
||||
isFloat
|
||||
width={800}
|
||||
/>
|
||||
</Sidebar>
|
||||
<WorkspacePanel>
|
||||
<WorkspaceView mode="code">
|
||||
<CodeEditor />
|
||||
</WorkspaceView>
|
||||
<WorkspaceView mode="design">
|
||||
<Sandbox
|
||||
onMessage={(e) => {
|
||||
if (e.type === 'done') {
|
||||
const sandboxWindow: any = sandboxQuery.window;
|
||||
if (sandboxWindow.TangoAntd) {
|
||||
if (sandboxWindow.TangoAntd.menuData) {
|
||||
setMenuData(sandboxWindow.TangoAntd.menuData);
|
||||
}
|
||||
if (sandboxWindow.TangoAntd.prototypes) {
|
||||
workspace.setComponentPrototypes(sandboxWindow.TangoAntd.prototypes);
|
||||
}
|
||||
}
|
||||
// if (sandboxWindow.TangoAntd) {
|
||||
// if (sandboxWindow.TangoAntd.menuData) {
|
||||
// setMenuData(sandboxWindow.TangoAntd.menuData);
|
||||
// }
|
||||
// if (sandboxWindow.TangoAntd.prototypes) {
|
||||
// workspace.setComponentPrototypes(sandboxWindow.TangoAntd.prototypes);
|
||||
// }
|
||||
// }
|
||||
if (sandboxWindow.localTangoComponentPrototypes) {
|
||||
workspace.setComponentPrototypes(sandboxWindow.localTangoComponentPrototypes);
|
||||
}
|
||||
setMenuLoading(false);
|
||||
}
|
||||
}}
|
||||
navigatorExtra={<Button size="small">hello world</Button>}
|
||||
/>
|
||||
</WorkspaceView>
|
||||
<WorkspaceView mode="code">
|
||||
<CodeEditor />
|
||||
</WorkspaceView>
|
||||
</WorkspacePanel>
|
||||
<SettingPanel />
|
||||
</DesignerPanel>
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Box } from 'coral-system';
|
||||
import { Button, Space } from 'antd';
|
||||
import {
|
||||
Designer,
|
||||
DesignerPanel,
|
||||
SettingPanel,
|
||||
Sidebar,
|
||||
Toolbar,
|
||||
WorkspacePanel,
|
||||
WorkspaceView,
|
||||
CodeEditor,
|
||||
Sandbox,
|
||||
DndQuery,
|
||||
themeLight,
|
||||
} from '@music163/tango-designer';
|
||||
import { createEngine, Workspace } from '@music163/tango-core';
|
||||
import { Logo, ProjectDetail, bootHelperVariables } from '@/helpers';
|
||||
import {
|
||||
AppstoreAddOutlined,
|
||||
BuildOutlined,
|
||||
ClusterOutlined,
|
||||
createFromIconfontCN,
|
||||
} from '@ant-design/icons';
|
||||
import { mailFiles } from '@/helpers/mail-files';
|
||||
|
||||
// 1. 实例化工作区
|
||||
const workspace = new Workspace({
|
||||
entry: '/src/index.js',
|
||||
files: mailFiles,
|
||||
});
|
||||
|
||||
// 2. 引擎初始化
|
||||
const engine = createEngine({
|
||||
workspace,
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
window.__mailWorkspace__ = workspace;
|
||||
|
||||
// 3. 沙箱初始化
|
||||
const sandboxQuery = new DndQuery({
|
||||
context: 'iframe',
|
||||
});
|
||||
|
||||
// 4. 图标库初始化(物料面板和组件树使用了 iconfont 里的图标)
|
||||
createFromIconfontCN({
|
||||
scriptUrl: '//at.alicdn.com/t/c/font_2891794_151xsllxqd7.js',
|
||||
});
|
||||
|
||||
/**
|
||||
* 5. 平台初始化,访问 https://local.netease.com:6006/
|
||||
*/
|
||||
export default function App() {
|
||||
const [menuLoading, setMenuLoading] = useState(true);
|
||||
const [menuData, setMenuData] = useState(false);
|
||||
return (
|
||||
<Designer
|
||||
theme={themeLight}
|
||||
engine={engine}
|
||||
config={{
|
||||
customActionVariables: bootHelperVariables,
|
||||
customExpressionVariables: bootHelperVariables,
|
||||
}}
|
||||
sandboxQuery={sandboxQuery}
|
||||
>
|
||||
<DesignerPanel
|
||||
logo={<Logo />}
|
||||
description={<ProjectDetail />}
|
||||
actions={
|
||||
<Box px="l">
|
||||
<Toolbar>
|
||||
<Toolbar.Item key="history" placement="left" />
|
||||
<Toolbar.Item key="preview" placement="left" />
|
||||
<Toolbar.Item key="modeSwitch" placement="right" />
|
||||
<Toolbar.Item key="togglePanel" placement="right" />
|
||||
<Toolbar.Separator />
|
||||
<Toolbar.Item placement="right">
|
||||
<Space>
|
||||
<Button type="primary">发布</Button>
|
||||
</Space>
|
||||
</Toolbar.Item>
|
||||
</Toolbar>
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<Sidebar>
|
||||
<Sidebar.Item
|
||||
key="components"
|
||||
label="组件"
|
||||
icon={<AppstoreAddOutlined />}
|
||||
widgetProps={{
|
||||
menuData: menuData as any,
|
||||
loading: menuLoading,
|
||||
}}
|
||||
/>
|
||||
<Sidebar.Item key="outline" label="结构" icon={<BuildOutlined />} />
|
||||
<Sidebar.Item
|
||||
key="dependency"
|
||||
label="依赖"
|
||||
icon={<ClusterOutlined />}
|
||||
isFloat
|
||||
width={800}
|
||||
/>
|
||||
</Sidebar>
|
||||
<WorkspacePanel>
|
||||
<WorkspaceView mode="design">
|
||||
<Sandbox
|
||||
onMessage={(e) => {
|
||||
if (e.type === 'done') {
|
||||
const sandboxWindow: any = sandboxQuery.window;
|
||||
if (sandboxWindow.TangoMail) {
|
||||
if (sandboxWindow.TangoMail.menuData) {
|
||||
setMenuData(sandboxWindow.TangoMail.menuData);
|
||||
engine.designer.setMenuData(sandboxWindow.TangoMail.menuData);
|
||||
}
|
||||
if (sandboxWindow.TangoMail.prototypes) {
|
||||
workspace.setComponentPrototypes(sandboxWindow.TangoMail.prototypes);
|
||||
}
|
||||
}
|
||||
setMenuLoading(false);
|
||||
}
|
||||
}}
|
||||
navigatorExtra={<Button size="small">hello world</Button>}
|
||||
/>
|
||||
</WorkspaceView>
|
||||
<WorkspaceView mode="code">
|
||||
<CodeEditor />
|
||||
</WorkspaceView>
|
||||
</WorkspacePanel>
|
||||
<SettingPanel />
|
||||
</DesignerPanel>
|
||||
</Designer>
|
||||
);
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { SystemProvider } from 'coral-system';
|
||||
import 'antd/dist/antd.css';
|
||||
|
||||
export const parameters = {
|
||||
actions: { argTypesRegex: '^on[A-Z].*' },
|
||||
actions: { argTypesRegex: '^on.*' },
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
"dependencies": {
|
||||
"@music163/tango-setting-form": "*",
|
||||
"@music163/tango-ui": "*",
|
||||
"mobx": "6.12.0",
|
||||
"mobx-react-lite": "4.0.5"
|
||||
"mobx": "6.12.3",
|
||||
"mobx-react-lite": "4.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ant-design/icons": "^4.8.0",
|
||||
|
||||
@@ -1,158 +1,419 @@
|
||||
import React from 'react';
|
||||
import { FormModel, SettingForm, register } from '@music163/tango-setting-form';
|
||||
import { ComponentPrototypeType } from '@music163/tango-helpers';
|
||||
import { BorderSetter } from '@music163/tango-designer/src/setters/style-setter';
|
||||
import { IComponentPrototype } from '@music163/tango-helpers';
|
||||
import { BUILT_IN_SETTERS } from '@music163/tango-designer/src/setters';
|
||||
import { Box } from 'coral-system';
|
||||
import { JsonView } from '@music163/tango-ui';
|
||||
import { toJS } from 'mobx';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { Card } from 'antd';
|
||||
import { createFromIconfontCN } from '@ant-design/icons';
|
||||
|
||||
register({
|
||||
name: 'borderSetter',
|
||||
component: BorderSetter,
|
||||
const BLACK_LIST = ['codeSetter', 'eventSetter', 'modelSetter', 'routerSetter'];
|
||||
|
||||
BUILT_IN_SETTERS.filter((setter) => !BLACK_LIST.includes(setter.name)).forEach(register);
|
||||
|
||||
createFromIconfontCN({
|
||||
scriptUrl: '//at.alicdn.com/t/c/font_2891794_151xsllxqd7.js',
|
||||
});
|
||||
|
||||
export default {
|
||||
title: 'SettingForm',
|
||||
};
|
||||
|
||||
const prototype: ComponentPrototypeType = {
|
||||
name: 'Test',
|
||||
exportType: 'namedExport',
|
||||
title: '测试',
|
||||
icon: 'icon-test',
|
||||
/**
|
||||
* 表单值预览
|
||||
*/
|
||||
const FormValuePreview = observer(({ model }: { model: FormModel }) => {
|
||||
const data = toJS(model.values);
|
||||
return <JsonView src={data} />;
|
||||
});
|
||||
|
||||
interface SettingFormDemoProps {
|
||||
initValues?: object;
|
||||
prototype?: IComponentPrototype;
|
||||
}
|
||||
|
||||
function SettingFormDemo({ initValues, prototype }: SettingFormDemoProps) {
|
||||
const model = new FormModel(initValues, { onChange: console.log });
|
||||
return (
|
||||
<Box display="flex">
|
||||
<Box flex="0 0 320px" overflow="hidden">
|
||||
<SettingForm
|
||||
model={model}
|
||||
prototype={prototype}
|
||||
showIdentifier={{
|
||||
identifierKey: 'tid',
|
||||
getIdentifier: () => `time${Date.now()}`,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Box position="relative">
|
||||
<Card title="表单状态预览" style={{ position: 'sticky', top: 0 }}>
|
||||
<FormValuePreview model={model} />
|
||||
</Card>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const prototypeHasBasicProps: IComponentPrototype = {
|
||||
name: 'Sample',
|
||||
title: '演示组件',
|
||||
package: 'sample-pkg',
|
||||
type: 'element',
|
||||
category: 'basic',
|
||||
package: '@music163/antd',
|
||||
hasChildren: false,
|
||||
docs: 'https://4x-ant-design.antgroup.com/components/slider-cn',
|
||||
props: [
|
||||
{
|
||||
name: 'code',
|
||||
title: 'codeSetter',
|
||||
setter: 'codeSetter',
|
||||
},
|
||||
{
|
||||
name: 'style',
|
||||
title: 'codeSetter(cssObject)',
|
||||
setter: 'codeSetter',
|
||||
setterProps: {
|
||||
expressionType: 'cssObject',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'text',
|
||||
title: 'textSetter',
|
||||
setter: 'textSetter',
|
||||
},
|
||||
{
|
||||
name: 'border',
|
||||
title: 'borderSetter',
|
||||
setter: 'borderSetter',
|
||||
name: 'text2',
|
||||
title: 'textAreaSetter',
|
||||
setter: 'textAreaSetter',
|
||||
},
|
||||
{
|
||||
name: 'router',
|
||||
title: 'routerSetter',
|
||||
setter: 'routerSetter',
|
||||
},
|
||||
{
|
||||
name: 'object',
|
||||
title: '对象属性',
|
||||
tip: '一个嵌套的对象',
|
||||
props: [
|
||||
{
|
||||
name: 'name',
|
||||
title: 'Name',
|
||||
setter: 'textSetter',
|
||||
},
|
||||
{
|
||||
name: 'age',
|
||||
title: 'Age',
|
||||
setter: 'numberSetter',
|
||||
},
|
||||
{
|
||||
name: 'address',
|
||||
title: 'Address',
|
||||
props: [
|
||||
{
|
||||
name: 'city',
|
||||
title: 'City',
|
||||
setter: 'textSetter',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
getVisible: (form) => {
|
||||
return form.getValue('text') !== 'test';
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'expression',
|
||||
title: 'expressionSetter',
|
||||
setter: 'expressionSetter',
|
||||
},
|
||||
{
|
||||
name: 'model',
|
||||
title: 'modelSetter',
|
||||
setter: 'modelSetter',
|
||||
},
|
||||
{
|
||||
name: 'image',
|
||||
name: 'src',
|
||||
title: 'imageSetter',
|
||||
setter: 'imageSetter',
|
||||
},
|
||||
{
|
||||
name: 'number',
|
||||
title: 'numberSetter',
|
||||
setter: 'numberSetter',
|
||||
},
|
||||
{
|
||||
name: 'number2',
|
||||
title: 'sliderSetter',
|
||||
setter: 'sliderSetter',
|
||||
},
|
||||
{
|
||||
name: 'bool',
|
||||
title: 'boolSetter',
|
||||
setter: 'boolSetter',
|
||||
},
|
||||
{
|
||||
name: 'enum',
|
||||
title: 'enumSetter',
|
||||
setter: 'enumSetter',
|
||||
},
|
||||
{
|
||||
name: 'list',
|
||||
title: 'listSetter',
|
||||
setter: 'listSetter',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export function Basic() {
|
||||
return (
|
||||
<SettingFormDemo
|
||||
initValues={{
|
||||
bool: true,
|
||||
enum: {
|
||||
aaa: 'aaa',
|
||||
bbb: 'bbb',
|
||||
ccc: 'ccc',
|
||||
},
|
||||
list: [{ key: 1 }, { key: 2 }],
|
||||
}}
|
||||
prototype={prototypeHasBasicProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function DeprecatedProp() {
|
||||
return (
|
||||
<SettingFormDemo
|
||||
prototype={{
|
||||
name: 'Deprecated',
|
||||
package: 'sample-pkg',
|
||||
type: 'element',
|
||||
props: [
|
||||
{
|
||||
name: 'number',
|
||||
title: 'numberSetter',
|
||||
setter: 'numberSetter',
|
||||
tip: '这是一个文本属性',
|
||||
docs: 'https://4x-ant-design.antgroup.com/components/slider-cn',
|
||||
deprecated: '使用 text2 替代',
|
||||
},
|
||||
{
|
||||
name: 'number1',
|
||||
title: 'sliderSetter',
|
||||
setter: 'sliderSetter',
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function Validate() {
|
||||
return (
|
||||
<SettingFormDemo
|
||||
prototype={{
|
||||
name: 'Validate',
|
||||
package: 'sample-pkg',
|
||||
type: 'element',
|
||||
props: [
|
||||
{
|
||||
name: 'number',
|
||||
title: 'numberSetter',
|
||||
setter: 'numberSetter',
|
||||
validate: (value) => {
|
||||
if (!value && value !== 0) {
|
||||
return '必填';
|
||||
}
|
||||
if (value < 0) {
|
||||
return '必须大于 0';
|
||||
}
|
||||
if (value > 10) {
|
||||
return '必须小于等于 10';
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function ObjectSetter() {
|
||||
return (
|
||||
<SettingFormDemo
|
||||
prototype={{
|
||||
name: 'Object',
|
||||
package: 'sample-pkg',
|
||||
type: 'element',
|
||||
props: [
|
||||
{
|
||||
name: 'text',
|
||||
title: 'textSetter',
|
||||
setter: 'textSetter',
|
||||
},
|
||||
{
|
||||
name: 'object',
|
||||
title: 'objectSetter',
|
||||
setter: 'objectSetter',
|
||||
props: [
|
||||
{
|
||||
name: 'text',
|
||||
title: 'textSetter',
|
||||
setter: 'textSetter',
|
||||
},
|
||||
{
|
||||
name: 'number',
|
||||
title: 'numberSetter',
|
||||
setter: 'numberSetter',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function InitValues() {
|
||||
return (
|
||||
<SettingFormDemo
|
||||
initValues={{
|
||||
bool: true,
|
||||
bool1: '{{true}}',
|
||||
style: {
|
||||
background: 'red',
|
||||
},
|
||||
object: {
|
||||
text: 'text',
|
||||
number: 10,
|
||||
},
|
||||
object1: {
|
||||
text: 'text',
|
||||
number: '{{tango.stores.user?.age}}',
|
||||
},
|
||||
// 只会有一种情况传下来的是字符串,就是用户代码里存在 rest operator,这时候不需要额外处理,提示用户就使用代码模式
|
||||
object2: '{{{ text: "text22", number: 22, ...{ extra: "some" } }}}',
|
||||
list: [{ key: 'aaa' }, { key: 'bbb' }], // list object
|
||||
list1: "{{[{ key: 'aaa' }, { key: 'bbb' }]}}", // raw code
|
||||
}}
|
||||
prototype={{
|
||||
name: 'InitValues',
|
||||
package: 'sample-pkg',
|
||||
type: 'element',
|
||||
props: [
|
||||
{
|
||||
name: 'bool',
|
||||
title: 'value初始化',
|
||||
setter: 'boolSetter',
|
||||
},
|
||||
{
|
||||
name: 'bool1',
|
||||
title: 'value初始化',
|
||||
setter: 'boolSetter',
|
||||
},
|
||||
{
|
||||
name: 'bool2',
|
||||
title: '无初值',
|
||||
setter: 'boolSetter',
|
||||
},
|
||||
{
|
||||
name: 'style',
|
||||
title: 'codeSetter',
|
||||
setter: 'codeSetter',
|
||||
setterProps: {
|
||||
expressionType: 'cssObject',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'object',
|
||||
props: [
|
||||
{
|
||||
name: 'text',
|
||||
title: 'text',
|
||||
setter: 'textSetter',
|
||||
},
|
||||
{
|
||||
name: 'number',
|
||||
title: 'number',
|
||||
setter: 'numberSetter',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'object1',
|
||||
props: [
|
||||
{
|
||||
name: 'text',
|
||||
title: 'text',
|
||||
setter: 'textSetter',
|
||||
},
|
||||
{
|
||||
name: 'number',
|
||||
title: 'number',
|
||||
setter: 'numberSetter',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'object2',
|
||||
props: [
|
||||
{
|
||||
name: 'text',
|
||||
title: 'text',
|
||||
setter: 'textSetter',
|
||||
},
|
||||
{
|
||||
name: 'number',
|
||||
title: 'number',
|
||||
setter: 'numberSetter',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'list',
|
||||
title: 'listSetter',
|
||||
setter: 'listSetter',
|
||||
},
|
||||
{
|
||||
name: 'list1',
|
||||
title: 'listSetter',
|
||||
setter: 'listSetter',
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function Lite() {
|
||||
return (
|
||||
<Box width={320} border="solid" borderColor="line2">
|
||||
<SettingForm
|
||||
showSearch={false}
|
||||
showGroups={false}
|
||||
showItemSubtitle={false}
|
||||
prototype={prototypeHasBasicProps}
|
||||
disableSwitchExpressionSetter
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export function HideToggleCode() {
|
||||
const model = new FormModel({});
|
||||
return (
|
||||
<Box width={320} border="solid" borderColor="line2">
|
||||
<SettingForm model={model} prototype={prototypeHasBasicProps} disableSwitchExpressionSetter />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const prototypeHasExtraProps: IComponentPrototype = {
|
||||
name: 'ExtraProps',
|
||||
type: 'element',
|
||||
package: '@music163/antd',
|
||||
props: [
|
||||
{
|
||||
name: 'choice',
|
||||
title: 'choiceSetter',
|
||||
setter: 'choiceSetter',
|
||||
options: [
|
||||
{ label: '选项1', value: '1' },
|
||||
{ label: '选项2', value: '2' },
|
||||
{ label: '选项3', value: '3' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'picker',
|
||||
title: 'pickerSetter',
|
||||
setter: 'pickerSetter',
|
||||
options: [
|
||||
{ label: '选项1', value: '1' },
|
||||
{ label: '选项2', value: '2' },
|
||||
{ label: '选项3', value: '3' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'actionList',
|
||||
title: 'actionListSetter',
|
||||
setter: 'actionListSetter',
|
||||
},
|
||||
{
|
||||
name: 'list',
|
||||
title: 'listSetter',
|
||||
setter: 'listSetter',
|
||||
},
|
||||
{
|
||||
name: 'options',
|
||||
title: 'optionSetter',
|
||||
setter: 'optionSetter',
|
||||
},
|
||||
{
|
||||
name: 'columns',
|
||||
title: 'tableColumnsSetter',
|
||||
setter: 'tableColumnsSetter',
|
||||
},
|
||||
{
|
||||
name: 'css',
|
||||
title: 'cssSetter',
|
||||
setter: 'cssSetter',
|
||||
},
|
||||
{
|
||||
name: 'extra',
|
||||
title: 'jsxSetter',
|
||||
setter: 'jsxSetter',
|
||||
},
|
||||
{
|
||||
name: 'icon',
|
||||
title: 'iconSetter',
|
||||
setter: 'iconSetter',
|
||||
},
|
||||
{
|
||||
name: 'iconType',
|
||||
title: 'iconTypeSetter',
|
||||
setter: 'iconTypeSetter',
|
||||
},
|
||||
{
|
||||
name: 'onClick',
|
||||
title: 'eventSetter',
|
||||
tip: '当点击按钮时',
|
||||
setter: 'eventSetter',
|
||||
group: 'event',
|
||||
},
|
||||
{
|
||||
name: 'onClick2',
|
||||
title: 'actionSetter',
|
||||
tip: '当点击按钮时',
|
||||
setter: 'actionSetter',
|
||||
group: 'event',
|
||||
},
|
||||
{
|
||||
name: 'disabled',
|
||||
title: 'boolSetter',
|
||||
tip: 'disabled 是否禁用',
|
||||
defaultValue: false,
|
||||
setter: 'boolSetter',
|
||||
},
|
||||
{
|
||||
name: 'size',
|
||||
title: 'choiceSetter',
|
||||
tip: 'size 按钮的尺寸',
|
||||
defaultValue: 'medium',
|
||||
setter: 'choiceSetter',
|
||||
setterProps: {
|
||||
options: [
|
||||
{ label: '小', value: 'small' },
|
||||
{ label: '中', value: 'medium' },
|
||||
{ label: '大', value: 'large' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'color',
|
||||
title: 'colorSetter',
|
||||
setter: 'colorSetter',
|
||||
},
|
||||
{
|
||||
name: 'columns',
|
||||
title: 'columnSetter',
|
||||
setter: 'columnSetter',
|
||||
},
|
||||
{
|
||||
name: 'date',
|
||||
title: 'dateSetter',
|
||||
@@ -163,127 +424,137 @@ const prototype: ComponentPrototypeType = {
|
||||
title: 'dateRangeSetter',
|
||||
setter: 'dateRangeSetter',
|
||||
},
|
||||
{
|
||||
name: 'enum',
|
||||
title: 'enumSetter',
|
||||
setter: 'enumSetter',
|
||||
setterProps: {},
|
||||
},
|
||||
{
|
||||
name: 'time',
|
||||
title: 'timeSetter',
|
||||
setter: 'timeSetter',
|
||||
},
|
||||
{
|
||||
name: 'timeRange',
|
||||
name: 'time',
|
||||
title: 'timeRangeSetter',
|
||||
setter: 'timeRangeSetter',
|
||||
},
|
||||
{
|
||||
name: 'dataSource',
|
||||
title: 'jsonSetter 不推荐',
|
||||
name: 'enum',
|
||||
title: 'enumSetter',
|
||||
setter: 'enumSetter',
|
||||
},
|
||||
{
|
||||
name: 'event',
|
||||
title: 'eventSetter',
|
||||
setter: 'eventSetter',
|
||||
},
|
||||
{
|
||||
name: 'json',
|
||||
title: 'jsonSetter',
|
||||
setter: 'jsonSetter',
|
||||
},
|
||||
{
|
||||
name: 'listSetter',
|
||||
title: 'listSetter',
|
||||
setter: 'listSetter',
|
||||
name: 'jsx',
|
||||
title: 'jsxSetter',
|
||||
setter: 'jsxSetter',
|
||||
},
|
||||
{
|
||||
name: 'count',
|
||||
title: 'numberSetter',
|
||||
setter: 'numberSetter',
|
||||
name: 'render',
|
||||
title: 'renderPropsSetter',
|
||||
setter: 'renderPropsSetter',
|
||||
},
|
||||
{
|
||||
name: 'options',
|
||||
title: 'optionSetter',
|
||||
setter: 'optionSetter',
|
||||
name: 'cell',
|
||||
title: 'tableCellSetter',
|
||||
setter: 'tableCellSetter',
|
||||
},
|
||||
{
|
||||
name: 'type',
|
||||
title: 'pickerSetter',
|
||||
defaultValue: 'solid',
|
||||
setter: 'pickerSetter',
|
||||
setterProps: {
|
||||
options: [
|
||||
{ label: '文本型', value: 'text' },
|
||||
{ label: '实体型', value: 'solid' },
|
||||
{ label: '幽灵', value: 'ghost' },
|
||||
],
|
||||
},
|
||||
name: 'expandable',
|
||||
title: 'tableExpandableSetter',
|
||||
setter: 'tableExpandableSetter',
|
||||
},
|
||||
{
|
||||
name: 'title',
|
||||
title: 'textSetter',
|
||||
setter: 'textSetter',
|
||||
},
|
||||
{
|
||||
name: 'invalid',
|
||||
title: 'invalidSetter',
|
||||
setter: 'invalidSetter',
|
||||
name: 'router',
|
||||
title: 'routerSetter',
|
||||
setter: 'routerSetter',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* 表单值预览
|
||||
*/
|
||||
const FormValuePreview = observer(({ model }: { model: FormModel }) => {
|
||||
const data = toJS(model.values);
|
||||
return <JsonView src={data} />;
|
||||
});
|
||||
|
||||
export function Basic() {
|
||||
const model = new FormModel(
|
||||
{
|
||||
router: 'www.163.com',
|
||||
expression: `{ foo: 'foo' }`,
|
||||
object: {
|
||||
name: 'Alice',
|
||||
},
|
||||
image:
|
||||
'https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/13270238619/2cc5/0782/1d6e/009b96bf90c557b9bbde09b1687a2c80.png',
|
||||
},
|
||||
{ onChange: console.log },
|
||||
);
|
||||
|
||||
export function ExtraSetters() {
|
||||
return (
|
||||
<Box display="flex">
|
||||
<SettingForm
|
||||
model={model}
|
||||
prototype={prototype}
|
||||
showIdentifier={{
|
||||
identifierKey: 'tid',
|
||||
}}
|
||||
/>
|
||||
<Box position="relative">
|
||||
<Card title="表单状态预览" style={{ position: 'sticky', top: 0 }}>
|
||||
<FormValuePreview model={model} />
|
||||
</Card>
|
||||
</Box>
|
||||
</Box>
|
||||
<SettingFormDemo
|
||||
initValues={{
|
||||
router: 'www.163.com',
|
||||
expression: `{ foo: 'foo' }`,
|
||||
object: {
|
||||
name: 'Alice',
|
||||
},
|
||||
image:
|
||||
'https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/13270238619/2cc5/0782/1d6e/009b96bf90c557b9bbde09b1687a2c80.png',
|
||||
}}
|
||||
prototype={prototypeHasExtraProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function Lite() {
|
||||
export function StyleProps() {
|
||||
return (
|
||||
<Box width={320} border="solid">
|
||||
<SettingForm
|
||||
showSearch={false}
|
||||
showGroups={false}
|
||||
showItemSubtitle={false}
|
||||
prototype={prototype}
|
||||
disableSwitchExpressionSetter
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export function NoExpressionSwitch() {
|
||||
const model = new FormModel({});
|
||||
return (
|
||||
<Box>
|
||||
<SettingForm model={model} prototype={prototype} disableSwitchExpressionSetter />
|
||||
</Box>
|
||||
<SettingFormDemo
|
||||
prototype={{
|
||||
name: 'Box',
|
||||
title: 'Box',
|
||||
type: 'element',
|
||||
package: '@music163/antd',
|
||||
props: [
|
||||
{
|
||||
name: 'style',
|
||||
title: 'styleSetter',
|
||||
setter: 'styleSetter',
|
||||
},
|
||||
{
|
||||
name: 'display',
|
||||
title: 'displaySetter',
|
||||
setter: 'displaySetter',
|
||||
},
|
||||
{
|
||||
name: 'flexDirection',
|
||||
title: 'flexDirectionSetter',
|
||||
setter: 'flexDirectionSetter',
|
||||
},
|
||||
{
|
||||
name: 'flexGap',
|
||||
title: 'flexGapSetter',
|
||||
setter: 'flexGapSetter',
|
||||
},
|
||||
{
|
||||
name: 'flexJustifyContent',
|
||||
title: 'flexJustifyContentSetter',
|
||||
setter: 'flexJustifyContentSetter',
|
||||
},
|
||||
{
|
||||
name: 'flexAlignItems',
|
||||
title: 'flexAlignItemsSetter',
|
||||
setter: 'flexAlignItemsSetter',
|
||||
},
|
||||
{
|
||||
name: 'spacing',
|
||||
title: 'spacingSetter',
|
||||
setter: 'spacingSetter',
|
||||
},
|
||||
{
|
||||
name: 'color',
|
||||
title: 'colorSetter',
|
||||
setter: 'colorSetter',
|
||||
},
|
||||
{
|
||||
name: 'bg',
|
||||
title: 'bgSetter',
|
||||
setter: 'bgSetter',
|
||||
},
|
||||
{
|
||||
name: 'border',
|
||||
title: 'borderSetter',
|
||||
setter: 'borderSetter',
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import { ActionSelect } from '@music163/tango-ui';
|
||||
|
||||
export default {
|
||||
title: 'UI/ActionSelect',
|
||||
component: ActionSelect,
|
||||
};
|
||||
|
||||
const options = [
|
||||
{ label: 'action1', value: 'action1' },
|
||||
{ label: 'action2', value: 'action2' },
|
||||
{ label: 'action3', value: 'action3' },
|
||||
];
|
||||
|
||||
export const Basic = {
|
||||
args: {
|
||||
defaultText: '选择动作',
|
||||
options,
|
||||
onSelect: console.log,
|
||||
},
|
||||
};
|
||||
|
||||
export const showInput = {
|
||||
args: {
|
||||
text: '选择动作',
|
||||
options,
|
||||
showInput: true,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,82 @@
|
||||
import React from 'react';
|
||||
import { DragPanel } from '@music163/tango-ui';
|
||||
import { Box, Text } from 'coral-system';
|
||||
import { Button } from 'antd';
|
||||
|
||||
export default {
|
||||
title: 'UI/DragPanel',
|
||||
};
|
||||
|
||||
export function Basic() {
|
||||
return (
|
||||
<>
|
||||
<DragPanel
|
||||
title="弹层标题"
|
||||
extra="右上角内容"
|
||||
width={350}
|
||||
body={<Box p="m">内容</Box>}
|
||||
footer={<Text>底部信息</Text>}
|
||||
>
|
||||
<Button>点击唤起浮层</Button>
|
||||
</DragPanel>
|
||||
<DragPanel
|
||||
title="弹层标题"
|
||||
extra="右上角内容"
|
||||
width={350}
|
||||
body={<Box p="m">内容</Box>}
|
||||
footer={<Text>底部信息</Text>}
|
||||
>
|
||||
<Button
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: 20,
|
||||
right: 20,
|
||||
}}
|
||||
>
|
||||
底部浮层自动修正弹出区域
|
||||
</Button>
|
||||
</DragPanel>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function FooterCustom() {
|
||||
return (
|
||||
<DragPanel
|
||||
title="弹层标题"
|
||||
extra="右上角内容"
|
||||
width={350}
|
||||
body={<Box p="m">内容</Box>}
|
||||
footer={(close) => (
|
||||
<Box display="flex" justifyContent="space-between">
|
||||
<Text>底部信息</Text>
|
||||
<Button size="small" onClick={() => close()}>
|
||||
点此关闭
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
>
|
||||
<Button>点击唤起浮层</Button>
|
||||
</DragPanel>
|
||||
);
|
||||
}
|
||||
|
||||
export function ResizeablePanel() {
|
||||
return (
|
||||
<DragPanel
|
||||
title="弹层标题"
|
||||
resizeable
|
||||
extra="右上角内容"
|
||||
width={350}
|
||||
height={400}
|
||||
body={
|
||||
<Box p="m" textAlign="center" background="#fa8484">
|
||||
内容
|
||||
</Box>
|
||||
}
|
||||
footer={<Text>底部信息</Text>}
|
||||
>
|
||||
<Button>点击唤起浮层</Button>
|
||||
</DragPanel>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Action, InputCode } from '@music163/tango-ui';
|
||||
import { Action, InputCode, InputStyleCode } from '@music163/tango-ui';
|
||||
import { BlockOutlined } from '@ant-design/icons';
|
||||
|
||||
export default {
|
||||
@@ -28,6 +28,10 @@ export function Basic() {
|
||||
);
|
||||
}
|
||||
|
||||
export function CSS() {
|
||||
return <InputStyleCode enableESLint suffix={<Action icon={<BlockOutlined />} size="small" />} />;
|
||||
}
|
||||
|
||||
const code = `
|
||||
function foo() {
|
||||
console.log("test");
|
||||
|
||||
+8
-7
@@ -15,10 +15,10 @@
|
||||
"test:watch": "jest --watch",
|
||||
"eslint": "eslint packages/**/src/*.{ts,tsx} --cache",
|
||||
"publish": "lerna publish",
|
||||
"ver": "lerna version --no-private",
|
||||
"release": "yarn eslint && yarn build && lerna version --no-private --conventional-commits && lerna publish from-git",
|
||||
"release:npm": "yarn eslint && yarn build && yarn run ver && lerna publish from-git",
|
||||
"release:beta": "yarn eslint && yarn build && yarn run ver && lerna publish from-package --dist-tag beta",
|
||||
"ver": "lerna version --no-private --conventional-commits",
|
||||
"release": "yarn eslint && yarn build && yarn ver && lerna publish from-git",
|
||||
"release:npm": "yarn eslint && yarn build && yarn ver && lerna publish from-package",
|
||||
"release:beta": "yarn eslint && yarn build && yarn ver && lerna publish from-package --dist-tag beta",
|
||||
"up": "yarn upgrade-interactive --latest",
|
||||
"prepare": "husky install"
|
||||
},
|
||||
@@ -40,6 +40,7 @@
|
||||
"@typescript-eslint/parser": "^6.13.2",
|
||||
"conventional-changelog-cli": "^4.1.0",
|
||||
"copyfiles": "^2.4.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.55.0",
|
||||
"eslint-config-ali": "^14.0.2",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
@@ -51,9 +52,9 @@
|
||||
"husky": "^8.0.3",
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"lerna": "^8.0.0",
|
||||
"lint-staged": "^15.2.0",
|
||||
"prettier": "^3.1.0",
|
||||
"lerna": "^8.1.2",
|
||||
"lint-staged": "^15.2.2",
|
||||
"prettier": "^3.2.5",
|
||||
"react": "^17.0.0",
|
||||
"react-dom": "^17.0.0",
|
||||
"styled-components": "^5.3.6",
|
||||
|
||||
@@ -3,6 +3,66 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.1.9](https://github.com/netease/tango/compare/@music163/tango-context@1.1.8...@music163/tango-context@1.1.9) (2024-09-02)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
## [1.1.8](https://github.com/netease/tango/compare/@music163/tango-context@1.1.7...@music163/tango-context@1.1.8) (2024-08-06)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
## [1.1.7](https://github.com/netease/tango/compare/@music163/tango-context@1.1.6...@music163/tango-context@1.1.7) (2024-08-05)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
## [1.1.6](https://github.com/netease/tango/compare/@music163/tango-context@1.1.5...@music163/tango-context@1.1.6) (2024-08-01)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
## [1.1.5](https://github.com/netease/tango/compare/@music163/tango-context@1.1.4...@music163/tango-context@1.1.5) (2024-07-12)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
## [1.1.4](https://github.com/netease/tango/compare/@music163/tango-context@1.1.3...@music163/tango-context@1.1.4) (2024-06-20)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
## [1.1.3](https://github.com/netease/tango/compare/@music163/tango-context@1.1.2...@music163/tango-context@1.1.3) (2024-06-05)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
## [1.1.2](https://github.com/netease/tango/compare/@music163/tango-context@1.1.1...@music163/tango-context@1.1.2) (2024-06-03)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
## [1.1.1](https://github.com/netease/tango/compare/@music163/tango-context@1.1.0...@music163/tango-context@1.1.1) (2024-05-21)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
# [1.1.0](https://github.com/netease/tango/compare/@music163/tango-context@1.0.2...@music163/tango-context@1.1.0) (2024-05-17)
|
||||
|
||||
### Features
|
||||
|
||||
- refactor parse attribute value ([#149](https://github.com/netease/tango/issues/149)) ([ffaa276](https://github.com/netease/tango/commit/ffaa276b5c205ed962d37e2fdb358a703f8fad01))
|
||||
|
||||
## [1.0.1](https://github.com/netease/tango/compare/@music163/tango-context@1.0.0...@music163/tango-context@1.0.1) (2024-04-22)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- allow reload state tree & display pageStore in variable tree ([#135](https://github.com/netease/tango/issues/135)) ([2664613](https://github.com/netease/tango/commit/2664613ab263aead2a5239fa012454fc7fd5ff99))
|
||||
|
||||
# [1.0.0-alpha.10](https://github.com/netease/tango/compare/@music163/tango-context@1.0.0-alpha.9...@music163/tango-context@1.0.0-alpha.10) (2024-04-09)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
# [1.0.0-alpha.9](https://github.com/netease/tango/compare/@music163/tango-context@1.0.0-alpha.8...@music163/tango-context@1.0.0-alpha.9) (2024-03-26)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
# [1.0.0-alpha.8](https://github.com/netease/tango/compare/@music163/tango-context@1.0.0-alpha.7...@music163/tango-context@1.0.0-alpha.8) (2024-03-18)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
# [1.0.0-alpha.7](https://github.com/netease/tango/compare/@music163/tango-context@1.0.0-alpha.6...@music163/tango-context@1.0.0-alpha.7) (2024-03-18)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-context",
|
||||
"version": "1.0.0-alpha.7",
|
||||
"version": "1.1.9",
|
||||
"description": "react context for tango-apps",
|
||||
"keywords": [
|
||||
"react",
|
||||
@@ -31,9 +31,9 @@
|
||||
"react": ">= 16.8"
|
||||
},
|
||||
"dependencies": {
|
||||
"@music163/tango-core": "^1.0.0-alpha.7",
|
||||
"@music163/tango-helpers": "^1.0.0-alpha.3",
|
||||
"mobx-react-lite": "4.0.5"
|
||||
"@music163/tango-core": "^1.4.3",
|
||||
"@music163/tango-helpers": "^1.2.3",
|
||||
"mobx-react-lite": "4.0.7"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
|
||||
@@ -35,8 +35,14 @@ export const useWorkspaceData = () => {
|
||||
const modelVariables: IVariableTreeNode[] = []; // 绑定变量列表
|
||||
const storeActionVariables: IVariableTreeNode[] = []; // 模型中的所有 actions
|
||||
const storeVariables: IVariableTreeNode[] = []; // 模型中的所有变量
|
||||
const pageStoreVariables: IVariableTreeNode[] = []; // 页面中的组件实例变量
|
||||
const serviceVariables: IVariableTreeNode[] = []; // 服务中的所有变量
|
||||
|
||||
pageStoreVariables.push({
|
||||
title: 'pageStore',
|
||||
key: 'pageStore',
|
||||
});
|
||||
|
||||
Object.values(workspace.storeModules).forEach((file) => {
|
||||
const prefix = `stores.${file.name}`;
|
||||
const states: IVariableTreeNode[] = file.states.map((item) => ({
|
||||
@@ -109,6 +115,7 @@ export const useWorkspaceData = () => {
|
||||
}
|
||||
|
||||
let expressionVariables: IVariableTreeNode[] = [
|
||||
buildVariableOptions('当前页面组件实例', '$pageStore', pageStoreVariables),
|
||||
buildVariableOptions('数据模型', '$stores', storeVariables),
|
||||
buildVariableOptions('服务函数', '$services', serviceVariables),
|
||||
];
|
||||
|
||||
@@ -3,6 +3,101 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.4.3](https://github.com/netease/tango/compare/@music163/tango-core@1.4.2...@music163/tango-core@1.4.3) (2024-09-02)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-core
|
||||
|
||||
## [1.4.2](https://github.com/netease/tango/compare/@music163/tango-core@1.4.1...@music163/tango-core@1.4.2) (2024-08-06)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- view module \_nodesTree set init value ([#194](https://github.com/netease/tango/issues/194)) ([269e304](https://github.com/netease/tango/commit/269e304d685c4fcc80635b9d04220cda333b80c4))
|
||||
|
||||
## [1.4.1](https://github.com/netease/tango/compare/@music163/tango-core@1.4.0...@music163/tango-core@1.4.1) (2024-08-05)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-core
|
||||
|
||||
# [1.4.0](https://github.com/netease/tango/compare/@music163/tango-core@1.3.3...@music163/tango-core@1.4.0) (2024-08-01)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- update types ([67d602a](https://github.com/netease/tango/commit/67d602a3ec6b7f74156bb78fda6f3b4bc2676e55))
|
||||
|
||||
### Features
|
||||
|
||||
- add isError and errorMessage to File & add isAstSynced to Module ([#190](https://github.com/netease/tango/issues/190)) ([8dd289c](https://github.com/netease/tango/commit/8dd289cd7daba628e54dc6b8929f22a1e2245160))
|
||||
|
||||
## [1.3.3](https://github.com/netease/tango/compare/@music163/tango-core@1.3.2...@music163/tango-core@1.3.3) (2024-07-12)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- update designer config ([#186](https://github.com/netease/tango/issues/186)) ([85c053b](https://github.com/netease/tango/commit/85c053b3db6d652b41c9873dba1366315174833f))
|
||||
|
||||
## [1.3.2](https://github.com/netease/tango/compare/@music163/tango-core@1.3.1...@music163/tango-core@1.3.2) (2024-06-20)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- update designer style ([#176](https://github.com/netease/tango/issues/176)) ([0f3f0af](https://github.com/netease/tango/commit/0f3f0afdfa8aee2532a97c5c2e92ef4230397d86))
|
||||
|
||||
## [1.3.1](https://github.com/netease/tango/compare/@music163/tango-core@1.3.0...@music163/tango-core@1.3.1) (2024-06-05)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- set value as jsxElement children ([#173](https://github.com/netease/tango/issues/173)) ([ae04850](https://github.com/netease/tango/commit/ae04850bb251f392575a72ca5fd960249ea9d06b))
|
||||
|
||||
# [1.3.0](https://github.com/netease/tango/compare/@music163/tango-core@1.2.0...@music163/tango-core@1.3.0) (2024-06-03)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- parse app entry file & pass correct routerType to sandbox ([#168](https://github.com/netease/tango/issues/168)) ([3f3981b](https://github.com/netease/tango/commit/3f3981bb9db874b738a67fd449c579c35c58d08b))
|
||||
|
||||
### Features
|
||||
|
||||
- add context menu ([#161](https://github.com/netease/tango/issues/161)) ([28040fc](https://github.com/netease/tango/commit/28040fc00604339a40ad3216b76baf7de93a13e0))
|
||||
|
||||
# [1.2.0](https://github.com/netease/tango/compare/@music163/tango-core@1.1.0...@music163/tango-core@1.2.0) (2024-05-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- enhance code value validate in SettingForm ([#152](https://github.com/netease/tango/issues/152)) ([791fbb1](https://github.com/netease/tango/commit/791fbb162a7147243924e01f54e9c0b586f14438))
|
||||
- prototype2code & go back history error ([#156](https://github.com/netease/tango/issues/156)) ([8bf53a7](https://github.com/netease/tango/commit/8bf53a76f8a71eaf261ea68b9ee44e5bf19893aa))
|
||||
- support add components with popover ([#155](https://github.com/netease/tango/issues/155)) ([f17ccbb](https://github.com/netease/tango/commit/f17ccbb7f645f8047ecd96d9f3f2185048a3b726))
|
||||
|
||||
### Features
|
||||
|
||||
- add select parent node of selected node ([#158](https://github.com/netease/tango/issues/158)) ([fe31246](https://github.com/netease/tango/commit/fe3124648325e72abfc58da8b2f8ff83301d40b8))
|
||||
- supoort set default active view ([#157](https://github.com/netease/tango/issues/157)) ([f854f75](https://github.com/netease/tango/commit/f854f75b8a8c25384d290bd76d6b8bb6fb69f22d))
|
||||
|
||||
# [1.1.0](https://github.com/netease/tango/compare/@music163/tango-core@1.0.2...@music163/tango-core@1.1.0) (2024-05-17)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- parse module with module alias ([#147](https://github.com/netease/tango/issues/147)) ([56d0877](https://github.com/netease/tango/commit/56d0877507b0138877eac6f36db288147b43c7d9))
|
||||
|
||||
### Features
|
||||
|
||||
- refactor parse attribute value ([#149](https://github.com/netease/tango/issues/149)) ([ffaa276](https://github.com/netease/tango/commit/ffaa276b5c205ed962d37e2fdb358a703f8fad01))
|
||||
|
||||
## [1.0.1](https://github.com/netease/tango/compare/@music163/tango-core@1.0.0...@music163/tango-core@1.0.1) (2024-04-22)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-core
|
||||
|
||||
# [1.0.0-alpha.10](https://github.com/netease/tango/compare/@music163/tango-core@1.0.0-alpha.9...@music163/tango-core@1.0.0-alpha.10) (2024-04-09)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- update setters and use tabOptions to filter props ([#129](https://github.com/netease/tango/issues/129)) ([93608d1](https://github.com/netease/tango/commit/93608d1037327afa4f755976b86427b6128ae3d0))
|
||||
|
||||
# [1.0.0-alpha.9](https://github.com/netease/tango/compare/@music163/tango-core@1.0.0-alpha.8...@music163/tango-core@1.0.0-alpha.9) (2024-03-26)
|
||||
|
||||
### Features
|
||||
|
||||
- quick add sibling nodes ([#127](https://github.com/netease/tango/issues/127)) ([9ed6a7d](https://github.com/netease/tango/commit/9ed6a7d1a4944d69d96e034f243b61531862e317))
|
||||
|
||||
# [1.0.0-alpha.8](https://github.com/netease/tango/compare/@music163/tango-core@1.0.0-alpha.7...@music163/tango-core@1.0.0-alpha.8) (2024-03-18)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-core
|
||||
|
||||
# [1.0.0-alpha.7](https://github.com/netease/tango/compare/@music163/tango-core@1.0.0-alpha.6...@music163/tango-core@1.0.0-alpha.7) (2024-03-18)
|
||||
|
||||
### Features
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-core",
|
||||
"version": "1.0.0-alpha.7",
|
||||
"version": "1.4.3",
|
||||
"description": "tango core",
|
||||
"author": "wwsun <ww.sun@outlook.com>",
|
||||
"homepage": "",
|
||||
@@ -28,10 +28,10 @@
|
||||
"@babel/parser": "^7.23.5",
|
||||
"@babel/traverse": "^7.23.5",
|
||||
"@babel/types": "^7.23.5",
|
||||
"@music163/tango-helpers": "^1.0.0-alpha.3",
|
||||
"@music163/tango-helpers": "^1.2.3",
|
||||
"@types/babel__generator": "^7.6.7",
|
||||
"@types/babel__traverse": "^7.20.4",
|
||||
"mobx": "6.12.0",
|
||||
"mobx": "6.12.3",
|
||||
"path-browserify": "^1.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Designer, Engine, SimulatorNameType } from './models';
|
||||
import { MenuDataType } from '@music163/tango-helpers';
|
||||
import { Designer, DesignerViewType, Engine, SimulatorNameType } from './models';
|
||||
import { IWorkspace } from './models/interfaces';
|
||||
|
||||
interface ICreateEngineOptions {
|
||||
@@ -6,6 +7,10 @@ interface ICreateEngineOptions {
|
||||
* 自定义工作区
|
||||
*/
|
||||
workspace?: IWorkspace;
|
||||
/**
|
||||
* 菜单信息
|
||||
*/
|
||||
menuData?: MenuDataType;
|
||||
/**
|
||||
* 默认的模拟器模式
|
||||
*/
|
||||
@@ -14,6 +19,10 @@ interface ICreateEngineOptions {
|
||||
* 默认激活的侧边栏
|
||||
*/
|
||||
defaultActiveSidebarPanel?: string;
|
||||
/**
|
||||
* 默认激活的视图
|
||||
*/
|
||||
defaultActiveView?: DesignerViewType;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -23,15 +32,19 @@ interface ICreateEngineOptions {
|
||||
*/
|
||||
export function createEngine({
|
||||
workspace,
|
||||
defaultActiveView = 'design',
|
||||
defaultSimulatorMode = 'desktop',
|
||||
defaultActiveSidebarPanel = '',
|
||||
menuData,
|
||||
}: ICreateEngineOptions) {
|
||||
const engine = new Engine({
|
||||
workspace,
|
||||
designer: new Designer({
|
||||
workspace,
|
||||
simulator: defaultSimulatorMode,
|
||||
activeView: defaultActiveView,
|
||||
activeSidebarPanel: defaultActiveSidebarPanel,
|
||||
menuData,
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -35,11 +35,12 @@ export function isTangoVariable(name: string) {
|
||||
return /^tango\??\.(stores|services)\??\./.test(name) && name.split('.').length > 2;
|
||||
}
|
||||
|
||||
const templatePattern = /^{(.+)}$/s;
|
||||
const templatePattern = /^{(.+)}$/;
|
||||
|
||||
/**
|
||||
* 判断给定字符串是否被表达式容器`{expCode}`包裹
|
||||
* @param code
|
||||
* @deprecated 新版改为 {{code}} 作为容器,使用 isWrappedCode 代替
|
||||
*/
|
||||
export function isWrappedByExpressionContainer(code: string, isStrict = true) {
|
||||
if (isStrict && isValidExpressionCode(code)) {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
import generator, { GeneratorOptions } from '@babel/generator';
|
||||
import * as t from '@babel/types';
|
||||
import { logger } from '@music163/tango-helpers';
|
||||
import { Dict, logger, wrapCode } from '@music163/tango-helpers';
|
||||
import { formatCode } from '../string';
|
||||
|
||||
const defaultGeneratorOptions: GeneratorOptions = {
|
||||
@@ -25,19 +25,7 @@ export function ast2code(ast: t.Node, options: GeneratorOptions = defaultGenerat
|
||||
return code;
|
||||
}
|
||||
|
||||
const bracketPattern = /^\(.+\)$/s;
|
||||
|
||||
/**
|
||||
* 是否被 () 包裹
|
||||
*
|
||||
* @example ({ foo: 'foo' }) -> true
|
||||
* @example { foo: 'foo' } -> false
|
||||
*
|
||||
* @param str 目标字符串
|
||||
*/
|
||||
function isWrappingWithBrackets(str: string) {
|
||||
return bracketPattern.test(str);
|
||||
}
|
||||
const bracketPattern = /^\(.+\)$/;
|
||||
|
||||
/**
|
||||
* 将表达式生成为块级代码
|
||||
@@ -54,7 +42,7 @@ export function expression2code(node: t.Expression) {
|
||||
|
||||
const isWrappingExpression = t.isObjectExpression(node) || t.isFunctionExpression(node);
|
||||
|
||||
if (isWrappingExpression && isWrappingWithBrackets(ret)) {
|
||||
if (isWrappingExpression && bracketPattern.test(ret)) {
|
||||
// 如果是对象,输出包含 ({}),则去掉首尾的括号
|
||||
ret = ret.slice(1, -1);
|
||||
}
|
||||
@@ -156,10 +144,10 @@ export function node2code(node: t.Node) {
|
||||
/**
|
||||
* 将 t.Node 生成为 js 值
|
||||
* @param node ast node
|
||||
* @param hasExpressionWrapper 是否包裹表达式
|
||||
* @param isWrapCode 是否包裹代码,例如 code -> {{code}}
|
||||
* @returns a plain javascript value
|
||||
*/
|
||||
export function node2value(node: t.Node, hasExpressionWrapper = true): any {
|
||||
export function node2value(node: t.Node, isWrapCode = true): any {
|
||||
let ret;
|
||||
switch (node.type) {
|
||||
case 'StringLiteral':
|
||||
@@ -171,39 +159,50 @@ export function node2value(node: t.Node, hasExpressionWrapper = true): any {
|
||||
case 'NullLiteral':
|
||||
ret = null;
|
||||
break;
|
||||
case 'Identifier': // {data}
|
||||
case 'MemberExpression': // {this.props.data}
|
||||
case 'OptionalMemberExpression': // {a?.b}
|
||||
case 'UnaryExpression': // {!false}
|
||||
case 'ArrowFunctionExpression': // {() => {}}
|
||||
case 'TemplateLiteral': // {`hello ${text}`}
|
||||
case 'ConditionalExpression': // {a ? 'foo' : 'bar'}
|
||||
case 'LogicalExpression': // { a || b}
|
||||
case 'BinaryExpression': // { a + b}
|
||||
case 'TaggedTemplateExpression': // {css``}
|
||||
case 'CallExpression': // {[1,2,3].map(fn)}
|
||||
case 'JSXElement': // {<Box>hello</Box>}
|
||||
case 'JSXFragment': // <><Box /></>
|
||||
case 'Identifier': // {{data}}
|
||||
case 'MemberExpression': // {{this.props.data}}
|
||||
case 'OptionalMemberExpression': // {{a?.b}}
|
||||
case 'UnaryExpression': // {{!false}}
|
||||
case 'ArrowFunctionExpression': // {{() => {}}}
|
||||
case 'TemplateLiteral': // {{`hello ${text}`}}
|
||||
case 'ConditionalExpression': // {{a ? 'foo' : 'bar'}}
|
||||
case 'LogicalExpression': // {{ a || b}}
|
||||
case 'BinaryExpression': // {{ a + b}}
|
||||
case 'TaggedTemplateExpression': // {{css``}}
|
||||
case 'CallExpression': // {{[1,2,3].map(fn)}}
|
||||
case 'JSXElement': // {{<Box>hello</Box>}}
|
||||
case 'JSXFragment': // {{<><Box /></>}}
|
||||
ret = expression2code(node);
|
||||
if (hasExpressionWrapper) {
|
||||
ret = `{${ret}}`;
|
||||
if (isWrapCode) {
|
||||
ret = wrapCode(ret);
|
||||
}
|
||||
break;
|
||||
case 'ObjectExpression': {
|
||||
ret = node.properties.reduce((prev, propertyNode) => {
|
||||
if (propertyNode.type === 'ObjectProperty') {
|
||||
const key = keyNode2value(propertyNode.key);
|
||||
const value = node2value(propertyNode.value, hasExpressionWrapper);
|
||||
// key 可能是字符串,也可能是数字
|
||||
prev[key] = value;
|
||||
const isSimpleObject = node.properties.every(
|
||||
(propertyNode) => propertyNode.type === 'ObjectProperty',
|
||||
);
|
||||
if (isSimpleObject) {
|
||||
// simple object: { key1, key2, key3 }
|
||||
ret = node.properties.reduce<Dict>((prev, propertyNode) => {
|
||||
if (propertyNode.type === 'ObjectProperty') {
|
||||
const key = keyNode2value(propertyNode.key);
|
||||
const value = node2value(propertyNode.value, isWrapCode);
|
||||
prev[key] = value; // key 可能是字符串,也可能是数字
|
||||
}
|
||||
return prev;
|
||||
}, {});
|
||||
} else {
|
||||
// mixed object, object property maybe SpreadElement or ObjectMethod, e.g. { key1, fn() {}, ...obj1 }
|
||||
ret = expression2code(node);
|
||||
if (wrapCode) {
|
||||
ret = wrapCode(ret);
|
||||
}
|
||||
// FIXME: property is a SpreadElement
|
||||
return prev;
|
||||
}, {});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'ArrayExpression': {
|
||||
ret = node.elements.map((elementNode) => node2value(elementNode, hasExpressionWrapper));
|
||||
// FIXME: 有可能会解析失败
|
||||
ret = node.elements.map((elementNode) => node2value(elementNode, isWrapCode));
|
||||
break;
|
||||
}
|
||||
default:
|
||||
@@ -214,7 +213,7 @@ export function node2value(node: t.Node, hasExpressionWrapper = true): any {
|
||||
}
|
||||
|
||||
/**
|
||||
* jsx 属性值节点转为 js value
|
||||
* jsx prop value 节点转为 js value
|
||||
*/
|
||||
export function jsxAttributeValueNode2value(node: t.Node): any {
|
||||
// e.g. <Checkbox checked /> 此时没有 value node
|
||||
@@ -232,9 +231,16 @@ export function jsxAttributeValueNode2value(node: t.Node): any {
|
||||
// <Foo bar={[]}>
|
||||
ret = jsxAttributeValueNode2value(node.expression);
|
||||
break;
|
||||
default:
|
||||
case 'ArrayExpression': {
|
||||
// 数组统一处理为 code
|
||||
ret = expression2code(node);
|
||||
ret = wrapCode(ret);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
ret = node2value(node);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
|
||||
@@ -5,12 +5,12 @@ import { parse, parseExpression, ParserOptions } from '@babel/parser';
|
||||
import * as t from '@babel/types';
|
||||
import {
|
||||
logger,
|
||||
isValidObjectString,
|
||||
getVariableContent,
|
||||
isPlainObject,
|
||||
Dict,
|
||||
isWrappedCode,
|
||||
getCodeOfWrappedCode,
|
||||
} from '@music163/tango-helpers';
|
||||
import { isWrappedByExpressionContainer } from '../assert';
|
||||
|
||||
// @see https://babeljs.io/docs/en/babel-parser#pluginss
|
||||
const babelParserConfig: ParserOptions = {
|
||||
@@ -68,11 +68,7 @@ export function isValidExpressionCode(code: string) {
|
||||
* @returns
|
||||
*/
|
||||
export function code2ast(code: string): t.File {
|
||||
try {
|
||||
return parse(code, babelParserConfig);
|
||||
} catch (err) {
|
||||
logger.error('[code2ast failed!]', err);
|
||||
}
|
||||
return parse(code, babelParserConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -97,8 +93,7 @@ export function code2expression(code: string) {
|
||||
try {
|
||||
expNode = t.cloneNode(parseExpression(code, babelParserConfig), false, true);
|
||||
} catch (err) {
|
||||
logger.error('invalid code', err);
|
||||
// expNode = t.identifier('undefined');
|
||||
logger.error('code2expression failed, invalid code:', code);
|
||||
}
|
||||
return expNode;
|
||||
}
|
||||
@@ -109,9 +104,6 @@ export function code2expression(code: string) {
|
||||
* @returns File
|
||||
*/
|
||||
export function expressionCode2ast(code: string) {
|
||||
if (isWrappedByExpressionContainer(code)) {
|
||||
code = getVariableContent(code);
|
||||
}
|
||||
const node = code2expression(code);
|
||||
return t.file(t.program([t.blockStatement([t.expressionStatement(node)])]));
|
||||
}
|
||||
@@ -132,18 +124,19 @@ export function value2node(
|
||||
| t.Expression {
|
||||
let ret;
|
||||
switch (typeof value) {
|
||||
case 'number':
|
||||
ret = t.numericLiteral(value);
|
||||
break;
|
||||
case 'string':
|
||||
if (isWrappedByExpressionContainer(value)) {
|
||||
// 再检查是否是表达式容器,例如 {this.foo}, {1}
|
||||
const innerString = getVariableContent(value);
|
||||
ret = code2expression(innerString);
|
||||
if (isWrappedCode(value)) {
|
||||
// 再检查是否是代码 {{code}},例如 {{this.foo}}, {{1}}
|
||||
const innerCode = getCodeOfWrappedCode(value);
|
||||
ret = code2expression(innerCode);
|
||||
} else {
|
||||
// 否则当成字符串处理
|
||||
ret = t.stringLiteral(value);
|
||||
}
|
||||
break;
|
||||
case 'number':
|
||||
ret = t.numericLiteral(value);
|
||||
break;
|
||||
case 'boolean':
|
||||
ret = t.booleanLiteral(value);
|
||||
break;
|
||||
@@ -167,7 +160,7 @@ export function value2node(
|
||||
ret = t.identifier('undefined');
|
||||
break;
|
||||
default: {
|
||||
logger.error(`value2node: unsupport value <${value}>`);
|
||||
logger.error(`value2node: value <${value}> transform failed!`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -178,7 +171,7 @@ export function value2node(
|
||||
* 将 js 普通对象解析为 t.Node
|
||||
*/
|
||||
export function object2node(
|
||||
obj: object,
|
||||
obj: Dict,
|
||||
getValueNode: (value: any, key?: string) => t.Expression = value2node,
|
||||
) {
|
||||
if (!isPlainObject(obj)) {
|
||||
@@ -196,21 +189,23 @@ export function code2jsxAttributeValueNode(code: string) {
|
||||
return t.jsxExpressionContainer(code2expression(code));
|
||||
}
|
||||
|
||||
/**
|
||||
* FIXME: 统一处理为 code2jsxAttributeValueNode
|
||||
* 将 js value 转为 JSXAttributeValueNode
|
||||
* @param value js value, or wrapped code
|
||||
* @returns 返回 JSXAttributeValueNode,转换失败返回Node为 {undefined}
|
||||
*/
|
||||
export function value2jsxAttributeValueNode(value: any) {
|
||||
let ret;
|
||||
switch (typeof value) {
|
||||
// FIXME: 重构这个逻辑,是不是统一当成 code 处理
|
||||
case 'string': {
|
||||
if (value.length > 1) {
|
||||
value = value.trim();
|
||||
}
|
||||
if (isValidObjectString(value)) {
|
||||
// 先检查是否是对象字符串
|
||||
ret = t.jsxExpressionContainer(code2expression(value));
|
||||
} else if (isWrappedByExpressionContainer(value)) {
|
||||
// 再检查是否是表达式容器,例如 {this.foo}, {1}
|
||||
const innerString = getVariableContent(value);
|
||||
ret = t.jsxExpressionContainer(code2expression(innerString));
|
||||
if (isWrappedCode(value)) {
|
||||
const innerCode = getCodeOfWrappedCode(value);
|
||||
const node = code2expression(innerCode);
|
||||
ret = t.jsxExpressionContainer(node || t.identifier('undefined'));
|
||||
} else {
|
||||
ret = t.stringLiteral(value);
|
||||
}
|
||||
@@ -227,10 +222,12 @@ export function value2jsxChildrenValueNode(value: any) {
|
||||
let ret: t.JSXElement | t.JSXFragment | t.JSXExpressionContainer | t.JSXSpreadChild | t.JSXText;
|
||||
switch (typeof value) {
|
||||
case 'string':
|
||||
if (isWrappedByExpressionContainer(value)) {
|
||||
if (isWrappedCode(value)) {
|
||||
// 代码模式
|
||||
const innerString = getVariableContent(value);
|
||||
ret = t.jsxExpressionContainer(code2expression(innerString));
|
||||
} else {
|
||||
// 普通的文本
|
||||
ret = t.jsxText(value);
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -391,8 +391,10 @@ export function updateJSXElementAttribute(node: t.JSXElement, attrName: string,
|
||||
isExist = true;
|
||||
// @ts-ignore
|
||||
if (t.isJSXAttribute(jsxAttributeNode)) {
|
||||
// 更新 jsx 属性值 <Foo bar="1" /> 的模式
|
||||
jsxAttributeNode.value = value2jsxAttributeValueNode(attrValue);
|
||||
} else if (name === 'children') {
|
||||
// 更新 jsx children 的情况 <Foo>children</Foo>
|
||||
node.children = value2jsxChildrenValueNode(attrValue);
|
||||
}
|
||||
}
|
||||
@@ -1007,7 +1009,7 @@ export function serviceConfig2Node(payload: object) {
|
||||
});
|
||||
}
|
||||
|
||||
export function updateServiceConfigToServiceFile(ast: t.File, config: Dict<object>) {
|
||||
export function updateServiceConfigToServiceFile(ast: t.File, config: Dict<Dict>) {
|
||||
traverse(ast, {
|
||||
CallExpression(path) {
|
||||
const calleeName = keyNode2value(path.node.callee) as string;
|
||||
@@ -1015,7 +1017,7 @@ export function updateServiceConfigToServiceFile(ast: t.File, config: Dict<objec
|
||||
const configNode = path.node.arguments[0];
|
||||
|
||||
if (t.isObjectExpression(configNode)) {
|
||||
const newPropertiesNodeMap = Object.keys(config).reduce((properties, key) => {
|
||||
const newPropertiesNodeMap = Object.keys(config).reduce<Dict>((properties, key) => {
|
||||
const serviceConfig = config[key];
|
||||
const property = t.objectProperty(t.identifier(key), serviceConfig2Node(serviceConfig));
|
||||
properties[key] = property;
|
||||
@@ -1425,3 +1427,22 @@ export function traverseFile(ast: t.File) {
|
||||
imports,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* app.js 入口文件解析
|
||||
* @param ast
|
||||
*/
|
||||
export function traverseEntryFile(ast: t.File) {
|
||||
let appConfig: any;
|
||||
traverse(ast, {
|
||||
CallExpression(path) {
|
||||
const { node } = path;
|
||||
const calleeName = keyNode2value(node.callee) as string;
|
||||
if (calleeName === 'runApp') {
|
||||
appConfig = node2value(node.arguments[0]);
|
||||
path.stop();
|
||||
}
|
||||
},
|
||||
});
|
||||
return appConfig;
|
||||
}
|
||||
|
||||
@@ -1,27 +1,8 @@
|
||||
import { getVariableContent } from '@music163/tango-helpers';
|
||||
import { value2node, expression2code, isValidExpressionCode } from './ast';
|
||||
import { isWrappedByExpressionContainer } from './assert';
|
||||
import { getCodeOfWrappedCode, isWrappedCode } from '@music163/tango-helpers';
|
||||
import { value2node, expression2code, code2expression, node2value } from './ast';
|
||||
|
||||
/**
|
||||
* 将 js value 转换为代码字符串
|
||||
*/
|
||||
export function value2code(value: any) {
|
||||
const node = value2node(value);
|
||||
const code = expression2code(node);
|
||||
return code;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否是字符串代码
|
||||
* @param code
|
||||
* @returns
|
||||
*/
|
||||
function isStringCode(code: string) {
|
||||
return /^".*"$/.test(code?.trim());
|
||||
}
|
||||
|
||||
/**
|
||||
* js value 转为表达式代码
|
||||
* js value 转为代码字符串
|
||||
* @example 1 => 1
|
||||
* @example hello => "hello"
|
||||
* @example { foo: bar } => {{ foo: bar }}
|
||||
@@ -30,34 +11,56 @@ function isStringCode(code: string) {
|
||||
* @param val js value
|
||||
* @returns 表达式代码
|
||||
*/
|
||||
export function value2expressionCode(val: any) {
|
||||
if (!val) return '';
|
||||
export function value2code(val: any) {
|
||||
if (val === undefined) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (val === null) {
|
||||
return 'null';
|
||||
}
|
||||
|
||||
let ret;
|
||||
|
||||
switch (typeof val) {
|
||||
case 'string': {
|
||||
if (isValidExpressionCode(val)) {
|
||||
ret = val;
|
||||
} else if (isWrappedByExpressionContainer(val, false)) {
|
||||
ret = getVariableContent(val);
|
||||
} else if (isStringCode(val)) {
|
||||
ret = val;
|
||||
if (isWrappedCode(val)) {
|
||||
ret = getCodeOfWrappedCode(val);
|
||||
} else {
|
||||
ret = `"${val}"`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'boolean':
|
||||
case 'function':
|
||||
case 'number':
|
||||
ret = String(val);
|
||||
break;
|
||||
case 'object':
|
||||
ret = value2code(val);
|
||||
break;
|
||||
default:
|
||||
ret = '';
|
||||
default: {
|
||||
// other cases, including array, object, null, undefined
|
||||
const node = value2node(val);
|
||||
ret = expression2code(node);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
export const value2expressionCode = value2code;
|
||||
|
||||
/**
|
||||
* 代码字符串转为具体的 js value
|
||||
* @example `() => {}` 返回 undefined
|
||||
*
|
||||
* @param rawCode 代码字符串
|
||||
* @returns 返回解析后的 js value,包括:string, number, boolean, simpleObject, simpleArray
|
||||
*/
|
||||
export function code2value(rawCode: string) {
|
||||
const node = code2expression(rawCode);
|
||||
const value = node2value(node);
|
||||
if (isWrappedCode(value)) {
|
||||
// 能转的就转,转不能的就返回空
|
||||
return;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ export function namesToImportDeclarations(
|
||||
names: string[],
|
||||
nameMap: Dict<IImportSpecifierSourceData>,
|
||||
) {
|
||||
const map = {};
|
||||
const map: Dict = {};
|
||||
names.forEach((name) => {
|
||||
const mod = nameMap[name];
|
||||
if (mod) {
|
||||
|
||||
@@ -1,22 +1,26 @@
|
||||
import * as t from '@babel/types';
|
||||
import {
|
||||
ComponentPropType,
|
||||
ComponentPrototypeType,
|
||||
IComponentProp,
|
||||
IComponentPrototype,
|
||||
Dict,
|
||||
isNil,
|
||||
logger,
|
||||
uuid,
|
||||
isWrappedCode,
|
||||
getCodeOfWrappedCode,
|
||||
wrapCodeWithJSXExpressionContainer,
|
||||
} from '@music163/tango-helpers';
|
||||
import { getRelativePath, isFilepath } from './string';
|
||||
import type { IImportDeclarationPayload, IImportSpecifierData } from '../types';
|
||||
import { code2expression } from './ast';
|
||||
import { isWrappedByExpressionContainer } from './assert';
|
||||
import { value2code } from './code-helpers';
|
||||
|
||||
export function prototype2importDeclarationData(
|
||||
prototype: ComponentPrototypeType,
|
||||
prototype: IComponentPrototype,
|
||||
relativeFilepath?: string,
|
||||
): { source: string; specifiers: IImportSpecifierData[] } {
|
||||
let source = prototype.package;
|
||||
const isSnippet = prototype.type === 'snippet';
|
||||
if (relativeFilepath && isFilepath(source)) {
|
||||
source = getRelativePath(relativeFilepath, source);
|
||||
}
|
||||
@@ -32,12 +36,15 @@ export function prototype2importDeclarationData(
|
||||
type: 'ImportDefaultSpecifier',
|
||||
});
|
||||
} else {
|
||||
[prototype.name, ...(prototype.relatedImports || [])].forEach((item) => {
|
||||
specifiers.push({
|
||||
localName: item,
|
||||
type: 'ImportSpecifier',
|
||||
});
|
||||
});
|
||||
// 忽略代码片段 name
|
||||
[...(isSnippet ? [] : [prototype.name]), ...(prototype.relatedImports || [])].forEach(
|
||||
(item) => {
|
||||
specifiers.push({
|
||||
localName: item,
|
||||
type: 'ImportSpecifier',
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -51,7 +58,7 @@ export function prototype2importDeclarationData(
|
||||
* @deprecated
|
||||
*/
|
||||
export function getImportDeclarationPayloadByPrototype(
|
||||
prototype: ComponentPrototypeType,
|
||||
prototype: IComponentPrototype,
|
||||
relativeFilepath?: string,
|
||||
): IImportDeclarationPayload {
|
||||
let defaultSpecifier;
|
||||
@@ -84,48 +91,56 @@ export function getImportDeclarationPayloadByPrototype(
|
||||
|
||||
/**
|
||||
* 基于 key-value 生成 prop={value} 字符串
|
||||
* @param key
|
||||
* @param value
|
||||
* @example { name: 'foo', initValue: false } >> name={false}
|
||||
* @example { name: 'foo', initValue: 1 } >> name={1}
|
||||
* @example { name: 'foo', initValue: () => {} } >> name={()=>{}}
|
||||
* @example { name: 'foo', initValue: { foo: 'bar' } } >> name={{ foo: 'bar' }}
|
||||
* @example { name: 'foo', initValue: [{ foo: 'bar' }] } >> name={[{ foo: 'bar' }]}
|
||||
* @example { name: 'foo', initValue: 'bar' } >> name="bar"
|
||||
* @example { name: 'foo', initValue: '{() => {}}' } >> name={()=>{}}
|
||||
* @example { name: 'foo', initValue: '{{() => {}}}' } >> name={() => {}}
|
||||
* @example { name: 'foo', initValue: '{bar}' } >> name={bar}
|
||||
* @returns
|
||||
*/
|
||||
function getPropKeyValuePair(item: ComponentPropType, generateValue: (...args: any[]) => string) {
|
||||
export function propDataToKeyValueString(
|
||||
item: IComponentProp,
|
||||
generateValue?: (...args: any[]) => string,
|
||||
) {
|
||||
const key = item.name;
|
||||
|
||||
let value = item.initValue;
|
||||
|
||||
if (!value && item.autoInitValue) {
|
||||
value = generateValue(3);
|
||||
value = generateValue?.(3) || uuid(key, 3);
|
||||
}
|
||||
|
||||
if (isNil(value)) {
|
||||
if (value === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (typeof value) {
|
||||
case 'number':
|
||||
case 'boolean': {
|
||||
value = `{${value}}`;
|
||||
case 'boolean':
|
||||
case 'function': {
|
||||
value = wrapCodeWithJSXExpressionContainer(String(value));
|
||||
break;
|
||||
}
|
||||
case 'object': {
|
||||
// TIP: bugfix 如果 object 里有 jsx 或者 function 会失败
|
||||
try {
|
||||
value = `{${JSON.stringify(value)}}`;
|
||||
value = wrapCodeWithJSXExpressionContainer(value2code(value));
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'function': {
|
||||
value = `{${(value as object).toString()}}`;
|
||||
break;
|
||||
}
|
||||
case 'string': {
|
||||
if (!isWrappedByExpressionContainer(value)) {
|
||||
// 不是变量字符串
|
||||
value = `"${value}"`;
|
||||
if (isWrappedCode(value)) {
|
||||
const innerCode = getCodeOfWrappedCode(value);
|
||||
value = wrapCodeWithJSXExpressionContainer(innerCode);
|
||||
} else if (isWrappedByExpressionContainer(value)) {
|
||||
// TIP: 兼容旧版逻辑,如果是变量字符串,无需处理
|
||||
} else {
|
||||
// 如果是变量字符串,无需处理
|
||||
value = `"${value}"`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -141,25 +156,25 @@ function getPropKeyValuePair(item: ComponentPropType, generateValue: (...args: a
|
||||
* @param prototype
|
||||
* @param extraProps
|
||||
*/
|
||||
export function prototype2code(prototype: ComponentPrototypeType, extraProps?: Dict) {
|
||||
export function prototype2code(prototype: IComponentPrototype, extraProps?: Dict) {
|
||||
let code;
|
||||
switch (prototype.type) {
|
||||
case 'snippet':
|
||||
code = prototype.initChildren || prototype.defaultChildren;
|
||||
break;
|
||||
default: {
|
||||
const propList: ComponentPropType[] = extraProps
|
||||
const propList: IComponentProp[] = extraProps
|
||||
? Object.keys(extraProps).map((key) => ({
|
||||
name: key,
|
||||
initValue: extraProps[key],
|
||||
}))
|
||||
: [];
|
||||
// merge extraProps to props
|
||||
const props = [...prototype.props, ...propList];
|
||||
const props = [...(prototype.props || []), ...propList];
|
||||
|
||||
const keys =
|
||||
props.reduce((acc, item) => {
|
||||
const pair = getPropKeyValuePair(item, (fractionDigits: number) =>
|
||||
const pair = propDataToKeyValueString(item, (fractionDigits: number) =>
|
||||
uuid(prototype.name, fractionDigits),
|
||||
);
|
||||
return pair ? ` ${acc} ${pair}` : acc;
|
||||
@@ -186,7 +201,7 @@ export function prototype2code(prototype: ComponentPrototypeType, extraProps?: D
|
||||
* @param prototype 组件的配置信息
|
||||
* @param props 额外的属性集
|
||||
*/
|
||||
export function prototype2jsxElement(prototype: ComponentPrototypeType, props?: Dict) {
|
||||
export function prototype2jsxElement(prototype: IComponentPrototype, props?: Dict) {
|
||||
const code = prototype2code(prototype, props);
|
||||
return code2expression(code) as t.JSXElement;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { parseDndId, uuid } from '@music163/tango-helpers';
|
||||
import { Dict, parseDndId, uuid } from '@music163/tango-helpers';
|
||||
|
||||
// { id: 1, component, props: { id: 1 }, children: [ { id: 2, component } ] }
|
||||
export function deepCloneNode(obj: any, component?: string): any {
|
||||
function deepCloneObject() {
|
||||
const target = {};
|
||||
const target: Dict = {};
|
||||
for (const key in obj) {
|
||||
if (Object.hasOwn(obj, key)) {
|
||||
target[key] = deepCloneNode(obj[key], component);
|
||||
|
||||
@@ -75,15 +75,20 @@ export function inferFileType(filename: string): FileType {
|
||||
/**
|
||||
* 判断组件名是否合法
|
||||
* @example Button -> valid
|
||||
* @example div -> invalid
|
||||
* @example isStrict ? div -> invalid : div -> valid
|
||||
* @param name
|
||||
* @param isStrict 是否严格模式(严格模式不匹配原生标签)
|
||||
* @returns
|
||||
*/
|
||||
export function isValidComponentName(name: string) {
|
||||
export function isValidComponentName(name: string, isStrict = false) {
|
||||
if (!name) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isStrict) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const firstChar = name.charAt(0);
|
||||
return firstChar === firstChar.toUpperCase();
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ export class TangoComponentsEntryModule extends TangoModule {
|
||||
|
||||
constructor(workspace: IWorkspace, props: IFileConfig) {
|
||||
super(workspace, props, false);
|
||||
this.update(props.code, false, false);
|
||||
this.update(props.code, true, false);
|
||||
makeObservable(this, {
|
||||
_code: observable,
|
||||
_cleanCode: observable,
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { action, computed, makeObservable, observable, toJS } from 'mobx';
|
||||
import { IWorkspace } from './interfaces';
|
||||
import { MenuDataType } from '@music163/tango-helpers';
|
||||
|
||||
export type SimulatorNameType = 'desktop' | 'phone';
|
||||
|
||||
export type DesignerViewType = 'design' | 'code';
|
||||
export type DesignerViewType = 'design' | 'code' | 'dual';
|
||||
|
||||
interface ISimulatorType {
|
||||
name: SimulatorNameType;
|
||||
@@ -19,7 +20,15 @@ interface IViewportBounding {
|
||||
interface IDesignerOptions {
|
||||
workspace: IWorkspace;
|
||||
simulator?: SimulatorNameType | ISimulatorType;
|
||||
/**
|
||||
* 菜单配置
|
||||
*/
|
||||
menuData: MenuDataType;
|
||||
activeSidebarPanel?: string;
|
||||
/**
|
||||
* 默认激活的视图模式
|
||||
*/
|
||||
activeView?: DesignerViewType;
|
||||
}
|
||||
|
||||
const ISimulatorTypes: Record<string, ISimulatorType> = {
|
||||
@@ -64,6 +73,26 @@ export class Designer {
|
||||
*/
|
||||
_showSmartWizard = false;
|
||||
|
||||
/**
|
||||
* 是否显示添加组件面板
|
||||
*/
|
||||
_showAddComponentPopover = false;
|
||||
|
||||
/**
|
||||
* 添加组件面板的位置
|
||||
*/
|
||||
_addComponentPopoverPosition = { clientX: 0, clientY: 0 };
|
||||
|
||||
/**
|
||||
* 是否显示右键菜单
|
||||
*/
|
||||
_showContextMenu = false;
|
||||
|
||||
/**
|
||||
* 右键菜单在 iframe 上的位置
|
||||
*/
|
||||
_contextMenuPosition = { clientX: 0, clientY: 0 };
|
||||
|
||||
/**
|
||||
* 是否显示右侧面板
|
||||
*/
|
||||
@@ -75,9 +104,9 @@ export class Designer {
|
||||
_isPreview = false;
|
||||
|
||||
/**
|
||||
* 默认展开的侧边栏
|
||||
* 菜单列表
|
||||
*/
|
||||
defaultActiveSidebarPanel?: string;
|
||||
_menuData?: MenuDataType = null;
|
||||
|
||||
private readonly workspace: IWorkspace;
|
||||
|
||||
@@ -109,10 +138,39 @@ export class Designer {
|
||||
return this._showRightPanel;
|
||||
}
|
||||
|
||||
get showAddComponentPopover() {
|
||||
return this._showAddComponentPopover;
|
||||
}
|
||||
|
||||
get addComponentPopoverPosition() {
|
||||
return this._addComponentPopoverPosition;
|
||||
}
|
||||
|
||||
get showContextMenu() {
|
||||
return this._showContextMenu;
|
||||
}
|
||||
|
||||
get contextMenuPosition() {
|
||||
return this._contextMenuPosition;
|
||||
}
|
||||
|
||||
get menuData() {
|
||||
return this._menuData ?? ([] as MenuDataType);
|
||||
}
|
||||
|
||||
constructor(options: IDesignerOptions) {
|
||||
this.workspace = options.workspace;
|
||||
|
||||
const { simulator, activeSidebarPanel: defaultActiveSidebarPanel } = options;
|
||||
const {
|
||||
simulator,
|
||||
menuData,
|
||||
activeSidebarPanel: defaultActiveSidebarPanel,
|
||||
activeView: defaultActiveView,
|
||||
} = options;
|
||||
|
||||
if (menuData) {
|
||||
this.setMenuData(menuData);
|
||||
}
|
||||
|
||||
// 默认设计器模式
|
||||
if (simulator) {
|
||||
@@ -124,6 +182,11 @@ export class Designer {
|
||||
this.setActiveSidebarPanel(defaultActiveSidebarPanel);
|
||||
}
|
||||
|
||||
// 默认激活的视图
|
||||
if (defaultActiveView) {
|
||||
this.setActiveView(defaultActiveView);
|
||||
}
|
||||
|
||||
makeObservable(this, {
|
||||
_simulator: observable,
|
||||
_viewport: observable,
|
||||
@@ -131,6 +194,11 @@ export class Designer {
|
||||
_activeSidebarPanel: observable,
|
||||
_showSmartWizard: observable,
|
||||
_showRightPanel: observable,
|
||||
_showAddComponentPopover: observable,
|
||||
_addComponentPopoverPosition: observable,
|
||||
_showContextMenu: observable,
|
||||
_contextMenuPosition: observable,
|
||||
_menuData: observable,
|
||||
_isPreview: observable,
|
||||
simulator: computed,
|
||||
viewport: computed,
|
||||
@@ -139,6 +207,11 @@ export class Designer {
|
||||
isPreview: computed,
|
||||
showRightPanel: computed,
|
||||
showSmartWizard: computed,
|
||||
showAddComponentPopover: computed,
|
||||
addComponentPopoverPosition: computed,
|
||||
showContextMenu: computed,
|
||||
contextMenuPosition: computed,
|
||||
menuData: computed,
|
||||
setSimulator: action,
|
||||
setViewport: action,
|
||||
setActiveView: action,
|
||||
@@ -147,6 +220,8 @@ export class Designer {
|
||||
toggleRightPanel: action,
|
||||
toggleSmartWizard: action,
|
||||
toggleIsPreview: action,
|
||||
toggleAddComponentPopover: action,
|
||||
toggleContextMenu: action,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -164,6 +239,13 @@ export class Designer {
|
||||
|
||||
setActiveView(view: DesignerViewType) {
|
||||
this._activeView = view;
|
||||
if (view === 'dual') {
|
||||
this.setActiveSidebarPanel('');
|
||||
this.toggleRightPanel(false);
|
||||
this.toggleIsPreview(true);
|
||||
} else if (view === 'design') {
|
||||
this.toggleIsPreview(false);
|
||||
}
|
||||
}
|
||||
|
||||
setActiveSidebarPanel(panel: string) {
|
||||
@@ -173,6 +255,11 @@ export class Designer {
|
||||
this._activeSidebarPanel = '';
|
||||
}
|
||||
}
|
||||
|
||||
setMenuData(menuData: MenuDataType) {
|
||||
this._menuData = menuData;
|
||||
}
|
||||
|
||||
closeSidebarPanel() {
|
||||
this._activeSidebarPanel = '';
|
||||
}
|
||||
@@ -185,10 +272,37 @@ export class Designer {
|
||||
this._showRightPanel = value ?? !this._showRightPanel;
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示添加组件面板
|
||||
* @param value 是否显示
|
||||
* @param position 坐标
|
||||
*/
|
||||
toggleAddComponentPopover(
|
||||
value: boolean,
|
||||
position: {
|
||||
clientX: number;
|
||||
clientY: number;
|
||||
} = this.addComponentPopoverPosition,
|
||||
) {
|
||||
this._showAddComponentPopover = value;
|
||||
this._addComponentPopoverPosition = position;
|
||||
}
|
||||
|
||||
toggleIsPreview(value: boolean) {
|
||||
this._isPreview = value ?? !this._isPreview;
|
||||
if (value) {
|
||||
this.workspace.selectSource.clear();
|
||||
}
|
||||
}
|
||||
|
||||
toggleContextMenu(
|
||||
value: boolean,
|
||||
position: {
|
||||
clientX: number;
|
||||
clientY: number;
|
||||
} = this.contextMenuPosition,
|
||||
) {
|
||||
this._showContextMenu = value;
|
||||
this._contextMenuPosition = position;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { traverseEntryFile } from '../helpers';
|
||||
import { IFileConfig } from '../types';
|
||||
import { IWorkspace } from './interfaces';
|
||||
import { TangoModule } from './module';
|
||||
|
||||
export class AppEntryModule extends TangoModule {
|
||||
routerType: string;
|
||||
|
||||
constructor(workspace: IWorkspace, props: IFileConfig) {
|
||||
super(workspace, props, false);
|
||||
this.update(props.code, true, false);
|
||||
}
|
||||
|
||||
_analysisAst() {
|
||||
const config = traverseEntryFile(this.ast);
|
||||
this.routerType = config?.router?.type;
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,16 @@ export class TangoFile {
|
||||
*/
|
||||
lastModified: number;
|
||||
|
||||
/**
|
||||
* 文件解析是否出错
|
||||
*/
|
||||
isError: boolean;
|
||||
|
||||
/**
|
||||
* 文件解析错误消息
|
||||
*/
|
||||
errorMessage: string;
|
||||
|
||||
_code: string;
|
||||
_cleanCode: string;
|
||||
|
||||
@@ -40,6 +50,7 @@ export class TangoFile {
|
||||
this.filename = props.filename;
|
||||
this.type = props.type;
|
||||
this.lastModified = Date.now();
|
||||
this.isError = false;
|
||||
|
||||
// 这里主要是为了解决 umi ts 编译错误的问题,@see https://github.com/umijs/umi/issues/7594
|
||||
if (isSyncCode) {
|
||||
|
||||
@@ -11,6 +11,8 @@ export enum HistoryMessage {
|
||||
ReplaceNode = 'replaceNode',
|
||||
CloneNode = 'cloneNode',
|
||||
InsertNode = 'insertNode',
|
||||
InsertBeforeNode = 'insertBeforeNode',
|
||||
InsertAfterNode = 'insertAfterNode',
|
||||
DropNode = 'dropNode',
|
||||
UpdateAttribute = 'updateAttribute',
|
||||
UpdateCode = 'updateCode',
|
||||
@@ -56,7 +58,7 @@ export class TangoHistory {
|
||||
}
|
||||
|
||||
get couldBack() {
|
||||
return this._records.length > 0 && this._index > -1;
|
||||
return this._records.length > 0 && this._index > 0;
|
||||
}
|
||||
|
||||
get couldForward() {
|
||||
|
||||
@@ -7,6 +7,7 @@ export * from './drag-source';
|
||||
export * from './history';
|
||||
export * from './file';
|
||||
export * from './module';
|
||||
export * from './entry-module';
|
||||
export * from './route-module';
|
||||
export * from './service-module';
|
||||
export * from './store-module';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ComponentPrototypeType, Dict, ITangoConfigJson } from '@music163/tango-helpers';
|
||||
import { IComponentPrototype, Dict, ITangoConfigJson } from '@music163/tango-helpers';
|
||||
import { TangoHistory } from './history';
|
||||
import { SelectSource } from './select-source';
|
||||
import { DragSource } from './drag-source';
|
||||
@@ -10,12 +10,14 @@ import {
|
||||
IPageConfigData,
|
||||
IImportSpecifierSourceData,
|
||||
IImportSpecifierData,
|
||||
IFileError,
|
||||
} from '../types';
|
||||
import { TangoFile, TangoJsonFile } from './file';
|
||||
import { TangoRouteModule } from './route-module';
|
||||
import { TangoStoreModule } from './store-module';
|
||||
import { TangoServiceModule } from './service-module';
|
||||
import { IdGenerator } from '../helpers';
|
||||
import { AppEntryModule } from './entry-module';
|
||||
|
||||
export interface IViewFile {
|
||||
readonly workspace: IWorkspace;
|
||||
@@ -141,7 +143,7 @@ export interface IWorkspace {
|
||||
dragSource: DragSource;
|
||||
|
||||
files: Map<string, TangoFile>;
|
||||
componentPrototypes: Map<string, ComponentPrototypeType>;
|
||||
componentPrototypes: Map<string, IComponentPrototype>;
|
||||
|
||||
entry: string;
|
||||
activeFile: string;
|
||||
@@ -152,6 +154,10 @@ export interface IWorkspace {
|
||||
* 解析后的 tango.config.json 文件,如果要获取项目配置,推荐使用 projectConfig 获取
|
||||
*/
|
||||
tangoConfigJson: TangoJsonFile;
|
||||
/**
|
||||
* app.js 入口文件解析后的模块
|
||||
*/
|
||||
appEntryModule: AppEntryModule;
|
||||
/**
|
||||
* 解析后的路由模块
|
||||
*/
|
||||
@@ -171,8 +177,8 @@ export interface IWorkspace {
|
||||
setActiveRoute: (path: string) => void;
|
||||
setActiveFile: (filename: string) => void;
|
||||
|
||||
setComponentPrototypes: (prototypes: Record<string, ComponentPrototypeType>) => void;
|
||||
getPrototype: (name: string | ComponentPrototypeType) => ComponentPrototypeType;
|
||||
setComponentPrototypes: (prototypes: Record<string, IComponentPrototype>) => void;
|
||||
getPrototype: (name: string | IComponentPrototype) => IComponentPrototype;
|
||||
|
||||
// ----------------- 文件操作 -----------------
|
||||
addFiles: (files: IFileConfig[]) => void;
|
||||
@@ -187,7 +193,18 @@ export interface IWorkspace {
|
||||
renameFile: (oldFilename: string, newFilename: string) => void;
|
||||
renameFolder: (oldFoldername: string, newFoldername: string) => void;
|
||||
|
||||
updateFile: (filename: string, code: string, shouldFormatCode?: boolean) => void;
|
||||
/**
|
||||
* 更新文件
|
||||
* @param filename 文件名
|
||||
* @param code 代码
|
||||
* @param isSyncAst 是否同步 ast
|
||||
*/
|
||||
updateFile: (filename: string, code: string, isSyncAst?: boolean) => void;
|
||||
|
||||
/**
|
||||
* 检查并同步文件的 ast
|
||||
*/
|
||||
syncFiles: () => void;
|
||||
|
||||
listFiles: () => Record<string, string>;
|
||||
getFile: (filename: string) => TangoFile;
|
||||
@@ -204,16 +221,12 @@ export interface IWorkspace {
|
||||
cloneSelectedNode: () => void;
|
||||
copySelectedNode: () => void;
|
||||
pasteSelectedNode: () => void;
|
||||
insertToSelectedNode: (childNameOrPrototype: string | ComponentPrototypeType) => void;
|
||||
insertToSelectedNode: (childNameOrPrototype: string | IComponentPrototype) => void;
|
||||
insertBeforeSelectedNode: (sourceNameOrPrototype: string | IComponentPrototype) => void;
|
||||
insertAfterSelectedNode: (sourceNameOrPrototype: string | IComponentPrototype) => void;
|
||||
dropNode: () => void;
|
||||
insertToNode: (
|
||||
targetNodeId: string,
|
||||
sourceNameOrPrototype: string | ComponentPrototypeType,
|
||||
) => void;
|
||||
replaceNode: (
|
||||
targetNodeId: string,
|
||||
sourceNameOrPrototype: string | ComponentPrototypeType,
|
||||
) => void;
|
||||
insertToNode: (targetNodeId: string, sourceNameOrPrototype: string | IComponentPrototype) => void;
|
||||
replaceNode: (targetNodeId: string, sourceNameOrPrototype: string | IComponentPrototype) => void;
|
||||
updateSelectedNodeAttributes: (
|
||||
attributes: Record<string, any>,
|
||||
relatedImports?: string[],
|
||||
@@ -289,9 +302,20 @@ export interface IWorkspace {
|
||||
* 解析后的项目配置信息
|
||||
*/
|
||||
get projectConfig(): ITangoConfigJson;
|
||||
/**
|
||||
* 当前活动的视图文件
|
||||
*/
|
||||
get activeViewModule(): IViewFile;
|
||||
get pages(): any[];
|
||||
get bizComps(): string[];
|
||||
get baseComps(): string[];
|
||||
get localComps(): string[];
|
||||
get fileErrors(): IFileError[];
|
||||
/**
|
||||
* 是否是有效的项目
|
||||
* - 包含 tango.config.json
|
||||
* - 包含视图模块
|
||||
* - 没有文件错误
|
||||
*/
|
||||
get isValid(): boolean;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,11 @@ import { IWorkspace } from './interfaces';
|
||||
export class TangoModule extends TangoFile {
|
||||
ast: t.File;
|
||||
|
||||
/**
|
||||
* ast 是否与 code 保持同步
|
||||
*/
|
||||
isAstSynced: boolean;
|
||||
|
||||
/**
|
||||
* 导入的依赖列表
|
||||
*/
|
||||
@@ -33,23 +38,53 @@ export class TangoModule extends TangoFile {
|
||||
/**
|
||||
* 基于最新的 ast 进行同步
|
||||
* @param code 如果传入 code,则基于 code 进行同步
|
||||
* @param isFormatCode 是否格式化代码
|
||||
* @param refreshWorkspace 是否刷新 workspace
|
||||
* @param isSyncAst 是否同步 ast
|
||||
* @param isRefreshWorkspace 是否刷新 workspace
|
||||
*/
|
||||
update(code?: string, isFormatCode = true, refreshWorkspace = true) {
|
||||
update(code?: string, isSyncAst = true, isRefreshWorkspace = true) {
|
||||
this.lastModified = Date.now();
|
||||
if (isNil(code)) {
|
||||
this._syncByAst();
|
||||
} else {
|
||||
this._syncByCode(code, isFormatCode);
|
||||
|
||||
try {
|
||||
if (isNil(code)) {
|
||||
this._syncByAst();
|
||||
} else {
|
||||
this._syncByCode(code, isSyncAst);
|
||||
}
|
||||
|
||||
if (isSyncAst) {
|
||||
this._analysisAst();
|
||||
}
|
||||
|
||||
this.isAstSynced = isSyncAst;
|
||||
this.isError = false;
|
||||
this.errorMessage = undefined;
|
||||
|
||||
this.workspace.onFilesChange([this.filename]);
|
||||
if (isRefreshWorkspace) {
|
||||
this.workspace.refresh([this.filename]);
|
||||
}
|
||||
} catch (err: any) {
|
||||
this.isError = true;
|
||||
this.errorMessage = err.message;
|
||||
}
|
||||
}
|
||||
|
||||
this._analysisAst();
|
||||
|
||||
this.workspace.onFilesChange([this.filename]);
|
||||
|
||||
if (refreshWorkspace) {
|
||||
this.workspace.refresh([this.filename]);
|
||||
/**
|
||||
* 基于当前的代码重新生成 ast
|
||||
*/
|
||||
updateAst() {
|
||||
if (!this.isAstSynced) {
|
||||
try {
|
||||
this.ast = code2ast(this._code);
|
||||
this._analysisAst();
|
||||
this.isAstSynced = true;
|
||||
this.isError = false;
|
||||
this.errorMessage = undefined;
|
||||
} catch (err: any) {
|
||||
this.isAstSynced = false;
|
||||
this.isError = true;
|
||||
this.errorMessage = err.message;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,22 +110,26 @@ export class TangoModule extends TangoFile {
|
||||
/**
|
||||
* 基于输入的源码进行同步
|
||||
* @param code 源码
|
||||
* @param isFormatCode 是否格式化代码
|
||||
* @param isSyncAst 是否同步 ast
|
||||
* @returns
|
||||
*/
|
||||
_syncByCode(code: string, isFormatCode = true) {
|
||||
_syncByCode(code: string, isSyncAst = true) {
|
||||
if (code === this._code) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 提前格式化代码
|
||||
if (isFormatCode) {
|
||||
try {
|
||||
code = formatCode(code);
|
||||
} catch (err) {
|
||||
// err ignored, format code failed
|
||||
}
|
||||
|
||||
this._code = code;
|
||||
this._cleanCode = code;
|
||||
this.ast = code2ast(code);
|
||||
if (isSyncAst) {
|
||||
this.ast = code2ast(code);
|
||||
}
|
||||
}
|
||||
|
||||
_analysisAst() {
|
||||
@@ -105,14 +144,17 @@ export class TangoModule extends TangoFile {
|
||||
export class TangoJsModule extends TangoModule {
|
||||
constructor(workspace: IWorkspace, props: IFileConfig) {
|
||||
super(workspace, props, false);
|
||||
this.update(props.code, false, false);
|
||||
this.update(props.code, true, false);
|
||||
|
||||
makeObservable(this, {
|
||||
_code: observable,
|
||||
_cleanCode: observable,
|
||||
isError: observable,
|
||||
errorMessage: observable,
|
||||
code: computed,
|
||||
cleanCode: computed,
|
||||
update: action,
|
||||
updateAst: action,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { action, computed, makeObservable, observable, toJS } from 'mobx';
|
||||
import { logger } from '@music163/tango-helpers';
|
||||
import {
|
||||
traverseRouteFile,
|
||||
addRouteToRouteFile,
|
||||
@@ -27,10 +28,13 @@ export class TangoRouteModule extends TangoModule {
|
||||
_routes: observable,
|
||||
_code: observable,
|
||||
_cleanCode: observable,
|
||||
isError: observable,
|
||||
errorMessage: observable,
|
||||
routes: computed,
|
||||
code: computed,
|
||||
cleanCode: computed,
|
||||
update: action,
|
||||
updateAst: action,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -74,7 +78,7 @@ export class TangoRouteModule extends TangoModule {
|
||||
*/
|
||||
removeRoute(route: string) {
|
||||
if (route === '/') {
|
||||
console.warn('index route should not be removed!');
|
||||
logger.warn('index route should not be removed!');
|
||||
return;
|
||||
}
|
||||
const record = this.getRouteByRoutePath(route);
|
||||
|
||||
@@ -109,6 +109,20 @@ export class SelectSource {
|
||||
this._start = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 选中当前选中节点的父节点
|
||||
*/
|
||||
selectParent() {
|
||||
const parents = this.first?.parents || [];
|
||||
if (parents.length) {
|
||||
const [parent, ...rest] = parents;
|
||||
this.select({
|
||||
...parent,
|
||||
parents: rest,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setStart(data: StartDataType) {
|
||||
this._start = data;
|
||||
}
|
||||
|
||||
@@ -49,11 +49,14 @@ export class TangoServiceModule extends TangoModule {
|
||||
_baseConfig: observable,
|
||||
_code: observable,
|
||||
_cleanCode: observable,
|
||||
isError: observable,
|
||||
errorMessage: observable,
|
||||
serviceFunctions: computed,
|
||||
baseConfig: computed,
|
||||
cleanCode: computed,
|
||||
code: computed,
|
||||
update: action,
|
||||
updateAst: action,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -31,10 +31,13 @@ export class TangoStoreEntryModule extends TangoModule {
|
||||
_stores: observable,
|
||||
_code: observable,
|
||||
_cleanCode: observable,
|
||||
isError: observable,
|
||||
errorMessage: observable,
|
||||
stores: computed,
|
||||
code: computed,
|
||||
cleanCode: computed,
|
||||
update: action,
|
||||
updateAst: action,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@ function nodeListToTreeData(list: ITangoViewNodeData[]) {
|
||||
*/
|
||||
export class TangoViewModule extends TangoModule implements IViewFile {
|
||||
// 解析为树结构的 jsxNodes 数组
|
||||
_nodesTree: ITangoViewNodeData[];
|
||||
_nodesTree: ITangoViewNodeData[] = [];
|
||||
/**
|
||||
* 通过导入组件名查找组件来自的包
|
||||
*/
|
||||
@@ -138,10 +138,14 @@ export class TangoViewModule extends TangoModule implements IViewFile {
|
||||
_code: observable,
|
||||
_cleanCode: observable,
|
||||
|
||||
isError: observable,
|
||||
errorMessage: observable,
|
||||
|
||||
code: computed,
|
||||
cleanCode: computed,
|
||||
|
||||
update: action,
|
||||
updateAst: action,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -320,7 +324,7 @@ export class TangoViewModule extends TangoModule implements IViewFile {
|
||||
updateNodeAttributes(nodeId: string, config: Record<string, any>, relatedImports?: string[]) {
|
||||
if (relatedImports && relatedImports.length) {
|
||||
// 导入依赖的组件
|
||||
const newImportData = relatedImports.reduce((prev, name) => {
|
||||
const newImportData = relatedImports.reduce<Dict<IImportSpecifierData[]>>((prev, name) => {
|
||||
const proto = this.workspace.getPrototype(name);
|
||||
const { source, specifiers } = prototype2importDeclarationData(proto, this.filename);
|
||||
const existSpecifiers: IImportSpecifierData[] = prev[source];
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { action, computed, makeObservable, observable } from 'mobx';
|
||||
import { JSXElement } from '@babel/types';
|
||||
import {
|
||||
ComponentPrototypeType,
|
||||
IComponentPrototype,
|
||||
Dict,
|
||||
ITangoConfigJson,
|
||||
hasFileExtension,
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
parseServiceVariablePath,
|
||||
parseStoreVariablePath,
|
||||
uniq,
|
||||
setValue,
|
||||
} from '@music163/tango-helpers';
|
||||
import {
|
||||
prototype2jsxElement,
|
||||
@@ -24,10 +25,10 @@ import {
|
||||
import { DropMethod } from './drop-target';
|
||||
import { HistoryMessage, TangoHistory } from './history';
|
||||
import { TangoNode } from './node';
|
||||
import { TangoJsModule } from './module';
|
||||
import { TangoJsModule, TangoModule } from './module';
|
||||
import { TangoFile, TangoJsonFile, TangoLessFile } from './file';
|
||||
import { IWorkspace } from './interfaces';
|
||||
import { IFileConfig, FileType, ITangoConfigPackages, IPageConfigData } from '../types';
|
||||
import { IFileConfig, FileType, ITangoConfigPackages, IPageConfigData, IFileError } from '../types';
|
||||
import { SelectSource } from './select-source';
|
||||
import { DragSource } from './drag-source';
|
||||
import { TangoRouteModule } from './route-module';
|
||||
@@ -35,6 +36,7 @@ import { TangoStoreEntryModule, TangoStoreModule } from './store-module';
|
||||
import { TangoServiceModule } from './service-module';
|
||||
import { TangoViewModule } from './view-module';
|
||||
import { TangoComponentsEntryModule } from './component-module';
|
||||
import { AppEntryModule } from './entry-module';
|
||||
|
||||
export interface IWorkspaceOptions {
|
||||
/**
|
||||
@@ -52,7 +54,7 @@ export interface IWorkspaceOptions {
|
||||
/**
|
||||
* 组件描述列表
|
||||
*/
|
||||
prototypes?: Record<string, ComponentPrototypeType>;
|
||||
prototypes?: Record<string, IComponentPrototype>;
|
||||
/**
|
||||
* 工作区文件变更事件
|
||||
*/
|
||||
@@ -75,7 +77,7 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
/**
|
||||
* 组件配置
|
||||
*/
|
||||
componentPrototypes: Map<string, ComponentPrototypeType>;
|
||||
componentPrototypes: Map<string, IComponentPrototype>;
|
||||
|
||||
/**
|
||||
* 入口文件
|
||||
@@ -97,6 +99,11 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
*/
|
||||
activeViewFile: string;
|
||||
|
||||
/**
|
||||
* 应用入口模块
|
||||
*/
|
||||
appEntryModule: AppEntryModule;
|
||||
|
||||
/**
|
||||
* 路由配置模块
|
||||
*/
|
||||
@@ -144,6 +151,10 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
*/
|
||||
private copyTempNodes: TangoNode[];
|
||||
|
||||
get isValid() {
|
||||
return !!this.tangoConfigJson && !!this.activeViewModule && this.fileErrors.length === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 项目配置,返回解析后的 tango.config.json 文件
|
||||
*/
|
||||
@@ -209,6 +220,19 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
return Object.keys(this.componentsEntryModule?.exportList || {});
|
||||
}
|
||||
|
||||
get fileErrors() {
|
||||
const errors: IFileError[] = [];
|
||||
this.files.forEach((file) => {
|
||||
if (file.isError) {
|
||||
errors.push({
|
||||
filename: file.filename,
|
||||
message: file.errorMessage,
|
||||
});
|
||||
}
|
||||
});
|
||||
return errors;
|
||||
}
|
||||
|
||||
constructor(options?: IWorkspaceOptions) {
|
||||
super();
|
||||
this.history = new TangoHistory(this);
|
||||
@@ -242,6 +266,8 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
activeViewFile: observable,
|
||||
pages: computed,
|
||||
bizComps: computed,
|
||||
fileErrors: computed,
|
||||
isValid: computed,
|
||||
setActiveRoute: action,
|
||||
setActiveFile: action,
|
||||
addFile: action,
|
||||
@@ -249,11 +275,11 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
});
|
||||
}
|
||||
|
||||
getPrototype(name: string | ComponentPrototypeType) {
|
||||
getPrototype(name: string | IComponentPrototype) {
|
||||
if (isString(name)) {
|
||||
return this.componentPrototypes.get(name);
|
||||
}
|
||||
return name as ComponentPrototypeType;
|
||||
return name as IComponentPrototype;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -299,7 +325,7 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
}
|
||||
}
|
||||
|
||||
setComponentPrototypes(prototypes: Record<string, ComponentPrototypeType>) {
|
||||
setComponentPrototypes(prototypes: Record<string, IComponentPrototype>) {
|
||||
Object.keys(prototypes).forEach((name) => {
|
||||
this.componentPrototypes.set(name, prototypes[name]);
|
||||
});
|
||||
@@ -322,6 +348,9 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
* @param fileType 模块类型
|
||||
*/
|
||||
addFile(filename: string, code: string, fileType?: FileType) {
|
||||
if (!fileType && filename === this.entry) {
|
||||
fileType = FileType.AppEntryModule;
|
||||
}
|
||||
const moduleType = fileType || inferFileType(filename);
|
||||
const props = {
|
||||
filename,
|
||||
@@ -331,6 +360,10 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
|
||||
let module;
|
||||
switch (moduleType) {
|
||||
case FileType.AppEntryModule:
|
||||
module = new AppEntryModule(this, props);
|
||||
this.appEntryModule = module;
|
||||
break;
|
||||
case FileType.StoreEntryModule:
|
||||
module = new TangoStoreEntryModule(this, props);
|
||||
this.storeEntryModule = module;
|
||||
@@ -418,10 +451,16 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
);
|
||||
}
|
||||
|
||||
updateFile(filename: string, code: string, shouldFormatCode = false) {
|
||||
updateFile(filename: string, code: string, isSyncAst = true) {
|
||||
const file = this.getFile(filename);
|
||||
file.update(code);
|
||||
if (shouldFormatCode && file instanceof TangoViewModule) {
|
||||
if (file instanceof TangoModule) {
|
||||
file.update(code, isSyncAst);
|
||||
} else {
|
||||
file.update(code);
|
||||
}
|
||||
|
||||
const shouldFormat = this.projectConfig?.designerConfig?.autoFormatCode;
|
||||
if (shouldFormat && file instanceof TangoViewModule) {
|
||||
file.removeUnusedImportSpecifiers().update();
|
||||
}
|
||||
this.history.push({
|
||||
@@ -432,12 +471,15 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除工作区的文件
|
||||
* @param filename
|
||||
*/
|
||||
syncFiles() {
|
||||
this.files.forEach((file) => {
|
||||
if (file instanceof TangoModule) {
|
||||
file.updateAst();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
removeFile(filename: string) {
|
||||
// TODO: refactor visitFile to share this logic
|
||||
if (this.files.get(filename)) {
|
||||
// 如果是文件,直接删除
|
||||
this.files.delete(filename);
|
||||
@@ -493,7 +535,7 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
* @returns { [filename]: fileCode }
|
||||
*/
|
||||
listFiles() {
|
||||
const ret = {};
|
||||
const ret: Dict<string> = {};
|
||||
this.files.forEach((file) => {
|
||||
ret[file.filename] = file.cleanCode;
|
||||
});
|
||||
@@ -721,6 +763,7 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
* 更新依赖,没有就添加
|
||||
* @param name
|
||||
* @param version
|
||||
* FIXME: 参数3的设置需要重新考虑下
|
||||
*/
|
||||
updateDependency(
|
||||
name: string,
|
||||
@@ -743,15 +786,12 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
if (!packages) {
|
||||
return undefined;
|
||||
}
|
||||
if (options?.package || packages[name]) {
|
||||
packages[name] = {
|
||||
type: packages[name]?.type,
|
||||
// 如果没有传入 package 信息,则沿用 tango.config.json 中已记录的数据
|
||||
// 如果传入了,则全量使用传入的信息,适配可能在某个版本中如 resources 等字段被删除的情况
|
||||
...(options?.package || packages[name]),
|
||||
version,
|
||||
};
|
||||
}
|
||||
|
||||
setValue(packages, name, {
|
||||
...packages[name], // 保留原有的配置
|
||||
version, // 更新版本号
|
||||
...options?.package, // 更新 package 配置
|
||||
});
|
||||
|
||||
return packages;
|
||||
})
|
||||
@@ -954,7 +994,7 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
* @param sourceName 插入的组件名称
|
||||
* @returns
|
||||
*/
|
||||
insertToNode(targetNodeId: string, sourceName: string | ComponentPrototypeType) {
|
||||
insertToNode(targetNodeId: string, sourceName: string | IComponentPrototype) {
|
||||
if (!targetNodeId || !sourceName) {
|
||||
return;
|
||||
}
|
||||
@@ -978,7 +1018,7 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
/**
|
||||
* 替换目标节点
|
||||
*/
|
||||
replaceNode(targetNodeId: string, sourceName: string | ComponentPrototypeType) {
|
||||
replaceNode(targetNodeId: string, sourceName: string | IComponentPrototype) {
|
||||
if (!targetNodeId || !sourceName) {
|
||||
return;
|
||||
}
|
||||
@@ -1000,7 +1040,7 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
* 在选中节点中插入子节点
|
||||
* @param childName 节点名
|
||||
*/
|
||||
insertToSelectedNode(childName: string | ComponentPrototypeType) {
|
||||
insertToSelectedNode(childName: string | IComponentPrototype) {
|
||||
const insertedPrototype = this.getPrototype(childName);
|
||||
if (insertedPrototype) {
|
||||
const newNode = prototype2jsxElement(insertedPrototype);
|
||||
@@ -1022,6 +1062,46 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
}
|
||||
}
|
||||
|
||||
insertBeforeSelectedNode(sourceName: string | IComponentPrototype) {
|
||||
if (!sourceName) {
|
||||
return;
|
||||
}
|
||||
const targetNodeId = this.selectSource.first.id;
|
||||
const sourcePrototype = this.getPrototype(sourceName);
|
||||
const newNode = prototype2jsxElement(sourcePrototype);
|
||||
const file = this.getNode(targetNodeId).file;
|
||||
const { source, specifiers } = prototype2importDeclarationData(sourcePrototype, file.filename);
|
||||
|
||||
file.insertBefore(targetNodeId, newNode).addImportSpecifiers(source, specifiers).update();
|
||||
|
||||
this.history.push({
|
||||
message: HistoryMessage.InsertBeforeNode,
|
||||
data: {
|
||||
[file.filename]: file.code,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
insertAfterSelectedNode(sourceName: string | IComponentPrototype) {
|
||||
if (!sourceName) {
|
||||
return;
|
||||
}
|
||||
const targetNodeId = this.selectSource.first.id;
|
||||
const sourcePrototype = this.getPrototype(sourceName);
|
||||
const newNode = prototype2jsxElement(sourcePrototype);
|
||||
const file = this.getNode(targetNodeId).file;
|
||||
const { source, specifiers } = prototype2importDeclarationData(sourcePrototype, file.filename);
|
||||
|
||||
file.insertAfter(targetNodeId, newNode).addImportSpecifiers(source, specifiers).update();
|
||||
|
||||
this.history.push({
|
||||
message: HistoryMessage.InsertAfterNode,
|
||||
data: {
|
||||
[file.filename]: file.code,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
updateSelectedNodeAttributes(
|
||||
attributes: Record<string, any> = {},
|
||||
relatedImports: string[] = [],
|
||||
@@ -1186,8 +1266,17 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
let filename: string;
|
||||
this.routeModule?.routes.forEach((route) => {
|
||||
if (isPathnameMatchRoute(routePath, route.path) && route.importPath) {
|
||||
const absolutePath = route.importPath.replace('.', '/src');
|
||||
filename = this.getRealViewFilePath(absolutePath);
|
||||
if (route.importPath.startsWith('@/')) {
|
||||
filename = route.importPath;
|
||||
const alias = this.tangoConfigJson.getValue('sandbox.alias') || {};
|
||||
if (alias['@']) {
|
||||
filename = filename.replace('@', alias['@']);
|
||||
}
|
||||
filename = this.getRealViewFilePath(filename);
|
||||
} else {
|
||||
const absolutePath = route.importPath.replace('.', '/src');
|
||||
filename = this.getRealViewFilePath(absolutePath);
|
||||
}
|
||||
}
|
||||
});
|
||||
return filename;
|
||||
|
||||
@@ -8,6 +8,7 @@ export type SimulatorMode = 'desktop' | 'tablet' | 'phone';
|
||||
export enum FileType {
|
||||
// js 文件
|
||||
Module = 'module',
|
||||
AppEntryModule = 'appEntryModule',
|
||||
StoreEntryModule = 'storeEntryModule',
|
||||
RouteModule = 'routeModule',
|
||||
ServiceModule = 'serviceModule',
|
||||
@@ -56,6 +57,11 @@ export interface IFileConfig {
|
||||
type?: FileType;
|
||||
}
|
||||
|
||||
export interface IFileError {
|
||||
filename: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 视图节点数据类型
|
||||
*/
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isTangoVariable, isWrappedByExpressionContainer } from '../src/helpers';
|
||||
import { isTangoVariable } from '../src/helpers';
|
||||
|
||||
describe('assert', () => {
|
||||
it('isTangoVariable', () => {
|
||||
@@ -7,19 +7,4 @@ describe('assert', () => {
|
||||
expect(isTangoVariable('tango.stores.app?.name')).toBeTruthy();
|
||||
// expect(isTangoVariable('tango.copyToClipboard')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('isWrappedByExpressionContainer', () => {
|
||||
expect(isWrappedByExpressionContainer('{this.foo}')).toBeTruthy();
|
||||
expect(isWrappedByExpressionContainer('{!false}')).toBeTruthy();
|
||||
expect(isWrappedByExpressionContainer('{[]}')).toBeTruthy();
|
||||
expect(isWrappedByExpressionContainer('{{ foo: "bar" }}')).toBeTruthy();
|
||||
expect(isWrappedByExpressionContainer('{[{ foo: "bar" }]}')).toBeTruthy();
|
||||
expect(isWrappedByExpressionContainer('{123}')).toBeTruthy();
|
||||
expect(isWrappedByExpressionContainer('{"hello"}')).toBeTruthy();
|
||||
expect(isWrappedByExpressionContainer('{ foo: "bar" }')).toBeFalsy();
|
||||
expect(isWrappedByExpressionContainer('{ type: tango.stores?.homePage?.tabKey }')).toBeFalsy();
|
||||
expect(isWrappedByExpressionContainer('{ type: tango.stores.homePage.tabKey }')).toBeFalsy();
|
||||
expect(isWrappedByExpressionContainer('{ foo: "bar" }')).toBeFalsy();
|
||||
expect(isWrappedByExpressionContainer('{ color: tango.stores.app.color }')).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,19 +1,28 @@
|
||||
import { Identifier } from '@babel/types';
|
||||
import {
|
||||
object2node,
|
||||
serviceConfig2Node,
|
||||
isValidCode,
|
||||
isValidExpressionCode,
|
||||
code2expression,
|
||||
value2jsxAttributeValueNode,
|
||||
} from '../src/helpers';
|
||||
|
||||
describe('ast helpers', () => {
|
||||
it('isValidCode', () => {
|
||||
expect(isValidCode('')).toBeTruthy();
|
||||
expect(isValidCode('1')).toBeTruthy();
|
||||
expect(isValidCode('"hello"')).toBeTruthy();
|
||||
expect(isValidCode('<div>hello</div>')).toBeTruthy();
|
||||
expect(isValidCode('function fn() {}')).toBeTruthy();
|
||||
|
||||
// invalid function body
|
||||
expect(isValidCode('() => { hello world }')).toBeFalsy();
|
||||
// invalid function
|
||||
expect(isValidCode('function() {}')).toBeFalsy();
|
||||
});
|
||||
|
||||
it('isValidExpression', () => {
|
||||
expect(isValidExpressionCode('() => { }')).toBeTruthy();
|
||||
it('isValidExpressionCode', () => {
|
||||
expect(isValidExpressionCode('1')).toBeTruthy();
|
||||
expect(isValidExpressionCode('a = 1')).toBeTruthy();
|
||||
expect(isValidExpressionCode('1 + 1')).toBeTruthy();
|
||||
@@ -22,8 +31,11 @@ describe('ast helpers', () => {
|
||||
expect(isValidExpressionCode('{ bizId: "vip", type: "category" }')).toBeTruthy();
|
||||
expect(isValidExpressionCode('[1,2,3]')).toBeTruthy();
|
||||
expect(isValidExpressionCode('<div>hello</div>')).toBeTruthy();
|
||||
expect(isValidExpressionCode('<div>hello</div>')).toBeTruthy();
|
||||
expect(isValidExpressionCode('tango.stores.app.title')).toBeTruthy();
|
||||
expect(isValidExpressionCode('"hello" + window.location.path')).toBeTruthy();
|
||||
expect(isValidExpressionCode('() => { }')).toBeTruthy();
|
||||
|
||||
expect(isValidExpressionCode('')).toBeFalsy();
|
||||
expect(isValidExpressionCode('{1}')).toBeFalsy();
|
||||
expect(isValidExpressionCode('{"1"}')).toBeFalsy();
|
||||
expect(isValidExpressionCode('{ 1+1 }')).toBeFalsy();
|
||||
@@ -54,8 +66,7 @@ describe('ast helpers', () => {
|
||||
|
||||
it('code2expression', () => {
|
||||
expect(code2expression('')).toBeUndefined();
|
||||
expect(code2expression('{tango.stores.app}')).toBeUndefined();
|
||||
|
||||
expect(code2expression('tango.stores.app').type).toBe('MemberExpression');
|
||||
expect(code2expression('{ type: window.bar }').type).toEqual('ObjectExpression');
|
||||
expect(code2expression('() => {};').type).toEqual('ArrowFunctionExpression');
|
||||
expect(code2expression('<Button>hello</Button>').type).toBe('JSXElement');
|
||||
@@ -69,4 +80,16 @@ describe('ast helpers', () => {
|
||||
`;
|
||||
expect(code2expression(arrayCode).type).toEqual('ArrayExpression');
|
||||
});
|
||||
|
||||
it('value2jsxAttributeValueNode', () => {
|
||||
expect(value2jsxAttributeValueNode('hello').type).toBe('StringLiteral');
|
||||
expect(value2jsxAttributeValueNode(true).type).toBe('JSXExpressionContainer');
|
||||
expect(value2jsxAttributeValueNode(1).type).toBe('JSXExpressionContainer');
|
||||
expect(value2jsxAttributeValueNode('{{{ foo: "foo"}}}').type).toBe('JSXExpressionContainer');
|
||||
|
||||
// invalid code will return undefined
|
||||
const node: any = value2jsxAttributeValueNode('{{tango.xx+}}');
|
||||
expect(node.type).toBe('JSXExpressionContainer');
|
||||
expect((node.expression as Identifier).name).toBe('undefined');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
getJSXElementAttributes,
|
||||
inferFileType,
|
||||
deepCloneNode,
|
||||
code2value,
|
||||
} from '../src/helpers';
|
||||
import { FileType } from '../src/types';
|
||||
|
||||
@@ -27,15 +28,18 @@ describe('helpers', () => {
|
||||
|
||||
it('parse jsxElement attributes', () => {
|
||||
const node = code2expression(
|
||||
"<XColumn dataIndex='col' enumMap={{ 1: '已解决', 2: '未解决' }} />",
|
||||
"<Foo id={tango.user.id} num={1} str='col' enumMap={{ 1: '已解决', 2: '未解决' }} list={[{ key: 1 }, { key: 2 }]} />",
|
||||
);
|
||||
const attributes = getJSXElementAttributes(node as JSXElement);
|
||||
expect(attributes).toEqual({
|
||||
dataIndex: 'col',
|
||||
id: '{{tango.user.id}}',
|
||||
num: 1,
|
||||
str: 'col',
|
||||
enumMap: {
|
||||
1: '已解决',
|
||||
2: '未解决',
|
||||
},
|
||||
list: '{{[{ key: 1 }, { key: 2 }]}}',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -80,15 +84,22 @@ describe('helpers', () => {
|
||||
|
||||
it('expressionCode2ast', () => {
|
||||
expect(expressionCode2ast('<Button>hello</Button>').type).toEqual('File');
|
||||
expect(expressionCode2ast('{<Button>hello</Button>}').type).toEqual('File');
|
||||
expect(expressionCode2ast('() => <Button>hello</Button>').type).toEqual('File');
|
||||
expect(expressionCode2ast('{() => <Button>hello</Button>}').type).toEqual('File');
|
||||
});
|
||||
});
|
||||
|
||||
describe('string helpers', () => {
|
||||
it('value2code: empty array', () => {
|
||||
expect(value2code([])).toEqual('[]');
|
||||
expect(value2code({})).toEqual('{}');
|
||||
expect(value2code(() => {})).toEqual('() => {}');
|
||||
expect(value2code(true)).toEqual('true');
|
||||
expect(value2code(false)).toEqual('false');
|
||||
expect(value2code(1)).toEqual('1');
|
||||
expect(value2code('hello')).toEqual('"hello"');
|
||||
expect(value2code('{{window.tango}}')).toEqual('window.tango');
|
||||
expect(value2code('{{() => {}}}')).toEqual('() => {}');
|
||||
expect(value2code('{{1111}}')).toEqual('1111');
|
||||
});
|
||||
|
||||
it('value2code: array', () => {
|
||||
@@ -161,6 +172,10 @@ describe('string helpers', () => {
|
||||
it('isValidComponentName', () => {
|
||||
expect(isValidComponentName('Button')).toBeTruthy();
|
||||
expect(isValidComponentName('Button.Group')).toBeTruthy();
|
||||
expect(isValidComponentName('div')).toBeTruthy();
|
||||
expect(isValidComponentName('')).toBeFalsy();
|
||||
expect(isValidComponentName(null)).toBeFalsy();
|
||||
expect(isValidComponentName(undefined)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('inferFileType', () => {
|
||||
@@ -197,10 +212,26 @@ describe('schema helpers', () => {
|
||||
},
|
||||
],
|
||||
};
|
||||
const cloned = deepCloneNode(schema);
|
||||
expect(cloned.props.id).toBe(schema.props.id);
|
||||
expect(cloned.children[0].props.id).toBe(schema.children[0].props.id);
|
||||
|
||||
expect(cloned.id).not.toBe(schema.id);
|
||||
expect(cloned.children[0].id).not.toBe(schema.children[0].id);
|
||||
it('deepCloneNode', () => {
|
||||
const cloned = deepCloneNode(schema);
|
||||
expect(cloned.props.id).toBe(schema.props.id);
|
||||
expect(cloned.children[0].props.id).toBe(schema.children[0].props.id);
|
||||
expect(cloned.id).not.toBe(schema.id);
|
||||
expect(cloned.children[0].id).not.toBe(schema.children[0].id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('code helper', () => {
|
||||
it('code2value', () => {
|
||||
expect(code2value(`1`)).toEqual(1);
|
||||
expect(code2value(`false`)).toEqual(false);
|
||||
expect(code2value(`"foo"`)).toEqual('foo');
|
||||
expect(code2value(`{ foo: "foo" }`)).toEqual({ foo: 'foo' });
|
||||
expect(code2value(`[{ foo: "foo" }]`)).toEqual([{ foo: 'foo' }]);
|
||||
expect(code2value(`{ foo: "foo", ...{ bar: "bar"} }`)).toBe(undefined);
|
||||
expect(code2value(`() => {}`)).toBe(undefined);
|
||||
expect(code2value(`tango.stores.app.name`)).toBe(undefined);
|
||||
expect(code2value(`window`)).toBe(undefined);
|
||||
expect(code2value(`<div>hello</div>`)).toBe(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { propDataToKeyValueString } from '../src/helpers';
|
||||
|
||||
describe('prototype helpers', () => {
|
||||
it('propDataToKeyValueString', () => {
|
||||
// basic
|
||||
expect(propDataToKeyValueString({ name: 'foo', initValue: 'bar' })).toEqual('foo="bar"');
|
||||
expect(propDataToKeyValueString({ name: 'foo', initValue: 1 })).toEqual('foo={1}');
|
||||
expect(propDataToKeyValueString({ name: 'foo', initValue: false })).toEqual('foo={false}');
|
||||
expect(propDataToKeyValueString({ name: 'foo', initValue: null })).toEqual('foo={null}');
|
||||
expect(propDataToKeyValueString({ name: 'foo', initValue: [] })).toEqual('foo={[]}');
|
||||
expect(propDataToKeyValueString({ name: 'foo', initValue: {} })).toEqual('foo={{}}');
|
||||
expect(propDataToKeyValueString({ name: 'foo', initValue: () => {} })).toEqual(
|
||||
'foo={() => {}}',
|
||||
);
|
||||
expect(propDataToKeyValueString({ name: 'foo', initValue: { foo: 'bar' } })).toEqual(
|
||||
'foo={{ foo: "bar" }}',
|
||||
);
|
||||
expect(propDataToKeyValueString({ name: 'foo', initValue: [{ foo: 'bar' }] })).toEqual(
|
||||
'foo={[{ foo: "bar" }]}',
|
||||
);
|
||||
|
||||
// wrapped code
|
||||
expect(
|
||||
propDataToKeyValueString({ name: 'foo', initValue: '{{<Placeholder text="放置替换" />}}' }),
|
||||
).toEqual('foo={<Placeholder text="放置替换" />}');
|
||||
expect(propDataToKeyValueString({ name: 'foo', initValue: '{{tango}}' })).toEqual(
|
||||
'foo={tango}',
|
||||
);
|
||||
expect(propDataToKeyValueString({ name: 'foo', initValue: '{{"bar"}}' })).toEqual(
|
||||
'foo={"bar"}',
|
||||
);
|
||||
expect(propDataToKeyValueString({ name: 'foo', initValue: '{{() => {}}}' })).toEqual(
|
||||
'foo={() => {}}',
|
||||
);
|
||||
|
||||
// compatible with old version
|
||||
expect(propDataToKeyValueString({ name: 'foo', initValue: '{() => {}}' })).toEqual(
|
||||
'foo={() => {}}',
|
||||
);
|
||||
expect(
|
||||
propDataToKeyValueString({ name: 'foo', initValue: '{<Placeholder text="放置替换" />}' }),
|
||||
).toEqual('foo={<Placeholder text="放置替换" />}');
|
||||
// expect(propDataToKeyValueString({ name: 'foo', initValue: '{tango}' })).toEqual('foo={tango}');
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,172 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.4.5](https://github.com/netease/tango/compare/@music163/tango-designer@1.4.4...@music163/tango-designer@1.4.5) (2024-09-02)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- add prop value template & fix components popover ([#201](https://github.com/netease/tango/issues/201)) ([c4e1ed6](https://github.com/netease/tango/commit/c4e1ed67f55deb17dae740559602cd0adcfc8eb4))
|
||||
|
||||
## [1.4.4](https://github.com/netease/tango/compare/@music163/tango-designer@1.4.3...@music163/tango-designer@1.4.4) (2024-08-16)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- CodeInput 支持 jsx css 代码补全 ([#198](https://github.com/netease/tango/issues/198)) ([6de5488](https://github.com/netease/tango/commit/6de54884b50d683c74d695a8c13184dc0ce8d3c4))
|
||||
|
||||
## [1.4.3](https://github.com/netease/tango/compare/@music163/tango-designer@1.4.2...@music163/tango-designer@1.4.3) (2024-08-09)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- file errors overlay support click to error file & optimize ui ([#197](https://github.com/netease/tango/issues/197)) ([ca56edd](https://github.com/netease/tango/commit/ca56edd9ae201dbeb84b03ad390a09085433a257))
|
||||
- route matching without query string & toggle button wrapping ([#195](https://github.com/netease/tango/issues/195)) ([9394666](https://github.com/netease/tango/commit/939466648d40331800f15e886256a0789b7e7623))
|
||||
- update mobile simulator ui ([#196](https://github.com/netease/tango/issues/196)) ([65149f1](https://github.com/netease/tango/commit/65149f1326eaf78e8a27222993c3cacc674cd378))
|
||||
|
||||
## [1.4.2](https://github.com/netease/tango/compare/@music163/tango-designer@1.4.1...@music163/tango-designer@1.4.2) (2024-08-06)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-designer
|
||||
|
||||
## [1.4.1](https://github.com/netease/tango/compare/@music163/tango-designer@1.4.0...@music163/tango-designer@1.4.1) (2024-08-05)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- parse template code & update maxRow of TextAreaSetter ([#192](https://github.com/netease/tango/issues/192)) ([a1eea02](https://github.com/netease/tango/commit/a1eea02f9409db73f042c74d7903ad84258f25c2))
|
||||
- show right panel when design view select node & optimize editor css ([#193](https://github.com/netease/tango/issues/193)) ([b83d965](https://github.com/netease/tango/commit/b83d965955c8d33ed8d37063482c7abe079e62b1))
|
||||
|
||||
# [1.4.0](https://github.com/netease/tango/compare/@music163/tango-designer@1.3.3...@music163/tango-designer@1.4.0) (2024-08-01)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- update types ([67d602a](https://github.com/netease/tango/commit/67d602a3ec6b7f74156bb78fda6f3b4bc2676e55))
|
||||
|
||||
### Features
|
||||
|
||||
- add isError and errorMessage to File & add isAstSynced to Module ([#190](https://github.com/netease/tango/issues/190)) ([8dd289c](https://github.com/netease/tango/commit/8dd289cd7daba628e54dc6b8929f22a1e2245160))
|
||||
|
||||
## [1.3.3](https://github.com/netease/tango/compare/@music163/tango-designer@1.3.2...@music163/tango-designer@1.3.3) (2024-07-12)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- selection bug without prototype & update ToggleButton style ([#180](https://github.com/netease/tango/issues/180)) ([1d0fb4f](https://github.com/netease/tango/commit/1d0fb4f274a5b767962c54583ab5863329d5bcc5))
|
||||
- update designer config ([#186](https://github.com/netease/tango/issues/186)) ([85c053b](https://github.com/netease/tango/commit/85c053b3db6d652b41c9873dba1366315174833f))
|
||||
|
||||
## [1.3.2](https://github.com/netease/tango/compare/@music163/tango-designer@1.3.1...@music163/tango-designer@1.3.2) (2024-06-20)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- drag-panel add resizeable & height prop ([#175](https://github.com/netease/tango/issues/175)) ([b53a9f7](https://github.com/netease/tango/commit/b53a9f7eefc748ba8a7c94270c3a374b9d6ed211))
|
||||
- fix global hotkey triggering on content editable elements ([#174](https://github.com/netease/tango/issues/174)) ([a2cc418](https://github.com/netease/tango/commit/a2cc418a73edfd9b7fe8aad456ae775a49df2166))
|
||||
- update designer style ([#176](https://github.com/netease/tango/issues/176)) ([0f3f0af](https://github.com/netease/tango/commit/0f3f0afdfa8aee2532a97c5c2e92ef4230397d86))
|
||||
|
||||
## [1.3.1](https://github.com/netease/tango/compare/@music163/tango-designer@1.3.0...@music163/tango-designer@1.3.1) (2024-06-05)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- fix context menu error on macOS ([#170](https://github.com/netease/tango/issues/170)) ([ca740c8](https://github.com/netease/tango/commit/ca740c83ae7f72a75c7db90d2e137a7e9a032326))
|
||||
|
||||
# [1.3.0](https://github.com/netease/tango/compare/@music163/tango-designer@1.2.3...@music163/tango-designer@1.3.0) (2024-06-03)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- parse app entry file & pass correct routerType to sandbox ([#168](https://github.com/netease/tango/issues/168)) ([3f3981b](https://github.com/netease/tango/commit/3f3981bb9db874b738a67fd449c579c35c58d08b))
|
||||
|
||||
### Features
|
||||
|
||||
- add context menu ([#161](https://github.com/netease/tango/issues/161)) ([28040fc](https://github.com/netease/tango/commit/28040fc00604339a40ad3216b76baf7de93a13e0))
|
||||
|
||||
## [1.2.3](https://github.com/netease/tango/compare/@music163/tango-designer@1.2.2...@music163/tango-designer@1.2.3) (2024-05-30)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- optimize variable panel scroll ui & onCancel bug ([#166](https://github.com/netease/tango/issues/166)) ([a0ed57f](https://github.com/netease/tango/commit/a0ed57ff4f332bd88749b8b1925bdb29319e4d13))
|
||||
|
||||
## [1.2.2](https://github.com/netease/tango/compare/@music163/tango-designer@1.2.1...@music163/tango-designer@1.2.2) (2024-05-27)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- export usePreviewSandboxQuery & add builtin sandboxQuery ([#163](https://github.com/netease/tango/issues/163)) ([163eced](https://github.com/netease/tango/commit/163ecedff3ed7baee59e200a8cc60dcc63a24e48))
|
||||
|
||||
## [1.2.1](https://github.com/netease/tango/compare/@music163/tango-designer@1.2.0...@music163/tango-designer@1.2.1) (2024-05-22)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- check local component prototypes ([#160](https://github.com/netease/tango/issues/160)) ([a0484e8](https://github.com/netease/tango/commit/a0484e8f64f20f67c30c25c3d5ce65de549b3e04))
|
||||
|
||||
# [1.2.0](https://github.com/netease/tango/compare/@music163/tango-designer@1.1.0...@music163/tango-designer@1.2.0) (2024-05-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- enhance code value validate in SettingForm ([#152](https://github.com/netease/tango/issues/152)) ([791fbb1](https://github.com/netease/tango/commit/791fbb162a7147243924e01f54e9c0b586f14438))
|
||||
- prototype2code & go back history error ([#156](https://github.com/netease/tango/issues/156)) ([8bf53a7](https://github.com/netease/tango/commit/8bf53a76f8a71eaf261ea68b9ee44e5bf19893aa))
|
||||
- support add components with popover ([#155](https://github.com/netease/tango/issues/155)) ([f17ccbb](https://github.com/netease/tango/commit/f17ccbb7f645f8047ecd96d9f3f2185048a3b726))
|
||||
|
||||
### Features
|
||||
|
||||
- add history forward & back shortcut key ([#154](https://github.com/netease/tango/issues/154)) ([df3021f](https://github.com/netease/tango/commit/df3021f75e057e229756b429321c14d181223698))
|
||||
- add select parent node of selected node ([#158](https://github.com/netease/tango/issues/158)) ([fe31246](https://github.com/netease/tango/commit/fe3124648325e72abfc58da8b2f8ff83301d40b8))
|
||||
- event-setter & expression-setter use popover ([#159](https://github.com/netease/tango/issues/159)) ([9daf387](https://github.com/netease/tango/commit/9daf3872e743fcf706184877020bcbf1f75ffd25))
|
||||
|
||||
# [1.1.0](https://github.com/netease/tango/compare/@music163/tango-designer@1.0.3...@music163/tango-designer@1.1.0) (2024-05-17)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- support quick set tid in SettingForm ([#151](https://github.com/netease/tango/issues/151)) ([1fc22a4](https://github.com/netease/tango/commit/1fc22a4535a8bdc12a018a41ebd5ab908fc46817))
|
||||
|
||||
### Features
|
||||
|
||||
- refactor parse attribute value ([#149](https://github.com/netease/tango/issues/149)) ([ffaa276](https://github.com/netease/tango/commit/ffaa276b5c205ed962d37e2fdb358a703f8fad01))
|
||||
|
||||
## [1.0.2](https://github.com/netease/tango/compare/@music163/tango-designer@1.0.1...@music163/tango-designer@1.0.2) (2024-04-22)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- allow reload state tree & display pageStore in variable tree ([#135](https://github.com/netease/tango/issues/135)) ([2664613](https://github.com/netease/tango/commit/2664613ab263aead2a5239fa012454fc7fd5ff99))
|
||||
|
||||
## [1.0.1](https://github.com/netease/tango/compare/@music163/tango-designer@1.0.0...@music163/tango-designer@1.0.1) (2024-04-16)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- render-setter expression value bug ([#133](https://github.com/netease/tango/issues/133)) ([27166ec](https://github.com/netease/tango/commit/27166ec94976979cb59130788399d90977cf0aec))
|
||||
- variable tree remove button event handling ([#134](https://github.com/netease/tango/issues/134)) ([52864b5](https://github.com/netease/tango/commit/52864b540095025232ae237134d6be5701d20549))
|
||||
|
||||
# [1.0.0-alpha.18](https://github.com/netease/tango/compare/@music163/tango-designer@1.0.0-alpha.17...@music163/tango-designer@1.0.0-alpha.18) (2024-04-09)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- render invalid icon in inserted dropdown ([#128](https://github.com/netease/tango/issues/128)) ([41a61cc](https://github.com/netease/tango/commit/41a61cce4e6a1f9d42c7a3f4e357c5acdcd6db5f))
|
||||
- update setters and use tabOptions to filter props ([#129](https://github.com/netease/tango/issues/129)) ([93608d1](https://github.com/netease/tango/commit/93608d1037327afa4f755976b86427b6128ae3d0))
|
||||
- update SettingForm style ([#125](https://github.com/netease/tango/issues/125)) ([3f42516](https://github.com/netease/tango/commit/3f42516816968888554f719332d3cef7d8576aa8))
|
||||
|
||||
# [1.0.0-alpha.17](https://github.com/netease/tango/compare/@music163/tango-designer@1.0.0-alpha.16...@music163/tango-designer@1.0.0-alpha.17) (2024-03-26)
|
||||
|
||||
### Features
|
||||
|
||||
- quick add sibling nodes ([#127](https://github.com/netease/tango/issues/127)) ([9ed6a7d](https://github.com/netease/tango/commit/9ed6a7d1a4944d69d96e034f243b61531862e317))
|
||||
|
||||
# [1.0.0-alpha.16](https://github.com/netease/tango/compare/@music163/tango-designer@1.0.0-alpha.15...@music163/tango-designer@1.0.0-alpha.16) (2024-03-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- copy node path in variable tree ([6215ed9](https://github.com/netease/tango/commit/6215ed96f241adf6b67aa3019745bf7d40751787))
|
||||
|
||||
# [1.0.0-alpha.15](https://github.com/netease/tango/compare/@music163/tango-designer@1.0.0-alpha.14...@music163/tango-designer@1.0.0-alpha.15) (2024-03-20)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- quick insert child ([d640429](https://github.com/netease/tango/commit/d64042922bc21723025b1a59586ef23f37f17ffc))
|
||||
- update selection helper style ([bceb7a5](https://github.com/netease/tango/commit/bceb7a5c9cea9dc2acb091bf7967e5d3b7ba5210))
|
||||
|
||||
# [1.0.0-alpha.14](https://github.com/netease/tango/compare/@music163/tango-designer@1.0.0-alpha.13...@music163/tango-designer@1.0.0-alpha.14) (2024-03-19)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- select outline node & render dependency list ([#118](https://github.com/netease/tango/issues/118)) ([07febb3](https://github.com/netease/tango/commit/07febb385f710f9a0bc43639ce22787bb05e97ed))
|
||||
|
||||
# [1.0.0-alpha.13](https://github.com/netease/tango/compare/@music163/tango-designer@1.0.0-alpha.12...@music163/tango-designer@1.0.0-alpha.13) (2024-03-18)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- add deprecated and data group to ComponentPropType ([#116](https://github.com/netease/tango/issues/116)) ([9f7441c](https://github.com/netease/tango/commit/9f7441c13400dba55c58b7dd6ce9429013edbc45))
|
||||
|
||||
# [1.0.0-alpha.12](https://github.com/netease/tango/compare/@music163/tango-designer@1.0.0-alpha.11...@music163/tango-designer@1.0.0-alpha.12) (2024-03-18)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-designer",
|
||||
"version": "1.0.0-alpha.12",
|
||||
"version": "1.4.5",
|
||||
"description": "lowcode designer",
|
||||
"keywords": [
|
||||
"react"
|
||||
@@ -32,25 +32,25 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^4.8.0",
|
||||
"@music163/request": "^0.1.2",
|
||||
"@music163/tango-context": "^1.0.0-alpha.7",
|
||||
"@music163/tango-core": "^1.0.0-alpha.7",
|
||||
"@music163/tango-helpers": "^1.0.0-alpha.3",
|
||||
"@music163/tango-sandbox": "^1.0.0-alpha.7",
|
||||
"@music163/tango-setting-form": "^1.0.0-alpha.10",
|
||||
"@music163/tango-ui": "^1.0.0-alpha.8",
|
||||
"@music163/request": "^0.2.0",
|
||||
"@music163/tango-context": "^1.1.9",
|
||||
"@music163/tango-core": "^1.4.3",
|
||||
"@music163/tango-helpers": "^1.2.3",
|
||||
"@music163/tango-sandbox": "^1.0.12",
|
||||
"@music163/tango-setting-form": "^1.2.14",
|
||||
"@music163/tango-ui": "^1.4.4",
|
||||
"antd": "^4.24.2",
|
||||
"cash-dom": "^8.1.2",
|
||||
"classnames": "^2.3.2",
|
||||
"classnames": "^2.5.1",
|
||||
"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",
|
||||
"moment": "^2.30.1",
|
||||
"react-color": "^2.19.3",
|
||||
"react-resizable": "^3.0.5",
|
||||
"semver": "^7.3.8"
|
||||
"semver": "^7.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/color": "^3.0.5",
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { Box } from 'coral-system';
|
||||
import { observer, useDesigner, useWorkspace } from '@music163/tango-context';
|
||||
import { IconFont, DragPanel } from '@music163/tango-ui';
|
||||
import { ComponentsPanel, ComponentsPanelProps } from '../sidebar';
|
||||
import { IComponentPrototype } from '@music163/tango-helpers';
|
||||
|
||||
interface ComponentsPopoverProps {
|
||||
// 添加组件位置
|
||||
type?: 'inner' | 'before' | 'after';
|
||||
// 弹出方式 手动触发/DOM 触发
|
||||
isControlled?: boolean;
|
||||
title?: string;
|
||||
prototype?: IComponentPrototype;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const ComponentsPopover = observer(
|
||||
({
|
||||
type = 'inner',
|
||||
title = '添加组件',
|
||||
isControlled = false,
|
||||
children,
|
||||
...popoverProps
|
||||
}: ComponentsPopoverProps) => {
|
||||
const [layout, setLayout] = useState<ComponentsPanelProps['layout']>('grid');
|
||||
const workspace = useWorkspace();
|
||||
const designer = useDesigner();
|
||||
|
||||
const { addComponentPopoverPosition, showAddComponentPopover } = designer;
|
||||
const selectedNode = workspace.selectSource.selected?.[0];
|
||||
const selectedNodeId = selectedNode?.codeId ?? '未选中';
|
||||
const prototype =
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
workspace.componentPrototypes.get(selectedNode?.name) ?? ({} as IComponentPrototype);
|
||||
|
||||
const recommendedList = useMemo(() => {
|
||||
if (type === 'inner') {
|
||||
return prototype?.childrenName
|
||||
? Array.isArray(prototype?.childrenName)
|
||||
? prototype.childrenName
|
||||
: [prototype.childrenName]
|
||||
: [];
|
||||
}
|
||||
// 默认推荐使用相同类型的组件作为兄弟节点
|
||||
return prototype.siblingNames || [prototype.name];
|
||||
}, [prototype.childrenName, prototype.siblingNames, prototype.name, type]);
|
||||
|
||||
const tipsTextMap = useMemo(
|
||||
() => ({
|
||||
before: `点击,在 ${selectedNodeId} 的前方添加节点`,
|
||||
after: `点击,在 ${selectedNodeId} 的后方添加节点`,
|
||||
inner: `点击,在 ${selectedNodeId} 内部添加节点`,
|
||||
}),
|
||||
[selectedNodeId],
|
||||
);
|
||||
|
||||
const handleSelect = useCallback(
|
||||
(name: string) => {
|
||||
switch (type) {
|
||||
case 'before':
|
||||
workspace.insertBeforeSelectedNode(name);
|
||||
break;
|
||||
case 'after':
|
||||
workspace.insertAfterSelectedNode(name);
|
||||
break;
|
||||
case 'inner':
|
||||
workspace.insertToSelectedNode(name);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
[type, workspace],
|
||||
);
|
||||
|
||||
const changeLayout = useCallback(() => {
|
||||
setLayout(layout === 'grid' ? 'line' : 'grid');
|
||||
}, [layout]);
|
||||
|
||||
const menuData = useMemo(() => {
|
||||
const menuList = JSON.parse(JSON.stringify(designer.menuData));
|
||||
|
||||
const commonList = menuList['common'] ?? [];
|
||||
if (commonList?.length && recommendedList.length) {
|
||||
commonList.unshift({
|
||||
title: '推荐使用',
|
||||
items: recommendedList,
|
||||
});
|
||||
}
|
||||
|
||||
return menuList;
|
||||
}, [recommendedList, designer.menuData]);
|
||||
|
||||
const innerTypeProps =
|
||||
// 手动触发 适用于 点击添加组件
|
||||
type === 'inner' && isControlled
|
||||
? {
|
||||
open: showAddComponentPopover,
|
||||
onOpenChange: (open: boolean) => designer.toggleAddComponentPopover(open),
|
||||
left: addComponentPopoverPosition.clientX,
|
||||
top: addComponentPopoverPosition.clientY,
|
||||
}
|
||||
: {};
|
||||
|
||||
return (
|
||||
<DragPanel
|
||||
{...innerTypeProps}
|
||||
title={title}
|
||||
extra={
|
||||
<Box fontSize="12px">
|
||||
{layout === 'grid' ? (
|
||||
<IconFont type="icon-liebiaoitem" onClick={changeLayout} />
|
||||
) : (
|
||||
<IconFont type="icon-grid1" onClick={changeLayout} />
|
||||
)}
|
||||
</Box>
|
||||
}
|
||||
footer={tipsTextMap[type]}
|
||||
width="330px"
|
||||
height="450px"
|
||||
resizeable
|
||||
maskClosable
|
||||
body={
|
||||
<ComponentsPanel
|
||||
isScope
|
||||
showBizComps={false}
|
||||
menuData={menuData}
|
||||
layout={layout}
|
||||
onItemSelect={handleSelect}
|
||||
/>
|
||||
}
|
||||
{...popoverProps}
|
||||
>
|
||||
{children}
|
||||
</DragPanel>
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,130 @@
|
||||
import React from 'react';
|
||||
import { getWidget } from '../widgets';
|
||||
import { Menu, MenuProps } from 'antd';
|
||||
import { observer, useWorkspace } from '@music163/tango-context';
|
||||
import { ISelectedItemData, isString } from '@music163/tango-helpers';
|
||||
import { Box, css } from 'coral-system';
|
||||
import { IconFont } from '@music163/tango-ui';
|
||||
|
||||
const contextMenuStyle = css`
|
||||
.ant-dropdown-menu {
|
||||
width: 240px;
|
||||
}
|
||||
|
||||
.ant-dropdown-menu-title-content {
|
||||
padding-left: 22px;
|
||||
}
|
||||
|
||||
.ant-dropdown-menu-item-icon {
|
||||
font-size: 14px;
|
||||
width: 14px;
|
||||
}
|
||||
|
||||
.ant-dropdown-menu-item-icon + .ant-dropdown-menu-title-content {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.ant-dropdown-menu-submenu-popup {
|
||||
padding: 0;
|
||||
margin-top: -4px;
|
||||
}
|
||||
`;
|
||||
|
||||
const ParentNodesMenuItem = ({
|
||||
record,
|
||||
onClick,
|
||||
key,
|
||||
}: {
|
||||
record: ISelectedItemData;
|
||||
onClick: () => any;
|
||||
key?: string;
|
||||
}) => {
|
||||
const workspace = useWorkspace();
|
||||
const componentPrototype = workspace.componentPrototypes.get(record.name);
|
||||
const icon = componentPrototype?.icon || 'icon-placeholder';
|
||||
|
||||
const iconRender = icon.startsWith('icon-') ? (
|
||||
<IconFont className="material-icon" type={icon} />
|
||||
) : (
|
||||
<img className="material-icon" src={icon} alt={componentPrototype.name} />
|
||||
);
|
||||
|
||||
return (
|
||||
<Menu.Item key={key || record.id} icon={iconRender} onClick={onClick}>
|
||||
{record.name}
|
||||
{!!record.codeId && ` (${record.codeId})`}
|
||||
</Menu.Item>
|
||||
);
|
||||
};
|
||||
|
||||
const ParentNodesMenu = observer(() => {
|
||||
const workspace = useWorkspace();
|
||||
const selectSource = workspace.selectSource;
|
||||
const parents = selectSource.first?.parents;
|
||||
|
||||
if (!parents?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Menu.SubMenu key="parentNodes" title="选取父节点">
|
||||
{parents.map((item, index) => (
|
||||
<ParentNodesMenuItem
|
||||
key={item.id}
|
||||
record={item}
|
||||
onClick={() => {
|
||||
selectSource.select({
|
||||
...item,
|
||||
parents: parents.slice(index + 1),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Menu.SubMenu>
|
||||
);
|
||||
});
|
||||
|
||||
export interface ContextMenuProps extends MenuProps {
|
||||
/**
|
||||
* 动作列表,默认列出全部
|
||||
*/
|
||||
actions?: Array<string | React.ReactElement>;
|
||||
/**
|
||||
* 是否显示父节点选项
|
||||
*/
|
||||
showParents?: boolean;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
menuStyle?: React.CSSProperties;
|
||||
}
|
||||
|
||||
export const ContextMenu = observer(
|
||||
({
|
||||
showParents,
|
||||
actions: actionsProp,
|
||||
className,
|
||||
style,
|
||||
menuStyle,
|
||||
...rest
|
||||
}: ContextMenuProps) => {
|
||||
const actions = actionsProp || Object.keys(getWidget('contextMenu'));
|
||||
const menus = actions.map((item) => {
|
||||
if (isString(item)) {
|
||||
const widget = getWidget(['contextMenu', item].join('.'));
|
||||
return widget ? React.createElement(widget) : null;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
if (showParents) {
|
||||
menus.unshift(<ParentNodesMenu />);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box display="inline-block" css={contextMenuStyle} className={className} style={style}>
|
||||
<Menu activeKey={null} {...rest} style={menuStyle}>
|
||||
{menus}
|
||||
</Menu>
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,133 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Box, css, Text } from 'coral-system';
|
||||
import { observer, useDesigner, useWorkspace } from '@music163/tango-context';
|
||||
import { CloseOutlined } from '@ant-design/icons';
|
||||
|
||||
const errorMessageStyle = css`
|
||||
padding: 35px;
|
||||
position: relative;
|
||||
white-space: pre-wrap;
|
||||
color: #f45755;
|
||||
font-weight: bold;
|
||||
height: 100%;
|
||||
background-color: #171717;
|
||||
border-radius: 8px;
|
||||
.top-bar {
|
||||
border-radius: 8px 8px 0 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 10px;
|
||||
background-color: #f45755;
|
||||
}
|
||||
.WorkspaceErrorOverlayItem {
|
||||
font-size: 18px;
|
||||
padding-top: 20px;
|
||||
padding-bottom: 20px;
|
||||
.WorkspaceErrorOverlayItemFilename {
|
||||
color: #48cacb;
|
||||
text-decoration: underline;
|
||||
letter-spacing: 1px;
|
||||
&:hover {
|
||||
color: #51b1ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* 文件错误提示浮层
|
||||
*/
|
||||
export const FileErrorsOverlay = observer(() => {
|
||||
const [isVisible, setIsVisible] = useState<boolean>(true);
|
||||
const workspace = useWorkspace();
|
||||
const designer = useDesigner();
|
||||
|
||||
const handleClose = () => {
|
||||
setIsVisible(false);
|
||||
};
|
||||
|
||||
const handleOpenErrorFile = (fileName: string) => {
|
||||
workspace.setActiveFile(fileName);
|
||||
designer.setActiveView('code');
|
||||
};
|
||||
|
||||
if (workspace.fileErrors.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
className="WorkspaceErrorOverlay"
|
||||
position="absolute"
|
||||
top="0"
|
||||
left="0"
|
||||
right="0"
|
||||
height="100%"
|
||||
bg="rgb(84, 84, 84)"
|
||||
padding="30px"
|
||||
color="red"
|
||||
overflow="auto"
|
||||
zIndex={1999}
|
||||
>
|
||||
<Box position="relative" p="4" boxShadow="0 0 10px rgba(0, 0, 0, 0.5)">
|
||||
<CloseOutlined
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 24,
|
||||
right: 12,
|
||||
color: '#fff',
|
||||
zIndex: 2,
|
||||
}}
|
||||
onClick={handleClose}
|
||||
/>
|
||||
<Box css={errorMessageStyle}>
|
||||
<Box className="top-bar" />
|
||||
<Box fontSize="18px" mb="l">
|
||||
<Text letterSpacing=".5px" color="#c29ef5">
|
||||
[plugin: @babel/parser]
|
||||
</Text>
|
||||
<Text marginLeft={'8px'} color="#f45755">
|
||||
SyntaxError in {workspace.fileErrors.length} files,Click file to modify the error
|
||||
code
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
{workspace.fileErrors.map((fileError, index) => (
|
||||
<Box
|
||||
className="WorkspaceErrorOverlayItem"
|
||||
key={fileError.filename}
|
||||
borderBottom={index === workspace.fileErrors.length - 1 ? 'none' : 'dashed'}
|
||||
borderBottomColor="line2"
|
||||
py="l"
|
||||
>
|
||||
<Box
|
||||
className="WorkspaceErrorOverlayItemFilename"
|
||||
fontWeight="bold"
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => handleOpenErrorFile(fileError.filename)}
|
||||
>
|
||||
{fileError.filename}
|
||||
</Box>
|
||||
<Box
|
||||
as="code"
|
||||
display="block"
|
||||
color="#daa15e"
|
||||
fontSize="16px"
|
||||
fontWeight={'normal'}
|
||||
>
|
||||
{fileError.message}
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
@@ -1,4 +1,7 @@
|
||||
export * from './context-menu';
|
||||
export * from './drag-box';
|
||||
export * from './input-kv';
|
||||
export * from './variable-tree';
|
||||
export * from './variable-tree-modal';
|
||||
export * from './components-popover';
|
||||
export * from './file-errors-overlay';
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { isValidFunctionCode, url2serviceName } from '@music163/tango-helpers';
|
||||
import { Dict, isValidFunctionCode, url2serviceName } from '@music163/tango-helpers';
|
||||
import { InputCode } from '@music163/tango-ui';
|
||||
import { Button, Dropdown, Form, FormProps, Input, Select, Space } from 'antd';
|
||||
import { Box } from 'coral-system';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
interface AddServiceFormProps extends FormProps {
|
||||
initialValues?: object;
|
||||
initialValues?: Dict;
|
||||
onSubmit: (values: object) => void;
|
||||
onCancel: () => void;
|
||||
serviceModules: object[];
|
||||
|
||||
@@ -166,7 +166,7 @@ export function VariableTree(props: VariableTreeProps) {
|
||||
|
||||
return (
|
||||
<Box display="flex" columnGap="l" className="VariableTree" css={varTreeStyle} {...rest}>
|
||||
<Box className="VariableList" width="40%">
|
||||
<Box className="VariableList" width="40%" overflow="auto">
|
||||
{renderHeaderExtra?.(props, state)}
|
||||
<Box mb="m" position="sticky" top="0" bg="white" zIndex={2}>
|
||||
<Search placeholder="请输入变量名" onChange={(val) => setKeyword(val?.trim())} />
|
||||
@@ -184,6 +184,12 @@ export function VariableTree(props: VariableTreeProps) {
|
||||
|
||||
if (isLeaf) {
|
||||
const showView = node.showViewButton ?? showViewButton;
|
||||
|
||||
let nodePath = node.key;
|
||||
if (/^(stores|services)\./.test(nodePath)) {
|
||||
nodePath = `tango.${node.key.replaceAll('.', '?.').replace('?.', '.')}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center">
|
||||
<Text flex="1" truncated>
|
||||
@@ -197,8 +203,13 @@ export function VariableTree(props: VariableTreeProps) {
|
||||
<Box flex="0 0 72px" textAlign="right">
|
||||
{node.showRemoveButton && (
|
||||
<Popconfirm
|
||||
zIndex={99999}
|
||||
title={`确认删除吗 ${node.title}?该操作会导致引用此模型的代码报错,请谨慎操作!`}
|
||||
onConfirm={() => {
|
||||
onCancel={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
onConfirm={(e) => {
|
||||
e.stopPropagation();
|
||||
if (isStoreVariablePath(node.key)) {
|
||||
onRemoveStoreVariable(node.key);
|
||||
} else {
|
||||
@@ -216,7 +227,7 @@ export function VariableTree(props: VariableTreeProps) {
|
||||
/>
|
||||
</Popconfirm>
|
||||
)}
|
||||
<CopyClipboard text={`tango.${node.key.replaceAll('.', '?.')}`}>
|
||||
<CopyClipboard text={nodePath}>
|
||||
{({ copied, onClick }) => {
|
||||
const label = copied ? '已复制' : '复制变量路径';
|
||||
return (
|
||||
@@ -326,7 +337,14 @@ export function VariableTree(props: VariableTreeProps) {
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<ValueDefine data={activeNode} onSave={onUpdateStoreVariable} />
|
||||
<ValueDefine
|
||||
data={activeNode}
|
||||
onSave={(variableKey, code) => {
|
||||
onUpdateStoreVariable(variableKey, code);
|
||||
// 更新当前节点 code
|
||||
setActiveNode((pre) => ({ ...pre, raw: code }));
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</ValueDetail>
|
||||
|
||||
@@ -3,7 +3,8 @@ import { Box } from 'coral-system';
|
||||
import { Button, Empty } from 'antd';
|
||||
import { PlayCircleOutlined } from '@ant-design/icons';
|
||||
import { InputCode, Panel, JsonView } from '@music163/tango-ui';
|
||||
import { isNil, logger, code2object, getValue } from '@music163/tango-helpers';
|
||||
import { isNil, logger, getValue } from '@music163/tango-helpers';
|
||||
import { code2value } from '@music163/tango-core';
|
||||
|
||||
export interface ServicePreviewProps {
|
||||
appContext?: any;
|
||||
@@ -22,7 +23,7 @@ export function ServicePreview({ appContext, functionKey }: ServicePreviewProps)
|
||||
editable
|
||||
showLineNumbers
|
||||
onChange={(value: string) => {
|
||||
const obj = code2object(value);
|
||||
const obj = code2value(value); // 转为 object 对象
|
||||
setPayload(obj);
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -84,6 +84,8 @@ export function ValueDefine({ data, onSave = noop }: ValueDefineProps) {
|
||||
onClick={() => {
|
||||
off();
|
||||
setError('');
|
||||
// 重置到原始值
|
||||
setValue(data.raw);
|
||||
}}
|
||||
>
|
||||
取消
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
import { useWorkspace, observer } from '@music163/tango-context';
|
||||
import { ContextAction } from '@music163/tango-ui';
|
||||
import { CopyOutlined } from '@ant-design/icons';
|
||||
|
||||
export const CopyNodeContextAction = observer(() => {
|
||||
const workspace = useWorkspace();
|
||||
return (
|
||||
<ContextAction
|
||||
icon={<CopyOutlined />}
|
||||
hotkey="Command+C"
|
||||
onClick={() => {
|
||||
workspace.copySelectedNode();
|
||||
}}
|
||||
>
|
||||
复制节点
|
||||
</ContextAction>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
import { useWorkspace, observer } from '@music163/tango-context';
|
||||
import { ContextAction } from '@music163/tango-ui';
|
||||
import { DeleteOutlined } from '@ant-design/icons';
|
||||
|
||||
export const DeleteNodeContextAction = observer(() => {
|
||||
const workspace = useWorkspace();
|
||||
return (
|
||||
<ContextAction
|
||||
icon={<DeleteOutlined />}
|
||||
hotkey="Backspace"
|
||||
onClick={() => {
|
||||
workspace.removeSelectedNode();
|
||||
}}
|
||||
>
|
||||
删除节点
|
||||
</ContextAction>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from './copy-node';
|
||||
export * from './delete-node';
|
||||
export * from './paste-node';
|
||||
export * from './view-source';
|
||||
@@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
import { useWorkspace, observer } from '@music163/tango-context';
|
||||
import { ContextAction } from '@music163/tango-ui';
|
||||
import { SnippetsOutlined } from '@ant-design/icons';
|
||||
|
||||
export const PasteNodeContextAction = observer(() => {
|
||||
const workspace = useWorkspace();
|
||||
return (
|
||||
<ContextAction
|
||||
icon={<SnippetsOutlined />}
|
||||
hotkey="Command+V"
|
||||
onClick={() => {
|
||||
workspace.pasteSelectedNode();
|
||||
}}
|
||||
>
|
||||
粘贴节点
|
||||
</ContextAction>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import { useDesigner, observer } from '@music163/tango-context';
|
||||
import { ContextAction } from '@music163/tango-ui';
|
||||
import { CodeOutlined } from '@ant-design/icons';
|
||||
|
||||
export const ViewSourceContextAction = observer(() => {
|
||||
const designer = useDesigner();
|
||||
return (
|
||||
<ContextAction
|
||||
icon={<CodeOutlined />}
|
||||
onClick={() => {
|
||||
designer.setActiveView('code');
|
||||
}}
|
||||
>
|
||||
查看源码
|
||||
</ContextAction>
|
||||
);
|
||||
});
|
||||
@@ -6,10 +6,14 @@ export interface IDesignerContext<S = any> {
|
||||
* 沙箱查询实例
|
||||
*/
|
||||
sandboxQuery: DndQuery;
|
||||
/**
|
||||
* 预览沙箱查询实例
|
||||
*/
|
||||
previewSandboxQuery: DndQuery;
|
||||
/**
|
||||
* 远程服务
|
||||
*/
|
||||
remoteServices?: Record<string, S>;
|
||||
remoteServices: Record<string, S>;
|
||||
}
|
||||
|
||||
const [DesignerProvider, useDesigner] = createContext<IDesignerContext>({
|
||||
@@ -19,9 +23,13 @@ const [DesignerProvider, useDesigner] = createContext<IDesignerContext>({
|
||||
export { DesignerProvider };
|
||||
|
||||
export const useSandboxQuery = () => {
|
||||
return useDesigner()?.sandboxQuery;
|
||||
return useDesigner().sandboxQuery;
|
||||
};
|
||||
|
||||
export const usePreviewSandboxQuery = () => {
|
||||
return useDesigner().previewSandboxQuery;
|
||||
};
|
||||
|
||||
export const useRemoteServices = () => {
|
||||
return useDesigner()?.remoteServices;
|
||||
return useDesigner().remoteServices;
|
||||
};
|
||||
|
||||
@@ -5,8 +5,18 @@ import { TangoEngineProvider, ITangoEngineContext } from '@music163/tango-contex
|
||||
import zhCN from 'antd/lib/locale/zh_CN';
|
||||
import { DesignerProvider, IDesignerContext } from './context';
|
||||
import defaultTheme from './themes/default';
|
||||
import { DndQuery } from './dnd';
|
||||
import { DESIGN_SANDBOX_ID, PREVIEW_SANDBOX_ID } from './helpers';
|
||||
|
||||
export interface DesignerProps extends IDesignerContext, ITangoEngineContext {
|
||||
const builtinSandboxQuery = new DndQuery({
|
||||
context: `#${DESIGN_SANDBOX_ID}`,
|
||||
});
|
||||
|
||||
const builtinPreviewSandboxQuery = new DndQuery({
|
||||
context: `#${PREVIEW_SANDBOX_ID}`,
|
||||
});
|
||||
|
||||
export interface DesignerProps extends Partial<IDesignerContext>, ITangoEngineContext {
|
||||
/**
|
||||
* 主题包
|
||||
*/
|
||||
@@ -20,13 +30,23 @@ export interface DesignerProps extends IDesignerContext, ITangoEngineContext {
|
||||
* @returns
|
||||
*/
|
||||
export function Designer(props: DesignerProps) {
|
||||
const { engine, config, theme: themeProp, sandboxQuery, remoteServices = {}, children } = props;
|
||||
const {
|
||||
engine,
|
||||
config,
|
||||
theme: themeProp,
|
||||
sandboxQuery = builtinSandboxQuery,
|
||||
previewSandboxQuery = builtinPreviewSandboxQuery,
|
||||
remoteServices = {},
|
||||
children,
|
||||
} = props;
|
||||
const theme = useMemo(() => extendTheme(themeProp, defaultTheme), [themeProp]);
|
||||
return (
|
||||
<SystemProvider theme={theme} prefix="--tango">
|
||||
<ConfigProvider locale={zhCN}>
|
||||
<TangoEngineProvider value={{ engine, config }}>
|
||||
<DesignerProvider value={{ sandboxQuery, remoteServices }}>{children}</DesignerProvider>
|
||||
<DesignerProvider value={{ sandboxQuery, previewSandboxQuery, remoteServices }}>
|
||||
{children}
|
||||
</DesignerProvider>
|
||||
</TangoEngineProvider>
|
||||
</ConfigProvider>
|
||||
</SystemProvider>
|
||||
|
||||
@@ -12,7 +12,6 @@ export const DRAGGABLE_SELECTOR = `[${SLOT.dnd}]`;
|
||||
interface DndQueryOptions {
|
||||
/**
|
||||
* DOM 查询上下文选择器
|
||||
* TODO: 是不是可以合并成一个 API
|
||||
*/
|
||||
context?: string;
|
||||
/**
|
||||
@@ -63,6 +62,9 @@ export class DndQuery {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 沙箱内的 window 对象
|
||||
*/
|
||||
get window() {
|
||||
if (this.context && 'defaultView' in this.context) {
|
||||
return (this.context as unknown as Document).defaultView;
|
||||
@@ -71,6 +73,9 @@ export class DndQuery {
|
||||
return window;
|
||||
}
|
||||
|
||||
/**
|
||||
* 沙箱内的全局滚动偏移
|
||||
*/
|
||||
get scrollTop() {
|
||||
if (this.context && 'documentElement' in this.context) {
|
||||
return (this.context as unknown as Document).documentElement.scrollTop;
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { Dict } from '@music163/tango-helpers';
|
||||
|
||||
/**
|
||||
* 快捷键
|
||||
*/
|
||||
export class Hotkey {
|
||||
private readonly hotkeyMap = {};
|
||||
private readonly hotkeyMap: Dict = {};
|
||||
|
||||
constructor(hotkeys: Record<string, Function>) {
|
||||
Object.keys(hotkeys).forEach((hotkey) => {
|
||||
|
||||
@@ -58,6 +58,15 @@ export function useDnd({
|
||||
'command+v,ctrl+v': () => {
|
||||
workspace.pasteSelectedNode();
|
||||
},
|
||||
'command+arrowup,ctrl+arrowup': () => {
|
||||
workspace.selectSource.selectParent();
|
||||
},
|
||||
'command+z,ctrl+z': () => {
|
||||
workspace.history.back();
|
||||
},
|
||||
'command+shift+z,ctrl+shift+z': () => {
|
||||
workspace.history.forward();
|
||||
},
|
||||
});
|
||||
}, [workspace]);
|
||||
|
||||
@@ -68,6 +77,9 @@ export function useDnd({
|
||||
if (designer.isPreview) {
|
||||
return;
|
||||
}
|
||||
if (designer.showContextMenu) {
|
||||
designer.toggleContextMenu(false);
|
||||
}
|
||||
|
||||
const point = sandboxQuery.getRelativePoint({ x: e.clientX, y: e.clientY });
|
||||
selectSource.setStart({
|
||||
@@ -84,10 +96,10 @@ export function useDnd({
|
||||
const onMouseMove = (e: React.MouseEvent) => {
|
||||
const point = sandboxQuery.getRelativePoint({ x: e.clientX, y: e.clientY });
|
||||
setElementStyle('.SelectionMask', {
|
||||
width: Math.abs(selectSource.start?.point.x - point.x) + 'px',
|
||||
height: Math.abs(selectSource.start?.point.y - point.y) + 'px',
|
||||
left: Math.min(selectSource.start?.point.x, point.x) + 'px',
|
||||
top: Math.min(selectSource.start?.point.y, point.y) + 'px',
|
||||
width: `${Math.abs(selectSource.start?.point.x - point.x)}px`,
|
||||
height: `${Math.abs(selectSource.start?.point.y - point.y)}px`,
|
||||
left: `${Math.min(selectSource.start?.point.x, point.x)}px`,
|
||||
top: `${Math.min(selectSource.start?.point.y, point.y)}px`,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -119,9 +131,14 @@ export function useDnd({
|
||||
};
|
||||
|
||||
const onClick = (e: React.MouseEvent) => {
|
||||
if (designer.showContextMenu) {
|
||||
designer.toggleContextMenu(false);
|
||||
}
|
||||
const data = sandboxQuery.getDraggableParentsData(e.target as HTMLElement, true);
|
||||
if (data && data.id) {
|
||||
selectSource.select(data);
|
||||
// 画布选中节点时,自动唤起 right-panel
|
||||
designer.toggleRightPanel(true);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -371,7 +388,8 @@ export function useDnd({
|
||||
};
|
||||
|
||||
const onKeyDown = (e: React.KeyboardEvent) => {
|
||||
const isInputElement = isInputElements((e.target as HTMLElement).nodeName);
|
||||
const target = e.target as HTMLElement;
|
||||
const isInputElement = target.isContentEditable || isInputElements(target.nodeName);
|
||||
// 禁用输入事件的触发的快捷键逻辑
|
||||
if (!isInputElement) {
|
||||
const key = getHotkey(e);
|
||||
@@ -379,6 +397,56 @@ export function useDnd({
|
||||
}
|
||||
};
|
||||
|
||||
const onContextMenu = (event: React.MouseEvent) => {
|
||||
if (designer.showContextMenu) {
|
||||
designer.toggleContextMenu(false);
|
||||
}
|
||||
// 按下其他按键时,视为用户有特殊操作,此时不展示右键菜单
|
||||
if (event.ctrlKey || event.altKey || event.metaKey || event.shiftKey) {
|
||||
return;
|
||||
}
|
||||
const { clientX, clientY } = event;
|
||||
let target;
|
||||
if (workspace.selectSource.isSelected) {
|
||||
for (const item of workspace.selectSource.selected) {
|
||||
if (
|
||||
// 如果选中的节点是页面根节点(无 parents),则忽略
|
||||
item.parents?.length &&
|
||||
item.bounding &&
|
||||
clientX >= item.bounding.left &&
|
||||
clientX <= item.bounding.left + item.bounding.width &&
|
||||
clientY >= item.bounding.top &&
|
||||
clientY <= item.bounding.top + item.bounding.height
|
||||
) {
|
||||
// 右键坐标已经在当前选中组件的选区内,直接展示右键菜单
|
||||
target = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 否则,根据右键的元素选中最接近的组件
|
||||
if (!target) {
|
||||
target = sandboxQuery.getDraggableParentsData(event.target as HTMLElement, true);
|
||||
}
|
||||
if (target && target.id) {
|
||||
if (!target.parents?.length) {
|
||||
// 页面根节点不展示右键菜单操作
|
||||
return;
|
||||
}
|
||||
// 右键时高亮选中当前元素
|
||||
// 以防之前选区有多个元素,即便已经是选中的元素也再选中一遍
|
||||
event.preventDefault();
|
||||
selectSource.select(target);
|
||||
// 在下一周期再展示右键菜单,以让先前的菜单先被销毁
|
||||
requestAnimationFrame(() => {
|
||||
designer.toggleContextMenu(true, {
|
||||
clientX,
|
||||
clientY,
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onTango = (e: CustomEvent) => {
|
||||
const detail = e.detail || {};
|
||||
|
||||
@@ -406,6 +474,13 @@ export function useDnd({
|
||||
// 打开智能向导弹窗
|
||||
designer.toggleSmartWizard(true);
|
||||
break;
|
||||
case 'addComponent':
|
||||
// 打开添加组件面板
|
||||
designer.toggleAddComponentPopover(true, {
|
||||
clientX: (e.detail.meta as any).clientX + 40,
|
||||
clientY: (e.detail.meta as any).clientY + 110,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -421,6 +496,7 @@ export function useDnd({
|
||||
onDragEnd,
|
||||
onScroll,
|
||||
onKeyDown,
|
||||
onContextMenu,
|
||||
onTango,
|
||||
...selectHandler,
|
||||
};
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import React, { useRef, useEffect, useCallback } from 'react';
|
||||
import { Box } from 'coral-system';
|
||||
import { MultiEditor, MultiEditorProps } from '@music163/tango-ui';
|
||||
import { observer, useWorkspace } from '@music163/tango-context';
|
||||
import { isValidCode } from '@music163/tango-core';
|
||||
import { Modal } from 'antd';
|
||||
import { observer, useDesigner, useWorkspace } from '@music163/tango-context';
|
||||
|
||||
const ideConfig = {
|
||||
// disableFileOps: {
|
||||
@@ -21,108 +19,89 @@ const ideConfig = {
|
||||
// disableSetting: true,
|
||||
};
|
||||
|
||||
export interface CodeEditorProps extends Partial<MultiEditorProps> {
|
||||
/**
|
||||
* 是否自动清楚未使用的导入
|
||||
*/
|
||||
autoRemoveUnusedImports?: boolean;
|
||||
}
|
||||
export type CodeEditorProps = Partial<MultiEditorProps>;
|
||||
|
||||
export const CodeEditor = observer(
|
||||
({ autoRemoveUnusedImports = true, ...rest }: CodeEditorProps) => {
|
||||
const editorRef = useRef(null);
|
||||
const workspace = useWorkspace();
|
||||
const files = workspace.listFiles();
|
||||
const activeFile = workspace.activeFile;
|
||||
export const CodeEditor = observer((props: CodeEditorProps) => {
|
||||
const editorRef = useRef(null);
|
||||
const workspace = useWorkspace();
|
||||
const designer = useDesigner();
|
||||
const files = workspace.listFiles();
|
||||
const activeFile = workspace.activeFile;
|
||||
|
||||
let loc: any; // 记录视图代码的选中位置
|
||||
const selectNode = workspace.selectSource.firstNode;
|
||||
if (selectNode && activeFile === workspace.activeViewFile) {
|
||||
loc = selectNode.loc;
|
||||
}
|
||||
let loc: any; // 记录视图代码的选中位置
|
||||
const selectNode = workspace.selectSource.firstNode;
|
||||
if (selectNode && activeFile === workspace.activeViewFile) {
|
||||
loc = selectNode.loc;
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
editorRef.current?.refresh(files, activeFile, loc);
|
||||
}, [files, activeFile, loc]);
|
||||
useEffect(() => {
|
||||
editorRef.current?.refresh(files, activeFile, loc);
|
||||
}, [files, activeFile, loc]);
|
||||
|
||||
const fileSave = useCallback(
|
||||
(path: string, value: string) => {
|
||||
if (!isJsFile(path)) {
|
||||
// 非 js 文件直接保存
|
||||
workspace.updateFile(path, value, autoRemoveUnusedImports);
|
||||
return;
|
||||
const fileSave = useCallback(
|
||||
(path: string, value: string) => {
|
||||
workspace.updateFile(path, value, false);
|
||||
},
|
||||
[workspace],
|
||||
);
|
||||
|
||||
const handleFileChange = useCallback(
|
||||
(type: string, info: any) => {
|
||||
switch (type) {
|
||||
case 'addFile':
|
||||
workspace.addFile(info.path, info.value);
|
||||
break;
|
||||
case 'deleteFile':
|
||||
case 'deleteFolder':
|
||||
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;
|
||||
}
|
||||
},
|
||||
[workspace],
|
||||
);
|
||||
|
||||
const handlePathChange = useCallback(
|
||||
(path: string) => {
|
||||
workspace.setActiveFile(path);
|
||||
},
|
||||
[workspace],
|
||||
);
|
||||
|
||||
const borderStyle =
|
||||
designer.activeView === 'dual'
|
||||
? {
|
||||
borderLeft: 'solid 1px var(--tango-colors-line2)',
|
||||
borderRight: 'solid 1px var(--tango-colors-line2)',
|
||||
}
|
||||
: {};
|
||||
|
||||
// js 文件需要先检查语法,只有语法正确才会保存
|
||||
if (isValidCode(value)) {
|
||||
workspace.updateFile(path, value, autoRemoveUnusedImports);
|
||||
} else {
|
||||
Modal.confirm({
|
||||
title: '检测到代码中存在语法错误,暂时无法将代码同步给设计器,是否回退到安全代码?',
|
||||
onOk: () => {
|
||||
editorRef.current?.refresh(files, activeFile);
|
||||
},
|
||||
onCancel: () => {},
|
||||
});
|
||||
}
|
||||
},
|
||||
[workspace, autoRemoveUnusedImports, activeFile, files],
|
||||
);
|
||||
|
||||
const handleFileChange = useCallback(
|
||||
(type: string, info: any) => {
|
||||
switch (type) {
|
||||
case 'addFile':
|
||||
workspace.addFile(info.path, info.value);
|
||||
break;
|
||||
case 'deleteFile':
|
||||
case 'deleteFolder':
|
||||
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;
|
||||
}
|
||||
},
|
||||
[workspace],
|
||||
);
|
||||
|
||||
const handlePathChange = useCallback(
|
||||
(path: string) => {
|
||||
workspace.setActiveFile(path);
|
||||
},
|
||||
[workspace],
|
||||
);
|
||||
|
||||
return (
|
||||
<Box display="flex" flexDirection="row" height="100%" bg="white">
|
||||
<MultiEditor
|
||||
ref={editorRef}
|
||||
options={{
|
||||
fontSize: 14,
|
||||
automaticLayout: true,
|
||||
}}
|
||||
ideConfig={ideConfig}
|
||||
onFileSave={fileSave}
|
||||
onPathChange={handlePathChange}
|
||||
onFileChange={handleFileChange}
|
||||
defaultPath={activeFile}
|
||||
defaultTheme="GithubLightDefault"
|
||||
defaultFiles={files}
|
||||
{...rest}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
function isJsFile(path: string) {
|
||||
return /.jsx?$/.test(path);
|
||||
}
|
||||
return (
|
||||
<Box display="flex" flexDirection="row" height="100%" bg="white" {...borderStyle}>
|
||||
<MultiEditor
|
||||
ref={editorRef}
|
||||
options={{
|
||||
fontSize: 14,
|
||||
automaticLayout: true,
|
||||
}}
|
||||
ideConfig={ideConfig}
|
||||
onFileSave={fileSave}
|
||||
onPathChange={handlePathChange}
|
||||
onFileChange={handleFileChange}
|
||||
defaultPath={activeFile}
|
||||
defaultTheme="GithubLightDefault"
|
||||
defaultFiles={files}
|
||||
{...props}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -7,6 +7,20 @@ import {
|
||||
parseDndId,
|
||||
} from '@music163/tango-helpers';
|
||||
|
||||
// -----------
|
||||
// CONSTANTS
|
||||
// -----------
|
||||
|
||||
export const DRAG_GHOST_ID = 'dragGhost';
|
||||
|
||||
export const DESIGN_SANDBOX_ID = 'sandbox-container';
|
||||
|
||||
export const PREVIEW_SANDBOX_ID = 'preview-sandbox-container';
|
||||
|
||||
// -----------
|
||||
// DOM Helpers
|
||||
// -----------
|
||||
|
||||
export function buildQueryBySlotId(id: string) {
|
||||
return `[${SLOT.dnd}="${id}"]`;
|
||||
}
|
||||
@@ -15,8 +29,6 @@ export function getElement(selector: Selector) {
|
||||
return $(selector).get(0);
|
||||
}
|
||||
|
||||
export const DRAG_GHOST_ID = 'dragGhost';
|
||||
|
||||
export const getDragGhostElement = () => getElement(`#${DRAG_GHOST_ID}`);
|
||||
|
||||
export function setElementStyle(selector: Selector, style: any) {
|
||||
@@ -75,7 +87,7 @@ export function getElementData(
|
||||
const display = getElementCSSDisplay(element);
|
||||
return {
|
||||
id: dnd.id,
|
||||
codeId: dnd.index,
|
||||
codeId: dnd.codeId,
|
||||
name: dnd.component || element.tagName.toLowerCase(),
|
||||
filename: dnd.filename,
|
||||
bounding,
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import React, { useImperativeHandle, ForwardedRef, useRef, useState, useEffect } from 'react';
|
||||
import { Box } from 'coral-system';
|
||||
import { CodeSandbox, CodeSandboxProps } from '@music163/tango-sandbox';
|
||||
import { getValue, isFunction, logger, pick, setValue } from '@music163/tango-helpers';
|
||||
import { Dict, 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';
|
||||
import { DndQuery, useDnd } from '../dnd';
|
||||
import { Navigator } from './navigator';
|
||||
import { SelectionToolsProps } from '../simulator/selection';
|
||||
import { DESIGN_SANDBOX_ID, PREVIEW_SANDBOX_ID } from '../helpers';
|
||||
|
||||
interface ISandboxEventHandlerConfig {
|
||||
sandboxQuery?: DndQuery;
|
||||
@@ -48,6 +49,10 @@ export interface CombinedSandboxRef {
|
||||
|
||||
const LANDING_PAGE_PATH = '/__background_landing_page__';
|
||||
|
||||
function fixRouterMode(routerType: string): CodeSandboxProps['routerMode'] {
|
||||
return routerType === 'hash' ? 'hash' : undefined;
|
||||
}
|
||||
|
||||
function useSandbox({
|
||||
isPreview: isPreviewProp,
|
||||
configFormatter,
|
||||
@@ -62,6 +67,7 @@ function useSandbox({
|
||||
const workspace = useWorkspace();
|
||||
const designer = useDesigner();
|
||||
const sandboxQuery = useSandboxQuery();
|
||||
|
||||
const isPreview = isPreviewProp ?? designer.isPreview;
|
||||
|
||||
// 组件不一定会立即刷新,因此 isActive 需要实时获取
|
||||
@@ -82,7 +88,7 @@ function useSandbox({
|
||||
onViewChange: (data) => onViewChange && onViewChange(data, getSandboxConfig()),
|
||||
});
|
||||
|
||||
let files = Array.from(workspace.files.keys()).reduce((prev, filename) => {
|
||||
let files = Array.from(workspace.files.keys()).reduce<Dict>((prev, filename) => {
|
||||
let code = workspace.getFile(filename).code;
|
||||
if (filename === '/tango.config.json') {
|
||||
code = mergeTangoConfigJson(code, { isPreview, injectScript, formatter: configFormatter });
|
||||
@@ -103,23 +109,26 @@ function useSandbox({
|
||||
// 根据当前 workspace 状态与组件传入的状态是否一致,控制是否需要切换到空白路由
|
||||
const display = isActive ? 'block' : 'none';
|
||||
const routePath = isActive ? startRoute || workspace.activeRoute : LANDING_PAGE_PATH;
|
||||
const routerMode = fixRouterMode(workspace.appEntryModule?.routerType);
|
||||
|
||||
const sandboxProps = isPreview
|
||||
? {
|
||||
files,
|
||||
eventHandlers: pick(dndHandlers, ['onTango']),
|
||||
onMessage,
|
||||
onLoad,
|
||||
display,
|
||||
startRoute: routePath,
|
||||
onLoad,
|
||||
routerMode,
|
||||
}
|
||||
: {
|
||||
files,
|
||||
eventHandlers: dndHandlers as any,
|
||||
onMessage,
|
||||
onLoad,
|
||||
display,
|
||||
startRoute: routePath,
|
||||
onLoad,
|
||||
routerMode,
|
||||
};
|
||||
|
||||
return sandboxProps;
|
||||
@@ -140,7 +149,7 @@ const PreviewSandbox = observer(
|
||||
|
||||
return (
|
||||
<Box display={display} width="100%" height="100%">
|
||||
<CodeSandbox ref={ref} iframeId="preview-sandbox-container" {...sandboxProps} {...rest} />
|
||||
<CodeSandbox ref={ref} iframeId={PREVIEW_SANDBOX_ID} {...sandboxProps} {...rest} />
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
@@ -188,7 +197,7 @@ const DesignSandbox = observer(
|
||||
|
||||
return (
|
||||
<Box display={display} width="100%" height="100%">
|
||||
<CodeSandbox ref={ref} {...sandboxProps} {...rest} />
|
||||
<CodeSandbox ref={ref} iframeId={DESIGN_SANDBOX_ID} {...sandboxProps} {...rest} />
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
export * from './copy-node';
|
||||
export * from './delete-node';
|
||||
export * from './more-actions';
|
||||
export * from './select-parent-node';
|
||||
export * from './view-source';
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
import { observer } from '@music163/tango-context';
|
||||
import { SelectAction } from '@music163/tango-ui';
|
||||
import { MoreOutlined } from '@ant-design/icons';
|
||||
import { Dropdown } from 'antd';
|
||||
import { ContextMenu } from '../components';
|
||||
|
||||
export const MoreActionsAction = observer(() => {
|
||||
return (
|
||||
<Dropdown placement="bottomRight" overlay={<ContextMenu showParents />}>
|
||||
<SelectAction tooltip="更多">
|
||||
<MoreOutlined />
|
||||
</SelectAction>
|
||||
</Dropdown>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import { useWorkspace, observer } from '@music163/tango-context';
|
||||
import { IconFont, SelectAction } from '@music163/tango-ui';
|
||||
|
||||
export const SelectParentNodeAction = observer(() => {
|
||||
const workspace = useWorkspace();
|
||||
|
||||
return (
|
||||
<SelectAction
|
||||
tooltip="选中父节点"
|
||||
onClick={() => {
|
||||
workspace.selectSource.selectParent();
|
||||
}}
|
||||
>
|
||||
<IconFont type="icon-huiche1" rotate={90} />
|
||||
</SelectAction>
|
||||
);
|
||||
});
|
||||
@@ -1,7 +1,6 @@
|
||||
import React from 'react';
|
||||
import { observer, useDesigner } from '@music163/tango-context';
|
||||
import { SelectAction } from '@music163/tango-ui';
|
||||
import { AimOutlined } from '@ant-design/icons';
|
||||
import { SelectAction, CodeOutlined } from '@music163/tango-ui';
|
||||
|
||||
export const ViewSourceAction = observer(() => {
|
||||
const designer = useDesigner();
|
||||
@@ -12,7 +11,7 @@ export const ViewSourceAction = observer(() => {
|
||||
designer.setActiveView('code');
|
||||
}}
|
||||
>
|
||||
<AimOutlined />
|
||||
<CodeOutlined />
|
||||
</SelectAction>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React from 'react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { Radio, RadioProps, Tooltip } from 'antd';
|
||||
import type { OptionType } from '@music163/tango-helpers';
|
||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||
import { IconFont } from '@music163/tango-ui';
|
||||
|
||||
interface ChoiceSetterProps {
|
||||
mode?: 'text' | 'icon';
|
||||
options?: OptionType[];
|
||||
}
|
||||
|
||||
@@ -13,6 +13,18 @@ export function ChoiceSetter({
|
||||
onChange,
|
||||
...props
|
||||
}: FormItemComponentProps<string> & ChoiceSetterProps) {
|
||||
const renderIcon = useCallback((icon: string) => {
|
||||
if (!icon) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (icon.startsWith('icon-')) {
|
||||
return <IconFont type={icon} />;
|
||||
}
|
||||
|
||||
return null;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Radio.Group
|
||||
optionType="button"
|
||||
@@ -24,6 +36,7 @@ export function ChoiceSetter({
|
||||
>
|
||||
{options.map((item) => (
|
||||
<RadioButton tip={item.tip} key={item.value} value={item.value}>
|
||||
{renderIcon(item.icon)}
|
||||
{item.label}
|
||||
</RadioButton>
|
||||
))}
|
||||
|
||||
@@ -2,9 +2,8 @@ import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { Box } from 'coral-system';
|
||||
// @ts-ignore
|
||||
import { toJSON } from 'cssjson';
|
||||
import { InputNumber, Space } from 'antd';
|
||||
import { InputNumber, Slider, Space } from 'antd';
|
||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||
import { SliderSetter } from './number-setter';
|
||||
import {
|
||||
BgSetter,
|
||||
BorderSetter,
|
||||
@@ -16,10 +15,11 @@ import {
|
||||
FlexJustifyContentSetter,
|
||||
SpacingSetter,
|
||||
} from './style-setter';
|
||||
import { wrapCode } from '@music163/tango-helpers';
|
||||
|
||||
const getRawCssValue = (value: string) => {
|
||||
if (value && value.startsWith('{css`')) {
|
||||
return value.split('').slice(5, -2).join('').trim();
|
||||
if (value && value.startsWith('{{css`')) {
|
||||
return value.split('').slice(6, -3).join('').trim();
|
||||
}
|
||||
return value;
|
||||
};
|
||||
@@ -51,7 +51,8 @@ const cssPattern = (string: string, name: string): any => {
|
||||
};
|
||||
|
||||
/**
|
||||
* 废弃
|
||||
* coral-system css prop
|
||||
* @example 提供 css-in-js 代码支持,例如 css`background: red;`
|
||||
* @deprecated 使用嵌套属性代替
|
||||
*/
|
||||
export function CssSetter(props: FormItemComponentProps<string>) {
|
||||
@@ -82,16 +83,14 @@ export function CssSetter(props: FormItemComponentProps<string>) {
|
||||
|
||||
useEffect(() => {
|
||||
if (contentValue || contentValue === '') {
|
||||
onChange(`{css\`${contentValue}\`}`, {
|
||||
// relatedImports: ['']
|
||||
});
|
||||
onChange(wrapCode(`css\`${contentValue}\``));
|
||||
}
|
||||
}, [contentValue]);
|
||||
|
||||
const isFlex = ['flex', 'inline-flex'].includes(display);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box bg="fill1">
|
||||
<ItemGroup title="布局方式">
|
||||
<DisplaySetter
|
||||
value={toJSON(contentValue).attributes?.display}
|
||||
@@ -204,12 +203,12 @@ export function CssSetter(props: FormItemComponentProps<string>) {
|
||||
/>
|
||||
</ItemGroup>
|
||||
<ItemGroup title="透明度">
|
||||
<SliderSetter
|
||||
<Slider
|
||||
max={1}
|
||||
min={0}
|
||||
step={0.1}
|
||||
value={toJSON(contentValue).attributes?.opacity || 1}
|
||||
onChange={(v) => {
|
||||
onChange={(v: number) => {
|
||||
changeStyle(v, 'opacity');
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -25,13 +25,18 @@ const style = {
|
||||
width: '100%',
|
||||
};
|
||||
|
||||
export function DateSetter({ value, onChange, format = 'YYYY-MM-DD', ...rest }: FormItemComponentProps<string>) {
|
||||
export function DateSetter({
|
||||
value,
|
||||
onChange,
|
||||
format = 'YYYY-MM-DD',
|
||||
...rest
|
||||
}: FormItemComponentProps<string>) {
|
||||
return (
|
||||
<DatePicker
|
||||
{...rest}
|
||||
format={format}
|
||||
style={style}
|
||||
value={toMoment(value, format)}
|
||||
value={value ? toMoment(value, format) : undefined}
|
||||
onChange={(val, str) => {
|
||||
onChange && onChange(str);
|
||||
}}
|
||||
@@ -46,7 +51,12 @@ function toMoments(value: string[], format: string): moment.Moment[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
export function DateRangeSetter({ value, onChange, format = 'YYYY-MM-DD', ...rest }: FormItemComponentProps<string[]>) {
|
||||
export function DateRangeSetter({
|
||||
value,
|
||||
onChange,
|
||||
format = 'YYYY-MM-DD',
|
||||
...rest
|
||||
}: FormItemComponentProps<string[]>) {
|
||||
return (
|
||||
<DatePicker.RangePicker
|
||||
{...rest}
|
||||
@@ -60,7 +70,12 @@ export function DateRangeSetter({ value, onChange, format = 'YYYY-MM-DD', ...res
|
||||
);
|
||||
}
|
||||
|
||||
export function TimeSetter({ value, onChange, format = 'HH:mm:ss', ...rest }: FormItemComponentProps<string>) {
|
||||
export function TimeSetter({
|
||||
value,
|
||||
onChange,
|
||||
format = 'HH:mm:ss',
|
||||
...rest
|
||||
}: FormItemComponentProps<string>) {
|
||||
return (
|
||||
<TimePicker
|
||||
{...rest}
|
||||
@@ -74,7 +89,12 @@ export function TimeSetter({ value, onChange, format = 'HH:mm:ss', ...rest }: Fo
|
||||
);
|
||||
}
|
||||
|
||||
export function TimeRangeSetter({ value, onChange, format = 'HH:mm:ss', ...rest }: FormItemComponentProps<string[]>) {
|
||||
export function TimeRangeSetter({
|
||||
value,
|
||||
onChange,
|
||||
format = 'HH:mm:ss',
|
||||
...rest
|
||||
}: FormItemComponentProps<string[]>) {
|
||||
return (
|
||||
<TimePicker.RangePicker
|
||||
{...rest}
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { css, Box } from 'coral-system';
|
||||
import { AutoComplete } from 'antd';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { css, Box, Text } from 'coral-system';
|
||||
import { AutoComplete, Input } from 'antd';
|
||||
import { ActionSelect } from '@music163/tango-ui';
|
||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||
import { useWorkspace, useWorkspaceData } from '@music163/tango-context';
|
||||
import { ExpressionModal, getWrappedExpressionCode } from './expression-setter';
|
||||
import { Dict, wrapCode } from '@music163/tango-helpers';
|
||||
import { ExpressionPopover, getCallbackValue } from './expression-setter';
|
||||
import { value2code } from '@music163/tango-core';
|
||||
|
||||
enum EventAction {
|
||||
NoAction = 'noAction',
|
||||
ConsoleLog = 'consoleLog',
|
||||
BindExpression = 'bindExpression',
|
||||
OpenModal = 'openModal',
|
||||
CloseModal = 'closeModal',
|
||||
@@ -22,72 +25,85 @@ const wrapperStyle = css`
|
||||
}
|
||||
`;
|
||||
|
||||
const options = [
|
||||
{ label: '无动作', value: EventAction.NoAction },
|
||||
{ label: '绑定 JS 表达式', value: EventAction.BindExpression },
|
||||
{ label: '打开页面', value: EventAction.NavigateTo },
|
||||
{ label: '打开弹窗', value: EventAction.OpenModal },
|
||||
{ label: '关闭弹窗', value: EventAction.CloseModal },
|
||||
];
|
||||
|
||||
export type EventSetterProps = FormItemComponentProps<string>;
|
||||
|
||||
/**
|
||||
* 事件监听函数绑定器
|
||||
*/
|
||||
export function EventSetter(props: EventSetterProps) {
|
||||
const { value, onChange, modalTitle } = props;
|
||||
const { value, onChange, modalTitle, modalTip, template } = props;
|
||||
const [type, setType] = useState<EventAction>(); // 事件类型
|
||||
const [temp, setTemp] = useState(''); // 二级暂存值
|
||||
const [expModalVisible, setExpModalVisible] = useState(false); // 弹窗是否显示
|
||||
|
||||
const { actionVariables, routeOptions } = useWorkspaceData();
|
||||
const workspace = useWorkspace();
|
||||
const modalOptions = workspace.activeViewModule.listModals() || [];
|
||||
|
||||
const code = value2code(value);
|
||||
|
||||
const handleChange = useCallback<FormItemComponentProps['onChange']>(
|
||||
(nextValue: any, ...args) => {
|
||||
const ret = getWrappedExpressionCode(nextValue);
|
||||
if (ret !== value) {
|
||||
onChange(ret, ...args);
|
||||
if (!nextValue) {
|
||||
onChange(undefined);
|
||||
}
|
||||
if (nextValue !== code) {
|
||||
onChange(wrapCode(nextValue), ...args);
|
||||
}
|
||||
},
|
||||
[onChange, value],
|
||||
[onChange, code],
|
||||
);
|
||||
const options = useMemo(
|
||||
() => [
|
||||
{ label: '无动作', value: EventAction.NoAction },
|
||||
{ label: '打印事件', value: EventAction.ConsoleLog },
|
||||
{
|
||||
label: (
|
||||
<ExpressionPopover
|
||||
title={modalTitle}
|
||||
subTitle={modalTip}
|
||||
value={value}
|
||||
template={template}
|
||||
onOk={(nextValue) => {
|
||||
handleChange(nextValue);
|
||||
}}
|
||||
dataSource={actionVariables}
|
||||
>
|
||||
<Text>绑定 JS 表达式</Text>
|
||||
</ExpressionPopover>
|
||||
),
|
||||
value: EventAction.BindExpression,
|
||||
},
|
||||
{ label: '打开页面', value: EventAction.NavigateTo },
|
||||
{ label: '打开弹窗', value: EventAction.OpenModal },
|
||||
{ label: '关闭弹窗', value: EventAction.CloseModal },
|
||||
],
|
||||
[modalTitle, value, actionVariables, template, handleChange],
|
||||
);
|
||||
|
||||
const onAction = (key: string) => {
|
||||
setType(key as EventAction); // 记录事件类型
|
||||
setTemp(''); // 重置二级选项值
|
||||
|
||||
switch (key) {
|
||||
case EventAction.BindExpression:
|
||||
setExpModalVisible(true);
|
||||
break;
|
||||
case EventAction.NoAction:
|
||||
handleChange(undefined);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
if (key === EventAction.NoAction) {
|
||||
handleChange(undefined);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const actionText = getActionText(type, temp, value);
|
||||
const inputValue = value;
|
||||
|
||||
return (
|
||||
<Box css={wrapperStyle}>
|
||||
<ActionSelect options={options} onSelect={onAction} text={actionText} />
|
||||
<ExpressionModal
|
||||
title={modalTitle}
|
||||
value={inputValue}
|
||||
visible={expModalVisible}
|
||||
onCancel={() => setExpModalVisible(false)}
|
||||
onOk={(nextValue) => {
|
||||
setExpModalVisible(false);
|
||||
handleChange(nextValue);
|
||||
}}
|
||||
dataSource={actionVariables}
|
||||
/>
|
||||
<ActionSelect options={options} onSelect={onAction} defaultText="请选择动作类型" />
|
||||
{type === EventAction.ConsoleLog && (
|
||||
<Input
|
||||
placeholder="输入 Console.log 日志内容"
|
||||
value={temp}
|
||||
onChange={(e) => setTemp(e.target.value)}
|
||||
onBlur={() => {
|
||||
if (temp) {
|
||||
handleChange(getExpressionValue(type, temp));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{type === EventAction.NavigateTo && (
|
||||
<AutoComplete
|
||||
placeholder="选择或输入页面路由"
|
||||
@@ -131,26 +147,16 @@ export function EventSetter(props: EventSetterProps) {
|
||||
);
|
||||
}
|
||||
|
||||
const handlerMap = {
|
||||
[EventAction.OpenModal]: 'openModal',
|
||||
[EventAction.CloseModal]: 'closeModal',
|
||||
[EventAction.NavigateTo]: 'navigateTo',
|
||||
const handlerMap: Dict = {
|
||||
[EventAction.ConsoleLog]: 'console.log',
|
||||
[EventAction.OpenModal]: 'tango.openModal',
|
||||
[EventAction.CloseModal]: 'tango.closeModal',
|
||||
[EventAction.NavigateTo]: 'tango.navigateTo',
|
||||
};
|
||||
|
||||
function getActionText(type: EventAction, temp: string, value: any) {
|
||||
let text;
|
||||
if (handlerMap[type]) {
|
||||
text = getExpressionValue(type, temp);
|
||||
} else if (value) {
|
||||
text = value;
|
||||
}
|
||||
text = text || '请选择';
|
||||
return text;
|
||||
}
|
||||
|
||||
function getExpressionValue(type: EventAction, value = '') {
|
||||
const handler = handlerMap[type];
|
||||
if (handler) {
|
||||
return `{() => tango.${handler}("${value}")}`;
|
||||
return getCallbackValue(`${handler}("${value}");`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -1,20 +1,18 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { Box, Text, css } from 'coral-system';
|
||||
import { Modal } from 'antd';
|
||||
import { Dropdown, Button } from 'antd';
|
||||
import { isValidExpressionCode } from '@music163/tango-core';
|
||||
import { getValue, interpolate, IVariableTreeNode, noop } from '@music163/tango-helpers';
|
||||
import { CloseCircleFilled, MenuOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
isValidExpressionCode,
|
||||
isWrappedByExpressionContainer,
|
||||
value2expressionCode,
|
||||
} from '@music163/tango-core';
|
||||
import {
|
||||
getVariableContent,
|
||||
noop,
|
||||
useBoolean,
|
||||
getValue,
|
||||
IVariableTreeNode,
|
||||
} from '@music163/tango-helpers';
|
||||
import { CloseCircleFilled, ExpandAltOutlined } from '@ant-design/icons';
|
||||
import { Panel, InputCode, Action } from '@music163/tango-ui';
|
||||
Panel,
|
||||
InputCode,
|
||||
Action,
|
||||
DragPanel,
|
||||
PopOutOutlined,
|
||||
InputCodeProps,
|
||||
InputStyleCode,
|
||||
} from '@music163/tango-ui';
|
||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||
import { useWorkspace, useWorkspaceData } from '@music163/tango-context';
|
||||
import { VariableTree } from '../components';
|
||||
@@ -23,43 +21,35 @@ import { CODE_TEMPLATES } from '../helpers';
|
||||
import { shapeServiceValues } from '../sidebar/datasource-panel/interface-config';
|
||||
|
||||
export const expressionValueValidate = (value: string) => {
|
||||
if (isWrappedByExpressionContainer(value)) {
|
||||
const exp = getVariableContent(value);
|
||||
if (!isValidExpressionCode(exp)) {
|
||||
return '表达式存在语法错误!';
|
||||
}
|
||||
if (!isValidExpressionCode(value)) {
|
||||
return '表达式存在语法错误!';
|
||||
}
|
||||
};
|
||||
|
||||
export const jsonValueValidate = (value: string) => {
|
||||
if (isWrappedByExpressionContainer(value)) {
|
||||
const jsonStr = getVariableContent(value);
|
||||
try {
|
||||
JSON.parse(jsonStr);
|
||||
} catch (e) {
|
||||
return '不是合法的 JSON 语法!';
|
||||
}
|
||||
try {
|
||||
JSON.parse(value);
|
||||
} catch (e) {
|
||||
return '不是合法的 JSON 语法!';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 返回 `{}` 包裹后的表达式代码
|
||||
* @param code 原始代码
|
||||
* 拼装回调函数
|
||||
* @param value 回调函数体
|
||||
* @param template 回调函数模板
|
||||
* @returns
|
||||
*/
|
||||
export function getWrappedExpressionCode(code: string) {
|
||||
let ret;
|
||||
if (!code) {
|
||||
// do nothing
|
||||
} else if (isWrappedByExpressionContainer(code)) {
|
||||
ret = code;
|
||||
} else {
|
||||
ret = `{${code}}`;
|
||||
export function getCallbackValue(value: string, template?: string) {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
return ret;
|
||||
return template ? interpolate(template, { content: value }) : `() => {\n ${value}\n}`;
|
||||
}
|
||||
|
||||
const suffixStyle = css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.anticon-close-circle {
|
||||
color: rgba(0, 0, 0, 0.25);
|
||||
&:hover {
|
||||
@@ -73,6 +63,11 @@ export interface ExpressionSetterProps extends FormItemComponentProps<string> {
|
||||
modalTip?: string;
|
||||
autoCompleteOptions?: string[];
|
||||
allowClear?: boolean;
|
||||
showOptionsDropDown?: boolean;
|
||||
/**
|
||||
* 表达式类型指定为 css object
|
||||
*/
|
||||
expressionType?: 'cssObject';
|
||||
}
|
||||
|
||||
export function ExpressionSetter(props: ExpressionSetterProps) {
|
||||
@@ -81,22 +76,21 @@ export function ExpressionSetter(props: ExpressionSetterProps) {
|
||||
modalTitle,
|
||||
modalTip,
|
||||
autoCompleteOptions,
|
||||
placeholder = '输入JS表达式代码',
|
||||
template,
|
||||
placeholder = '在这里输入代码',
|
||||
value: valueProp,
|
||||
status,
|
||||
allowClear = true,
|
||||
newStoreTemplate,
|
||||
showOptionsDropDown = true,
|
||||
expressionType,
|
||||
} = props;
|
||||
const [visible, { on, off }] = useBoolean();
|
||||
const [inputValue, setInputValue] = useState(() => {
|
||||
return value2expressionCode(valueProp);
|
||||
});
|
||||
const sandbox = useSandboxQuery();
|
||||
const evaluateContext = sandbox.window;
|
||||
// const codeValue = getCodeOfWrappedCode(valueProp);
|
||||
const [inputValue, setInputValue] = useState(valueProp);
|
||||
|
||||
// when receive new value, sync state
|
||||
useEffect(() => {
|
||||
setInputValue(value2expressionCode(valueProp));
|
||||
setInputValue(valueProp);
|
||||
}, [valueProp]);
|
||||
|
||||
const change = useCallback(
|
||||
@@ -104,21 +98,20 @@ export function ExpressionSetter(props: ExpressionSetterProps) {
|
||||
if (code === valueProp) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ret = getWrappedExpressionCode(code);
|
||||
|
||||
if (ret === valueProp) {
|
||||
return;
|
||||
}
|
||||
|
||||
onChange(ret);
|
||||
onChange(code);
|
||||
},
|
||||
[valueProp, onChange],
|
||||
);
|
||||
|
||||
const sandbox = useSandboxQuery();
|
||||
const evaluateContext = sandbox.window;
|
||||
|
||||
const InputCodeComponent = expressionType === 'cssObject' ? InputStyleCode : InputCode;
|
||||
|
||||
return (
|
||||
<Box className="ExpressionSetter">
|
||||
<InputCode
|
||||
{/* 同时支持下拉框展示 */}
|
||||
<InputCodeComponent
|
||||
placeholder={placeholder}
|
||||
suffix={
|
||||
<Box css={suffixStyle}>
|
||||
@@ -126,15 +119,42 @@ export function ExpressionSetter(props: ExpressionSetterProps) {
|
||||
<CloseCircleFilled
|
||||
title="清空"
|
||||
onClick={() => {
|
||||
change('');
|
||||
change(undefined);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Action
|
||||
tooltip="打开表达式变量选择面板"
|
||||
icon={<ExpandAltOutlined />}
|
||||
onClick={on}
|
||||
size="small"
|
||||
{showOptionsDropDown && autoCompleteOptions?.length && (
|
||||
<Dropdown
|
||||
menu={{
|
||||
items: autoCompleteOptions.map((option) => ({
|
||||
key: option,
|
||||
label: (
|
||||
<Text
|
||||
onClick={() => {
|
||||
change(option);
|
||||
}}
|
||||
>
|
||||
{option}
|
||||
</Text>
|
||||
),
|
||||
})),
|
||||
}}
|
||||
>
|
||||
<Action tooltip="使用预设代码片段" icon={<MenuOutlined />} size="small" />
|
||||
</Dropdown>
|
||||
)}
|
||||
<ExpressionPopover
|
||||
title={modalTitle}
|
||||
subTitle={modalTip}
|
||||
placeholder={placeholder}
|
||||
autoCompleteOptions={autoCompleteOptions}
|
||||
template={template}
|
||||
newStoreTemplate={newStoreTemplate}
|
||||
value={inputValue}
|
||||
expressionType={expressionType}
|
||||
onOk={(value) => {
|
||||
change(value);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
}
|
||||
@@ -148,57 +168,54 @@ export function ExpressionSetter(props: ExpressionSetterProps) {
|
||||
status={status}
|
||||
maxHeight="200px"
|
||||
/>
|
||||
<ExpressionModal
|
||||
title={modalTitle}
|
||||
subTitle={modalTip}
|
||||
placeholder={placeholder}
|
||||
autoCompleteOptions={autoCompleteOptions}
|
||||
newStoreTemplate={newStoreTemplate}
|
||||
visible={visible}
|
||||
value={inputValue}
|
||||
onCancel={() => off()}
|
||||
onOk={(value) => {
|
||||
change(value);
|
||||
off();
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export interface ExpressionModalProps {
|
||||
export interface ExpressionPopoverProps extends InputCodeProps {
|
||||
title?: string;
|
||||
subTitle?: string;
|
||||
placeholder?: string;
|
||||
visible?: boolean;
|
||||
defaultValue?: string;
|
||||
value?: string;
|
||||
onCancel?: () => void;
|
||||
onOk?: (value: string) => void;
|
||||
dataSource?: IVariableTreeNode[];
|
||||
autoCompleteOptions?: string[];
|
||||
/**
|
||||
* 值的模板,一般用于定义函数模板
|
||||
*/
|
||||
template?: string;
|
||||
/**
|
||||
* 新建 store 的模板代码
|
||||
*/
|
||||
newStoreTemplate?: string;
|
||||
/**
|
||||
* 表达式类型指定为 css object
|
||||
*/
|
||||
expressionType?: 'cssObject';
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function ExpressionModal({
|
||||
export function ExpressionPopover({
|
||||
title,
|
||||
subTitle,
|
||||
placeholder,
|
||||
visible,
|
||||
onCancel = noop,
|
||||
onOk = noop,
|
||||
defaultValue,
|
||||
value,
|
||||
dataSource,
|
||||
autoCompleteOptions,
|
||||
template,
|
||||
newStoreTemplate = CODE_TEMPLATES.newStoreTemplate,
|
||||
}: ExpressionModalProps) {
|
||||
children,
|
||||
expressionType,
|
||||
}: ExpressionPopoverProps) {
|
||||
const [exp, setExp] = useState(value ?? defaultValue);
|
||||
const [error, setError] = useState('');
|
||||
const workspace = useWorkspace();
|
||||
const InputCodeComponent = expressionType === 'cssObject' ? InputStyleCode : InputCode;
|
||||
|
||||
const selectNodePath = workspace.selectSource.selected[0]?.codeId;
|
||||
|
||||
const { expressionVariables } = useWorkspaceData();
|
||||
const serviceModules = Object.keys(workspace.serviceModules).map((key) => ({
|
||||
@@ -207,7 +224,7 @@ export function ExpressionModal({
|
||||
}));
|
||||
|
||||
const sandbox = useSandboxQuery();
|
||||
const evaluateContext = sandbox.window;
|
||||
const evaluateContext: any = sandbox.window;
|
||||
|
||||
const handleExpInputChange = (val: string) => {
|
||||
setExp(val?.trim());
|
||||
@@ -218,109 +235,162 @@ export function ExpressionModal({
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
closable={false}
|
||||
destroyOnClose
|
||||
width="60%"
|
||||
open={visible}
|
||||
onCancel={onCancel}
|
||||
onOk={() => {
|
||||
onOk(exp);
|
||||
}}
|
||||
bodyStyle={{
|
||||
padding: 0,
|
||||
<DragPanel
|
||||
width={700}
|
||||
height={615}
|
||||
resizeable
|
||||
title={`使用代码设置 ${selectNodePath} 组件的${title}`}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setExp(undefined);
|
||||
setError('');
|
||||
}
|
||||
}}
|
||||
body={
|
||||
<Box display="flex" flexDirection="column">
|
||||
<Box p="m">
|
||||
<InputCodeComponent
|
||||
shape="inset"
|
||||
minHeight="56px"
|
||||
maxHeight="160px"
|
||||
value={exp}
|
||||
placeholder={placeholder}
|
||||
onChange={handleExpInputChange}
|
||||
onBlur={() => {
|
||||
if (exp) {
|
||||
setError(expressionValueValidate(exp));
|
||||
}
|
||||
}}
|
||||
autoCompleteContext={evaluateContext}
|
||||
autoCompleteOptions={autoCompleteOptions}
|
||||
/>
|
||||
{error ? (
|
||||
<Text color="red" fontSize="12px" as="div">
|
||||
出错了!输入的表达式存在语法错误,请修改后再提交!
|
||||
</Text>
|
||||
) : null}
|
||||
<Box fontSize="12px" color="text2">
|
||||
<Text display="block">说明:</Text>
|
||||
{subTitle && <Text display="block">{subTitle}</Text>}
|
||||
<Text display="block">
|
||||
你可以在上面的代码输入框里输入常规的 javascript 代码,还可以直接使用 jsx
|
||||
代码,但需要符合该属性的接受值定义。
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
<Panel
|
||||
title="从变量列表中选中"
|
||||
shape="solid"
|
||||
borderLeft="0"
|
||||
borderRight="0"
|
||||
flex={1}
|
||||
bodyProps={{
|
||||
style: {
|
||||
overflow: 'hidden',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<VariableTree
|
||||
height="100%"
|
||||
showViewButton
|
||||
dataSource={dataSource || expressionVariables}
|
||||
appContext={evaluateContext['tango']}
|
||||
getStoreNames={() => Object.keys(workspace.storeModules)}
|
||||
serviceModules={serviceModules}
|
||||
getServiceData={(serviceKey) => {
|
||||
const data = workspace.getServiceFunction(serviceKey);
|
||||
return {
|
||||
name: data.name,
|
||||
moduleName: data.moduleName,
|
||||
method: 'get',
|
||||
...data.config,
|
||||
};
|
||||
}}
|
||||
onSelect={(node) => {
|
||||
if (!node.key) {
|
||||
return;
|
||||
}
|
||||
if (node.key.split('.').length < 2) {
|
||||
return;
|
||||
}
|
||||
let str;
|
||||
if (/^(stores|services)\./.test(node.key)) {
|
||||
// 从匹配到的第二个点开始替换为 ?.,因为第一个点是 stores 或 services
|
||||
str = `tango.${node.key.replace(/(?<=\..*?)\./g, '?.')}`;
|
||||
if (node.type === 'function') {
|
||||
str = getCallbackValue(`${str}();`, template);
|
||||
}
|
||||
} else {
|
||||
str = `${node.key}`;
|
||||
}
|
||||
setExp(str);
|
||||
}}
|
||||
onAddStoreVariable={(storeName, data) => {
|
||||
workspace.addStoreState(storeName, data.name, data.initialValue);
|
||||
}}
|
||||
onUpdateStoreVariable={(variableKey, code) => {
|
||||
workspace.updateStoreVariable(variableKey, code);
|
||||
}}
|
||||
onAddStore={(storeName) => {
|
||||
workspace.addStoreFile(storeName, newStoreTemplate);
|
||||
}}
|
||||
onRemoveStoreVariable={(variableKey) => {
|
||||
workspace.removeStoreVariable(variableKey);
|
||||
}}
|
||||
onRemoveService={(serviceKey) => {
|
||||
workspace.removeServiceFunction(serviceKey);
|
||||
}}
|
||||
onAddService={(data) => {
|
||||
const { name, moduleName, ...payload } = shapeServiceValues(data);
|
||||
workspace.addServiceFunction(name, payload, moduleName);
|
||||
}}
|
||||
onUpdateService={(data) => {
|
||||
const { name, moduleName, ...payload } = shapeServiceValues(data);
|
||||
workspace.updateServiceFunction(name, payload, moduleName);
|
||||
}}
|
||||
getPreviewValue={(node) => {
|
||||
if (!node || !node.key) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.type === 'function') {
|
||||
return;
|
||||
}
|
||||
|
||||
return getValue(evaluateContext['tango'], node.key);
|
||||
}}
|
||||
/>
|
||||
</Panel>
|
||||
</Box>
|
||||
}
|
||||
footer={(close) => (
|
||||
<Box display="flex" width="100%" justifyContent="flex-end" gap="5px">
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
if (!error) {
|
||||
onOk(exp);
|
||||
close();
|
||||
}
|
||||
}}
|
||||
>
|
||||
确认
|
||||
</Button>
|
||||
<Button size="small" onClick={close}>
|
||||
取消
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
>
|
||||
<Panel title={`将 ${title} 设置为引用变量或自定义表达式`} subTitle={subTitle} shape="solid">
|
||||
<InputCode
|
||||
shape="inset"
|
||||
minHeight="56px"
|
||||
maxHeight="200px"
|
||||
value={exp}
|
||||
placeholder={placeholder}
|
||||
onChange={handleExpInputChange}
|
||||
onBlur={() => {
|
||||
setError(expressionValueValidate(exp));
|
||||
}}
|
||||
autoCompleteContext={evaluateContext}
|
||||
autoCompleteOptions={autoCompleteOptions}
|
||||
{children || (
|
||||
<Action
|
||||
tooltip="弹出代码编辑和变量选择面板"
|
||||
tooltipProps={{ placement: 'topLeft' }}
|
||||
icon={<PopOutOutlined />}
|
||||
size="small"
|
||||
/>
|
||||
{error ? <Text color="red">输入的表达式存在语法错误,请修改后再提交!</Text> : null}
|
||||
</Panel>
|
||||
<Panel
|
||||
title="从变量列表中选中"
|
||||
shape="solid"
|
||||
borderTop="0"
|
||||
overflow="hidden"
|
||||
bodyProps={{ overflow: 'hidden' }}
|
||||
>
|
||||
<VariableTree
|
||||
height={380}
|
||||
showViewButton
|
||||
dataSource={dataSource || expressionVariables}
|
||||
appContext={sandbox?.window['tango']}
|
||||
getStoreNames={() => Object.keys(workspace.storeModules)}
|
||||
serviceModules={serviceModules}
|
||||
getServiceData={(serviceKey) => {
|
||||
const data = workspace.getServiceFunction(serviceKey);
|
||||
return {
|
||||
name: data.name,
|
||||
moduleName: data.moduleName,
|
||||
method: 'get',
|
||||
...data.config,
|
||||
};
|
||||
}}
|
||||
onSelect={(node) => {
|
||||
if (!node.key) {
|
||||
return;
|
||||
}
|
||||
if (node.key.split('.').length < 2) {
|
||||
return;
|
||||
}
|
||||
let str;
|
||||
if (/^(stores|services)\./.test(node.key)) {
|
||||
str = `{tango.${node.key.replaceAll('.', '?.')}}`;
|
||||
} else {
|
||||
str = `{${node.key}}`;
|
||||
}
|
||||
setExp(str);
|
||||
}}
|
||||
onAddStoreVariable={(storeName, data) => {
|
||||
workspace.addStoreState(storeName, data.name, data.initialValue);
|
||||
}}
|
||||
onUpdateStoreVariable={(variableKey, code) => {
|
||||
workspace.updateStoreVariable(variableKey, code);
|
||||
}}
|
||||
onAddStore={(storeName) => {
|
||||
workspace.addStoreFile(storeName, newStoreTemplate);
|
||||
}}
|
||||
onRemoveStoreVariable={(variableKey) => {
|
||||
workspace.removeStoreVariable(variableKey);
|
||||
}}
|
||||
onRemoveService={(serviceKey) => {
|
||||
workspace.removeServiceFunction(serviceKey);
|
||||
}}
|
||||
onAddService={(data) => {
|
||||
const { name, moduleName, ...payload } = shapeServiceValues(data);
|
||||
workspace.addServiceFunction(name, payload, moduleName);
|
||||
}}
|
||||
onUpdateService={(data) => {
|
||||
const { name, moduleName, ...payload } = shapeServiceValues(data);
|
||||
workspace.updateServiceFunction(name, payload, moduleName);
|
||||
}}
|
||||
getPreviewValue={(node) => {
|
||||
if (!node || !node.key) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.type === 'function') {
|
||||
return;
|
||||
}
|
||||
|
||||
return getValue(evaluateContext['tango'], node.key);
|
||||
}}
|
||||
/>
|
||||
</Panel>
|
||||
</Modal>
|
||||
)}
|
||||
</DragPanel>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ 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 { RenderSetter, TableCellSetter, TableExpandableSetter } from './render-setter';
|
||||
import { RouterSetter } from './router-setter';
|
||||
import {
|
||||
SpacingSetter,
|
||||
@@ -25,44 +25,46 @@ import {
|
||||
FlexAlignItemsSetter,
|
||||
FlexDirectionSetter,
|
||||
} from './style-setter';
|
||||
import { BoolSetter } from './bool-setter';
|
||||
import { ChoiceSetter } from './choice-setter';
|
||||
import { NumberSetter, SliderSetter } from './number-setter';
|
||||
import { TextAreaSetter, TextSetter } from './text-setter';
|
||||
import { isValidExpressionCode } from '@music163/tango-core';
|
||||
|
||||
const setters: IFormItemCreateOptions[] = [
|
||||
const codeValidate: IFormItemCreateOptions['validate'] = (value, field) => {
|
||||
if (!value) return;
|
||||
const rawCode = field.detail.rawCode;
|
||||
if (!rawCode) return;
|
||||
return isValidExpressionCode(rawCode) ? '' : '请输入合法的 Javascript 代码片段';
|
||||
};
|
||||
|
||||
const jsonValidate: IFormItemCreateOptions['validate'] = (value, field) => {
|
||||
if (!value) return;
|
||||
try {
|
||||
JSON.parse(field.detail.rawCode);
|
||||
return;
|
||||
} catch (e) {
|
||||
return '请输入合法的 JSON 字符串';
|
||||
}
|
||||
};
|
||||
|
||||
export const BUILT_IN_SETTERS: IFormItemCreateOptions[] = [
|
||||
{
|
||||
name: 'boolSetter',
|
||||
component: BoolSetter,
|
||||
},
|
||||
{
|
||||
name: 'choiceSetter',
|
||||
component: ChoiceSetter,
|
||||
},
|
||||
{
|
||||
name: 'expSetter',
|
||||
alias: ['expressionSetter'],
|
||||
name: 'codeSetter',
|
||||
alias: ['expSetter', 'expressionSetter'],
|
||||
component: ExpressionSetter,
|
||||
type: 'code',
|
||||
validate: codeValidate,
|
||||
},
|
||||
{
|
||||
name: 'numberSetter',
|
||||
component: NumberSetter,
|
||||
},
|
||||
{
|
||||
name: 'textSetter',
|
||||
component: TextSetter,
|
||||
},
|
||||
{ name: 'textAreaSetter', component: TextAreaSetter },
|
||||
{
|
||||
name: 'sliderSetter',
|
||||
component: SliderSetter,
|
||||
name: 'radioGroupSetter',
|
||||
alias: ['choiceSetter'],
|
||||
component: ChoiceSetter,
|
||||
},
|
||||
{
|
||||
name: 'actionListSetter',
|
||||
component: ActionListSetter,
|
||||
},
|
||||
{
|
||||
name: 'columnSetter',
|
||||
name: 'tableColumnsSetter',
|
||||
alias: ['columnSetter'], // 兼容
|
||||
component: ColumnSetter,
|
||||
},
|
||||
{
|
||||
@@ -94,18 +96,17 @@ const setters: IFormItemCreateOptions[] = [
|
||||
alias: ['actionSetter', 'functionSetter', 'callbackSetter'],
|
||||
component: EventSetter,
|
||||
},
|
||||
{
|
||||
name: 'expressionSetter',
|
||||
alias: ['expSetter'],
|
||||
component: ExpressionSetter,
|
||||
},
|
||||
{
|
||||
name: 'jsonSetter',
|
||||
component: JSONSetter,
|
||||
type: 'code',
|
||||
validate: jsonValidate,
|
||||
},
|
||||
{
|
||||
name: 'jsxSetter',
|
||||
component: JsxSetter,
|
||||
type: 'code',
|
||||
validate: codeValidate,
|
||||
},
|
||||
{
|
||||
name: 'listSetter',
|
||||
@@ -120,20 +121,26 @@ const setters: IFormItemCreateOptions[] = [
|
||||
component: OptionSetter,
|
||||
},
|
||||
{
|
||||
name: 'pickerSetter',
|
||||
name: 'selectSetter',
|
||||
alias: ['pickerSetter'],
|
||||
component: PickerSetter,
|
||||
},
|
||||
{
|
||||
name: 'renderPropsSetter',
|
||||
component: RenderSetter,
|
||||
type: 'code',
|
||||
validate: codeValidate,
|
||||
},
|
||||
{
|
||||
name: 'tableCellSetter',
|
||||
component: TableCellSetter,
|
||||
type: 'code',
|
||||
},
|
||||
{
|
||||
name: 'tableExpandableSetter',
|
||||
component: TableExpandableSetter,
|
||||
type: 'code',
|
||||
validate: codeValidate,
|
||||
},
|
||||
{
|
||||
name: 'routerSetter',
|
||||
@@ -169,5 +176,5 @@ const setters: IFormItemCreateOptions[] = [
|
||||
];
|
||||
|
||||
export function registerBuiltinSetters() {
|
||||
setters.forEach(register);
|
||||
BUILT_IN_SETTERS.forEach(register);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { Box } from 'coral-system';
|
||||
import { SingleMonacoEditor } from '@music163/tango-ui';
|
||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||
@@ -6,14 +6,18 @@ import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||
/**
|
||||
* JSON Setter
|
||||
*/
|
||||
export function JSONSetter({ value, onChange }: FormItemComponentProps) {
|
||||
export function JSONSetter({ value: valueProp, onChange }: FormItemComponentProps) {
|
||||
const [value, setValue] = useState(valueProp || '');
|
||||
return (
|
||||
<Box height="120px" border="solid" borderColor="line.normal" borderRadius="s">
|
||||
<SingleMonacoEditor
|
||||
defaultValue={value}
|
||||
onChange={(newValue) => {
|
||||
setValue(newValue);
|
||||
}}
|
||||
onBlur={(newValue) => {
|
||||
if (newValue !== value) {
|
||||
onChange(`{${newValue}}`);
|
||||
onChange(newValue);
|
||||
}
|
||||
}}
|
||||
language="json"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { isPlainString } from '@music163/tango-helpers';
|
||||
import { ActionSelect } from '@music163/tango-ui';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { ActionSelect, InputCode } from '@music163/tango-ui';
|
||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||
import { Box } from 'coral-system';
|
||||
|
||||
const options = [
|
||||
{ label: '取消自定义', value: '' },
|
||||
@@ -25,26 +25,27 @@ interface JsxSetterProps extends FormItemComponentProps {
|
||||
}
|
||||
|
||||
const defaultGetTemplate = (key: string) => {
|
||||
let ret;
|
||||
const deps = [];
|
||||
let ret: [string, string[]];
|
||||
switch (key) {
|
||||
case 'Box':
|
||||
ret = '<Box></Box>';
|
||||
ret = ['<Box></Box>', ['Box']];
|
||||
break;
|
||||
case 'Text':
|
||||
ret = '<Text>示例文本</Text>';
|
||||
ret = ['<Text>示例文本</Text>', ['Text']];
|
||||
break;
|
||||
case 'Placeholder':
|
||||
ret = '<Placeholder text="拖拽到此进行替换" />';
|
||||
ret = ['<Placeholder text="拖拽到此进行替换" />', ['Placeholder']];
|
||||
break;
|
||||
case 'ButtonGroup':
|
||||
ret = '<ButtonGroup><Button>按钮1</Button><Button>按钮2</Button></ButtonGroup>';
|
||||
deps.push('Button');
|
||||
ret = [
|
||||
'<ButtonGroup><Button>按钮1</Button><Button>按钮2</Button></ButtonGroup>',
|
||||
['ButtonGroup', 'Button'],
|
||||
];
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return [ret, deps];
|
||||
return ret;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -52,21 +53,36 @@ const defaultGetTemplate = (key: string) => {
|
||||
*/
|
||||
export function JsxSetter(props: JsxSetterProps) {
|
||||
const { showInput, getTemplate = defaultGetTemplate, value, onChange } = props;
|
||||
const [inputValue, setInputValue] = useState(value);
|
||||
useEffect(() => {
|
||||
setInputValue(value);
|
||||
}, [value]);
|
||||
return (
|
||||
<ActionSelect
|
||||
showInput={showInput}
|
||||
defaultInputValue={isPlainString(value) ? value : undefined}
|
||||
options={options}
|
||||
text="将此区域初始化为"
|
||||
onInputChange={onChange}
|
||||
onSelect={(key) => {
|
||||
const [tpl, deps] = getTemplate(key);
|
||||
if (tpl) {
|
||||
onChange(`{${tpl}}`, { relatedImports: [...deps, key] });
|
||||
} else {
|
||||
onChange(undefined);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Box>
|
||||
<ActionSelect
|
||||
showInput={showInput}
|
||||
defaultInputValue={value}
|
||||
options={options}
|
||||
text="设置此区域为"
|
||||
onInputChange={onChange}
|
||||
onSelect={(key) => {
|
||||
const [tpl, deps] = getTemplate(key);
|
||||
if (tpl) {
|
||||
onChange(tpl, { relatedImports: deps });
|
||||
} else {
|
||||
onChange(undefined);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{value && (
|
||||
<InputCode
|
||||
value={value}
|
||||
onChange={(val) => setInputValue(val)}
|
||||
onBlur={() => {
|
||||
onChange(inputValue);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,10 +3,11 @@ import { css, Box } from 'coral-system';
|
||||
import { Form, Input, Button, Popover } from 'antd';
|
||||
import { DeleteOutlined, EditOutlined, HolderOutlined } from '@ant-design/icons';
|
||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||
import { Dict } from '@music163/tango-helpers';
|
||||
import { DragBox } from '../components';
|
||||
|
||||
function copyWithoutUndefined(source: Record<string, any>) {
|
||||
const ret = {};
|
||||
function copyWithoutUndefined(source: Dict) {
|
||||
const ret: Dict = {};
|
||||
for (const key in source) {
|
||||
if (source[key] !== undefined) {
|
||||
ret[key] = source[key];
|
||||
@@ -234,6 +235,7 @@ function NewOptionForm({ fields = [], initialValues = {}, onSubmit }: NewOptionF
|
||||
label={item.label}
|
||||
name={item.name}
|
||||
required={item.required}
|
||||
rules={[{ required: item.required }]}
|
||||
valuePropName={item.valuePropName}
|
||||
extra={item.extra}
|
||||
style={{
|
||||
@@ -260,7 +262,7 @@ interface ListSetterProps extends FormItemComponentProps<any[]> {
|
||||
addBtnText?: string;
|
||||
getListItemKey?: (item: any) => React.Key;
|
||||
renderItem?: (item: any) => React.ReactNode;
|
||||
};
|
||||
}
|
||||
|
||||
const defaultListItemFormFields: ListSetterItemProps['formFields'] = [
|
||||
{ label: 'key', name: 'key', required: true },
|
||||
|
||||
@@ -60,7 +60,7 @@ export function ModelSetter({
|
||||
}: FormItemComponentProps) {
|
||||
const [inputValue, setInputValue] = useState(value);
|
||||
const { modelVariables } = useWorkspaceData();
|
||||
const evaluateContext = useSandboxQuery().window || {};
|
||||
const evaluateContext: any = useSandboxQuery().window || {};
|
||||
const workspace = useWorkspace();
|
||||
const definedVariables = useMemo(() => {
|
||||
const list: string[] = [];
|
||||
|
||||
+41
-11
@@ -1,6 +1,8 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { ActionSelect } from '@music163/tango-ui';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { ActionSelect, InputCode } from '@music163/tango-ui';
|
||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||
import { Box } from 'coral-system';
|
||||
import { Dict } from '@music163/tango-helpers';
|
||||
|
||||
interface IRenderOption {
|
||||
label: string;
|
||||
@@ -15,17 +17,28 @@ export interface RenderSetterProps {
|
||||
fallbackOption?: IRenderOption;
|
||||
}
|
||||
|
||||
const defaultOptions: IRenderOption[] = [
|
||||
{ label: '取消自定义', value: '' },
|
||||
{ label: '自定义渲染', value: 'Box', render: '() => <Box></Box>' },
|
||||
];
|
||||
|
||||
/**
|
||||
* Render Props Setters
|
||||
*/
|
||||
export function RenderSetter({
|
||||
value,
|
||||
onChange,
|
||||
text = '自定义渲染为',
|
||||
options = [],
|
||||
options = defaultOptions,
|
||||
fallbackOption,
|
||||
}: FormItemComponentProps & RenderSetterProps) {
|
||||
const [inputValue, setInputValue] = useState(value || '');
|
||||
useEffect(() => {
|
||||
setInputValue(value);
|
||||
}, [value]);
|
||||
|
||||
const optionsMap = useMemo(() => {
|
||||
return options.reduce((prev, cur) => {
|
||||
return options.reduce<Dict>((prev, cur) => {
|
||||
prev[cur.value] = cur;
|
||||
return prev;
|
||||
}, {});
|
||||
@@ -42,21 +55,37 @@ export function RenderSetter({
|
||||
},
|
||||
[optionsMap, fallbackOption, onChange],
|
||||
);
|
||||
return <ActionSelect text={text} options={options} onSelect={onSelect} />;
|
||||
return (
|
||||
<Box>
|
||||
<ActionSelect text={text} options={options} onSelect={onSelect} />
|
||||
{inputValue && (
|
||||
<InputCode
|
||||
value={inputValue}
|
||||
onChange={(val) => setInputValue(val)}
|
||||
onBlur={() => onChange(inputValue)}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const getRender = (content: string, type?: 'tableCell' | 'tableExpandable') => {
|
||||
let code;
|
||||
switch (type) {
|
||||
case 'tableCell':
|
||||
return `{(value, record, index) => ${content}}`;
|
||||
code = `(value, record, index) => ${content}`;
|
||||
break;
|
||||
case 'tableExpandable':
|
||||
return `{{
|
||||
expandedRowRender: (record) => ${content},
|
||||
rowExpandable: (record) => true
|
||||
}}`;
|
||||
code = `{
|
||||
expandedRowRender: (record) => ${content},
|
||||
rowExpandable: (record) => true
|
||||
}`;
|
||||
break;
|
||||
default:
|
||||
return `{() => ${content}}`;
|
||||
code = `() => ${content}`;
|
||||
break;
|
||||
}
|
||||
return code;
|
||||
};
|
||||
|
||||
const tableCellOptions: RenderSetterProps['options'] = [
|
||||
@@ -101,6 +130,7 @@ export function TableCellSetter(props: FormItemComponentProps) {
|
||||
return <RenderSetter options={tableCellOptions} {...props} />;
|
||||
}
|
||||
|
||||
// FIXME: 应该直接用 props 嵌套的模式
|
||||
export function TableExpandableSetter(props: FormItemComponentProps) {
|
||||
return <RenderSetter options={tableExpandableOptions} text="配置表格可展开行" {...props} />;
|
||||
}
|
||||
@@ -1,11 +1,17 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Box } from 'coral-system';
|
||||
import { Input, Radio } from 'antd';
|
||||
import { isValidUrl } from '@music163/tango-helpers';
|
||||
import { Dict, isValidUrl } from '@music163/tango-helpers';
|
||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||
import { useWorkspaceData } from '@music163/tango-context';
|
||||
import { PickerSetter } from './picker-setter';
|
||||
|
||||
const routerModeMap: Dict = {
|
||||
link: '链接',
|
||||
router: '路由',
|
||||
both: '',
|
||||
};
|
||||
|
||||
export function RouterSetter(props: FormItemComponentProps) {
|
||||
const [type, setType] = useState<'input' | 'select'>(() => {
|
||||
return isValidUrl(props.value) ? 'input' : 'select';
|
||||
@@ -37,7 +43,7 @@ export function RouterSetter(props: FormItemComponentProps) {
|
||||
)}
|
||||
{type === 'input' && (
|
||||
<Input
|
||||
placeholder={`请输入任意${{ link: '链接', router: '路由', both: '' }[displayType]}地址`}
|
||||
placeholder={`请输入任意${routerModeMap[displayType]}地址`}
|
||||
{...props}
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
|
||||
@@ -7,8 +7,6 @@ import { BgColorsOutlined, EyeInvisibleOutlined, FileImageOutlined } from '@ant-
|
||||
import { SingleMonacoEditor, LineSolidOutlined, LineDashedOutlined } from '@music163/tango-ui';
|
||||
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`')) {
|
||||
@@ -17,12 +15,13 @@ function getRawCssValue(value: string) {
|
||||
return value;
|
||||
}
|
||||
|
||||
// TODO: style object setter
|
||||
|
||||
/**
|
||||
* 不稳定,暂不推荐使用
|
||||
*/
|
||||
export function CssCodeSetter({ value, onChange }: FormItemComponentProps<string>) {
|
||||
const contentValue = getRawCssValue(value);
|
||||
// TODO: relatedImports
|
||||
return (
|
||||
<Popover
|
||||
placement="leftBottom"
|
||||
@@ -34,9 +33,7 @@ export function CssCodeSetter({ value, onChange }: FormItemComponentProps<string
|
||||
value={contentValue}
|
||||
onBlur={(newCode) => {
|
||||
if (newCode != contentValue) {
|
||||
onChange(`{css\`${newCode}\`}`, {
|
||||
// relatedImports: ['']
|
||||
});
|
||||
onChange(`{css\`${newCode}\`}`, {});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
@@ -239,9 +236,10 @@ export function BgSetter({ value, onChange }: FormItemComponentProps<string>) {
|
||||
<Box mt="m">
|
||||
{mode === 'color' && <ColorSetter value={value} onChange={onChange} />}
|
||||
{mode === 'image' && (
|
||||
<TextSetter
|
||||
<Input
|
||||
value={getImageUrl(value)}
|
||||
onChange={(imgUrl) => {
|
||||
onChange={(e) => {
|
||||
const imgUrl = e.target.value;
|
||||
onChange(imgUrl ? `url(${imgUrl})` : undefined);
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -2,11 +2,15 @@ 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 { clone, Dict, parseDndId } from '@music163/tango-helpers';
|
||||
import { observer, useDesigner, useWorkspace } from '@music163/tango-context';
|
||||
import { registerBuiltinSetters } from './setters';
|
||||
|
||||
registerBuiltinSetters();
|
||||
let registered = false;
|
||||
if (!registered) {
|
||||
registerBuiltinSetters();
|
||||
registered = true;
|
||||
}
|
||||
|
||||
export interface SettingPanelProps extends SettingFormProps {
|
||||
title?: React.ReactNode;
|
||||
@@ -30,7 +34,7 @@ export const SettingPanel = observer((props: SettingPanelProps) => {
|
||||
}
|
||||
|
||||
const getSettingValue = (attributes: Record<string, string>) => {
|
||||
const ret = {};
|
||||
const ret: Dict = {};
|
||||
const keys = Object.keys(attributes);
|
||||
for (const key of keys) {
|
||||
if (/^data-/.test(key)) {
|
||||
@@ -91,6 +95,9 @@ export const SettingPanel = observer((props: SettingPanelProps) => {
|
||||
borderLeftColor="line2"
|
||||
bg="white"
|
||||
headerProps={headerProps}
|
||||
bodyProps={{
|
||||
overflowY: 'hidden',
|
||||
}}
|
||||
className="SettingPanel"
|
||||
>
|
||||
{prototype ? (
|
||||
@@ -100,6 +107,11 @@ export const SettingPanel = observer((props: SettingPanelProps) => {
|
||||
prototype={prototype}
|
||||
showIdentifier={{
|
||||
identifierKey: 'tid',
|
||||
getIdentifier: () => {
|
||||
// 直接拿当前的虚拟 id 作为 tid
|
||||
const { codeId } = parseDndId(workspace.selectSource.first.id);
|
||||
return codeId;
|
||||
},
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@@ -1,21 +1,30 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { Box, Grid, Text } from 'coral-system';
|
||||
import styled from 'styled-components';
|
||||
import styled, { css } from 'styled-components';
|
||||
import {
|
||||
ComponentPrototypeType,
|
||||
Dict,
|
||||
IComponentPrototype,
|
||||
MenuDataType,
|
||||
MenuValueType,
|
||||
createContext,
|
||||
logger,
|
||||
PartialRecord,
|
||||
upperCamelCase,
|
||||
} from '@music163/tango-helpers';
|
||||
import { CollapsePanel, IconFont, Search, Tabs } from '@music163/tango-ui';
|
||||
import { observer, useWorkspace } from '@music163/tango-context';
|
||||
import { QuestionCircleOutlined } from '@ant-design/icons';
|
||||
import { Button, Empty, Spin, Popover } from 'antd';
|
||||
import { Button, Empty, Spin, Popover, TabsProps } from 'antd';
|
||||
import { getDragGhostElement } from '../helpers';
|
||||
|
||||
type MenuKeyType = 'common' | 'atom' | 'snippet' | 'bizComp' | 'localComp';
|
||||
type MenuValueType = Array<{ title: string; items: string[] }>;
|
||||
export type MenuDataType = PartialRecord<MenuKeyType, MenuValueType>;
|
||||
type IComponentsPanelContext = {
|
||||
isScope: boolean;
|
||||
onItemSelect: (name: string) => void;
|
||||
layout: 'grid' | 'line';
|
||||
};
|
||||
|
||||
const [ComponentsPanelProvider, usePanelContext] = createContext<IComponentsPanelContext>({
|
||||
name: 'ComponentsPanelContext',
|
||||
});
|
||||
|
||||
export interface ComponentsPanelProps {
|
||||
/**
|
||||
@@ -36,9 +45,29 @@ export interface ComponentsPanelProps {
|
||||
* @returns
|
||||
*/
|
||||
getBizCompName?: (name: string) => string;
|
||||
/**
|
||||
* 是否局部模式 (快捷添加组件面板中使用)
|
||||
*/
|
||||
isScope?: boolean;
|
||||
/**
|
||||
* 组件选中回调
|
||||
*/
|
||||
onItemSelect?: (name: string) => void;
|
||||
/**
|
||||
* 自定义样式
|
||||
*/
|
||||
style?: React.CSSProperties;
|
||||
/**
|
||||
* tabProps
|
||||
*/
|
||||
tabProps?: TabsProps;
|
||||
/**
|
||||
* 布局模式,默认网格布局
|
||||
*/
|
||||
layout?: 'grid' | 'line';
|
||||
}
|
||||
|
||||
const localeMap = {
|
||||
const localeMap: Dict = {
|
||||
common: '基础组件',
|
||||
atom: '原子组件',
|
||||
snippet: '组合',
|
||||
@@ -72,10 +101,15 @@ export function useFlatMenuData<T>(menuData: T) {
|
||||
|
||||
export const ComponentsPanel = observer(
|
||||
({
|
||||
isScope = false,
|
||||
menuData = emptyMenuData,
|
||||
showBizComps = true,
|
||||
getBizCompName = upperCamelCase,
|
||||
loading = false,
|
||||
style,
|
||||
tabProps,
|
||||
onItemSelect,
|
||||
layout = 'grid',
|
||||
}: ComponentsPanelProps) => {
|
||||
const [keyword, setKeyword] = useState<string>('');
|
||||
const allList = useFlatMenuData<MenuDataType>(menuData);
|
||||
@@ -91,7 +125,7 @@ export const ComponentsPanel = observer(
|
||||
const tabs = Object.keys(menuData).map((key) => ({
|
||||
key,
|
||||
label: localeMap[key],
|
||||
children: <MaterialList data={menuData[key]} />,
|
||||
children: <MaterialList data={(menuData as any)[key]} />,
|
||||
}));
|
||||
|
||||
if (showBizComps) {
|
||||
@@ -109,22 +143,44 @@ export const ComponentsPanel = observer(
|
||||
children: <MaterialList data={localCompData} />,
|
||||
});
|
||||
}
|
||||
|
||||
const contentNode =
|
||||
tabs.length === 1 ? (
|
||||
tabs[0].children
|
||||
) : (
|
||||
<Tabs centered isTabBarSticky tabBarStickyOffset={48} items={tabs} />
|
||||
<Tabs
|
||||
size={isScope ? 'small' : 'middle'}
|
||||
centered
|
||||
isTabBarSticky
|
||||
tabBarStickyOffset={48}
|
||||
items={tabs}
|
||||
{...tabProps}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<Box className="ComponentsView" overflowY="auto">
|
||||
<Box px="l" py="m" position="sticky" top="0" zIndex={1} bg="white">
|
||||
<Search placeholder="搜索物料" onChange={setKeyword} />
|
||||
<ComponentsPanelProvider
|
||||
value={{
|
||||
isScope,
|
||||
onItemSelect,
|
||||
layout,
|
||||
}}
|
||||
>
|
||||
<Box className="ComponentsView" overflowY="auto" style={style}>
|
||||
<Box px="l" py="m" position="sticky" top="0" zIndex={3} bg="white">
|
||||
<Search
|
||||
style={{
|
||||
borderRadius: '4px',
|
||||
}}
|
||||
placeholder="搜索物料"
|
||||
onChange={setKeyword}
|
||||
/>
|
||||
</Box>
|
||||
<Spin spinning={loading} tip="正在加载物料列表...">
|
||||
{!keyword ? contentNode : <MaterialList data={allList} filterKeyword={keyword} />}
|
||||
</Spin>
|
||||
</Box>
|
||||
<Spin spinning={loading} tip="正在加载物料列表...">
|
||||
{!keyword ? contentNode : <MaterialList data={allList} filterKeyword={keyword} />}
|
||||
</Spin>
|
||||
</Box>
|
||||
</ComponentsPanelProvider>
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -146,6 +202,9 @@ interface MaterialListProps {
|
||||
|
||||
function MaterialList({ data, filterKeyword, type = 'common' }: MaterialListProps) {
|
||||
const workspace = useWorkspace();
|
||||
const { layout } = usePanelContext();
|
||||
const isGrid = layout === 'grid';
|
||||
|
||||
return (
|
||||
<Box className="ComponentsViewList">
|
||||
{data.map((cate) => {
|
||||
@@ -166,15 +225,21 @@ function MaterialList({ data, filterKeyword, type = 'common' }: MaterialListProp
|
||||
title={cate.title}
|
||||
borderBottom="solid"
|
||||
borderColor="line.normal"
|
||||
showBottomBorder={false}
|
||||
bodyProps={{
|
||||
padding: isGrid ? '4px 12px 12px' : '0',
|
||||
}}
|
||||
>
|
||||
{!items.length && (
|
||||
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="没有匹配到任何组件" />
|
||||
)}
|
||||
<Grid
|
||||
columns={type === 'localComp' ? 1 : 2}
|
||||
templateColumns={isGrid ? 'repeat(auto-fit,minmax(55px,1fr))' : '1fr'}
|
||||
spacing="1px"
|
||||
bg="background.normal"
|
||||
padding="0"
|
||||
gap={isGrid ? '12px 8px' : '0'}
|
||||
backgroundColor="white"
|
||||
>
|
||||
{items.map((item) => {
|
||||
const prototype = workspace.componentPrototypes.get(item);
|
||||
@@ -194,7 +259,7 @@ function MaterialList({ data, filterKeyword, type = 'common' }: MaterialListProp
|
||||
}
|
||||
|
||||
interface MaterialProps {
|
||||
data: ComponentPrototypeType;
|
||||
data: IComponentPrototype;
|
||||
}
|
||||
|
||||
const StyledCommonGridItem = styled.div`
|
||||
@@ -202,9 +267,8 @@ const StyledCommonGridItem = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 8px;
|
||||
cursor: move;
|
||||
justify-content: start;
|
||||
cursor: ${(props) => (props.draggable ? 'grab' : 'pointer')};
|
||||
text-align: center;
|
||||
color: var(--tango-colors-text-body);
|
||||
background-color: #fff;
|
||||
@@ -213,7 +277,23 @@ const StyledCommonGridItem = styled.div`
|
||||
white-space: nowrap;
|
||||
|
||||
.material-icon {
|
||||
font-size: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 36px;
|
||||
background: #f9f9f9;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ebebeb;
|
||||
width: 100%;
|
||||
height: 52px;
|
||||
position: relative;
|
||||
transition: 0.15s ease-in-out;
|
||||
transition-property: transform;
|
||||
will-change: transform;
|
||||
img {
|
||||
width: unset;
|
||||
height: 85%;
|
||||
}
|
||||
}
|
||||
|
||||
.info {
|
||||
@@ -225,19 +305,13 @@ const StyledCommonGridItem = styled.div`
|
||||
|
||||
.anticon-question-circle {
|
||||
display: none;
|
||||
font-size: 13px;
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
}
|
||||
|
||||
img {
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 0 10px rgb(0 0 0 / 10%);
|
||||
|
||||
> span {
|
||||
color: var(--tango-colors-brand);
|
||||
}
|
||||
@@ -245,11 +319,26 @@ const StyledCommonGridItem = styled.div`
|
||||
.anticon-question-circle {
|
||||
display: inline-block;
|
||||
}
|
||||
.material-icon {
|
||||
border-color: #c7c7c7;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const GridLineItemStyle = css`
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--tango-colors-fill2);
|
||||
}
|
||||
`;
|
||||
|
||||
function MaterialGrid({ data }: MaterialProps) {
|
||||
const workspace = useWorkspace();
|
||||
const { isScope, onItemSelect, layout } = usePanelContext();
|
||||
|
||||
const isLine = layout === 'line';
|
||||
|
||||
const handleDragStart = (e: React.DragEvent) => {
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
@@ -266,28 +355,92 @@ function MaterialGrid({ data }: MaterialProps) {
|
||||
workspace.dragSource.clear();
|
||||
};
|
||||
|
||||
const handleSelect = () => {
|
||||
onItemSelect?.(data.name);
|
||||
};
|
||||
|
||||
const icon = data.icon || 'icon-placeholder';
|
||||
|
||||
const iconNode = icon.startsWith('icon-') ? (
|
||||
<IconFont className="material-icon" type={data.icon || 'icon-placeholder'} />
|
||||
) : (
|
||||
<Box className="material-icon">
|
||||
<img src={icon} alt={data.name} height="32" />
|
||||
</Box>
|
||||
);
|
||||
|
||||
if (isLine) {
|
||||
return (
|
||||
<Box
|
||||
key={data.name}
|
||||
display="flex"
|
||||
columnGap="m"
|
||||
px="l"
|
||||
py="m"
|
||||
fontSize="12px"
|
||||
css={GridLineItemStyle}
|
||||
onClick={handleSelect}
|
||||
>
|
||||
<Box
|
||||
p="m"
|
||||
fontSize="32px"
|
||||
width="46px"
|
||||
height="46px"
|
||||
background="fill1"
|
||||
border="1px solid"
|
||||
borderRadius="4px"
|
||||
borderColor="line2"
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
{iconNode}
|
||||
</Box>
|
||||
<Box flex={1}>
|
||||
<Box fontWeight="500" display="flex" justifyContent="space-between">
|
||||
<Text flex={1}>{data.title}</Text>
|
||||
{data.docs ? (
|
||||
<Popover
|
||||
zIndex={9999}
|
||||
placement="right"
|
||||
title={data.title}
|
||||
content={<CommonMaterialInfoBox docs={data.docs} />}
|
||||
>
|
||||
<QuestionCircleOutlined />
|
||||
</Popover>
|
||||
) : null}
|
||||
</Box>
|
||||
<Box color="text2" fontStyle="italic">
|
||||
{data.help ?? data.title}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledCommonGridItem
|
||||
draggable
|
||||
key={data.name}
|
||||
draggable={!isScope}
|
||||
data-name={data.name}
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
onClick={handleSelect}
|
||||
>
|
||||
{icon.startsWith('icon-') ? (
|
||||
<IconFont className="material-icon" type={data.icon || 'icon-placeholder'} />
|
||||
) : (
|
||||
<img src={icon} alt={data.name} />
|
||||
)}
|
||||
<Text fontSize="12px" lineHeight="1.5">
|
||||
{data.title}
|
||||
{iconNode}
|
||||
<Text fontSize="12px" marginTop="4px">
|
||||
{data.title ?? data.name}
|
||||
</Text>
|
||||
<Text fontSize="12px" color="gray.50">
|
||||
<Text fontSize="10px" color="gray.50">
|
||||
{data.name}
|
||||
</Text>
|
||||
{data.docs || data.help ? (
|
||||
<Popover placement="right" title={data.title} content={<CommonMaterialInfoBox {...data} />}>
|
||||
<Popover
|
||||
zIndex={9999}
|
||||
placement="right"
|
||||
title={data.title}
|
||||
content={<CommonMaterialInfoBox {...data} />}
|
||||
>
|
||||
<QuestionCircleOutlined />
|
||||
</Popover>
|
||||
) : null}
|
||||
@@ -295,7 +448,7 @@ function MaterialGrid({ data }: MaterialProps) {
|
||||
);
|
||||
}
|
||||
|
||||
function CommonMaterialInfoBox({ help, docs }: ComponentPrototypeType) {
|
||||
function CommonMaterialInfoBox({ help, docs }: Pick<IComponentPrototype, 'help' | 'docs'>) {
|
||||
return (
|
||||
<Box maxWidth={300}>
|
||||
{!!help && <Box mb="m">{help}</Box>}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import React from 'react';
|
||||
import { observer, useWorkspace, useWorkspaceData } from '@music163/tango-context';
|
||||
import { Box } from 'coral-system';
|
||||
import { getVariableContent } from '@music163/tango-helpers';
|
||||
import { isWrappedByExpressionContainer } from '@music163/tango-core';
|
||||
import { VariableTree } from '../../components';
|
||||
import { useSandboxQuery } from '../../context';
|
||||
|
||||
@@ -10,10 +8,6 @@ import { useSandboxQuery } from '../../context';
|
||||
export function shapeServiceValues(val: any) {
|
||||
const shapeValues = { ...val };
|
||||
delete shapeValues.type;
|
||||
// 兼容旧版,如果 formatter 包裹了 {} 则删掉首尾
|
||||
if (shapeValues.formatter && isWrappedByExpressionContainer(shapeValues.formatter)) {
|
||||
shapeValues.formatter = getVariableContent(shapeValues.formatter);
|
||||
}
|
||||
return shapeValues;
|
||||
}
|
||||
|
||||
@@ -30,7 +24,7 @@ const DataSourceView = observer(() => {
|
||||
<Box className="ServiceFunctionList" p="m">
|
||||
<VariableTree
|
||||
dataSource={serviceVariables}
|
||||
appContext={sandbox?.window['tango']}
|
||||
appContext={(sandbox?.window as any)['tango']}
|
||||
serviceModules={serviceModules}
|
||||
onRemoveService={(variableKey) => {
|
||||
workspace.removeServiceFunction(variableKey);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user