Compare commits

..

30 Commits

Author SHA1 Message Date
Wells c48a49961a chore: upgrade dependencies (#209)
* fix: check object value

* chore: up

* chore: upgrade mobx

* chore: up
2024-09-10 17:04:52 +08:00
Wells eae45003c6 feat!: refactor base class (#191)
* feat: refactor types

* feat: rename models

* fix: update types

* fix: init workspace and viewNode

* fix: update fileTypes

* fix: update

* fix: update
2024-09-10 16:02:53 +08:00
wwsun 1802fc0eb8 docs: update 2024-09-10 15:40:45 +08:00
wwsun 8a3ab3c093 chore(release): publish
- @music163/tango-context@1.1.10
 - @music163/tango-core@1.4.4
 - @music163/tango-designer@1.4.6
 - @music163/tango-helpers@1.2.4
 - @music163/tango-sandbox@1.0.13
 - @music163/tango-setting-form@1.2.15
 - @music163/tango-ui@1.4.5
2024-09-09 15:58:16 +08:00
Wells 55602fc961 fix: add classNameSetter and classNameInput (#207)
* feat: add basic classNameInput

* fix: add classNameSetter
2024-09-09 15:50:56 +08:00
864907600cc a3c0e463ca chore: update code editor (#205) 2024-09-09 15:08:02 +08:00
BoBoooooo 79f9a8c179 fix: service support set description field (#206)
* fix: service support set description field

* fix: undefined string bug

* fix: update title display
2024-09-09 15:07:38 +08:00
Wells ea96872261 fix: renderSetter with template (#202)
* fix: renderSetter with template

* fix: update types
2024-09-03 16:52:59 +08:00
wwsun a86e0e049c chore(release): publish
- @music163/tango-context@1.1.9
 - @music163/tango-core@1.4.3
 - @music163/tango-designer@1.4.5
 - @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
2024-09-02 10:50:59 +08:00
Wells c4e1ed67f5 fix: add prop value template & fix components popover (#201)
* fix: clear value in CodeSetter

* fix: update variable path in codesetter

* fix: add prop value template

* fix: update  recommendedList for ComponentsPopover

* fix: update icon size in line mode for ComponentsPanel
2024-08-30 18:09:01 +08:00
wwsun 179de0cc52 chore(release): publish
- @music163/tango-designer@1.4.4
 - @music163/tango-setting-form@1.2.13
 - @music163/tango-ui@1.4.3
2024-08-16 17:50:55 +08:00
BoBoooooo 6de54884b5 fix: CodeInput 支持 jsx css 代码补全 (#198)
* feat: code setter support jsx css completion

* fix: refactor expression css mode

* fix: update story

* fix: revert code
2024-08-16 11:16:39 +08:00
wwsun 55bf417ddf chore(release): publish
- @music163/tango-designer@1.4.3
 - @music163/tango-setting-form@1.2.12
 - @music163/tango-ui@1.4.2
2024-08-09 15:49:28 +08:00
BoBoooooo ca56edd9ae fix: file errors overlay support click to error file & optimize ui (#197) 2024-08-09 13:52:35 +08:00
Wells 65149f1326 fix: update mobile simulator ui (#196)
* fix: update mobile simulator styles

* docs: update

* fix: update icon
2024-08-08 11:20:20 +08:00
864907600cc 939466648d fix: route matching without query string & toggle button wrapping (#195)
* fix: fix route is not matched when url contains query string

* fix: fix mode switch button group wrapping

* fix: fix toggle button text wrapping
2024-08-07 15:51:36 +08:00
wwsun 8fe57333a0 chore(release): publish
- @music163/tango-context@1.1.8
 - @music163/tango-core@1.4.2
 - @music163/tango-designer@1.4.2
 - @music163/tango-sandbox@1.0.11
 - @music163/tango-setting-form@1.2.11
2024-08-06 13:54:27 +08:00
BoBoooooo 269e304d68 fix: view module _nodesTree set init value (#194) 2024-08-06 11:08:42 +08:00
wwsun 8c80255f81 chore: up 2024-08-05 12:31:12 +08:00
wwsun 359aa0b148 chore: update 2024-08-05 12:30:37 +08:00
wwsun 51ca4a72ce chore(release): publish
- @music163/tango-context@1.1.7
 - @music163/tango-core@1.4.1
 - @music163/tango-designer@1.4.1
 - @music163/tango-helpers@1.2.2
 - @music163/tango-sandbox@1.0.10
 - @music163/tango-setting-form@1.2.10
 - @music163/tango-ui@1.4.1
2024-08-05 12:28:43 +08:00
BoBoooooo b83d965955 fix: show right panel when design view select node & optimize editor css (#193) 2024-08-05 11:45:41 +08:00
Wells a1eea02f94 fix: parse template code & update maxRow of TextAreaSetter (#192)
* fix: update templatePattern regex to allow for multiline code wrapping

* fix: update error overlay display to use designer.activeView instead of mode

* fix: update maxRows to 6 in TextAreaSetter
2024-08-05 11:20:41 +08:00
wwsun 0d0479e3e5 chore(release): publish
- @music163/tango-context@1.1.6
 - @music163/tango-core@1.4.0
 - @music163/tango-designer@1.4.0
 - @music163/tango-helpers@1.2.1
 - @music163/tango-sandbox@1.0.9
 - @music163/tango-setting-form@1.2.9
 - @music163/tango-ui@1.4.0
2024-08-01 14:28:53 +08:00
wwsun 67d602a3ec fix: update types 2024-08-01 14:26:37 +08:00
Wells 8dd289cd7d feat: add isError and errorMessage to File & add isAstSynced to Module (#190)
* feat: update file without sync ast

* feat: parse and display file errors

* fix: update

* fix: refactor toolbar

* fix: add activeViews config to toolbarItem

* fix: update icon
2024-07-30 20:54:05 +08:00
wwsun 82bc5db4d8 chore(release): publish
- @music163/tango-context@1.1.5
 - @music163/tango-core@1.3.3
 - @music163/tango-designer@1.3.3
 - @music163/tango-sandbox@1.0.8
 - @music163/tango-setting-form@1.2.8
 - @music163/tango-ui@1.3.3
2024-07-12 13:51:34 +08:00
Wells 85c053b3db fix: update designer config (#186)
* docs: add section to generate local https ca

* fix: support disable format code

* fix: disable autoRemoveUnusedImports in CodeEditor

* fix: update toggle view setting
2024-07-12 10:52:12 +08:00
wwsun 32f5667cae docs: update readme 2024-07-03 15:44:25 +08:00
Wells 1d0fb4f274 fix: selection bug without prototype & update ToggleButton style (#180)
* fix: update toggleButton arrow style

* fix: check if prototype exists in selection tool

* fix: update state tree data
2024-06-26 16:54:38 +08:00
130 changed files with 9918 additions and 7940 deletions
+1
View File
@@ -9,3 +9,4 @@ coverage/
.umi-production/ .umi-production/
.log .log
.eslintcache .eslintcache
*.pem
+22 -5
View File
@@ -35,13 +35,13 @@ You can view the detailed usage guide through the following links:
You can use tango to build lowcode builders, for example: You can use tango to build lowcode builders, for example:
| Preview | Description | | Preview | Description |
|---------|-------------------------| | ---------------------------------------------------------------------------------------------- | ----------------------- |
| ![image](https://github.com/NetEase/tango/assets/6984035/56a71741-4536-4336-803a-d81d51128a76) | Internal App Builder | | ![image](https://github.com/NetEase/tango/assets/6984035/56a71741-4536-4336-803a-d81d51128a76) | Internal App Builder |
| ![image](https://github.com/NetEase/tango/assets/6984035/7f817e59-f7aa-4476-bc29-ac3e33fadbf7) | Dashboard App Builder | | ![dashboard app](public/dashboard-builder.png) | Dashboard App Builder |
| ![image](https://github.com/NetEase/tango/assets/6984035/790d88e1-d716-4f99-868a-31dda25e9fb1) | Mobile App Builder | | ![image](https://github.com/NetEase/tango/assets/6984035/790d88e1-d716-4f99-868a-31dda25e9fb1) | Mobile App Builder |
| ![rn app](public/rn-builder.png) | ReactNative App Builder | | ![rn app](public/rn-builder.png) | ReactNative App Builder |
| ![mail app](public/mail-builder.png) | Email Builder | | ![mail app](public/mail-builder.png) | Email Builder |
## 🌐 Compatibility ## 🌐 Compatibility
@@ -70,6 +70,23 @@ yarn
yarn start 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 ## 💬 Community
Join NetEase Tango Community to share your ideas, suggestions, or questions and connect with other users and contributors. Join NetEase Tango Community to share your ideas, suggestions, or questions and connect with other users and contributors.
+24 -8
View File
@@ -35,14 +35,13 @@
你可以使用 Tango 快速构建多种类型的基于源码的低代码搭建工具,例如: 你可以使用 Tango 快速构建多种类型的基于源码的低代码搭建工具,例如:
| 预览图 | 说明 | | 预览图 | 说明 |
|---------|-------------------------| | ---------------------------------------------------------------------------------------------- | -------------------- |
| ![image](https://github.com/NetEase/tango/assets/6984035/56a71741-4536-4336-803a-d81d51128a76) | 中后台系统搭建 | | ![image](https://github.com/NetEase/tango/assets/6984035/56a71741-4536-4336-803a-d81d51128a76) | 中后台系统搭建 |
| ![image](https://github.com/NetEase/tango/assets/6984035/7f817e59-f7aa-4476-bc29-ac3e33fadbf7) | 仪表盘应用搭建 | | ![dashboard app](public/dashboard-builder.png) | 仪表盘应用搭建 |
| ![image](https://github.com/NetEase/tango/assets/6984035/790d88e1-d716-4f99-868a-31dda25e9fb1) | H5活动页面搭建 | | ![image](https://github.com/NetEase/tango/assets/6984035/790d88e1-d716-4f99-868a-31dda25e9fb1) | H5活动页面搭建 |
| ![rn app](public/rn-builder.png) | ReactNative 应用搭建 | | ![rn app](public/rn-builder.png) | ReactNative 应用搭建 |
| ![mail app](public/mail-builder.png) | 营销类邮件搭建 | | ![mail app](public/mail-builder.png) | 营销类邮件搭建 |
## 🌐 兼容环境 ## 🌐 兼容环境
@@ -71,6 +70,23 @@ yarn
yarn start yarn start
``` ```
### 本地 https 证书
如果需要在本地开发环境中使用 https,可以使用以下命令生成证书:
```bash
brew install mkcert
# 将 mkcert 添加到本地根 CA,仅在本地生效
mkcert -install
# 进入 playground 应用目录
cd apps/playground
# 为网站生成一个由 mkcert 签名的证书
mkcert local.netease.com
```
## 💬 社区讨论 ## 💬 社区讨论
参与 NetEase Tango 的社区,以分享您的想法、建议或问题,并与其他用户和贡献者建立联系。 参与 NetEase Tango 的社区,以分享您的想法、建议或问题,并与其他用户和贡献者建立联系。
-28
View File
@@ -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-----
-25
View File
@@ -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-----
+5 -5
View File
@@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { Box, Group } from 'coral-system'; import { Box, Group, Text } from 'coral-system';
import { Avatar, Space, Switch } from 'antd'; import { Avatar, Space, Switch } from 'antd';
import { BranchesOutlined, MenuOutlined, QuestionCircleOutlined } from '@ant-design/icons'; import { BranchesOutlined, MenuOutlined, QuestionCircleOutlined } from '@ant-design/icons';
import { registerSetter } from '@music163/tango-designer'; import { registerSetter } from '@music163/tango-designer';
@@ -66,12 +66,12 @@ export function Logo() {
export function ProjectDetail() { export function ProjectDetail() {
return ( return (
<Box display="flex" alignItems="center" columnGap="l"> <Box display="flex" alignItems="center" columnGap="l">
<Box className="ProjectName" fontSize="18px" fontWeight="bold"> <Text className="ProjectName" fontSize="18px" fontWeight="bold" truncated>
community-test community-test
</Box> </Text>
<Box className="BranchName" as="code" fontSize="14px"> <Text className="BranchName" as="code" fontSize="14px" truncated>
<BranchesOutlined /> feature/list <BranchesOutlined /> feature/list
</Box> </Text>
</Box> </Box>
); );
} }
+10 -4
View File
@@ -3,7 +3,7 @@ const packageJson = {
private: true, private: true,
dependencies: { dependencies: {
'@music163/antd': '0.2.5', '@music163/antd': '0.2.5',
'@music163/tango-boot': '0.2.5', '@music163/tango-boot': '0.3.5',
react: '17.0.2', react: '17.0.2',
'react-dom': '17.0.2', 'react-dom': '17.0.2',
'prop-types': '15.7.2', 'prop-types': '15.7.2',
@@ -48,7 +48,7 @@ const tangoConfigJson = {
}, },
'@music163/tango-boot': { '@music163/tango-boot': {
description: '云音乐低代码运行时框架', description: '云音乐低代码运行时框架',
version: '0.2.5', version: '0.3.5',
library: 'TangoBoot', library: 'TangoBoot',
type: 'baseDependency', type: 'baseDependency',
resources: ['https://unpkg.com/@music163/tango-boot@{{version}}/dist/boot.js'], resources: ['https://unpkg.com/@music163/tango-boot@{{version}}/dist/boot.js'],
@@ -153,6 +153,7 @@ import { definePage } from "@music163/tango-boot";
import { import {
Page, Page,
Section, Section,
Box,
Button, Button,
Input, Input,
FormilyForm, FormilyForm,
@@ -166,8 +167,10 @@ import { OutButton } from "../components";
class App extends React.Component { class App extends React.Component {
render() { render() {
return ( return (
<Page title={tango.stores.app.title} subTitle={111}> <Page title={tango.stores.app.title} subTitle={<><Button>hello</Button></>}>
<Section tid="section0" /> <Section tid="section0">
<Box></Box>
</Section>
<Section tid="section1" title="Section Title"> <Section tid="section1" title="Section Title">
your input: <Input tid="input1" defaultValue="hello" /> your input: <Input tid="input1" defaultValue="hello" />
copy input: <Input value={tango.page.input1?.value} /> copy input: <Input value={tango.page.input1?.value} />
@@ -374,13 +377,16 @@ export default defineServices({
add: { add: {
url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/api/users', url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/api/users',
method: 'post', method: 'post',
description: '新增用户'
}, },
update: { update: {
url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/api/users', url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/api/users',
method: 'post', method: 'post',
description: '更新用户'
}, },
delete: { delete: {
url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/api/users?id=1', url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/api/users?id=1',
description: '删除用户'
}, },
}); });
`; `;
+68 -2
View File
@@ -48,7 +48,6 @@ const Snippet3ColumnLayout: IComponentPrototype = {
icon: 'icon-column-3', icon: 'icon-column-3',
type: 'snippet', type: 'snippet',
package: '@music163/antd', package: '@music163/antd',
// FIXME: Column 组件 需要更新为 defineComponent
initChildren: ` initChildren: `
<Columns columns={12}> <Columns columns={12}>
<Column colSpan={4}> <Column colSpan={4}>
@@ -132,11 +131,14 @@ export const nativeDomPrototypes = () => {
title: '样式', title: '样式',
group: 'style', group: 'style',
setter: 'expressionSetter', setter: 'expressionSetter',
setterProps: {
expressionType: 'cssObject',
},
}, },
{ {
name: 'className', name: 'className',
title: '类名', title: '类名',
setter: 'textSetter', setter: 'classNameSetter',
}, },
{ {
name: 'id', name: 'id',
@@ -166,6 +168,38 @@ const prototypes: Dict<IComponentPrototype> = {
Snippet2ColumnLayout, Snippet2ColumnLayout,
Snippet3ColumnLayout, Snippet3ColumnLayout,
SnippetButtonGroup, SnippetButtonGroup,
Section: {
...basePrototypes['Section'],
props: [
// ...(basePrototypes['Section'].props as any),
{
name: 'title',
title: '标题',
setter: 'textSetter',
},
{
name: 'extra',
title: '额外内容',
setter: 'renderSetter',
options: [
{
label: '按钮组',
value: 'ButtonGroup',
renderBody:
'<ButtonGroup><Button>button1</Button><Button>button2</Button></ButtonGroup>',
relatedImports: ['ButtonGroup', 'Button'],
},
],
template: '(v) => {\n return {{content}};\n}',
},
{
name: 'className',
title: '类名',
setter: 'classNameSetter',
group: 'style',
},
],
},
Box: { Box: {
name: 'Box', name: 'Box',
title: '盒子', title: '盒子',
@@ -197,6 +231,38 @@ const prototypes: Dict<IComponentPrototype> = {
title: 'd', title: 'd',
setter: 'textSetter', setter: 'textSetter',
}, },
{
name: 'onClick',
title: '点击事件',
setter: 'eventSetter',
template: '(e) => {\n {{content}}\n}',
tip: '回调参数说明:e 为事件对象',
},
{
name: 'renderFoo',
title: 'foo自定义渲染',
setter: 'renderSetter',
options: [
{
label: '占位空间',
value: 'Box',
render: '() => <Box>test</Box>',
relatedImports: ['Box'],
},
{
label: '占位文本',
value: 'Text',
render: '() => <Text>text</Text>',
relatedImports: ['Text'],
},
],
},
{
name: 'className',
title: '类名',
setter: 'classNameSetter',
group: 'style',
},
], ],
}, },
Columns: { Columns: {
+8 -8
View File
@@ -113,8 +113,8 @@ export default function App() {
actions={ actions={
<Box px="l"> <Box px="l">
<Toolbar> <Toolbar>
<Toolbar.Item key="routeSwitch" placement="left" /> <Toolbar.Item key="routeSwitch" placement="left" activeViews={['design']} />
<Toolbar.Item key="addPage" placement="left"> <Toolbar.Item key="addPage" placement="left" activeViews={['design']}>
<Action <Action
tooltip="添加页面" tooltip="添加页面"
shape="outline" shape="outline"
@@ -122,10 +122,10 @@ export default function App() {
onClick={() => setShowNewPageModal(true)} onClick={() => setShowNewPageModal(true)}
/> />
</Toolbar.Item> </Toolbar.Item>
<Toolbar.Item key="history" placement="left" /> <Toolbar.Item key="history" placement="left" activeViews={['design']} />
<Toolbar.Item key="preview" placement="left" /> <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="modeSwitch" placement="right" />
<Toolbar.Item key="togglePanel" placement="right" />
<Toolbar.Separator /> <Toolbar.Separator />
<Toolbar.Item placement="right"> <Toolbar.Item placement="right">
<Space> <Space>
@@ -187,6 +187,9 @@ export default function App() {
/> />
</Sidebar> </Sidebar>
<WorkspacePanel> <WorkspacePanel>
<WorkspaceView mode="code">
<CodeEditor />
</WorkspaceView>
<WorkspaceView mode="design"> <WorkspaceView mode="design">
<Sandbox <Sandbox
onMessage={(e) => { onMessage={(e) => {
@@ -208,9 +211,6 @@ export default function App() {
navigatorExtra={<Button size="small">hello world</Button>} navigatorExtra={<Button size="small">hello world</Button>}
/> />
</WorkspaceView> </WorkspaceView>
<WorkspaceView mode="code">
<CodeEditor />
</WorkspaceView>
</WorkspacePanel> </WorkspacePanel>
<SettingPanel /> <SettingPanel />
</DesignerPanel> </DesignerPanel>
+1 -1
View File
@@ -3,7 +3,7 @@ import { SystemProvider } from 'coral-system';
import 'antd/dist/antd.css'; import 'antd/dist/antd.css';
export const parameters = { export const parameters = {
actions: { argTypesRegex: '^on[A-Z].*' }, actions: { argTypesRegex: '^on.*' },
controls: { controls: {
matchers: { matchers: {
color: /(background|color)$/i, color: /(background|color)$/i,
+1 -1
View File
@@ -16,7 +16,7 @@
"dependencies": { "dependencies": {
"@music163/tango-setting-form": "*", "@music163/tango-setting-form": "*",
"@music163/tango-ui": "*", "@music163/tango-ui": "*",
"mobx": "6.12.3", "mobx": "6.13.2",
"mobx-react-lite": "4.0.7" "mobx-react-lite": "4.0.7"
}, },
"devDependencies": { "devDependencies": {
@@ -69,6 +69,14 @@ const prototypeHasBasicProps: IComponentPrototype = {
title: 'codeSetter', title: 'codeSetter',
setter: 'codeSetter', setter: 'codeSetter',
}, },
{
name: 'style',
title: 'codeSetter(cssObject)',
setter: 'codeSetter',
setterProps: {
expressionType: 'cssObject',
},
},
{ {
name: 'text', name: 'text',
title: 'textSetter', title: 'textSetter',
@@ -268,6 +276,9 @@ export function InitValues() {
name: 'style', name: 'style',
title: 'codeSetter', title: 'codeSetter',
setter: 'codeSetter', setter: 'codeSetter',
setterProps: {
expressionType: 'cssObject',
},
}, },
{ {
name: 'object', name: 'object',
@@ -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,15 @@
import React, { useState } from 'react';
import { ClassNameInput } from '@music163/tango-ui';
export default {
title: 'UI/ClassNameInput',
};
export function Basic() {
return <ClassNameInput defaultValue="hello world" onChange={console.log} />;
}
export function Controlled() {
const [value, setValue] = useState('');
return <ClassNameInput value={value} onChange={setValue} />;
}
+5 -1
View File
@@ -1,5 +1,5 @@
import React from 'react'; 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'; import { BlockOutlined } from '@ant-design/icons';
export default { export default {
@@ -28,6 +28,10 @@ export function Basic() {
); );
} }
export function CSS() {
return <InputStyleCode enableESLint suffix={<Action icon={<BlockOutlined />} size="small" />} />;
}
const code = ` const code = `
function foo() { function foo() {
console.log("test"); console.log("test");
+8 -8
View File
@@ -15,17 +15,17 @@
"test:watch": "jest --watch", "test:watch": "jest --watch",
"eslint": "eslint packages/**/src/*.{ts,tsx} --cache", "eslint": "eslint packages/**/src/*.{ts,tsx} --cache",
"publish": "lerna publish", "publish": "lerna publish",
"ver": "lerna version --no-private", "ver": "lerna version --no-private --conventional-commits",
"release": "yarn eslint && yarn build && lerna version --no-private --conventional-commits && lerna publish from-git", "release": "yarn eslint && yarn build && yarn ver && lerna publish from-git",
"release:npm": "yarn eslint && yarn build && yarn run ver && lerna publish from-git", "release:npm": "yarn eslint && yarn build && yarn ver && lerna publish from-package",
"release:beta": "yarn eslint && yarn build && yarn run ver && lerna publish from-package --dist-tag beta", "release:beta": "yarn eslint && yarn build && yarn ver && lerna publish from-package --dist-tag beta",
"up": "yarn upgrade-interactive --latest", "up": "yarn upgrade-interactive --latest",
"prepare": "husky install" "prepare": "husky install"
}, },
"devDependencies": { "devDependencies": {
"@babel/preset-env": "^7.23.6", "@babel/preset-env": "^7.25.4",
"@babel/preset-react": "^7.23.3", "@babel/preset-react": "^7.24.7",
"@babel/preset-typescript": "^7.23.3", "@babel/preset-typescript": "^7.24.7",
"@commitlint/cli": "^18.4.3", "@commitlint/cli": "^18.4.3",
"@commitlint/config-conventional": "^18.4.3", "@commitlint/config-conventional": "^18.4.3",
"@types/jest": "^29.5.7", "@types/jest": "^29.5.7",
@@ -58,7 +58,7 @@
"react": "^17.0.0", "react": "^17.0.0",
"react-dom": "^17.0.0", "react-dom": "^17.0.0",
"styled-components": "^5.3.6", "styled-components": "^5.3.6",
"typedoc": "^0.25.4", "typedoc": "^0.26.7",
"typescript": "^5.3.3" "typescript": "^5.3.3"
}, },
"engines": { "engines": {
+26
View File
@@ -3,6 +3,32 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.1.10](https://github.com/netease/tango/compare/@music163/tango-context@1.1.9...@music163/tango-context@1.1.10) (2024-09-09)
### Bug Fixes
- service support set description field ([#206](https://github.com/netease/tango/issues/206)) ([79f9a8c](https://github.com/netease/tango/commit/79f9a8c1791424f35f194ab2a9aa7c263ef39f40))
## [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) ## [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 **Note:** Version bump only for package @music163/tango-context
+3 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "@music163/tango-context", "name": "@music163/tango-context",
"version": "1.1.4", "version": "1.1.10",
"description": "react context for tango-apps", "description": "react context for tango-apps",
"keywords": [ "keywords": [
"react", "react",
@@ -31,8 +31,8 @@
"react": ">= 16.8" "react": ">= 16.8"
}, },
"dependencies": { "dependencies": {
"@music163/tango-core": "^1.3.2", "@music163/tango-core": "^1.4.4",
"@music163/tango-helpers": "^1.2.0", "@music163/tango-helpers": "^1.2.4",
"mobx-react-lite": "4.0.7" "mobx-react-lite": "4.0.7"
}, },
"publishConfig": { "publishConfig": {
+15 -8
View File
@@ -1,4 +1,4 @@
import type { Engine } from '@music163/tango-core'; import type { AbstractCodeWorkspace, Engine } from '@music163/tango-core';
import { IVariableTreeNode, createContext } from '@music163/tango-helpers'; import { IVariableTreeNode, createContext } from '@music163/tango-helpers';
export interface ITangoEngineContext { export interface ITangoEngineContext {
@@ -21,8 +21,12 @@ const [TangoEngineProvider, useTangoEngine] = createContext<ITangoEngineContext>
export { TangoEngineProvider }; export { TangoEngineProvider };
/**
* 获取 CodeWorkspace 实例
* @returns
*/
export const useWorkspace = () => { export const useWorkspace = () => {
return useTangoEngine()?.engine.workspace; return useTangoEngine()?.engine.workspace as AbstractCodeWorkspace;
}; };
export const useDesigner = () => { export const useDesigner = () => {
@@ -90,12 +94,15 @@ export const useWorkspaceData = () => {
key: prefix, key: prefix,
selectable: false, selectable: false,
showAddButton: true, showAddButton: true,
children: Object.keys(file.serviceFunctions || {}).map((key) => ({ children: Object.entries(file.serviceFunctions || {}).map(([key, value]) => {
title: key, const title = value.description ? `${key}(${value.description})` : key;
key: [prefix, key].join('.'), return {
type: 'function', title,
showRemoveButton: true, key: [prefix, key].join('.'),
})), type: 'function',
showRemoveButton: true,
};
}),
}); });
}); });
+34
View File
@@ -3,6 +3,40 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.4.4](https://github.com/netease/tango/compare/@music163/tango-core@1.4.3...@music163/tango-core@1.4.4) (2024-09-09)
**Note:** Version bump only for package @music163/tango-core
## [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) ## [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 ### Bug Fixes
+8 -8
View File
@@ -1,6 +1,6 @@
{ {
"name": "@music163/tango-core", "name": "@music163/tango-core",
"version": "1.3.2", "version": "1.4.4",
"description": "tango core", "description": "tango core",
"author": "wwsun <ww.sun@outlook.com>", "author": "wwsun <ww.sun@outlook.com>",
"homepage": "", "homepage": "",
@@ -24,14 +24,14 @@
"prepublishOnly": "yarn build" "prepublishOnly": "yarn build"
}, },
"dependencies": { "dependencies": {
"@babel/generator": "^7.23.5", "@babel/generator": "^7.25.6",
"@babel/parser": "^7.23.5", "@babel/parser": "^7.25.6",
"@babel/traverse": "^7.23.5", "@babel/traverse": "^7.25.6",
"@babel/types": "^7.23.5", "@babel/types": "^7.25.6",
"@music163/tango-helpers": "^1.2.0", "@music163/tango-helpers": "^1.2.4",
"@types/babel__generator": "^7.6.7", "@types/babel__generator": "^7.6.7",
"@types/babel__traverse": "^7.20.4", "@types/babel__traverse": "^7.20.6",
"mobx": "6.12.3", "mobx": "6.13.2",
"path-browserify": "^1.0.1" "path-browserify": "^1.0.1"
}, },
"peerDependencies": { "peerDependencies": {
+2 -2
View File
@@ -1,12 +1,12 @@
import { MenuDataType } from '@music163/tango-helpers'; import { MenuDataType } from '@music163/tango-helpers';
import { Designer, DesignerViewType, Engine, SimulatorNameType } from './models'; import { Designer, DesignerViewType, Engine, SimulatorNameType } from './models';
import { IWorkspace } from './models/interfaces'; import { AbstractWorkspace } from './models/abstract-workspace';
interface ICreateEngineOptions { interface ICreateEngineOptions {
/** /**
* 自定义工作区 * 自定义工作区
*/ */
workspace?: IWorkspace; workspace?: AbstractWorkspace;
/** /**
* 菜单信息 * 菜单信息
*/ */
+1 -1
View File
@@ -35,7 +35,7 @@ export function isTangoVariable(name: string) {
return /^tango\??\.(stores|services)\??\./.test(name) && name.split('.').length > 2; return /^tango\??\.(stores|services)\??\./.test(name) && name.split('.').length > 2;
} }
const templatePattern = /^{(.+)}$/s; const templatePattern = /^{(.+)}$/;
/** /**
* 判断给定字符串是否被表达式容器`{expCode}`包裹 * 判断给定字符串是否被表达式容器`{expCode}`包裹
+4 -16
View File
@@ -3,7 +3,7 @@
*/ */
import generator, { GeneratorOptions } from '@babel/generator'; import generator, { GeneratorOptions } from '@babel/generator';
import * as t from '@babel/types'; import * as t from '@babel/types';
import { logger, wrapCode } from '@music163/tango-helpers'; import { Dict, logger, wrapCode } from '@music163/tango-helpers';
import { formatCode } from '../string'; import { formatCode } from '../string';
const defaultGeneratorOptions: GeneratorOptions = { const defaultGeneratorOptions: GeneratorOptions = {
@@ -25,19 +25,7 @@ export function ast2code(ast: t.Node, options: GeneratorOptions = defaultGenerat
return code; return code;
} }
const bracketPattern = /^\(.+\)$/s; const bracketPattern = /^\(.+\)$/;
/**
* 是否被 () 包裹
*
* @example ({ foo: 'foo' }) -> true
* @example { foo: 'foo' } -> false
*
* @param str 目标字符串
*/
function isWrappingWithBrackets(str: string) {
return bracketPattern.test(str);
}
/** /**
* 将表达式生成为块级代码 * 将表达式生成为块级代码
@@ -54,7 +42,7 @@ export function expression2code(node: t.Expression) {
const isWrappingExpression = t.isObjectExpression(node) || t.isFunctionExpression(node); const isWrappingExpression = t.isObjectExpression(node) || t.isFunctionExpression(node);
if (isWrappingExpression && isWrappingWithBrackets(ret)) { if (isWrappingExpression && bracketPattern.test(ret)) {
// 如果是对象,输出包含 ({}),则去掉首尾的括号 // 如果是对象,输出包含 ({}),则去掉首尾的括号
ret = ret.slice(1, -1); ret = ret.slice(1, -1);
} }
@@ -195,7 +183,7 @@ export function node2value(node: t.Node, isWrapCode = true): any {
); );
if (isSimpleObject) { if (isSimpleObject) {
// simple object: { key1, key2, key3 } // simple object: { key1, key2, key3 }
ret = node.properties.reduce((prev, propertyNode) => { ret = node.properties.reduce<Dict>((prev, propertyNode) => {
if (propertyNode.type === 'ObjectProperty') { if (propertyNode.type === 'ObjectProperty') {
const key = keyNode2value(propertyNode.key); const key = keyNode2value(propertyNode.key);
const value = node2value(propertyNode.value, isWrapCode); const value = node2value(propertyNode.value, isWrapCode);
+2 -6
View File
@@ -68,11 +68,7 @@ export function isValidExpressionCode(code: string) {
* @returns * @returns
*/ */
export function code2ast(code: string): t.File { export function code2ast(code: string): t.File {
try { return parse(code, babelParserConfig);
return parse(code, babelParserConfig);
} catch (err) {
logger.error('[code2ast failed!]', err);
}
} }
/** /**
@@ -175,7 +171,7 @@ export function value2node(
* 将 js 普通对象解析为 t.Node * 将 js 普通对象解析为 t.Node
*/ */
export function object2node( export function object2node(
obj: object, obj: Dict,
getValueNode: (value: any, key?: string) => t.Expression = value2node, getValueNode: (value: any, key?: string) => t.Expression = value2node,
) { ) {
if (!isPlainObject(obj)) { if (!isPlainObject(obj)) {
+4 -4
View File
@@ -28,7 +28,7 @@ import { isDefineService, isDefineStore, isTangoVariable } from '../assert';
import type { import type {
IRouteData, IRouteData,
IStorePropertyData, IStorePropertyData,
ITangoViewNodeData, IViewNodeData,
IImportDeclarationPayload, IImportDeclarationPayload,
InsertChildPositionType, InsertChildPositionType,
IImportSpecifierData, IImportSpecifierData,
@@ -1009,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, { traverse(ast, {
CallExpression(path) { CallExpression(path) {
const calleeName = keyNode2value(path.node.callee) as string; const calleeName = keyNode2value(path.node.callee) as string;
@@ -1017,7 +1017,7 @@ export function updateServiceConfigToServiceFile(ast: t.File, config: Dict<objec
const configNode = path.node.arguments[0]; const configNode = path.node.arguments[0];
if (t.isObjectExpression(configNode)) { 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 serviceConfig = config[key];
const property = t.objectProperty(t.identifier(key), serviceConfig2Node(serviceConfig)); const property = t.objectProperty(t.identifier(key), serviceConfig2Node(serviceConfig));
properties[key] = property; properties[key] = property;
@@ -1234,7 +1234,7 @@ export function cloneJSXElement(node: t.JSXElement, overrideProps?: Dict) {
export function traverseViewFile(ast: t.File, idGenerator: IdGenerator) { export function traverseViewFile(ast: t.File, idGenerator: IdGenerator) {
const imports: Record<string, IImportSpecifierData[]> = {}; const imports: Record<string, IImportSpecifierData[]> = {};
const importedModules: Dict<IImportDeclarationPayload | IImportDeclarationPayload[]> = {}; const importedModules: Dict<IImportDeclarationPayload | IImportDeclarationPayload[]> = {};
const nodes: Array<ITangoViewNodeData<t.JSXElement>> = []; const nodes: Array<IViewNodeData<t.JSXElement>> = [];
const cloneAst = t.cloneNode(ast, true, true); const cloneAst = t.cloneNode(ast, true, true);
const cleanAst = clearTrackingData(cloneAst); const cleanAst = clearTrackingData(cloneAst);
const variables: string[] = []; // 使用的 tango 变量 const variables: string[] = []; // 使用的 tango 变量
+1 -1
View File
@@ -11,7 +11,7 @@ export function namesToImportDeclarations(
names: string[], names: string[],
nameMap: Dict<IImportSpecifierSourceData>, nameMap: Dict<IImportSpecifierSourceData>,
) { ) {
const map = {}; const map: Dict = {};
names.forEach((name) => { names.forEach((name) => {
const mod = nameMap[name]; const mod = nameMap[name];
if (mod) { if (mod) {
+2 -2
View File
@@ -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 } ] } // { id: 1, component, props: { id: 1 }, children: [ { id: 2, component } ] }
export function deepCloneNode(obj: any, component?: string): any { export function deepCloneNode(obj: any, component?: string): any {
function deepCloneObject() { function deepCloneObject() {
const target = {}; const target: Dict = {};
for (const key in obj) { for (const key in obj) {
if (Object.hasOwn(obj, key)) { if (Object.hasOwn(obj, key)) {
target[key] = deepCloneNode(obj[key], component); target[key] = deepCloneNode(obj[key], component);
+13 -21
View File
@@ -8,65 +8,57 @@ import { FileType } from './../types';
export function inferFileType(filename: string): FileType { export function inferFileType(filename: string): FileType {
// 增加 tangoConfigJson Module // 增加 tangoConfigJson Module
if (/\/tango\.config\.json$/.test(filename)) { if (/\/tango\.config\.json$/.test(filename)) {
return FileType.TangoConfigJson; return FileType.TangoConfigJsonFile;
} }
if (/\/appJson\.json$/.test(filename)) { if (/\/appJson\.json$/.test(filename)) {
return FileType.AppJson; return FileType.AppJsonFile;
} }
if (/\/package\.json$/.test(filename)) { if (/\/package\.json$/.test(filename)) {
return FileType.PackageJson; return FileType.PackageJsonFile;
} }
if (/\/routes\.js$/.test(filename)) { if (/\/routes\.js$/.test(filename)) {
return FileType.RouteModule; return FileType.JsRouteConfigFile;
} }
// 所有 pages 下的 js 文件均认为是有效的 viewModule // 所有 pages 下的 js 文件均认为是有效的 viewModule
if (/\/pages\/.+\.jsx?$/.test(filename)) { if (/\/pages\/.+\.jsx?$/.test(filename)) {
return FileType.JsxViewModule; return FileType.JsViewFile;
} }
// 所有 pages 下的 js 文件均认为是有效的 viewModule // 所有 pages 下的 js 文件均认为是有效的 viewModule
if (/\/pages\/.+\.schema\.json?$/.test(filename)) { if (/\/pages\/.+\.schema\.json?$/.test(filename)) {
return FileType.JsonViewModule; return FileType.JsonViewFile;
} }
if (/\/(blocks|components)\/index\.js/.test(filename)) { if (/\/(blocks|components)\/index\.js/.test(filename)) {
return FileType.ComponentsEntryModule; return FileType.JsLocalComponentsEntryFile;
} }
if (/\/services\/.+\.js$/.test(filename)) { if (/\/services\/.+\.js$/.test(filename)) {
return FileType.ServiceModule; return FileType.JsServiceFile;
} }
if (/service\.js$/.test(filename)) { if (/service\.js$/.test(filename)) {
return FileType.ServiceModule; return FileType.JsServiceFile;
} }
if (/\/stores\/index\.js$/.test(filename)) { if (/\/stores\/index\.js$/.test(filename)) {
return FileType.StoreEntryModule; return FileType.JsStoreEntryFile;
} }
if (/\/stores\/.+\.js$/.test(filename)) { if (/\/stores\/.+\.js$/.test(filename)) {
return FileType.StoreModule; return FileType.JsStoreFile;
} }
if (/\.jsx?$/.test(filename)) { if (/\.jsx?$/.test(filename)) {
return FileType.Module; return FileType.JsFile;
} }
if (/\.json$/.test(filename)) { if (/\.json$/.test(filename)) {
return FileType.Json; return FileType.JsonFile;
}
if (/\.less$/.test(filename)) {
return FileType.Less;
}
if (/\.scss$/.test(filename)) {
return FileType.Scss;
} }
return FileType.File; return FileType.File;
@@ -0,0 +1,264 @@
import {
Dict,
isStoreVariablePath,
parseServiceVariablePath,
parseStoreVariablePath,
} from '@music163/tango-helpers';
import { inferFileType, getFilepath } from '../helpers';
import { TangoFile } from './file';
import { FileType } from '../types';
import { JsRouteConfigFile } from './js-route-config-file';
import { JsStoreEntryFile } from './js-store-entry-file';
import { JsServiceFile } from './js-service-file';
import { JsViewFile } from './js-view-file';
import { JsLocalComponentsEntryFile } from './js-local-components-entry-file';
import { JsAppEntryFile } from './js-app-entry-file';
import { JsFile } from './js-file';
import { AbstractWorkspace, IWorkspaceInitConfig } from './abstract-workspace';
import { JsonFile } from './json-file';
import { JsStoreFile } from './js-store-file';
/**
* CodeWorkspace 抽象基类
*/
export abstract class AbstractCodeWorkspace extends AbstractWorkspace {
/**
* 模型入口配置模块
*/
storeEntryModule: JsStoreEntryFile;
/**
* 状态管理模块
*/
storeModules: Record<string, JsStoreFile>;
/**
* 数据服务模块
*/
serviceModules: Record<string, JsServiceFile>;
constructor(options: IWorkspaceInitConfig) {
super(options);
this.storeModules = {};
this.serviceModules = {};
if (options?.files) {
this.addFiles(options.files);
}
}
/**
* 添加文件到工作区
* @param filename 文件名
* @param code 代码片段
* @param fileType 模块类型
*/
addFile(filename: string, code: string, fileType?: FileType) {
if (!fileType && filename === this.entry) {
fileType = FileType.JsAppEntryFile;
}
const moduleType = fileType || inferFileType(filename);
const props = {
filename,
code,
type: moduleType,
};
let module;
switch (moduleType) {
case FileType.JsAppEntryFile:
module = new JsAppEntryFile(this, props);
this.jsAppEntryFile = module;
break;
case FileType.JsStoreEntryFile:
module = new JsStoreEntryFile(this, props);
this.storeEntryModule = module;
break;
case FileType.JsLocalComponentsEntryFile:
module = new JsLocalComponentsEntryFile(this, props);
this.componentsEntryModule = module;
break;
case FileType.JsRouteConfigFile: {
module = new JsRouteConfigFile(this, props);
this.routeModule = module;
// check if activeRoute exists
const route = module.routes.find((item) => item.path === this.activeRoute);
if (!route) {
this.setActiveRoute(module.routes[0]?.path);
}
break;
}
case FileType.JsViewFile:
module = new JsViewFile(this, props);
break;
case FileType.JsServiceFile:
module = new JsServiceFile(this, props);
this.serviceModules[module.name] = module;
break;
case FileType.JsStoreFile:
module = new JsStoreFile(this, props);
this.storeModules[module.name] = module;
break;
case FileType.JsFile:
module = new JsFile(this, props);
break;
case FileType.PackageJsonFile:
module = new JsonFile(this, props);
this.packageJson = module;
break;
case FileType.TangoConfigJsonFile:
module = new JsonFile(this, props);
this.tangoConfigJson = module;
break;
case FileType.JsonFile:
module = new JsonFile(this, props);
break;
default:
module = new TangoFile(this, props);
}
this.files.set(filename, module);
}
addServiceFile(serviceName: string, code: string) {
const filename = `/src/services/${serviceName}.js`;
this.addFile(filename, code, FileType.JsServiceFile);
const indexServiceModule = this.serviceModules.index;
indexServiceModule?.addImportDeclaration(`./${serviceName}`, []).update();
}
addStoreFile(storeName: string, code: string) {
const filename = `/src/stores/${storeName}.js`;
this.addFile(filename, code);
if (!this.storeEntryModule) {
this.addFile('/src/stores/index.js', '');
}
this.storeEntryModule.addStore(storeName).update();
}
/**
* 添加新的模型文件
* @deprecated 使用 addStoreFile 代替
*/
addStoreModule(name: string, code: string) {
this.addStoreFile(name, code);
}
/**
* 删除模型文件
* @param name
*/
removeStoreModule(name: string) {
const filename = getFilepath(name, '/src/stores', '.js');
this.storeEntryModule.removeStore(name).update();
this.removeFile(filename);
}
/**
* 添加模型属性
* @param storeName
* @param stateName
* @param initValue
*/
addStoreState(storeName: string, stateName: string, initValue: string) {
this.storeModules[storeName]?.addState(stateName, initValue).update();
}
/**
* 删除模型属性
* @param storeName
* @param stateName
*/
removeStoreState(storeName: string, stateName: string) {
this.storeModules[storeName]?.removeState(stateName).update();
}
/**
* 根据变量路径删除状态变量
* @param variablePath
*/
removeStoreVariable(variablePath: string) {
const { storeName, variableName } = parseStoreVariablePath(variablePath);
this.removeStoreState(storeName, variableName);
}
/**
* 根据变量路径更新状态变量的值
* @param variablePath 变量路径
* @param code 变量代码
*/
updateStoreVariable(variablePath: string, code: string) {
if (isStoreVariablePath(variablePath)) {
const { storeName, variableName } = parseStoreVariablePath(variablePath);
this.storeModules[storeName]?.updateState(variableName, code).update();
}
}
/**
* 获取服务函数的详情
* TODO: 不要 services 前缀
* @param serviceKey `services.list` 或 `services.sub.list`
* @returns
*/
getServiceFunction(serviceKey: string) {
const { name, moduleName } = parseServiceVariablePath(serviceKey);
if (!name) {
return;
}
return {
name,
moduleName,
config: this.serviceModules[moduleName]?.serviceFunctions[name],
};
}
/**
* 获取服务函数的列表
* @returns 返回服务函数的列表 { [serviceKey: string]: Dict }
*/
listServiceFunctions() {
const ret: Record<string, Dict> = {};
Object.keys(this.serviceModules).forEach((moduleName) => {
const module = this.serviceModules[moduleName];
Object.keys(module.serviceFunctions).forEach((name) => {
const serviceKey = moduleName === 'index' ? name : [moduleName, name].join('.');
ret[serviceKey] = module.serviceFunctions[name];
});
});
return ret;
}
/**
* 更新服务函数
*/
updateServiceFunction(serviceName: string, payload: Dict, moduleName = 'index') {
this.serviceModules[moduleName].updateServiceFunction(serviceName, payload).update();
}
/**
* 新增服务函数,支持批量添加
*/
addServiceFunction(name: string, config: Dict, moduleName = 'index') {
this.serviceModules[moduleName]?.addServiceFunction(name, config).update();
}
addServiceFunctions(configs: Dict<Dict>, modName = 'index') {
this.serviceModules[modName]?.addServiceFunctions(configs).update();
}
/**
* 删除服务函数
* @param name
*/
removeServiceFunction(serviceKey: string) {
const { moduleName, name } = parseServiceVariablePath(serviceKey);
this.serviceModules[moduleName]?.deleteServiceFunction(name).update();
}
/**
* 更新服务的基础配置
*/
updateServiceBaseConfig(config: Dict, moduleName = 'index') {
this.serviceModules[moduleName]?.updateBaseConfig(config).update();
}
}
+63
View File
@@ -0,0 +1,63 @@
import { AbstractWorkspace } from './abstract-workspace';
import type { FileType, IFileConfig } from '../types';
/**
* 普通文件抽象基类,不进行 AST 解析
*/
export abstract class AbstractFile {
readonly workspace: AbstractWorkspace;
/**
* 文件名
*/
readonly filename: string;
/**
* 文件类型
*/
readonly type: FileType;
/**
* 最近修改的时间戳
*/
lastModified: number;
/**
* 文件解析是否出错
*/
isError: boolean;
/**
* 文件解析错误消息
*/
errorMessage: string;
_code: string;
_cleanCode: string;
get code() {
return this._code;
}
// FIXME: cleanCode 是不是只有 viewFile 有 ????
get cleanCode() {
return this._cleanCode;
}
constructor(workspace: AbstractWorkspace, props: IFileConfig, isSyncCode = true) {
this.workspace = workspace;
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) {
this.update(props.code);
}
}
/**
* 更新文件内容
*/
abstract update(code?: string): void;
}
@@ -0,0 +1,138 @@
import * as t from '@babel/types';
import { isNil } from '@music163/tango-helpers';
import {
code2ast,
ast2code,
formatCode,
traverseFile,
addImportDeclaration,
updateImportDeclaration,
} from '../helpers';
import { IFileConfig, IImportSpecifierData, ImportDeclarationDataType } from '../types';
import { AbstractWorkspace } from './abstract-workspace';
import { AbstractFile } from './abstract-file';
/**
* JS 文件抽象基类
* - ast 操纵类方法,统一返回 this,支持外层链式调用
* - observable state 统一用 _foo 格式,并提供 getter 方法
*/
export abstract class AbstractJsFile extends AbstractFile {
ast: t.File;
/**
* ast 是否与 code 保持同步
*/
isAstSynced: boolean;
/**
* 导入的依赖列表
*/
importList: ImportDeclarationDataType;
constructor(workspace: AbstractWorkspace, props: IFileConfig, isSyncCode = true) {
super(workspace, props, isSyncCode);
}
/**
* 基于最新的 ast 进行同步
* @param code 如果传入 code,则基于 code 进行同步
* @param isSyncAst 是否同步 ast
* @param isRefreshWorkspace 是否刷新 workspace
*/
update(code?: string, isSyncAst = true, isRefreshWorkspace = true) {
this.lastModified = Date.now();
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;
}
}
/**
* 基于当前的代码重新生成 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;
}
}
}
addImportDeclaration(source: string, specifiers: IImportSpecifierData[]) {
this.ast = addImportDeclaration(this.ast, source, specifiers);
return this;
}
updateImportDeclaration(source: string, specifiers: IImportSpecifierData[]) {
this.ast = updateImportDeclaration(this.ast, source, specifiers);
return this;
}
/**
* 基于最新的 ast 进行源码同步
*/
_syncByAst() {
const code = ast2code(this.ast);
this._code = code;
this._cleanCode = code;
}
/**
* 基于输入的源码进行同步
* @param code 源码
* @param isSyncAst 是否同步 ast
* @returns
*/
_syncByCode(code: string, isSyncAst = true) {
if (code === this._code) {
return;
}
// 提前格式化代码
try {
code = formatCode(code);
} catch (err) {
// err ignored, format code failed
}
this._code = code;
this._cleanCode = code;
if (isSyncAst) {
this.ast = code2ast(code);
}
}
_analysisAst() {
const { imports } = traverseFile(this.ast);
this.importList = imports;
}
}
@@ -0,0 +1,106 @@
import { getValue, isNil, logger, setValue } from '@music163/tango-helpers';
import type { IFileConfig } from '../types';
import { formatCode } from '../helpers';
import { AbstractWorkspace } from './abstract-workspace';
import { AbstractFile } from './abstract-file';
export abstract class AbstractJsonFile extends AbstractFile {
_object;
abstract get json(): object;
constructor(workspace: AbstractWorkspace, props: IFileConfig) {
super(workspace, props, false);
this._object = {};
this.update(props.code);
}
update(code?: string) {
this.lastModified = Date.now();
if (isNil(code)) {
// 基于最新的 json 同步代码
let newCode = JSON.stringify(this._object);
try {
newCode = formatCode(newCode, 'json');
} catch (err) {
logger.error(err);
return;
}
this._code = newCode;
this._cleanCode = newCode;
} else {
try {
// 基于传入的代码,同步 json 对象
code = formatCode(code, 'json');
} catch (err) {
logger.error(err);
return;
}
this._code = code;
this._cleanCode = code;
try {
const json = JSON.parse(code);
this._object = json;
} catch (err) {
logger.error(err);
}
}
this.workspace.onFilesChange([this.filename]);
}
/**
* 根据路径取值
* @param valuePath
* @returns
*/
getValue(valuePath: string) {
return getValue(this.json, valuePath);
}
/**
* 根据路径设置值
* @param valuePath
* @param visitor
*/
setValue(valuePath: string, visitor: (targetValue: any) => any) {
const target = this.getValue(valuePath);
let next: unknown;
if (typeof visitor === 'function') {
next = visitor?.(target);
} else {
next = visitor;
}
if (next !== undefined) {
setValue(this._object, valuePath, next);
}
return this;
}
/**
* 根据路径删除值
* @param valuePath
* @param visitor
*/
deleteValue(valuePath: string) {
const pathList = valuePath.split('.');
const lastPath = pathList.pop();
const parentPath = pathList.join('.');
let target;
if (parentPath) {
target = this.getValue(parentPath);
} else {
target = this.json;
}
if (!target) {
return this;
}
delete target[lastPath];
if (parentPath) {
this.setValue(parentPath, target);
} else {
this._object = target;
}
return this;
}
}
@@ -0,0 +1,59 @@
import { Dict } from '@music163/tango-helpers';
import { AbstractFile } from './abstract-file';
export interface IViewNodeInitConfig<RawNodeType = unknown, ViewFileType = AbstractFile> {
id: string;
component: string;
rawNode: RawNodeType;
file: ViewFileType;
}
export abstract class AbstractViewNode<RawNodeType = unknown, ViewFileType = AbstractFile> {
/**
* 节点 ID
*/
readonly id: string;
/**
* 节点对应的组件名
*/
readonly component: string;
readonly rawNode: RawNodeType;
/**
* 节点所属的文件对象
*/
file: ViewFileType;
/**
* 节点所属的文件对象
*/
props: Record<string, any>;
/**
* 节点的位置信息
*/
abstract get loc(): unknown;
constructor(props: IViewNodeInitConfig<RawNodeType, ViewFileType>) {
this.id = props.id;
this.component = props.component;
this.rawNode = props.rawNode;
this.file = props.file;
}
/**
* 销毁当前节点,清空文件和节点的关联关系
*/
destroy() {
this.file = undefined;
}
/**
* 返回克隆后的 ast 节点
* @param overrideProps 额外设置给克隆节点的属性
* @returns 返回克隆的原始节点
*/
abstract cloneRawNode(overrideProps?: Dict): RawNodeType;
}
File diff suppressed because it is too large Load Diff
+10 -3
View File
@@ -1,6 +1,6 @@
import { action, computed, makeObservable, observable, toJS } from 'mobx'; import { action, computed, makeObservable, observable, toJS } from 'mobx';
import { IWorkspace } from './interfaces';
import { MenuDataType } from '@music163/tango-helpers'; import { MenuDataType } from '@music163/tango-helpers';
import { AbstractWorkspace } from './abstract-workspace';
export type SimulatorNameType = 'desktop' | 'phone'; export type SimulatorNameType = 'desktop' | 'phone';
@@ -18,7 +18,7 @@ interface IViewportBounding {
} }
interface IDesignerOptions { interface IDesignerOptions {
workspace: IWorkspace; workspace: AbstractWorkspace;
simulator?: SimulatorNameType | ISimulatorType; simulator?: SimulatorNameType | ISimulatorType;
/** /**
* 菜单配置 * 菜单配置
@@ -108,7 +108,7 @@ export class Designer {
*/ */
_menuData?: MenuDataType = null; _menuData?: MenuDataType = null;
private readonly workspace: IWorkspace; private readonly workspace: AbstractWorkspace;
get simulator(): ISimulatorType { get simulator(): ISimulatorType {
return toJS(this._simulator); return toJS(this._simulator);
@@ -239,6 +239,13 @@ export class Designer {
setActiveView(view: DesignerViewType) { setActiveView(view: DesignerViewType) {
this._activeView = view; this._activeView = view;
if (view === 'dual') {
this.setActiveSidebarPanel('');
this.toggleRightPanel(false);
this.toggleIsPreview(true);
} else if (view === 'design') {
this.toggleIsPreview(false);
}
} }
setActiveSidebarPanel(panel: string) { setActiveSidebarPanel(panel: string) {
+3 -3
View File
@@ -1,7 +1,7 @@
import { action, computed, makeObservable, observable } from 'mobx'; import { action, computed, makeObservable, observable } from 'mobx';
import { ISelectedItemData } from '@music163/tango-helpers'; import { ISelectedItemData } from '@music163/tango-helpers';
import { DropTarget } from './drop-target'; import { DropTarget } from './drop-target';
import { IWorkspace } from './interfaces'; import { AbstractWorkspace } from './abstract-workspace';
/** /**
* 拖拽来源类,被拖拽的物体 * 拖拽来源类,被拖拽的物体
@@ -22,7 +22,7 @@ export class DragSource {
*/ */
dropTarget: DropTarget; dropTarget: DropTarget;
private readonly workspace: IWorkspace; private readonly workspace: AbstractWorkspace;
get node() { get node() {
return this.workspace.getNode(this.data?.id, this.data?.filename); return this.workspace.getNode(this.data?.id, this.data?.filename);
@@ -47,7 +47,7 @@ export class DragSource {
return this.data?.bounding; return this.data?.bounding;
} }
constructor(workspace: IWorkspace) { constructor(workspace: AbstractWorkspace) {
this.workspace = workspace; this.workspace = workspace;
this.data = null; this.data = null;
this.isDragging = false; this.isDragging = false;
+3 -3
View File
@@ -1,6 +1,6 @@
import { action, computed, makeObservable, observable } from 'mobx'; import { action, computed, makeObservable, observable } from 'mobx';
import { ISelectedItemData } from '@music163/tango-helpers'; import { ISelectedItemData } from '@music163/tango-helpers';
import { IWorkspace } from './interfaces'; import { AbstractWorkspace } from './abstract-workspace';
export enum DropMethod { export enum DropMethod {
ReplaceNode = 'replaceNode', // 替换节点 ReplaceNode = 'replaceNode', // 替换节点
@@ -23,7 +23,7 @@ export class DropTarget {
*/ */
data: ISelectedItemData; data: ISelectedItemData;
private readonly workspace: IWorkspace; private readonly workspace: AbstractWorkspace;
get node() { get node() {
return this.workspace.getNode(this.data.id, this.data.filename); return this.workspace.getNode(this.data.id, this.data.filename);
@@ -48,7 +48,7 @@ export class DropTarget {
return this.data?.display; return this.data?.display;
} }
constructor(workspace: IWorkspace) { constructor(workspace: AbstractWorkspace) {
this.workspace = workspace; this.workspace = workspace;
this.method = DropMethod.InsertAfter; this.method = DropMethod.InsertAfter;
this.data = null; this.data = null;
+2 -2
View File
@@ -1,5 +1,5 @@
import { AbstractWorkspace } from './abstract-workspace';
import { Designer } from './designer'; import { Designer } from './designer';
import { IWorkspace } from './interfaces';
/** /**
* 设计器引擎 * 设计器引擎
@@ -8,7 +8,7 @@ export class Engine {
/** /**
* 工作区状态 * 工作区状态
*/ */
workspace: IWorkspace; workspace: AbstractWorkspace;
/** /**
* 设计器状态 * 设计器状态
*/ */
+16 -183
View File
@@ -1,55 +1,22 @@
import { action, computed, makeObservable, observable, toJS } from 'mobx'; import { action, computed, makeObservable, observable } from 'mobx';
import { getValue, isNil, logger, setValue } from '@music163/tango-helpers'; import { isNil } from '@music163/tango-helpers';
import type { FileType, IFileConfig } from '../types'; import type { IFileConfig } from '../types';
import { IWorkspace } from './interfaces'; import { AbstractWorkspace } from './abstract-workspace';
import { formatCode } from '../helpers'; import { AbstractFile } from './abstract-file';
/** export class TangoFile extends AbstractFile {
* 普通文件,不进行 AST 解析 constructor(workspace: AbstractWorkspace, props: IFileConfig) {
*/ super(workspace, props, false);
export class TangoFile { this.update(props.code);
readonly workspace: IWorkspace; makeObservable(this, {
/** _code: observable,
* 文件名 _cleanCode: observable,
*/ code: computed,
readonly filename: string; cleanCode: computed,
update: action,
/** });
* 文件类型
*/
readonly type: FileType;
/**
* 最近修改的时间戳
*/
lastModified: number;
_code: string;
_cleanCode: string;
get code() {
return this._code;
} }
get cleanCode() {
return this._cleanCode;
}
constructor(workspace: IWorkspace, props: IFileConfig, isSyncCode = true) {
this.workspace = workspace;
this.filename = props.filename;
this.type = props.type;
this.lastModified = Date.now();
// 这里主要是为了解决 umi ts 编译错误的问题,@see https://github.com/umijs/umi/issues/7594
if (isSyncCode) {
this.update(props.code);
}
}
/**
* 更新文件内容
*/
update(code?: string) { update(code?: string) {
if (!isNil(code)) { if (!isNil(code)) {
this.lastModified = Date.now(); this.lastModified = Date.now();
@@ -59,137 +26,3 @@ export class TangoFile {
this.workspace.onFilesChange([this.filename]); this.workspace.onFilesChange([this.filename]);
} }
} }
export class TangoLessFile extends TangoFile {
constructor(workspace: IWorkspace, props: IFileConfig) {
super(workspace, props, false);
this.update(props.code);
makeObservable(this, {
_code: observable,
_cleanCode: observable,
code: computed,
cleanCode: computed,
update: action,
});
}
}
export class TangoJsonFile extends TangoFile {
_object = {};
/**
* @deprecated 使用 file.json 代替
*/
get object() {
return toJS(this._object);
}
get json() {
return toJS(this._object);
}
constructor(workspace: IWorkspace, props: IFileConfig) {
super(workspace, props, false);
this.update(props.code);
makeObservable(this, {
_code: observable,
_cleanCode: observable,
_object: observable,
code: computed,
cleanCode: computed,
object: computed,
json: computed,
update: action,
setValue: action,
});
}
update(code?: string) {
this.lastModified = Date.now();
if (isNil(code)) {
// 基于最新的 json 同步代码
let newCode = JSON.stringify(this._object);
try {
newCode = formatCode(newCode, 'json');
} catch (err) {
logger.error(err);
return;
}
this._code = newCode;
this._cleanCode = newCode;
} else {
try {
// 基于传入的代码,同步 json 对象
code = formatCode(code, 'json');
} catch (err) {
logger.error(err);
return;
}
this._code = code;
this._cleanCode = code;
try {
const json = JSON.parse(code);
this._object = json;
} catch (err) {
logger.error(err);
}
}
this.workspace.onFilesChange([this.filename]);
}
/**
* 根据路径取值
* @param valuePath
* @returns
*/
getValue(valuePath: string) {
return getValue(this.json, valuePath);
}
/**
* 根据路径设置值
* @param valuePath
* @param visitor
*/
setValue(valuePath: string, visitor: (targetValue: any) => any) {
const target = this.getValue(valuePath);
let next: unknown;
if (typeof visitor === 'function') {
next = visitor?.(target);
} else {
next = visitor;
}
if (next !== undefined) {
setValue(this._object, valuePath, next);
}
return this;
}
/**
* 根据路径删除值
* @param valuePath
* @param visitor
*/
deleteValue(valuePath: string) {
const pathList = valuePath.split('.');
const lastPath = pathList.pop();
const parentPath = pathList.join('.');
let target;
if (parentPath) {
target = this.getValue(parentPath);
} else {
target = this.json;
}
if (!target) {
return this;
}
delete target[lastPath];
if (parentPath) {
this.setValue(parentPath, target);
} else {
this._object = target;
}
return this;
}
}
+3 -3
View File
@@ -1,5 +1,5 @@
import { action, computed, makeObservable, observable, toJS } from 'mobx'; import { action, computed, makeObservable, observable, toJS } from 'mobx';
import { IWorkspace } from './interfaces'; import { AbstractWorkspace } from './abstract-workspace';
export enum HistoryMessage { export enum HistoryMessage {
InitView = 'initView', InitView = 'initView',
@@ -43,7 +43,7 @@ export class TangoHistory {
// 最多记录数 // 最多记录数
_maxSize = 100; _maxSize = 100;
private readonly workspace: IWorkspace; private readonly workspace: AbstractWorkspace;
get index() { get index() {
return this._index; return this._index;
@@ -65,7 +65,7 @@ export class TangoHistory {
return this._records.length > this._index + 1; return this._records.length > this._index + 1;
} }
constructor(workspace: IWorkspace) { constructor(workspace: AbstractWorkspace) {
this.workspace = workspace; this.workspace = workspace;
makeObservable(this, { makeObservable(this, {
+21 -13
View File
@@ -1,17 +1,25 @@
export * from './engine'; export * from './abstract-workspace';
export * from './workspace'; export * from './abstract-code-workspace';
export * from './abstract-file';
export * from './abstract-js-file';
export * from './abstract-json-file';
export * from './abstract-view-node';
export * from './designer'; export * from './designer';
export * from './drop-target';
export * from './select-source';
export * from './drag-source'; export * from './drag-source';
export * from './history'; export * from './drop-target';
export * from './engine';
export * from './file'; export * from './file';
export * from './module'; export * from './history';
export * from './entry-module';
export * from './route-module';
export * from './service-module';
export * from './store-module';
export * from './view-module';
export * from './component-module';
export * from './node';
export * from './interfaces'; export * from './interfaces';
export * from './js-app-entry-file';
export * from './js-file';
export * from './js-local-components-entry-file';
export * from './js-route-config-file';
export * from './js-service-file';
export * from './js-store-entry-file';
export * from './js-store-file';
export * from './js-view-file';
export * from './json-file';
export * from './select-source';
export * from './view-node';
export * from './workspace';
+20 -244
View File
@@ -1,27 +1,17 @@
import { IComponentPrototype, Dict, ITangoConfigJson } from '@music163/tango-helpers'; import { Dict } from '@music163/tango-helpers';
import { TangoHistory } from './history';
import { SelectSource } from './select-source';
import { DragSource } from './drag-source';
import { import {
IFileConfig,
FileType,
InsertChildPositionType, InsertChildPositionType,
ITangoConfigPackages,
IPageConfigData,
IImportSpecifierSourceData, IImportSpecifierSourceData,
IImportSpecifierData, IImportSpecifierData,
} from '../types'; } 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 { IdGenerator } from '../helpers';
import { AppEntryModule } from './entry-module'; import { AbstractViewNode } from './abstract-view-node';
export interface IViewFile { export interface IViewFile {
readonly workspace: IWorkspace; /**
readonly filename: string; * 文件名
readonly type: FileType; */
filename: string;
/** /**
* ID 生成器 * ID 生成器
@@ -54,252 +44,38 @@ export interface IViewFile {
*/ */
addImportSpecifiers: (source: string, newSpecifiers: IImportSpecifierData[]) => IViewFile; addImportSpecifiers: (source: string, newSpecifiers: IImportSpecifierData[]) => IViewFile;
getNode: (targetNodeId: string) => IViewNode; getNode: (targetNodeId: string) => AbstractViewNode;
removeNode: (targetNodeId: string) => IViewFile; removeNode: (targetNodeId: string) => this;
insertChild: ( insertChild: (targetNodeId: string, newNode: any, position?: InsertChildPositionType) => this;
targetNodeId: string,
newNode: any,
position?: InsertChildPositionType,
) => IViewFile;
insertAfter: (targetNodeId: string, newNode: any) => IViewFile; insertAfter: (targetNodeId: string, newNode: any) => this;
insertBefore: (targetNodeId: string, newNode: any) => IViewFile; insertBefore: (targetNodeId: string, newNode: any) => this;
replaceNode: (targetNodeId: string, newNode: any) => IViewFile; replaceNode: (targetNodeId: string, newNode: any) => this;
replaceViewChildren: (rawNodes: any[]) => IViewFile; replaceViewChildren: (rawNodes: any[]) => this;
updateNodeAttribute: ( updateNodeAttribute: (
nodeId: string, nodeId: string,
attrName: string, attrName: string,
attrValue?: any, attrValue?: any,
relatedImports?: string[], relatedImports?: string[],
) => IViewFile; ) => this;
updateNodeAttributes: ( updateNodeAttributes: (
nodeId: string, nodeId: string,
config: Record<string, any>, config: Record<string, any>,
relatedImports?: string[], relatedImports?: string[],
) => IViewFile; ) => this;
get code(): string; get nodes(): Map<string, AbstractViewNode>;
get nodes(): Map<string, IViewNode>;
get nodesTree(): object[]; get nodesTree(): object[];
get tree(): any; get tree(): any;
} /**
* 文件中的代码
export interface IViewNode { */
/** get code(): string;
* 所属的文件
*/
file: IViewFile;
/**
* 节点 ID
*/
readonly id: string;
/**
* 对应的组件
*/
readonly component: string;
/**
* 原始节点对象
*/
readonly rawNode: unknown;
/**
* 属性集合
*/
readonly props: Record<string, any>;
/**
* 克隆原始节点
* @param overrideProps 额外设置给克隆节点的属性
* @returns
*/
cloneRawNode: (overrideProps?: Dict) => unknown;
/**
* 销毁节点
* @returns
*/
destroy: () => void;
/**
* 原始节点的位置信息
*/
get loc(): unknown;
}
export interface IWorkspace {
history: TangoHistory;
selectSource: SelectSource;
dragSource: DragSource;
files: Map<string, TangoFile>;
componentPrototypes: Map<string, IComponentPrototype>;
entry: string;
activeFile: string;
activeViewFile: string;
activeRoute: string;
/**
* 解析后的 tango.config.json 文件,如果要获取项目配置,推荐使用 projectConfig 获取
*/
tangoConfigJson: TangoJsonFile;
/**
* app.js 入口文件解析后的模块
*/
appEntryModule: AppEntryModule;
/**
* 解析后的路由模块
*/
routeModule?: TangoRouteModule;
/**
* 解析后的状态管理模块 Map
*/
storeModules?: Record<string, TangoStoreModule>;
/**
* 解析后的服务模块 Map
*/
serviceModules?: Record<string, TangoServiceModule>;
ready: () => void;
refresh: (names: string[]) => void;
setActiveRoute: (path: string) => void;
setActiveFile: (filename: string) => void;
setComponentPrototypes: (prototypes: Record<string, IComponentPrototype>) => void;
getPrototype: (name: string | IComponentPrototype) => IComponentPrototype;
// ----------------- 文件操作 -----------------
addFiles: (files: IFileConfig[]) => void;
addFile: (filename: string, code: string, fileType?: FileType) => void;
addServiceFile: (serviceName: string, code: string) => void;
addStoreFile: (storeName: string, code: string) => void;
addViewFile: (viewName: string, code: string) => void;
removeFile: (filename: string) => void;
renameFile: (oldFilename: string, newFilename: string) => void;
renameFolder: (oldFoldername: string, newFoldername: string) => void;
updateFile: (filename: string, code: string, shouldFormatCode?: boolean) => void;
listFiles: () => Record<string, string>;
getFile: (filename: string) => TangoFile;
/**
* 文件变化回调
* @param filenames 文件名列表
*/
onFilesChange: (filenames: string[]) => void;
// ----------------- 节点操作 -----------------
removeSelectedNode: () => void;
cloneSelectedNode: () => void;
copySelectedNode: () => void;
pasteSelectedNode: () => void;
insertToSelectedNode: (childNameOrPrototype: string | IComponentPrototype) => void;
insertBeforeSelectedNode: (sourceNameOrPrototype: string | IComponentPrototype) => void;
insertAfterSelectedNode: (sourceNameOrPrototype: string | IComponentPrototype) => void;
dropNode: () => void;
insertToNode: (targetNodeId: string, sourceNameOrPrototype: string | IComponentPrototype) => void;
replaceNode: (targetNodeId: string, sourceNameOrPrototype: string | IComponentPrototype) => void;
updateSelectedNodeAttributes: (
attributes: Record<string, any>,
relatedImports?: string[],
) => void;
/**
* 查询节点
* @param id 节点 ID
* @param module 节点所在的模块名
* @returns 返回节点对象
*/
getNode: (id: string, module?: string) => IViewNode;
// ----------------- 服务函数文件操作 -----------------
getServiceFunction?: (serviceKey: string) => {
name: string;
moduleName: string;
config: Dict<object>;
};
listServiceFunctions?: () => Dict<object>;
removeServiceFunction?: (serviceKey: string) => void;
addServiceFunction?: (serviceName: string, config: Dict, modName?: string) => void;
addServiceFunctions?: (configs: Dict<object>, modName?: string) => void;
updateServiceFunction?: (serviceName: string, payload: Dict, modName?: string) => void;
updateServiceBaseConfig?: (config: Dict, modName?: string) => void;
// ----------------- 状态管理文件操作 -----------------
addStoreState?: (storeName: string, stateName: string, initValue: string) => void;
removeStoreModule?: (storeName: string) => void;
removeStoreVariable?: (variablePath: string) => void;
updateStoreVariable?: (variablePath: string, code: string) => void;
// ----------------- 视图文件操作 -----------------
removeViewModule: (routePath: string) => void;
copyViewPage: (sourceRoutePath: string, targetPageData: IPageConfigData) => void;
// ----------------- 路由文件操作 -----------------
updateRoute: (sourceRoutePath: string, targetPageData: IPageConfigData) => void;
// ----------------- 依赖包操作 -----------------
addDependency?: (data: any) => void;
listDependencies?: () => any;
getDependency?: (pkgName: string) => object;
updateDependency?: (
name: string,
version: string,
options?: {
package?: ITangoConfigPackages;
[x: string]: any;
},
) => void;
removeDependency?: (name: string) => void;
addBizComp?: (
name: string,
version: string,
options?: {
package?: ITangoConfigPackages;
[x: string]: any;
},
) => void;
removeBizComp?: (name: string) => void;
// ----------------- getter -----------------
/**
* 解析后的项目配置信息
*/
get projectConfig(): ITangoConfigJson;
/**
* 当前活动的视图文件
*/
get activeViewModule(): IViewFile;
get pages(): any[];
get bizComps(): string[];
get baseComps(): string[];
get localComps(): string[];
/**
* 是否是合法的项目
*/
get isValid(): boolean;
} }
@@ -1,12 +1,12 @@
import { traverseEntryFile } from '../helpers'; import { traverseEntryFile } from '../helpers';
import { IFileConfig } from '../types'; import { IFileConfig } from '../types';
import { IWorkspace } from './interfaces'; import { AbstractJsFile } from './abstract-js-file';
import { TangoModule } from './module'; import { AbstractWorkspace } from './abstract-workspace';
export class AppEntryModule extends TangoModule { export class JsAppEntryFile extends AbstractJsFile {
routerType: string; routerType: string;
constructor(workspace: IWorkspace, props: IFileConfig) { constructor(workspace: AbstractWorkspace, props: IFileConfig) {
super(workspace, props, false); super(workspace, props, false);
this.update(props.code, true, false); this.update(props.code, true, false);
} }
+25
View File
@@ -0,0 +1,25 @@
import { action, computed, makeObservable, observable } from 'mobx';
import { IFileConfig } from '../types';
import { AbstractWorkspace } from './abstract-workspace';
import { AbstractJsFile } from './abstract-js-file';
/**
* 普通 JS 文件
*/
export class JsFile extends AbstractJsFile {
constructor(workspace: AbstractWorkspace, props: IFileConfig) {
super(workspace, props, 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,19 +1,19 @@
import path from 'path'; import path from 'path';
import { action, computed, makeObservable, observable } from 'mobx'; import { action, computed, makeObservable, observable } from 'mobx';
import { TangoModule } from './module';
import { IWorkspace } from './interfaces';
import { IExportSpecifierData, IFileConfig } from '../types'; import { IExportSpecifierData, IFileConfig } from '../types';
import { traverseComponentsEntryFile } from '../helpers'; import { traverseComponentsEntryFile } from '../helpers';
import { AbstractWorkspace } from './abstract-workspace';
import { AbstractJsFile } from './abstract-js-file';
/** /**
* '/components/index.js' `/blocks/index.js` * '/components/index.js' `/blocks/index.js`
*/ */
export class TangoComponentsEntryModule extends TangoModule { export class JsLocalComponentsEntryFile extends AbstractJsFile {
exportList: Record<string, IExportSpecifierData>; exportList: Record<string, IExportSpecifierData>;
constructor(workspace: IWorkspace, props: IFileConfig) { constructor(workspace: AbstractWorkspace, props: IFileConfig) {
super(workspace, props, false); super(workspace, props, false);
this.update(props.code, false, false); this.update(props.code, true, false);
makeObservable(this, { makeObservable(this, {
_code: observable, _code: observable,
_cleanCode: observable, _cleanCode: observable,
@@ -1,4 +1,5 @@
import { action, computed, makeObservable, observable, toJS } from 'mobx'; import { action, computed, makeObservable, observable, toJS } from 'mobx';
import { logger } from '@music163/tango-helpers';
import { import {
traverseRouteFile, traverseRouteFile,
addRouteToRouteFile, addRouteToRouteFile,
@@ -6,20 +7,20 @@ import {
updateRouteToRouteFile, updateRouteToRouteFile,
} from '../helpers'; } from '../helpers';
import { IRouteData, IFileConfig } from '../types'; import { IRouteData, IFileConfig } from '../types';
import { IWorkspace } from './interfaces'; import { AbstractJsFile } from './abstract-js-file';
import { TangoModule } from './module'; import { AbstractCodeWorkspace } from './abstract-code-workspace';
/** /**
* *
*/ */
export class TangoRouteModule extends TangoModule { export class JsRouteConfigFile extends AbstractJsFile {
_routes: IRouteData[]; _routes: IRouteData[];
get routes() { get routes() {
return toJS(this._routes); return toJS(this._routes);
} }
constructor(workspace: IWorkspace, props: IFileConfig) { constructor(workspace: AbstractCodeWorkspace, props: IFileConfig) {
super(workspace, props, false); super(workspace, props, false);
this.update(props.code, true, false); this.update(props.code, true, false);
@@ -27,10 +28,13 @@ export class TangoRouteModule extends TangoModule {
_routes: observable, _routes: observable,
_code: observable, _code: observable,
_cleanCode: observable, _cleanCode: observable,
isError: observable,
errorMessage: observable,
routes: computed, routes: computed,
code: computed, code: computed,
cleanCode: computed, cleanCode: computed,
update: action, update: action,
updateAst: action,
}); });
} }
@@ -74,7 +78,7 @@ export class TangoRouteModule extends TangoModule {
*/ */
removeRoute(route: string) { removeRoute(route: string) {
if (route === '/') { if (route === '/') {
console.warn('index route should not be removed!'); logger.warn('index route should not be removed!');
return; return;
} }
const record = this.getRouteByRoutePath(route); const record = this.getRouteByRoutePath(route);
@@ -8,13 +8,13 @@ import {
updateBaseConfigToServiceFile, updateBaseConfigToServiceFile,
} from '../helpers'; } from '../helpers';
import { IFileConfig } from '../types'; import { IFileConfig } from '../types';
import { IWorkspace } from './interfaces'; import { AbstractWorkspace } from './abstract-workspace';
import { TangoModule } from './module'; import { AbstractJsFile } from './abstract-js-file';
/** /**
* *
*/ */
export class TangoServiceModule extends TangoModule { export class JsServiceFile extends AbstractJsFile {
/** /**
* index * index
*/ */
@@ -39,7 +39,7 @@ export class TangoServiceModule extends TangoModule {
return toJS(this._baseConfig); return toJS(this._baseConfig);
} }
constructor(workspace: IWorkspace, props: IFileConfig) { constructor(workspace: AbstractWorkspace, props: IFileConfig) {
super(workspace, props, false); super(workspace, props, false);
this.name = getModuleNameByFilename(props.filename); this.name = getModuleNameByFilename(props.filename);
this.update(props.code, true, false); this.update(props.code, true, false);
@@ -49,11 +49,14 @@ export class TangoServiceModule extends TangoModule {
_baseConfig: observable, _baseConfig: observable,
_code: observable, _code: observable,
_cleanCode: observable, _cleanCode: observable,
isError: observable,
errorMessage: observable,
serviceFunctions: computed, serviceFunctions: computed,
baseConfig: computed, baseConfig: computed,
cleanCode: computed, cleanCode: computed,
code: computed, code: computed,
update: action, update: action,
updateAst: action,
}); });
} }
@@ -0,0 +1,56 @@
import { action, computed, makeObservable, observable, toJS } from 'mobx';
import { traverseStoreEntryFile, addStoreToEntryFile, removeStoreToEntryFile } from '../helpers';
import { IFileConfig } from '../types';
import { AbstractWorkspace } from './abstract-workspace';
import { AbstractJsFile } from './abstract-js-file';
/**
* stores 入口文件
*/
export class JsStoreEntryFile extends AbstractJsFile {
_stores: string[] = [];
get stores() {
return toJS(this._stores);
}
constructor(workspace: AbstractWorkspace, props: IFileConfig) {
super(workspace, props, false);
this.update(props.code, true, false);
makeObservable(this, {
_stores: observable,
_code: observable,
_cleanCode: observable,
isError: observable,
errorMessage: observable,
stores: computed,
code: computed,
cleanCode: computed,
update: action,
updateAst: action,
});
}
_analysisAst() {
this._stores = traverseStoreEntryFile(this.ast);
}
/**
* 新建模型
* @param name
*/
addStore(name: string) {
this.ast = addStoreToEntryFile(this.ast, name);
return this;
}
/**
* 删除模型
* @param name
*/
removeStore(name: string) {
this.ast = removeStoreToEntryFile(this.ast, name);
return this;
}
}
@@ -1,70 +1,19 @@
import { action, computed, makeObservable, observable, toJS } from 'mobx'; import { action, computed, makeObservable, observable } from 'mobx';
import { import {
traverseStoreFile, traverseStoreFile,
traverseStoreEntryFile,
addStoreToEntryFile,
getModuleNameByFilename, getModuleNameByFilename,
addStoreState, addStoreState,
updateStoreState, updateStoreState,
removeStoreState, removeStoreState,
removeStoreToEntryFile,
} from '../helpers'; } from '../helpers';
import { IFileConfig, IStorePropertyData } from '../types'; import { IFileConfig, IStorePropertyData } from '../types';
import { IWorkspace } from './interfaces'; import { AbstractWorkspace } from './abstract-workspace';
import { TangoModule } from './module'; import { AbstractJsFile } from './abstract-js-file';
/**
*
*/
export class TangoStoreEntryModule extends TangoModule {
_stores: string[] = [];
get stores() {
return toJS(this._stores);
}
constructor(workspace: IWorkspace, props: IFileConfig) {
super(workspace, props, false);
this.update(props.code, true, false);
makeObservable(this, {
_stores: observable,
_code: observable,
_cleanCode: observable,
stores: computed,
code: computed,
cleanCode: computed,
update: action,
});
}
_analysisAst() {
this._stores = traverseStoreEntryFile(this.ast);
}
/**
*
* @param name
*/
addStore(name: string) {
this.ast = addStoreToEntryFile(this.ast, name);
return this;
}
/**
*
* @param name
*/
removeStore(name: string) {
this.ast = removeStoreToEntryFile(this.ast, name);
return this;
}
}
/** /**
* *
*/ */
export class TangoStoreModule extends TangoModule { export class JsStoreFile extends AbstractJsFile {
/** /**
* *
*/ */
@@ -76,7 +25,7 @@ export class TangoStoreModule extends TangoModule {
actions: IStorePropertyData[]; actions: IStorePropertyData[];
constructor(workspace: IWorkspace, props: IFileConfig) { constructor(workspace: AbstractWorkspace, props: IFileConfig) {
super(workspace, props, false); super(workspace, props, false);
this.name = getModuleNameByFilename(props.filename); this.name = getModuleNameByFilename(props.filename);
this.update(props.code, true, false); this.update(props.code, true, false);
@@ -20,18 +20,19 @@ import {
addImportDeclarationLegacy, addImportDeclarationLegacy,
updateImportDeclarationLegacy, updateImportDeclarationLegacy,
} from '../helpers'; } from '../helpers';
import { TangoNode } from './node'; import { JsxViewNode } from './view-node';
import { import {
IFileConfig, IFileConfig,
ITangoViewNodeData, IViewNodeData,
IImportDeclarationPayload, IImportDeclarationPayload,
InsertChildPositionType, InsertChildPositionType,
IImportSpecifierSourceData, IImportSpecifierSourceData,
ImportDeclarationDataType, ImportDeclarationDataType,
IImportSpecifierData, IImportSpecifierData,
} from '../types'; } from '../types';
import { IViewFile, IWorkspace } from './interfaces'; import { AbstractWorkspace } from './abstract-workspace';
import { TangoModule } from './module'; import { IViewFile } from './interfaces';
import { AbstractJsFile } from './abstract-js-file';
/** /**
* -> map * -> map
@@ -56,8 +57,8 @@ function buildImportMap(importedModules: ImportDeclarationDataType) {
* tree data * tree data
* @param list * @param list
*/ */
function nodeListToTreeData(list: ITangoViewNodeData[]) { function nodeListToTreeData(list: IViewNodeData[]) {
const map: Record<string, ITangoViewNodeData> = {}; const map: Record<string, IViewNodeData> = {};
list.forEach((item) => { list.forEach((item) => {
// 如果不存在,则初始化 // 如果不存在,则初始化
@@ -82,9 +83,9 @@ function nodeListToTreeData(list: ITangoViewNodeData[]) {
/** /**
* *
*/ */
export class TangoViewModule extends TangoModule implements IViewFile { export class JsViewFile extends AbstractJsFile implements IViewFile {
// 解析为树结构的 jsxNodes 数组 // 解析为树结构的 jsxNodes 数组
_nodesTree: ITangoViewNodeData[]; _nodesTree: IViewNodeData[] = [];
/** /**
* *
*/ */
@@ -108,7 +109,7 @@ export class TangoViewModule extends TangoModule implements IViewFile {
/** /**
* <id, Node> * <id, Node>
*/ */
private _nodes: Map<string, TangoNode>; private _nodes: Map<string, JsxViewNode>;
/** /**
* *
* @deprecated * @deprecated
@@ -127,7 +128,7 @@ export class TangoViewModule extends TangoModule implements IViewFile {
return this.ast; return this.ast;
} }
constructor(workspace: IWorkspace, props: IFileConfig) { constructor(workspace: AbstractWorkspace, props: IFileConfig) {
super(workspace, props, false); super(workspace, props, false);
this._nodes = new Map(); this._nodes = new Map();
this.idGenerator = new IdGenerator({ prefix: props.filename }); this.idGenerator = new IdGenerator({ prefix: props.filename });
@@ -138,10 +139,14 @@ export class TangoViewModule extends TangoModule implements IViewFile {
_code: observable, _code: observable,
_cleanCode: observable, _cleanCode: observable,
isError: observable,
errorMessage: observable,
code: computed, code: computed,
cleanCode: computed, cleanCode: computed,
update: action, update: action,
updateAst: action,
}); });
} }
@@ -172,8 +177,10 @@ export class TangoViewModule extends TangoModule implements IViewFile {
this._codeIdList = []; this._codeIdList = [];
nodes.forEach((cur) => { nodes.forEach((cur) => {
const node = new TangoNode({ const node = new JsxViewNode({
...cur, id: cur.id,
component: cur.component,
rawNode: cur.rawNode,
file: this, file: this,
}); });
this._nodes.set(cur.id, node); this._nodes.set(cur.id, node);
@@ -320,7 +327,7 @@ export class TangoViewModule extends TangoModule implements IViewFile {
updateNodeAttributes(nodeId: string, config: Record<string, any>, relatedImports?: string[]) { updateNodeAttributes(nodeId: string, config: Record<string, any>, relatedImports?: string[]) {
if (relatedImports && relatedImports.length) { 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 proto = this.workspace.getPrototype(name);
const { source, specifiers } = prototype2importDeclarationData(proto, this.filename); const { source, specifiers } = prototype2importDeclarationData(proto, this.filename);
const existSpecifiers: IImportSpecifierData[] = prev[source]; const existSpecifiers: IImportSpecifierData[] = prev[source];
+24
View File
@@ -0,0 +1,24 @@
import { action, computed, makeObservable, observable, toJS } from 'mobx';
import type { IFileConfig } from '../types';
import { AbstractWorkspace } from './abstract-workspace';
import { AbstractJsonFile } from './abstract-json-file';
export class JsonFile extends AbstractJsonFile {
get json(): object {
return toJS(this._object);
}
constructor(workspace: AbstractWorkspace, props: IFileConfig) {
super(workspace, props);
makeObservable(this, {
_code: observable,
_cleanCode: observable,
_object: observable,
code: computed,
cleanCode: computed,
json: computed,
update: action,
setValue: action,
});
}
}
-118
View File
@@ -1,118 +0,0 @@
import * as t from '@babel/types';
import { action, computed, makeObservable, observable } from 'mobx';
import { isNil } from '@music163/tango-helpers';
import {
code2ast,
ast2code,
formatCode,
traverseFile,
addImportDeclaration,
updateImportDeclaration,
} from '../helpers';
import { TangoFile } from './file';
import { IFileConfig, IImportSpecifierData, ImportDeclarationDataType } from '../types';
import { IWorkspace } from './interfaces';
/**
* JS 模块实现规范
* - ast 操纵类方法,统一返回 this,支持外层链式调用
* - observable state 统一用 _foo 格式,并提供 getter 方法
*/
export class TangoModule extends TangoFile {
ast: t.File;
/**
* 导入的依赖列表
*/
importList: ImportDeclarationDataType;
constructor(workspace: IWorkspace, props: IFileConfig, isSyncCode = true) {
super(workspace, props, isSyncCode);
}
/**
* 基于最新的 ast 进行同步
* @param code 如果传入 code,则基于 code 进行同步
* @param isFormatCode 是否格式化代码
* @param refreshWorkspace 是否刷新 workspace
*/
update(code?: string, isFormatCode = true, refreshWorkspace = true) {
this.lastModified = Date.now();
if (isNil(code)) {
this._syncByAst();
} else {
this._syncByCode(code, isFormatCode);
}
this._analysisAst();
this.workspace.onFilesChange([this.filename]);
if (refreshWorkspace) {
this.workspace.refresh([this.filename]);
}
}
addImportDeclaration(source: string, specifiers: IImportSpecifierData[]) {
this.ast = addImportDeclaration(this.ast, source, specifiers);
return this;
}
updateImportDeclaration(source: string, specifiers: IImportSpecifierData[]) {
this.ast = updateImportDeclaration(this.ast, source, specifiers);
return this;
}
/**
* 基于最新的 ast 进行源码同步
*/
_syncByAst() {
const code = ast2code(this.ast);
this._code = code;
this._cleanCode = code;
}
/**
* 基于输入的源码进行同步
* @param code 源码
* @param isFormatCode 是否格式化代码
* @returns
*/
_syncByCode(code: string, isFormatCode = true) {
if (code === this._code) {
return;
}
// 提前格式化代码
if (isFormatCode) {
code = formatCode(code);
}
this._code = code;
this._cleanCode = code;
this.ast = code2ast(code);
}
_analysisAst() {
const { imports } = traverseFile(this.ast);
this.importList = imports;
}
}
/**
* 普通 JS 文件
*/
export class TangoJsModule extends TangoModule {
constructor(workspace: IWorkspace, props: IFileConfig) {
super(workspace, props, false);
this.update(props.code, false, false);
makeObservable(this, {
_code: observable,
_cleanCode: observable,
code: computed,
cleanCode: computed,
update: action,
});
}
}
-62
View File
@@ -1,62 +0,0 @@
import { JSXElement, SourceLocation } from '@babel/types';
import { Dict } from '@music163/tango-helpers';
import { cloneJSXElement, getJSXElementAttributes } from '../helpers';
import { ITangoViewNodeData } from '../types';
import { TangoViewModule } from './view-module';
import { IViewNode } from './interfaces';
type TangoNodeConstructorPropsType = ITangoViewNodeData & {
file: TangoViewModule;
};
/**
* 视图节点类
*/
export class TangoNode implements IViewNode {
/**
* 节点 ID
*/
readonly id: string;
/**
* 节点对应的组件名
*/
readonly component: string;
readonly rawNode: JSXElement;
/**
* 节点所属的文件对象
*/
file: TangoViewModule;
props: Record<string, any>;
get loc(): SourceLocation {
return this.rawNode?.loc;
}
constructor(props: TangoNodeConstructorPropsType) {
this.file = props.file;
this.id = props.id;
this.component = props.component;
this.rawNode = props.rawNode;
this.props = getJSXElementAttributes(cloneJSXElement(props.rawNode));
}
/**
* 返回克隆后的 ast 节点
* @param overrideProps 额外设置给克隆节点的属性
* @returns
*/
cloneRawNode(overrideProps?: Dict) {
return cloneJSXElement(this.rawNode, overrideProps);
}
/**
* 清空节点的指向,交给 GC 去回收
*/
destroy() {
this.file = null;
}
}
+4 -3
View File
@@ -1,6 +1,7 @@
import { ISelectedItemData, MousePoint } from '@music163/tango-helpers'; import { ISelectedItemData, MousePoint } from '@music163/tango-helpers';
import { action, computed, makeObservable, observable, toJS } from 'mobx'; import { action, computed, makeObservable, observable, toJS } from 'mobx';
import { IViewFile, IWorkspace } from './interfaces'; import { AbstractWorkspace } from './abstract-workspace';
import { IViewFile } from './interfaces';
type StartDataType = { type StartDataType = {
point: MousePoint; point: MousePoint;
@@ -24,7 +25,7 @@ export class SelectSource {
element: null, element: null,
}; };
private readonly workspace: IWorkspace; private readonly workspace: AbstractWorkspace;
get start() { get start() {
return toJS(this._start); return toJS(this._start);
@@ -74,7 +75,7 @@ export class SelectSource {
.filter((node) => !!node); .filter((node) => !!node);
} }
constructor(workspace: IWorkspace) { constructor(workspace: AbstractWorkspace) {
this.workspace = workspace; this.workspace = workspace;
makeObservable(this, { makeObservable(this, {
_items: observable, _items: observable,
+28
View File
@@ -0,0 +1,28 @@
import { JSXElement } from '@babel/types';
import { Dict } from '@music163/tango-helpers';
import { cloneJSXElement, getJSXElementAttributes } from '../helpers';
import { JsViewFile } from './js-view-file';
import { AbstractViewNode, IViewNodeInitConfig } from './abstract-view-node';
/**
* 视图节点类
*/
export class JsxViewNode extends AbstractViewNode<JSXElement, JsViewFile> {
get loc() {
return this.rawNode?.loc;
}
constructor(props: IViewNodeInitConfig<JSXElement, JsViewFile>) {
super(props);
this.props = getJSXElementAttributes(cloneJSXElement(props.rawNode));
}
/**
* 返回克隆后的 ast 节点
* @param overrideProps 额外设置给克隆节点的属性
* @returns
*/
cloneRawNode(overrideProps?: Dict) {
return cloneJSXElement(this.rawNode, overrideProps);
}
}
File diff suppressed because it is too large Load Diff
+24 -31
View File
@@ -6,40 +6,28 @@ export type SimulatorMode = 'desktop' | 'tablet' | 'phone';
* 文件类型枚举 * 文件类型枚举
*/ */
export enum FileType { export enum FileType {
// js 文件 File = 'file',
Module = 'module',
AppEntryModule = 'appEntryModule', JsFile = 'jsFile',
StoreEntryModule = 'storeEntryModule', JsAppEntryFile = 'jsAppEntryFile',
RouteModule = 'routeModule', JsRouteConfigFile = 'jsRouteConfigFile',
ServiceModule = 'serviceModule', JsStoreEntryFile = 'jsStoreEntryFile',
StoreModule = 'storeModule', JsStoreFile = 'jsStoreFile',
JsServiceFile = 'jsServiceFile',
JsLocalComponentsEntryFile = 'jsLocalComponentsEntryFile',
JsViewFile = 'jsViewFile',
JsonViewFile = 'jsonViewFile',
JsonFile = 'jsonFile',
PackageJsonFile = 'packageJsonFile',
TangoConfigJsonFile = 'tangoConfigJsonFile',
AppJsonFile = 'appJsonFile',
// 组件配置文件 // 组件配置文件
ComponentPrototypeModule = 'componentPrototypeModule', ComponentPrototypeModule = 'componentPrototypeModule',
// 组件运行调试入口文件,一般为 `/app.js` // 组件运行调试入口文件,一般为 `/app.js`
ComponentDemoEntryModule = 'componentDemoEntryModule', ComponentDemoEntryModule = 'componentDemoEntryModule',
/**
* 本地组件目录的入口文件
*/
ComponentsEntryModule = 'componentsEntryModule',
/**
* @deprecated 已废弃
*/
BlockEntryModule = 'blockEntryModule',
// jsx 类型视图文件
JsxViewModule = 'jsxViewModule',
// json 类型视图文件
JsonViewModule = 'jsonViewModule',
// 非 js 文件
PackageJson = 'packageJson',
TangoConfigJson = 'tangoConfigJson',
AppJson = 'appJson',
File = 'file',
Json = 'json',
Less = 'less',
Scss = 'scss',
} }
export interface IFileConfig { export interface IFileConfig {
@@ -57,10 +45,15 @@ export interface IFileConfig {
type?: FileType; type?: FileType;
} }
export interface IFileError {
filename: string;
message: string;
}
/** /**
* 视图节点数据类型 * 视图节点数据类型
*/ */
export interface ITangoViewNodeData<T = JSXElement> { export interface IViewNodeData<T = JSXElement> {
/** /**
* 节点 ID * 节点 ID
*/ */
@@ -92,7 +85,7 @@ export interface ITangoViewNodeData<T = JSXElement> {
/** /**
* 子节点列表 * 子节点列表
*/ */
children?: Array<ITangoViewNodeData<T>>; children?: Array<IViewNodeData<T>>;
} }
/** /**
+5 -5
View File
@@ -179,12 +179,12 @@ describe('string helpers', () => {
}); });
it('inferFileType', () => { it('inferFileType', () => {
expect(inferFileType('/src/pages/template.js')).toBe(FileType.JsxViewModule); expect(inferFileType('/src/pages/template.js')).toBe(FileType.JsViewFile);
expect(inferFileType('/src/pages/template.jsx')).toBe(FileType.JsxViewModule); expect(inferFileType('/src/pages/template.jsx')).toBe(FileType.JsViewFile);
expect(inferFileType('/src/pages/template.ejs')).toBe(FileType.File); expect(inferFileType('/src/pages/template.ejs')).toBe(FileType.File);
expect(inferFileType('/src/index.scss')).toBe(FileType.Scss); expect(inferFileType('/src/index.scss')).toBe(FileType.File);
expect(inferFileType('/src/index.less')).toBe(FileType.Less); expect(inferFileType('/src/index.less')).toBe(FileType.File);
expect(inferFileType('/src/index.json')).toBe(FileType.Json); expect(inferFileType('/src/index.json')).toBe(FileType.JsonFile);
}); });
}); });
+56
View File
@@ -3,6 +3,62 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.4.6](https://github.com/netease/tango/compare/@music163/tango-designer@1.4.5...@music163/tango-designer@1.4.6) (2024-09-09)
### Bug Fixes
- add classNameSetter and classNameInput ([#207](https://github.com/netease/tango/issues/207)) ([55602fc](https://github.com/netease/tango/commit/55602fc961b5c20d923891ce13d7a51588bd047f))
- renderSetter with template ([#202](https://github.com/netease/tango/issues/202)) ([ea96872](https://github.com/netease/tango/commit/ea9687226123c839f7a9e001b4c01aa8315a135d))
- service support set description field ([#206](https://github.com/netease/tango/issues/206)) ([79f9a8c](https://github.com/netease/tango/commit/79f9a8c1791424f35f194ab2a9aa7c263ef39f40))
## [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) ## [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 ### Bug Fixes
+7 -7
View File
@@ -1,6 +1,6 @@
{ {
"name": "@music163/tango-designer", "name": "@music163/tango-designer",
"version": "1.3.2", "version": "1.4.6",
"description": "lowcode designer", "description": "lowcode designer",
"keywords": [ "keywords": [
"react" "react"
@@ -33,12 +33,12 @@
"dependencies": { "dependencies": {
"@ant-design/icons": "^4.8.0", "@ant-design/icons": "^4.8.0",
"@music163/request": "^0.2.0", "@music163/request": "^0.2.0",
"@music163/tango-context": "^1.1.4", "@music163/tango-context": "^1.1.10",
"@music163/tango-core": "^1.3.2", "@music163/tango-core": "^1.4.4",
"@music163/tango-helpers": "^1.2.0", "@music163/tango-helpers": "^1.2.4",
"@music163/tango-sandbox": "^1.0.7", "@music163/tango-sandbox": "^1.0.13",
"@music163/tango-setting-form": "^1.2.7", "@music163/tango-setting-form": "^1.2.15",
"@music163/tango-ui": "^1.3.2", "@music163/tango-ui": "^1.4.5",
"antd": "^4.24.2", "antd": "^4.24.2",
"cash-dom": "^8.1.2", "cash-dom": "^8.1.2",
"classnames": "^2.5.1", "classnames": "^2.5.1",
@@ -34,17 +34,17 @@ export const ComponentsPopover = observer(
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
workspace.componentPrototypes.get(selectedNode?.name) ?? ({} as IComponentPrototype); workspace.componentPrototypes.get(selectedNode?.name) ?? ({} as IComponentPrototype);
// 推荐使用的子组件 const recommendedList = useMemo(() => {
const insertedList = useMemo( if (type === 'inner') {
() => return prototype?.childrenName
Array.isArray(prototype?.childrenName) ? Array.isArray(prototype?.childrenName)
? prototype?.childrenName ? prototype.childrenName
: [prototype?.childrenName].filter(Boolean), : [prototype.childrenName]
[prototype?.childrenName], : [];
); }
// 默认推荐使用相同类型的组件作为兄弟节点
// 推荐使用的代码片段 return prototype.siblingNames || [prototype.name];
const siblingList = useMemo(() => prototype?.siblingNames ?? [], [prototype.siblingNames]); }, [prototype.childrenName, prototype.siblingNames, prototype.name, type]);
const tipsTextMap = useMemo( const tipsTextMap = useMemo(
() => ({ () => ({
@@ -82,22 +82,15 @@ export const ComponentsPopover = observer(
const menuList = JSON.parse(JSON.stringify(designer.menuData)); const menuList = JSON.parse(JSON.stringify(designer.menuData));
const commonList = menuList['common'] ?? []; const commonList = menuList['common'] ?? [];
if (commonList?.length && siblingList?.length) { if (commonList?.length && recommendedList.length) {
commonList.unshift({
title: '代码片段',
items: siblingList,
});
}
if (commonList?.length && insertedList?.length) {
commonList.unshift({ commonList.unshift({
title: '推荐使用', title: '推荐使用',
items: insertedList, items: recommendedList,
}); });
} }
return menuList; return menuList;
}, [insertedList, siblingList, designer.menuData]); }, [recommendedList, designer.menuData]);
const innerTypeProps = const innerTypeProps =
// 手动触发 适用于 点击添加组件 // 手动触发 适用于 点击添加组件
@@ -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} filesClick 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>
);
});
@@ -4,3 +4,4 @@ export * from './input-kv';
export * from './variable-tree'; export * from './variable-tree';
export * from './variable-tree-modal'; export * from './variable-tree-modal';
export * from './components-popover'; 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 { InputCode } from '@music163/tango-ui';
import { Button, Dropdown, Form, FormProps, Input, Select, Space } from 'antd'; import { Button, Dropdown, Form, FormProps, Input, Select, Space } from 'antd';
import { Box } from 'coral-system'; import { Box } from 'coral-system';
import React, { useState } from 'react'; import React, { useState } from 'react';
interface AddServiceFormProps extends FormProps { interface AddServiceFormProps extends FormProps {
initialValues?: object; initialValues?: Dict;
onSubmit: (values: object) => void; onSubmit: (values: object) => void;
onCancel: () => void; onCancel: () => void;
serviceModules: object[]; serviceModules: object[];
@@ -88,6 +88,10 @@ export function AddServiceForm({
> >
<Input placeholder="请输入数据服务调用名称" disabled={disabled || isModifyMode} /> <Input placeholder="请输入数据服务调用名称" disabled={disabled || isModifyMode} />
</Form.Item> </Form.Item>
{/* 备注 */}
<Form.Item label="备注" name="description">
<Input placeholder="请输入备注信息" />
</Form.Item>
<Form.Item label="路径" name="url" rules={[{ required: true, type: 'url' }]}> <Form.Item label="路径" name="url" rules={[{ required: true, type: 'url' }]}>
<Input placeholder="请输入数据服务调用名称" disabled={disabled || isModifyMode} /> <Input placeholder="请输入数据服务调用名称" disabled={disabled || isModifyMode} />
</Form.Item> </Form.Item>
+3 -1
View File
@@ -1,8 +1,10 @@
import { Dict } from '@music163/tango-helpers';
/** /**
* 快捷键 * 快捷键
*/ */
export class Hotkey { export class Hotkey {
private readonly hotkeyMap = {}; private readonly hotkeyMap: Dict = {};
constructor(hotkeys: Record<string, Function>) { constructor(hotkeys: Record<string, Function>) {
Object.keys(hotkeys).forEach((hotkey) => { Object.keys(hotkeys).forEach((hotkey) => {
+5 -3
View File
@@ -1,5 +1,5 @@
import React, { useEffect, useMemo } from 'react'; import React, { useEffect, useMemo } from 'react';
import { DropMethod, FileType, Designer, IWorkspace } from '@music163/tango-core'; import { DropMethod, FileType, Designer, AbstractCodeWorkspace } from '@music163/tango-core';
import { ISelectedItemData, events, getHotkey } from '@music163/tango-helpers'; import { ISelectedItemData, events, getHotkey } from '@music163/tango-helpers';
import { import {
setElementStyle, setElementStyle,
@@ -12,7 +12,7 @@ import { Hotkey } from './hotkey';
import { SelectModeType } from '../types'; import { SelectModeType } from '../types';
interface UseDndProps { interface UseDndProps {
workspace: IWorkspace; workspace: AbstractCodeWorkspace;
designer: Designer; designer: Designer;
/** /**
* 沙箱内的 DOM 查询操作 * 沙箱内的 DOM 查询操作
@@ -137,6 +137,8 @@ export function useDnd({
const data = sandboxQuery.getDraggableParentsData(e.target as HTMLElement, true); const data = sandboxQuery.getDraggableParentsData(e.target as HTMLElement, true);
if (data && data.id) { if (data && data.id) {
selectSource.select(data); selectSource.select(data);
// 画布选中节点时,自动唤起 right-panel
designer.toggleRightPanel(true);
} }
}; };
@@ -343,7 +345,7 @@ export function useDnd({
// 区块不能拖拽到区块中 // 区块不能拖拽到区块中
if ( if (
workspace.dragSource.prototype.type === 'block' && workspace.dragSource.prototype.type === 'block' &&
closetDropTargetNode.file.type === FileType.BlockEntryModule closetDropTargetNode.file.type === FileType.JsLocalComponentsEntryFile
) { ) {
return; return;
} }
+80 -109
View File
@@ -2,8 +2,6 @@ import React, { useRef, useEffect, useCallback } from 'react';
import { Box } from 'coral-system'; import { Box } from 'coral-system';
import { MultiEditor, MultiEditorProps } from '@music163/tango-ui'; import { MultiEditor, MultiEditorProps } from '@music163/tango-ui';
import { observer, useDesigner, useWorkspace } from '@music163/tango-context'; import { observer, useDesigner, useWorkspace } from '@music163/tango-context';
import { isValidCode } from '@music163/tango-core';
import { Modal } from 'antd';
const ideConfig = { const ideConfig = {
// disableFileOps: { // disableFileOps: {
@@ -21,116 +19,89 @@ const ideConfig = {
// disableSetting: true, // disableSetting: true,
}; };
export interface CodeEditorProps extends Partial<MultiEditorProps> { export type CodeEditorProps = Partial<MultiEditorProps>;
/**
* 是否自动清楚未使用的导入
*/
autoRemoveUnusedImports?: boolean;
}
export const CodeEditor = observer( export const CodeEditor = observer((props: CodeEditorProps) => {
({ autoRemoveUnusedImports = true, ...rest }: CodeEditorProps) => { const editorRef = useRef(null);
const editorRef = useRef(null); const workspace = useWorkspace();
const workspace = useWorkspace(); const designer = useDesigner();
const designer = useDesigner(); const files = workspace.listFiles();
const files = workspace.listFiles(); const activeFile = workspace.activeFile;
const activeFile = workspace.activeFile;
let loc: any; // 记录视图代码的选中位置 let loc: any; // 记录视图代码的选中位置
const selectNode = workspace.selectSource.firstNode; const selectNode = workspace.selectSource.firstNode;
if (selectNode && activeFile === workspace.activeViewFile) { if (selectNode && activeFile === workspace.activeViewFile) {
loc = selectNode.loc; loc = selectNode.loc;
} }
useEffect(() => { useEffect(() => {
editorRef.current?.refresh(files, activeFile, loc); editorRef.current?.refresh(files, activeFile, loc);
}, [files, activeFile, loc]); }, [files, activeFile, loc]);
const fileSave = useCallback( const fileSave = useCallback(
(path: string, value: string) => { (path: string, value: string) => {
if (!isJsFile(path)) { workspace.updateFile(path, value, false);
// 非 js 文件直接保存 },
workspace.updateFile(path, value, autoRemoveUnusedImports); [workspace],
return; );
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 文件需要先检查语法,只有语法正确才会保存 return (
if (isValidCode(value)) { <Box display="flex" flexDirection="row" height="100%" bg="white" {...borderStyle}>
workspace.updateFile(path, value, autoRemoveUnusedImports); <MultiEditor
} else { ref={editorRef}
Modal.confirm({ options={{
title: '检测到代码中存在语法错误,暂时无法将代码同步给设计器,是否回退到安全代码?', fontSize: 14,
onOk: () => { automaticLayout: true,
editorRef.current?.refresh(files, activeFile); }}
}, ideConfig={ideConfig}
onCancel: () => {}, onFileSave={fileSave}
}); onPathChange={handlePathChange}
} onFileChange={handleFileChange}
}, defaultPath={activeFile}
[workspace, autoRemoveUnusedImports, activeFile, files], defaultTheme="GithubLightDefault"
); defaultFiles={files}
{...props}
const handleFileChange = useCallback( />
(type: string, info: any) => { </Box>
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)',
}
: {};
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}
{...rest}
/>
</Box>
);
},
);
function isJsFile(path: string) {
return /.jsx?$/.test(path);
}
+3 -3
View File
@@ -1,7 +1,7 @@
import React, { useImperativeHandle, ForwardedRef, useRef, useState, useEffect } from 'react'; import React, { useImperativeHandle, ForwardedRef, useRef, useState, useEffect } from 'react';
import { Box } from 'coral-system'; import { Box } from 'coral-system';
import { CodeSandbox, CodeSandboxProps } from '@music163/tango-sandbox'; 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 { observer, useWorkspace, useDesigner } from '@music163/tango-context';
import { Simulator, Viewport } from '../simulator'; import { Simulator, Viewport } from '../simulator';
import { useSandboxQuery } from '../context'; import { useSandboxQuery } from '../context';
@@ -88,7 +88,7 @@ function useSandbox({
onViewChange: (data) => onViewChange && onViewChange(data, getSandboxConfig()), 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; let code = workspace.getFile(filename).code;
if (filename === '/tango.config.json') { if (filename === '/tango.config.json') {
code = mergeTangoConfigJson(code, { isPreview, injectScript, formatter: configFormatter }); code = mergeTangoConfigJson(code, { isPreview, injectScript, formatter: configFormatter });
@@ -109,7 +109,7 @@ function useSandbox({
// 根据当前 workspace 状态与组件传入的状态是否一致,控制是否需要切换到空白路由 // 根据当前 workspace 状态与组件传入的状态是否一致,控制是否需要切换到空白路由
const display = isActive ? 'block' : 'none'; const display = isActive ? 'block' : 'none';
const routePath = isActive ? startRoute || workspace.activeRoute : LANDING_PAGE_PATH; const routePath = isActive ? startRoute || workspace.activeRoute : LANDING_PAGE_PATH;
const routerMode = fixRouterMode(workspace.appEntryModule?.routerType); const routerMode = fixRouterMode(workspace.jsAppEntryFile?.routerType);
const sandboxProps = isPreview const sandboxProps = isPreview
? { ? {
@@ -1,11 +1,11 @@
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { Radio, RadioProps, Tooltip } from 'antd'; import { Radio, RadioProps, Tooltip } from 'antd';
import type { OptionType } from '@music163/tango-helpers'; import type { IOptionItem } from '@music163/tango-helpers';
import { FormItemComponentProps } from '@music163/tango-setting-form'; import { FormItemComponentProps } from '@music163/tango-setting-form';
import { IconFont } from '@music163/tango-ui'; import { IconFont } from '@music163/tango-ui';
interface ChoiceSetterProps { interface ChoiceSetterProps {
options?: OptionType[]; options?: IOptionItem[];
} }
export function ChoiceSetter({ export function ChoiceSetter({
@@ -0,0 +1,7 @@
import { FormItemComponentProps } from '@music163/tango-setting-form';
import { ClassNameInput } from '@music163/tango-ui';
import React from 'react';
export function ClassNameSetter({ value, onChange }: FormItemComponentProps<string>) {
return <ClassNameInput value={value} onChange={onChange} />;
}
@@ -2,9 +2,17 @@ import React, { useState, useEffect, useCallback } from 'react';
import { Box, Text, css } from 'coral-system'; import { Box, Text, css } from 'coral-system';
import { Dropdown, Button } from 'antd'; import { Dropdown, Button } from 'antd';
import { isValidExpressionCode } from '@music163/tango-core'; import { isValidExpressionCode } from '@music163/tango-core';
import { getValue, IVariableTreeNode, noop } from '@music163/tango-helpers'; import { getValue, interpolate, IVariableTreeNode, noop } from '@music163/tango-helpers';
import { CloseCircleFilled, MenuOutlined } from '@ant-design/icons'; import { CloseCircleFilled, MenuOutlined } from '@ant-design/icons';
import { Panel, InputCode, Action, DragPanel, PopOutOutlined } from '@music163/tango-ui'; import {
Panel,
InputCode,
Action,
DragPanel,
PopOutOutlined,
InputCodeProps,
InputStyleCode,
} from '@music163/tango-ui';
import { FormItemComponentProps } from '@music163/tango-setting-form'; import { FormItemComponentProps } from '@music163/tango-setting-form';
import { useWorkspace, useWorkspaceData } from '@music163/tango-context'; import { useWorkspace, useWorkspaceData } from '@music163/tango-context';
import { VariableTree } from '../components'; import { VariableTree } from '../components';
@@ -26,6 +34,19 @@ export const jsonValueValidate = (value: string) => {
} }
}; };
/**
*
* @param value
* @param template
* @returns
*/
export function getCallbackValue(value: string, template?: string) {
if (!value) {
return;
}
return template ? interpolate(template, { content: value }) : `() => {\n ${value}\n}`;
}
const suffixStyle = css` const suffixStyle = css`
display: flex; display: flex;
align-items: center; align-items: center;
@@ -37,26 +58,32 @@ const suffixStyle = css`
} }
`; `;
export interface ExpressionSetterProps extends FormItemComponentProps<string> { export interface CodeSetterProps extends FormItemComponentProps<string> {
modalTitle?: string; modalTitle?: string;
modalTip?: string; modalTip?: string;
autoCompleteOptions?: string[]; autoCompleteOptions?: string[];
allowClear?: boolean; allowClear?: boolean;
showOptionsDropDown?: boolean; showOptionsDropDown?: boolean;
/**
* css object
*/
expressionType?: 'cssObject';
} }
export function ExpressionSetter(props: ExpressionSetterProps) { export function CodeSetter(props: CodeSetterProps) {
const { const {
onChange, onChange,
modalTitle, modalTitle,
modalTip, modalTip,
autoCompleteOptions, autoCompleteOptions,
placeholder = '在这里输入JS代码', template,
placeholder = '在这里输入代码',
value: valueProp, value: valueProp,
status, status,
allowClear = true, allowClear = true,
newStoreTemplate, newStoreTemplate,
showOptionsDropDown = true, showOptionsDropDown = true,
expressionType,
} = props; } = props;
// const codeValue = getCodeOfWrappedCode(valueProp); // const codeValue = getCodeOfWrappedCode(valueProp);
const [inputValue, setInputValue] = useState(valueProp); const [inputValue, setInputValue] = useState(valueProp);
@@ -79,10 +106,12 @@ export function ExpressionSetter(props: ExpressionSetterProps) {
const sandbox = useSandboxQuery(); const sandbox = useSandboxQuery();
const evaluateContext = sandbox.window; const evaluateContext = sandbox.window;
const InputCodeComponent = expressionType === 'cssObject' ? InputStyleCode : InputCode;
return ( return (
<Box className="ExpressionSetter"> <Box className="ExpressionSetter">
{/* 同时支持下拉框展示 */} {/* 同时支持下拉框展示 */}
<InputCode <InputCodeComponent
placeholder={placeholder} placeholder={placeholder}
suffix={ suffix={
<Box css={suffixStyle}> <Box css={suffixStyle}>
@@ -90,7 +119,7 @@ export function ExpressionSetter(props: ExpressionSetterProps) {
<CloseCircleFilled <CloseCircleFilled
title="清空" title="清空"
onClick={() => { onClick={() => {
change(''); change(undefined);
}} }}
/> />
)} )}
@@ -119,8 +148,10 @@ export function ExpressionSetter(props: ExpressionSetterProps) {
subTitle={modalTip} subTitle={modalTip}
placeholder={placeholder} placeholder={placeholder}
autoCompleteOptions={autoCompleteOptions} autoCompleteOptions={autoCompleteOptions}
template={template}
newStoreTemplate={newStoreTemplate} newStoreTemplate={newStoreTemplate}
value={inputValue} value={inputValue}
expressionType={expressionType}
onOk={(value) => { onOk={(value) => {
change(value); change(value);
}} }}
@@ -141,7 +172,7 @@ export function ExpressionSetter(props: ExpressionSetterProps) {
); );
} }
export interface ExpressionPopoverProps { export interface ExpressionPopoverProps extends InputCodeProps {
title?: string; title?: string;
subTitle?: string; subTitle?: string;
placeholder?: string; placeholder?: string;
@@ -150,10 +181,18 @@ export interface ExpressionPopoverProps {
onOk?: (value: string) => void; onOk?: (value: string) => void;
dataSource?: IVariableTreeNode[]; dataSource?: IVariableTreeNode[];
autoCompleteOptions?: string[]; autoCompleteOptions?: string[];
/**
*
*/
template?: string;
/** /**
* store * store
*/ */
newStoreTemplate?: string; newStoreTemplate?: string;
/**
* css object
*/
expressionType?: 'cssObject';
children?: React.ReactNode; children?: React.ReactNode;
} }
@@ -166,12 +205,15 @@ export function ExpressionPopover({
value, value,
dataSource, dataSource,
autoCompleteOptions, autoCompleteOptions,
template,
newStoreTemplate = CODE_TEMPLATES.newStoreTemplate, newStoreTemplate = CODE_TEMPLATES.newStoreTemplate,
children, children,
expressionType,
}: ExpressionPopoverProps) { }: ExpressionPopoverProps) {
const [exp, setExp] = useState(value ?? defaultValue); const [exp, setExp] = useState(value ?? defaultValue);
const [error, setError] = useState(''); const [error, setError] = useState('');
const workspace = useWorkspace(); const workspace = useWorkspace();
const InputCodeComponent = expressionType === 'cssObject' ? InputStyleCode : InputCode;
const selectNodePath = workspace.selectSource.selected[0]?.codeId; const selectNodePath = workspace.selectSource.selected[0]?.codeId;
@@ -182,7 +224,7 @@ export function ExpressionPopover({
})); }));
const sandbox = useSandboxQuery(); const sandbox = useSandboxQuery();
const evaluateContext = sandbox.window; const evaluateContext: any = sandbox.window;
const handleExpInputChange = (val: string) => { const handleExpInputChange = (val: string) => {
setExp(val?.trim()); setExp(val?.trim());
@@ -207,7 +249,7 @@ export function ExpressionPopover({
body={ body={
<Box display="flex" flexDirection="column"> <Box display="flex" flexDirection="column">
<Box p="m"> <Box p="m">
<InputCode <InputCodeComponent
shape="inset" shape="inset"
minHeight="56px" minHeight="56px"
maxHeight="160px" maxHeight="160px"
@@ -223,19 +265,18 @@ export function ExpressionPopover({
autoCompleteOptions={autoCompleteOptions} autoCompleteOptions={autoCompleteOptions}
/> />
{error ? ( {error ? (
<Text color="red" fontSize="12px" as="p"> <Text color="red" fontSize="12px" as="div">
</Text> </Text>
) : null} ) : null}
{subTitle && ( <Box fontSize="12px" color="text2">
<Text fontSize="12px" color="text3" as="p"> <Text display="block"></Text>
{subTitle} {subTitle && <Text display="block">{subTitle}</Text>}
<Text display="block">
javascript 使 jsx
</Text> </Text>
)} </Box>
<Text fontSize="12px" color="text3" as="p">
javascript 使 jsx
</Text>
</Box> </Box>
<Panel <Panel
title="从变量列表中选中" title="从变量列表中选中"
@@ -253,7 +294,7 @@ export function ExpressionPopover({
height="100%" height="100%"
showViewButton showViewButton
dataSource={dataSource || expressionVariables} dataSource={dataSource || expressionVariables}
appContext={sandbox?.window['tango']} appContext={evaluateContext?.['tango']}
getStoreNames={() => Object.keys(workspace.storeModules)} getStoreNames={() => Object.keys(workspace.storeModules)}
serviceModules={serviceModules} serviceModules={serviceModules}
getServiceData={(serviceKey) => { getServiceData={(serviceKey) => {
@@ -274,7 +315,11 @@ export function ExpressionPopover({
} }
let str; let str;
if (/^(stores|services)\./.test(node.key)) { if (/^(stores|services)\./.test(node.key)) {
str = `tango.${node.key.replaceAll('.', '?.')}`; // 从匹配到的第二个点开始替换为 ?.,因为第一个点是 stores 或 services
str = `tango.${node.key.replace(/(?<=\..*?)\./g, '?.')}`;
if (node.type === 'function') {
str = getCallbackValue(`${str}();`, template);
}
} else { } else {
str = `${node.key}`; str = `${node.key}`;
} }
+29 -33
View File
@@ -1,11 +1,11 @@
import React, { useCallback, useMemo, useState } from 'react'; import React, { useCallback, useMemo, useState } from 'react';
import { css, Box, Text } from 'coral-system'; import { css, Box, Text } from 'coral-system';
import { AutoComplete } from 'antd'; import { AutoComplete, Input } from 'antd';
import { ActionSelect } from '@music163/tango-ui'; import { ActionSelect } from '@music163/tango-ui';
import { FormItemComponentProps } from '@music163/tango-setting-form'; import { FormItemComponentProps } from '@music163/tango-setting-form';
import { useWorkspace, useWorkspaceData } from '@music163/tango-context'; import { useWorkspace, useWorkspaceData } from '@music163/tango-context';
import { wrapCode } from '@music163/tango-helpers'; import { Dict, wrapCode } from '@music163/tango-helpers';
import { ExpressionPopover } from './expression-setter'; import { ExpressionPopover, getCallbackValue } from './code-setter';
import { value2code } from '@music163/tango-core'; import { value2code } from '@music163/tango-core';
enum EventAction { enum EventAction {
@@ -31,7 +31,7 @@ export type EventSetterProps = FormItemComponentProps<string>;
* 事件监听函数绑定器 * 事件监听函数绑定器
*/ */
export function EventSetter(props: EventSetterProps) { export function EventSetter(props: EventSetterProps) {
const { value, onChange, modalTitle } = props; const { value, onChange, modalTitle, modalTip, template } = props;
const [type, setType] = useState<EventAction>(); // 事件类型 const [type, setType] = useState<EventAction>(); // 事件类型
const [temp, setTemp] = useState(''); // 二级暂存值 const [temp, setTemp] = useState(''); // 二级暂存值
const { actionVariables, routeOptions } = useWorkspaceData(); const { actionVariables, routeOptions } = useWorkspaceData();
@@ -59,7 +59,9 @@ export function EventSetter(props: EventSetterProps) {
label: ( label: (
<ExpressionPopover <ExpressionPopover
title={modalTitle} title={modalTitle}
subTitle={modalTip}
value={value} value={value}
template={template}
onOk={(nextValue) => { onOk={(nextValue) => {
handleChange(nextValue); handleChange(nextValue);
}} }}
@@ -74,30 +76,34 @@ export function EventSetter(props: EventSetterProps) {
{ label: '打开弹窗', value: EventAction.OpenModal }, { label: '打开弹窗', value: EventAction.OpenModal },
{ label: '关闭弹窗', value: EventAction.CloseModal }, { label: '关闭弹窗', value: EventAction.CloseModal },
], ],
[modalTitle, value, actionVariables, handleChange], [modalTitle, value, actionVariables, template, handleChange],
); );
const onAction = (key: string) => { const onAction = (key: string) => {
setType(key as EventAction); // 记录事件类型 setType(key as EventAction); // 记录事件类型
setTemp(''); // 重置二级选项值 setTemp(''); // 重置二级选项值
switch (key) { if (key === EventAction.NoAction) {
case EventAction.ConsoleLog: handleChange(undefined);
handleChange('(...args) => console.log(...args)'); return;
break;
case EventAction.NoAction:
handleChange(undefined);
break;
default:
break;
} }
}; };
const actionText = getActionText(type, temp, code);
return ( return (
<Box css={wrapperStyle}> <Box css={wrapperStyle}>
<ActionSelect options={options} onSelect={onAction} text={actionText} /> <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 && ( {type === EventAction.NavigateTo && (
<AutoComplete <AutoComplete
placeholder="选择或输入页面路由" placeholder="选择或输入页面路由"
@@ -141,26 +147,16 @@ export function EventSetter(props: EventSetterProps) {
); );
} }
const handlerMap = { const handlerMap: Dict = {
[EventAction.OpenModal]: 'openModal', [EventAction.ConsoleLog]: 'console.log',
[EventAction.CloseModal]: 'closeModal', [EventAction.OpenModal]: 'tango.openModal',
[EventAction.NavigateTo]: 'navigateTo', [EventAction.CloseModal]: 'tango.closeModal',
[EventAction.NavigateTo]: 'tango.navigateTo',
}; };
function getActionText(type: EventAction, temp: string, fallbackCode: string) {
let text;
if (handlerMap[type]) {
text = getExpressionValue(type, temp);
} else if (fallbackCode) {
text = fallbackCode;
}
text = text || '请选择';
return text;
}
function getExpressionValue(type: EventAction, value = '') { function getExpressionValue(type: EventAction, value = '') {
const handler = handlerMap[type]; const handler = handlerMap[type];
if (handler) { if (handler) {
return `() => tango.${handler}("${value}")`; return getCallbackValue(`${handler}("${value}");`);
} }
} }
+10 -15
View File
@@ -5,14 +5,14 @@ import { CssSetter } from './css-setter';
import { DateRangeSetter, DateSetter, TimeRangeSetter } from './date-setter'; import { DateRangeSetter, DateSetter, TimeRangeSetter } from './date-setter';
import { EnumSetter } from './enum-setter'; import { EnumSetter } from './enum-setter';
import { EventSetter } from './event-setter'; import { EventSetter } from './event-setter';
import { ExpressionSetter } from './expression-setter'; import { CodeSetter } from './code-setter';
import { JSONSetter } from './json-setter'; import { JSONSetter } from './json-setter';
import { JsxSetter } from './jsx-setter'; import { JsxSetter } from './jsx-setter';
import { ListSetter } from './list-setter'; import { ListSetter } from './list-setter';
import { ModelSetter } from './model-setter'; import { ModelSetter } from './model-setter';
import { OptionSetter } from './option-setter'; import { OptionSetter } from './option-setter';
import { PickerSetter } from './picker-setter'; import { PickerSetter } from './picker-setter';
import { RenderSetter, TableCellSetter, TableExpandableSetter } from './render-setter'; import { RenderSetter } from './render-setter';
import { RouterSetter } from './router-setter'; import { RouterSetter } from './router-setter';
import { import {
SpacingSetter, SpacingSetter,
@@ -26,6 +26,7 @@ import {
FlexDirectionSetter, FlexDirectionSetter,
} from './style-setter'; } from './style-setter';
import { ChoiceSetter } from './choice-setter'; import { ChoiceSetter } from './choice-setter';
import { ClassNameSetter } from './classname-setter';
import { isValidExpressionCode } from '@music163/tango-core'; import { isValidExpressionCode } from '@music163/tango-core';
const codeValidate: IFormItemCreateOptions['validate'] = (value, field) => { const codeValidate: IFormItemCreateOptions['validate'] = (value, field) => {
@@ -49,10 +50,14 @@ export const BUILT_IN_SETTERS: IFormItemCreateOptions[] = [
{ {
name: 'codeSetter', name: 'codeSetter',
alias: ['expSetter', 'expressionSetter'], alias: ['expSetter', 'expressionSetter'],
component: ExpressionSetter, component: CodeSetter,
type: 'code', type: 'code',
validate: codeValidate, validate: codeValidate,
}, },
{
name: 'classNameSetter',
component: ClassNameSetter,
},
{ {
name: 'radioGroupSetter', name: 'radioGroupSetter',
alias: ['choiceSetter'], alias: ['choiceSetter'],
@@ -126,22 +131,12 @@ export const BUILT_IN_SETTERS: IFormItemCreateOptions[] = [
component: PickerSetter, component: PickerSetter,
}, },
{ {
name: 'renderPropsSetter', name: 'renderSetter',
alias: ['renderPropsSetter'],
component: RenderSetter, component: RenderSetter,
type: 'code', type: 'code',
validate: codeValidate, validate: codeValidate,
}, },
{
name: 'tableCellSetter',
component: TableCellSetter,
type: 'code',
},
{
name: 'tableExpandableSetter',
component: TableExpandableSetter,
type: 'code',
validate: codeValidate,
},
{ {
name: 'routerSetter', name: 'routerSetter',
component: RouterSetter, component: RouterSetter,
@@ -3,10 +3,11 @@ import { css, Box } from 'coral-system';
import { Form, Input, Button, Popover } from 'antd'; import { Form, Input, Button, Popover } from 'antd';
import { DeleteOutlined, EditOutlined, HolderOutlined } from '@ant-design/icons'; import { DeleteOutlined, EditOutlined, HolderOutlined } from '@ant-design/icons';
import { FormItemComponentProps } from '@music163/tango-setting-form'; import { FormItemComponentProps } from '@music163/tango-setting-form';
import { Dict } from '@music163/tango-helpers';
import { DragBox } from '../components'; import { DragBox } from '../components';
function copyWithoutUndefined(source: Record<string, any>) { function copyWithoutUndefined(source: Dict) {
const ret = {}; const ret: Dict = {};
for (const key in source) { for (const key in source) {
if (source[key] !== undefined) { if (source[key] !== undefined) {
ret[key] = source[key]; ret[key] = source[key];
@@ -60,7 +60,7 @@ export function ModelSetter({
}: FormItemComponentProps) { }: FormItemComponentProps) {
const [inputValue, setInputValue] = useState(value); const [inputValue, setInputValue] = useState(value);
const { modelVariables } = useWorkspaceData(); const { modelVariables } = useWorkspaceData();
const evaluateContext = useSandboxQuery().window || {}; const evaluateContext: any = useSandboxQuery().window || {};
const workspace = useWorkspace(); const workspace = useWorkspace();
const definedVariables = useMemo(() => { const definedVariables = useMemo(() => {
const list: string[] = []; const list: string[] = [];
+11 -80
View File
@@ -2,21 +2,16 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { ActionSelect, InputCode } from '@music163/tango-ui'; import { ActionSelect, InputCode } from '@music163/tango-ui';
import { FormItemComponentProps } from '@music163/tango-setting-form'; import { FormItemComponentProps } from '@music163/tango-setting-form';
import { Box } from 'coral-system'; import { Box } from 'coral-system';
import { Dict, IOptionItem } from '@music163/tango-helpers';
interface IRenderOption { import { getCallbackValue } from './code-setter';
label: string;
value: string;
render?: string;
relatedImports?: string[];
}
export interface RenderSetterProps { export interface RenderSetterProps {
text?: string; text?: string;
options?: IRenderOption[]; options?: IOptionItem[];
fallbackOption?: IRenderOption; fallbackOption?: IOptionItem;
} }
const defaultOptions: IRenderOption[] = [ const defaultOptions: IOptionItem[] = [
{ label: '取消自定义', value: '' }, { label: '取消自定义', value: '' },
{ label: '自定义渲染', value: 'Box', render: '() => <Box></Box>' }, { label: '自定义渲染', value: 'Box', render: '() => <Box></Box>' },
]; ];
@@ -29,6 +24,7 @@ export function RenderSetter({
onChange, onChange,
text = '自定义渲染为', text = '自定义渲染为',
options = defaultOptions, options = defaultOptions,
template = `() => {{content}}`,
fallbackOption, fallbackOption,
}: FormItemComponentProps & RenderSetterProps) { }: FormItemComponentProps & RenderSetterProps) {
const [inputValue, setInputValue] = useState(value || ''); const [inputValue, setInputValue] = useState(value || '');
@@ -37,7 +33,7 @@ export function RenderSetter({
}, [value]); }, [value]);
const optionsMap = useMemo(() => { const optionsMap = useMemo(() => {
return options.reduce((prev, cur) => { return options.reduce<Dict>((prev, cur) => {
prev[cur.value] = cur; prev[cur.value] = cur;
return prev; return prev;
}, {}); }, {});
@@ -46,13 +42,14 @@ export function RenderSetter({
const onSelect = useCallback( const onSelect = useCallback(
(key: string) => { (key: string) => {
const option = optionsMap[key] || fallbackOption; const option = optionsMap[key] || fallbackOption;
if (option?.render) { const next = option?.render || getCallbackValue(option.renderBody, template);
onChange(option.render, { relatedImports: option.relatedImports }); if (next) {
onChange(next, { relatedImports: option.relatedImports });
} else { } else {
onChange(undefined); onChange(undefined);
} }
}, },
[optionsMap, fallbackOption, onChange], [optionsMap, fallbackOption, onChange, template],
); );
return ( return (
<Box> <Box>
@@ -67,69 +64,3 @@ export function RenderSetter({
</Box> </Box>
); );
} }
const getRender = (content: string, type?: 'tableCell' | 'tableExpandable') => {
let code;
switch (type) {
case 'tableCell':
code = `(value, record, index) => ${content}`;
break;
case 'tableExpandable':
code = `{
expandedRowRender: (record) => ${content},
rowExpandable: (record) => true
}`;
break;
default:
code = `() => ${content}`;
break;
}
return code;
};
const tableCellOptions: RenderSetterProps['options'] = [
{ label: '取消自定义', value: '' },
{
label: '自定义区域',
value: 'Box',
render: getRender('<Box></Box>', 'tableCell'),
relatedImports: ['Box'],
},
{
label: '标签',
value: 'Tag',
render: getRender('<Tag>tag</Tag>', 'tableCell'),
relatedImports: ['Tag'],
},
{
label: '按钮',
value: 'Button',
render: getRender('<Button>button</Button>', 'tableCell'),
relatedImports: ['Button'],
},
{
label: '图片',
value: 'Image',
render: getRender('<Image width={150} src="https://picsum.photos/100" />', 'tableCell'),
relatedImports: ['Image'],
},
];
const tableExpandableOptions: RenderSetterProps['options'] = [
{
label: '设置可展开行',
value: 'Box',
render: getRender('<Box></Box>', 'tableExpandable'),
relatedImports: ['Box'],
},
{ label: '取消可展开行', value: '' },
];
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 React, { useEffect, useState } from 'react';
import { Box } from 'coral-system'; import { Box } from 'coral-system';
import { Input, Radio } from 'antd'; 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 { FormItemComponentProps } from '@music163/tango-setting-form';
import { useWorkspaceData } from '@music163/tango-context'; import { useWorkspaceData } from '@music163/tango-context';
import { PickerSetter } from './picker-setter'; import { PickerSetter } from './picker-setter';
const routerModeMap: Dict = {
link: '链接',
router: '路由',
both: '',
};
export function RouterSetter(props: FormItemComponentProps) { export function RouterSetter(props: FormItemComponentProps) {
const [type, setType] = useState<'input' | 'select'>(() => { const [type, setType] = useState<'input' | 'select'>(() => {
return isValidUrl(props.value) ? 'input' : 'select'; return isValidUrl(props.value) ? 'input' : 'select';
@@ -37,7 +43,7 @@ export function RouterSetter(props: FormItemComponentProps) {
)} )}
{type === 'input' && ( {type === 'input' && (
<Input <Input
placeholder={`请输入任意${{ link: '链接', router: '路由', both: '' }[displayType]}地址`} placeholder={`请输入任意${routerModeMap[displayType]}地址`}
{...props} {...props}
value={input} value={input}
onChange={(e) => setInput(e.target.value)} onChange={(e) => setInput(e.target.value)}
+2 -2
View File
@@ -2,7 +2,7 @@ import React from 'react';
import { Box } from 'coral-system'; import { Box } from 'coral-system';
import { SettingForm, FormModel, SettingFormProps } from '@music163/tango-setting-form'; import { SettingForm, FormModel, SettingFormProps } from '@music163/tango-setting-form';
import { Panel } from '@music163/tango-ui'; import { Panel } from '@music163/tango-ui';
import { clone, parseDndId } from '@music163/tango-helpers'; import { clone, Dict, parseDndId } from '@music163/tango-helpers';
import { observer, useDesigner, useWorkspace } from '@music163/tango-context'; import { observer, useDesigner, useWorkspace } from '@music163/tango-context';
import { registerBuiltinSetters } from './setters'; import { registerBuiltinSetters } from './setters';
@@ -34,7 +34,7 @@ export const SettingPanel = observer((props: SettingPanelProps) => {
} }
const getSettingValue = (attributes: Record<string, string>) => { const getSettingValue = (attributes: Record<string, string>) => {
const ret = {}; const ret: Dict = {};
const keys = Object.keys(attributes); const keys = Object.keys(attributes);
for (const key of keys) { for (const key of keys) {
if (/^data-/.test(key)) { if (/^data-/.test(key)) {
@@ -2,6 +2,7 @@ import React, { useMemo, useState } from 'react';
import { Box, Grid, Text } from 'coral-system'; import { Box, Grid, Text } from 'coral-system';
import styled, { css } from 'styled-components'; import styled, { css } from 'styled-components';
import { import {
Dict,
IComponentPrototype, IComponentPrototype,
MenuDataType, MenuDataType,
MenuValueType, MenuValueType,
@@ -66,7 +67,7 @@ export interface ComponentsPanelProps {
layout?: 'grid' | 'line'; layout?: 'grid' | 'line';
} }
const localeMap = { const localeMap: Dict = {
common: '基础组件', common: '基础组件',
atom: '原子组件', atom: '原子组件',
snippet: '组合', snippet: '组合',
@@ -124,7 +125,7 @@ export const ComponentsPanel = observer(
const tabs = Object.keys(menuData).map((key) => ({ const tabs = Object.keys(menuData).map((key) => ({
key, key,
label: localeMap[key], label: localeMap[key],
children: <MaterialList data={menuData[key]} />, children: <MaterialList data={(menuData as any)[key]} />,
})); }));
if (showBizComps) { if (showBizComps) {
@@ -364,7 +365,7 @@ function MaterialGrid({ data }: MaterialProps) {
<IconFont className="material-icon" type={data.icon || 'icon-placeholder'} /> <IconFont className="material-icon" type={data.icon || 'icon-placeholder'} />
) : ( ) : (
<Box className="material-icon"> <Box className="material-icon">
<img src={icon} alt={data.name} /> <img src={icon} alt={data.name} height="32" />
</Box> </Box>
); );
@@ -24,7 +24,7 @@ const DataSourceView = observer(() => {
<Box className="ServiceFunctionList" p="m"> <Box className="ServiceFunctionList" p="m">
<VariableTree <VariableTree
dataSource={serviceVariables} dataSource={serviceVariables}
appContext={sandbox?.window['tango']} appContext={(sandbox?.window as any)['tango']}
serviceModules={serviceModules} serviceModules={serviceModules}
onRemoveService={(variableKey) => { onRemoveService={(variableKey) => {
workspace.removeServiceFunction(variableKey); workspace.removeServiceFunction(variableKey);
@@ -4,6 +4,7 @@ import { ConfigGroup, ConfigItem } from '@music163/tango-ui';
import { Box, css } from 'coral-system'; import { Box, css } from 'coral-system';
import { Input, Switch, Form, Button, message, FormProps } from 'antd'; import { Input, Switch, Form, Button, message, FormProps } from 'antd';
import { CloseCircleOutlined, CloseOutlined } from '@ant-design/icons'; import { CloseCircleOutlined, CloseOutlined } from '@ant-design/icons';
import { Dict } from '@music163/tango-helpers';
export default observer(() => { export default observer(() => {
const workspace = useWorkspace(); const workspace = useWorkspace();
@@ -63,7 +64,7 @@ function ProxyManage({
initialValues: initialValuesProp = {}, initialValues: initialValuesProp = {},
onSave, onSave,
}: { }: {
initialValues?: object; initialValues?: Dict;
onSave?: (values: any) => void; onSave?: (values: any) => void;
}) { }) {
const initialValues = useMemo(() => { const initialValues = useMemo(() => {
@@ -84,7 +85,7 @@ function ProxyManage({
{...proxyManageFormStyle} {...proxyManageFormStyle}
initialValues={initialValues} initialValues={initialValues}
onFinish={(values) => { onFinish={(values) => {
const proxy = values.proxy.reduce((prev: object, { path, ...rest }: any) => { const proxy = values.proxy.reduce((prev: any, { path, ...rest }: any) => {
prev[path] = rest; prev[path] = rest;
return prev; return prev;
}, {}); }, {});
@@ -21,7 +21,6 @@ import { ColorTag, ConfigGroup, ConfigItem } from '@music163/tango-ui';
import { MinusCircleOutlined, PlusOutlined, QuestionCircleOutlined } from '@ant-design/icons'; import { MinusCircleOutlined, PlusOutlined, QuestionCircleOutlined } from '@ant-design/icons';
import { useBoolean } from '@music163/tango-helpers'; import { useBoolean } from '@music163/tango-helpers';
import { isUndefined } from 'lodash-es'; import { isUndefined } from 'lodash-es';
import { Workspace } from '@music163/tango-core';
import { useSandboxQuery } from '../context'; import { useSandboxQuery } from '../context';
enum DependencyItemType { enum DependencyItemType {
@@ -169,7 +168,7 @@ function RenderItem({
}: RenderItemProps) { }: RenderItemProps) {
const [open, { on, off }] = useBoolean(false); const [open, { on, off }] = useBoolean(false);
const workspace = useWorkspace() as Workspace; const workspace = useWorkspace();
const basePackage = useMemo(() => { const basePackage = useMemo(() => {
if (type !== DependencyItemType. || !templateBaseDependencies) return undefined; if (type !== DependencyItemType. || !templateBaseDependencies) return undefined;
@@ -4,7 +4,7 @@ import { Box, css } from 'coral-system';
import { IconFont } from '@music163/tango-ui'; import { IconFont } from '@music163/tango-ui';
import { EyeOutlined, EyeInvisibleOutlined, EllipsisOutlined } from '@ant-design/icons'; import { EyeOutlined, EyeInvisibleOutlined, EllipsisOutlined } from '@ant-design/icons';
import { observer, useWorkspace } from '@music163/tango-context'; import { observer, useWorkspace } from '@music163/tango-context';
import { DropMethod, ITangoViewNodeData } from '@music163/tango-core'; import { DropMethod, IViewNodeData } from '@music163/tango-core';
import { noop, parseDndId } from '@music163/tango-helpers'; import { noop, parseDndId } from '@music163/tango-helpers';
import { useSandboxQuery } from '../../context'; import { useSandboxQuery } from '../../context';
import { buildQueryBySlotId } from '../../helpers'; import { buildQueryBySlotId } from '../../helpers';
@@ -65,7 +65,7 @@ const filedNames = {
children: 'children', children: 'children',
}; };
const getNodeKeys = (data: ITangoViewNodeData[]) => { const getNodeKeys = (data: IViewNodeData[]) => {
const ids: string[] = []; const ids: string[] = [];
data?.forEach((node) => { data?.forEach((node) => {
ids.push(node.id); ids.push(node.id);
@@ -76,7 +76,7 @@ const getNodeKeys = (data: ITangoViewNodeData[]) => {
return ids; return ids;
}; };
const OutlineTreeNode: React.FC<{ node: ITangoViewNodeData } & ComponentsTreeProps> = observer( const OutlineTreeNode: React.FC<{ node: IViewNodeData } & ComponentsTreeProps> = observer(
({ node, showToggleVisibleIcon, actionItems }) => { ({ node, showToggleVisibleIcon, actionItems }) => {
const workspace = useWorkspace(); const workspace = useWorkspace();
const sandboxQuery = useSandboxQuery(); const sandboxQuery = useSandboxQuery();
@@ -142,7 +142,7 @@ export const ComponentsTree: React.FC<ComponentsTreeProps> = observer(
workspace.selectSource.selected.map((item) => item.id), workspace.selectSource.selected.map((item) => item.id),
); );
const file = workspace.activeViewModule; const file = workspace.activeViewModule;
const nodesTree = (file?.nodesTree ?? []) as ITangoViewNodeData[]; const nodesTree = (file?.nodesTree ?? []) as IViewNodeData[];
const [expandedKeys, setExpandedKeys] = useState(getNodeKeys(nodesTree)); const [expandedKeys, setExpandedKeys] = useState(getNodeKeys(nodesTree));
const [contextMenuOpen, setContextMenuOpen] = useState(false); const [contextMenuOpen, setContextMenuOpen] = useState(false);
@@ -1,12 +1,12 @@
import React, { useEffect, useReducer } from 'react'; import React, { useEffect, useReducer } from 'react';
import { observer } from '@music163/tango-context'; import { observer } from '@music163/tango-context';
import { isFunction, isObject, pick } from '@music163/tango-helpers'; import { Dict, isFunction, isObject, pick } from '@music163/tango-helpers';
import { CollapsePanel, IconButton, JsonView } from '@music163/tango-ui'; import { CollapsePanel, IconButton, JsonView } from '@music163/tango-ui';
import { ReloadOutlined } from '@ant-design/icons'; import { ReloadOutlined } from '@ant-design/icons';
import { useSandboxQuery } from '../../context'; import { useSandboxQuery } from '../../context';
function processTango(obj: any = {}, index = 0) { function processTango(obj: Dict = {}, index = 0) {
const ret = {}; const ret: Dict = {};
Object.keys(obj).forEach((key) => { Object.keys(obj).forEach((key) => {
const val = obj[key]; const val = obj[key];
if (index > 2) { if (index > 2) {
@@ -25,11 +25,12 @@ function processTango(obj: any = {}, index = 0) {
export const StateTree = observer(() => { export const StateTree = observer(() => {
const sandboxQuery = useSandboxQuery(); const sandboxQuery = useSandboxQuery();
const [, forceUpdate] = useReducer((x) => x + 1, 0); const [, forceUpdate] = useReducer((x) => x + 1, 0);
const tangoContext = pick(sandboxQuery.window['tango'] || {}, [ const tangoContext = pick((sandboxQuery.window as any)['tango'] || {}, [
'stores', 'stores',
'pageStore', 'pageStore',
'services', 'services',
'config', 'config',
'routeData',
]); ]);
const callback = () => { const callback = () => {
forceUpdate(); forceUpdate();
@@ -19,7 +19,7 @@ const resizeHandleStyle = (axis: ResizableBoxProps['axis'], barStyle: HTMLCoralP
cursor: ${axis === 'x' ? 'col-resize' : 'row-resize'}; cursor: ${axis === 'x' ? 'col-resize' : 'row-resize'};
${barStyle && ${barStyle &&
Object.keys(barStyle) Object.keys(barStyle)
.map((key) => `${key}: ${barStyle[key]};`) .map((key) => `${key}: ${(barStyle as any)[key]};`)
.join('')} .join('')}
&:hover, &:hover,
&:active { &:active {
+28 -18
View File
@@ -3,7 +3,7 @@ import cx from 'classnames';
import { Box, Text, css, HTMLCoralProps } from 'coral-system'; import { Box, Text, css, HTMLCoralProps } from 'coral-system';
import { Badge } from 'antd'; import { Badge } from 'antd';
import { ReactComponentProps } from '@music163/tango-helpers'; import { ReactComponentProps } from '@music163/tango-helpers';
import { observer, useDesigner } from '@music163/tango-context'; import { observer, useDesigner, useWorkspace } from '@music163/tango-context';
import { ResizableBox } from './resizable-box'; import { ResizableBox } from './resizable-box';
import { getWidget } from '../widgets'; import { getWidget } from '../widgets';
@@ -19,15 +19,22 @@ const sidebarStyle = css`
list-style: none; list-style: none;
user-select: none; user-select: none;
transition: all 0.3s ease; transition: all 0.3s ease;
&.disabled {
pointer-events: none;
}
} }
.SidebarPanelBarListItem { .SidebarPanelBarListItem {
color: var(--tango-colors-text2);
&.active { &.active {
background-color: var(--tango-colors-custom-sidebarItemActiveBg); color: var(--tango-colors-brand);
background-color: var(--tango-colors-custom-sidebarItemActiveBg, --tango-colors-fill2);
} }
&:hover { &:hover {
background-color: var(--tango-colors-custom-sidebarItemHoverBg); background-color: var(--tango-colors-custom-sidebarItemHoverBg, --tango-colors-fill2);
} }
} }
`; `;
@@ -76,6 +83,7 @@ export interface SidebarPanelItemProps
function BaseSidebarPanel({ panelWidth: defaultPanelWidth = 266, footer, children }: SidebarProps) { function BaseSidebarPanel({ panelWidth: defaultPanelWidth = 266, footer, children }: SidebarProps) {
const designer = useDesigner(); const designer = useDesigner();
const workspace = useWorkspace();
const items = useMemo(() => { const items = useMemo(() => {
const ret: Record<string, SidebarPanelItemProps> = {}; const ret: Record<string, SidebarPanelItemProps> = {};
@@ -92,10 +100,23 @@ function BaseSidebarPanel({ panelWidth: defaultPanelWidth = 266, footer, childre
return ret; return ret;
}, [children]); }, [children]);
if (designer.activeView !== 'design') {
return null;
}
return ( return (
<Box display="flex" flexShrink={0} css={sidebarStyle} className="SidebarPanel"> <Box display="flex" flexShrink={0} css={sidebarStyle} className="SidebarPanel">
<Box className="SidebarPanelBar" display="flex" flexDirection="column" position="relative"> <Box className="SidebarPanelBar" display="flex" flexDirection="column" position="relative">
<Box as="ul" flex="0" p="0" m="0" textAlign="center" className="SidebarPanelBarList"> <Box
as="ul"
flex="0"
p="0"
m="0"
textAlign="center"
className={cx('SidebarPanelBarList', {
disabled: workspace.fileErrors.length > 0,
})}
>
{Object.keys(items).map((key) => { {Object.keys(items).map((key) => {
const item = items[key]; const item = items[key];
const isActive = key === designer.activeSidebarPanel; const isActive = key === designer.activeSidebarPanel;
@@ -112,7 +133,6 @@ function BaseSidebarPanel({ panelWidth: defaultPanelWidth = 266, footer, childre
icon={item.icon} icon={item.icon}
label={item.label} label={item.label}
showBadge={item.showBadge} showBadge={item.showBadge}
isActive={isActive}
/> />
</li> </li>
); );
@@ -183,10 +203,6 @@ BaseSidebarPanel.Item = function ({
export const Sidebar = observer(BaseSidebarPanel); export const Sidebar = observer(BaseSidebarPanel);
interface SidebarPanelBarItemProps { interface SidebarPanelBarItemProps {
/**
* 是否选中
*/
isActive?: boolean;
/** /**
* 侧边栏图标 * 侧边栏图标
*/ */
@@ -212,15 +228,9 @@ interface SidebarPanelBarItemProps {
}; };
} }
function SidebarPanelBarItem({ function SidebarPanelBarItem({ icon: iconProp, label, showBadge }: SidebarPanelBarItemProps) {
isActive,
icon: iconProp,
label,
showBadge,
}: SidebarPanelBarItemProps) {
const color = isActive ? 'brand' : 'text.body';
let icon = ( let icon = (
<Text fontSize="24px" lineHeight={1} color={color}> <Text fontSize="24px" lineHeight={1}>
{iconProp} {iconProp}
</Text> </Text>
); );
@@ -241,7 +251,7 @@ function SidebarPanelBarItem({
py="12px" py="12px"
> >
{icon} {icon}
<Text fontSize="12px" mt="s" color={color}> <Text fontSize="12px" mt="s">
{label} {label}
</Text> </Text>
</Box> </Box>
@@ -206,7 +206,7 @@ function SelectionBox({ showActions, actions, data }: SelectionBoxProps) {
} }
/> />
<SelectionToolSet>{!isPage && actions}</SelectionToolSet> <SelectionToolSet>{!isPage && actions}</SelectionToolSet>
{isFromCurrentFile && prototype.hasChildren && ( {isFromCurrentFile && prototype?.hasChildren && (
<ComponentsPopover> <ComponentsPopover>
<Tooltip title="快捷添加子元素"> <Tooltip title="快捷添加子元素">
<SelectionHelper icon={<PlusOutlined />} /> <SelectionHelper icon={<PlusOutlined />} />
+35 -12
View File
@@ -25,22 +25,45 @@ function DesktopSimulator({ children }: HTMLCoralProps<'div'>) {
} }
const mobileStyle = css` const mobileStyle = css`
--device-viewport-width: 375px; --device-viewport-width: 360px;
--device-viewport-height: 812px; --device-viewport-height: 640px;
--device-frame-width: 13px; position: relative;
background-image: url(https://p5.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/21897838692/b1c5/2665/56db/10558c1bb2030d55ceabbcc74431de99.png); margin: auto;
background-repeat: no-repeat; border: 16px black solid;
background-size: 100% 100%; border-top-width: 60px;
box-shadow: 0 2px 8px rgb(2 8 20 / 10%), 0 8px 16px rgb(2 8 20 / 8%); border-bottom-width: 60px;
border-radius: 62px; border-radius: 36px;
overflow: hidden;
&::before {
content: '';
display: block;
width: 60px;
height: 5px;
position: absolute;
top: -30px;
left: 50%;
transform: translate(-50%, -50%);
background: #333;
border-radius: 10px;
}
&::after {
content: '';
display: block;
width: 35px;
height: 35px;
position: absolute;
left: 50%;
bottom: -65px;
transform: translate(-50%, -50%);
background: #333;
border-radius: 50%;
}
.MobileSimulatorDeviceFrame { .MobileSimulatorDeviceFrame {
border-radius: 50px;
width: var(--device-viewport-width); width: var(--device-viewport-width);
height: var(--device-viewport-height); height: var(--device-viewport-height);
margin: var(--device-frame-width); background: white;
overflow: hidden;
} }
`; `;
+3 -2
View File
@@ -5,11 +5,12 @@ import { observer, useWorkspace } from '@music163/tango-context';
export const HistoryTool = observer(() => { export const HistoryTool = observer(() => {
const workspace = useWorkspace(); const workspace = useWorkspace();
const disabled = !workspace.isValid;
return ( return (
<Box display="flex" columnGap="m"> <Box display="flex" columnGap="m">
<ToggleButton <ToggleButton
shape="ghost" shape="ghost"
disabled={!workspace.history.couldBack || workspace.history.index === 0} disabled={disabled || !workspace.history.couldBack || workspace.history.index === 0}
tooltip="撤销" tooltip="撤销"
onClick={() => { onClick={() => {
workspace.history.back(); workspace.history.back();
@@ -20,7 +21,7 @@ export const HistoryTool = observer(() => {
</ToggleButton> </ToggleButton>
<ToggleButton <ToggleButton
shape="ghost" shape="ghost"
disabled={!workspace.history.couldForward} disabled={disabled || !workspace.history.couldForward}
tooltip="重做" tooltip="重做"
onClick={() => { onClick={() => {
workspace.history.forward(); workspace.history.forward();
+3 -16
View File
@@ -1,6 +1,5 @@
import React, { useCallback } from 'react'; import React from 'react';
import { Group } from 'coral-system'; import { Group } from 'coral-system';
import { Modal } from 'antd';
import { ToggleButton, CodeOutlined, DualOutlined } from '@music163/tango-ui'; import { ToggleButton, CodeOutlined, DualOutlined } from '@music163/tango-ui';
import { observer, useDesigner, useWorkspace } from '@music163/tango-context'; import { observer, useDesigner, useWorkspace } from '@music163/tango-context';
import { BorderOutlined } from '@ant-design/icons'; import { BorderOutlined } from '@ant-design/icons';
@@ -9,23 +8,13 @@ export const ModeSwitchTool = observer(() => {
const workspace = useWorkspace(); const workspace = useWorkspace();
const designer = useDesigner(); const designer = useDesigner();
const activeFileCheck = useCallback(() => {
if (workspace.activeFile !== workspace.activeViewFile) {
Modal.confirm({
title: '当前打开的文件与视图不匹配,是否切换到当前视图对应的文件?',
onOk: () => {
workspace.setActiveFile(workspace.activeViewFile);
},
});
}
}, [workspace]);
return ( return (
<Group attached> <Group attached whiteSpace="nowrap">
<ToggleButton <ToggleButton
shape="ghost" shape="ghost"
selected={designer.activeView === 'design'} selected={designer.activeView === 'design'}
onClick={() => { onClick={() => {
workspace.syncFiles(); // 保证 ast 与 code 是同步的
designer.setActiveView('design'); designer.setActiveView('design');
}} }}
tooltip="设计视图" tooltip="设计视图"
@@ -38,7 +27,6 @@ export const ModeSwitchTool = observer(() => {
onClick={() => { onClick={() => {
designer.setActiveView('code'); // 切换到源码视图 designer.setActiveView('code'); // 切换到源码视图
designer.setActiveSidebarPanel(''); // 关闭左侧面板 designer.setActiveSidebarPanel(''); // 关闭左侧面板
activeFileCheck();
}} }}
tooltip="源码视图" tooltip="源码视图"
> >
@@ -50,7 +38,6 @@ export const ModeSwitchTool = observer(() => {
onClick={() => { onClick={() => {
designer.setActiveView('dual'); // 切换到双屏视图 designer.setActiveView('dual'); // 切换到双屏视图
designer.setActiveSidebarPanel(''); // 关闭左侧面板 designer.setActiveSidebarPanel(''); // 关闭左侧面板
activeFileCheck();
}} }}
tooltip="双屏视图" tooltip="双屏视图"
> >
+3 -1
View File
@@ -1,13 +1,15 @@
import React from 'react'; import React from 'react';
import { ToggleButton } from '@music163/tango-ui'; import { ToggleButton } from '@music163/tango-ui';
import { EyeOutlined } from '@ant-design/icons'; import { EyeOutlined } from '@ant-design/icons';
import { observer, useDesigner } from '@music163/tango-context'; import { observer, useDesigner, useWorkspace } from '@music163/tango-context';
export const PreviewTool = observer(() => { export const PreviewTool = observer(() => {
const designer = useDesigner(); const designer = useDesigner();
const workspace = useWorkspace();
return ( return (
<ToggleButton <ToggleButton
disabled={!workspace.isValid}
shape="ghost" shape="ghost"
selected={designer.isPreview} selected={designer.isPreview}
onClick={() => { onClick={() => {
@@ -47,11 +47,14 @@ function PageSelect({ value, onSelect, onRemove, onUpdate, onCopy, options }: Pa
setVisible(false); setVisible(false);
}; };
const current = optionMap[value]; const pathname = value?.split?.('?')[0];
const current = optionMap[pathname];
return ( return (
<ToggleButton <ToggleButton
shape="ghost" shape="ghost"
minWidth={100} minWidth={100}
maxWidth={200}
textAlign="left"
dropdownProps={{ dropdownProps={{
open: visible, open: visible,
onOpenChange: setVisible, onOpenChange: setVisible,
+46 -16
View File
@@ -1,6 +1,8 @@
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import { Box, Group } from 'coral-system'; import { Box, Group } from 'coral-system';
import { ReactComponentProps } from '@music163/tango-helpers'; import { ReactComponentProps } from '@music163/tango-helpers';
import { DesignerViewType } from '@music163/tango-core';
import { observer, useDesigner } from '@music163/tango-context';
import { getWidget } from '../widgets'; import { getWidget } from '../widgets';
export interface ToolbarProps { export interface ToolbarProps {
@@ -15,25 +17,23 @@ export function Toolbar({ children }: ToolbarProps) {
let prevPlacement: string; let prevPlacement: string;
React.Children.forEach(children, (child: React.ReactElement, index) => { React.Children.forEach(children, (child: React.ReactElement, index) => {
let fallbackNode; const childProps = child.props;
let fallbackChildren;
if (child.key) { if (child.key) {
const Widget = getWidget(['toolbar', child.key].join('.')); const Widget = getWidget(['toolbar', child.key].join('.'));
if (Widget) { if (Widget) {
fallbackNode = React.createElement(Widget, child.props.widgetProps); fallbackChildren = <Widget {...childProps.widgetProps} />;
} }
} }
let node = child.props.children ?? fallbackNode ?? null;
if (!node) {
node = child; // separator
}
const key = child.key || index; const key = child.key || index;
node = ( const node = React.cloneElement(child, {
<div key={key} className="ToolbarPanelItem" data-key={key}> key,
{node} 'data-key': key,
</div> children: childProps.children || fallbackChildren,
); });
const placement = child.props.placement || prevPlacement || 'center';
const placement = childProps.placement || prevPlacement || 'center';
switch (placement) { switch (placement) {
case 'left': case 'left':
left.push(node); left.push(node);
@@ -68,17 +68,47 @@ export function Toolbar({ children }: ToolbarProps) {
} }
export interface ToolbarItemProps extends ReactComponentProps { export interface ToolbarItemProps extends ReactComponentProps {
/**
* 类型
*/
type?: 'divider';
placement?: 'left' | 'center' | 'right'; placement?: 'left' | 'center' | 'right';
children?: React.ReactElement; children?: React.ReactElement;
/** /**
* 如果 key 匹配到内置组件的话,传递给子节点的属性 * 如果 key 匹配到内置组件的话,传递给子节点的属性
*/ */
widgetProps?: object; widgetProps?: object;
/**
* 工具箱展示的视图
*/
activeViews?: DesignerViewType[];
} }
function ToolbarItem({ placement, widgetProps, children }: ToolbarItemProps) { const ToolbarItem = observer(
return <div>{children}</div>; ({
} activeViews = ['code', 'design', 'dual'],
type,
placement,
widgetProps,
children,
...rest
}: ToolbarItemProps) => {
const designer = useDesigner();
if (!activeViews.includes(designer.activeView)) {
return null;
}
if (type === 'divider') {
return <Separator />;
}
return (
<div className="ToolbarPanelItem" {...rest}>
{children}
</div>
);
},
);
function Separator() { function Separator() {
return <Box className="Separator" width={1} height={16} bg="colors.custom.toolbarDividerColor" />; return <Box className="Separator" width={1} height={16} bg="colors.custom.toolbarDividerColor" />;
+1 -1
View File
@@ -30,7 +30,7 @@ import {
const widgets = {}; const widgets = {};
export function registerWidget(key: string, component: React.ComponentType<any>) { export function registerWidget(key: string, component: React.ComponentType<any>) {
if (!/^(toolbar|sidebar|selectionMenu|contextMenu)\.[a-zA-z]+$/.test(key)) { if (!/^(toolbar|sidebar|selectionMenu|contextMenu)\.[a-zA-Z]+$/.test(key)) {
throw new Error( throw new Error(
`Invalid widget key(${key}), should start with toolbar, sidebar, contextMenu or selectionMenu`, `Invalid widget key(${key}), should start with toolbar, sidebar, contextMenu or selectionMenu`,
); );
+9 -7
View File
@@ -1,35 +1,37 @@
import React from 'react'; import React from 'react';
import { Box } from 'coral-system'; import { Box, HTMLCoralProps } from 'coral-system';
import { observer, useDesigner } from '@music163/tango-context'; import { observer, useDesigner } from '@music163/tango-context';
import { DesignerViewType } from '@music163/tango-core'; import { DesignerViewType } from '@music163/tango-core';
import { ComponentsPopover } from './components'; import cx from 'classnames';
import { ComponentsPopover, FileErrorsOverlay } from './components';
export interface WorkspaceViewProps { export interface WorkspaceViewProps extends HTMLCoralProps<'div'> {
/** /**
* 视图面板模式,对应 Workspace 的模式 * 视图面板模式,对应 Workspace 的模式
*/ */
mode?: DesignerViewType; mode?: DesignerViewType;
children: React.ReactNode;
} }
export const WorkspaceView = observer((props: WorkspaceViewProps) => { export const WorkspaceView = observer((props: WorkspaceViewProps) => {
const { mode = 'design', children } = props; const { mode = 'design', children, className, ...rest } = props;
const designer = useDesigner(); const designer = useDesigner();
const display = mode !== designer.activeView ? 'none' : 'block'; const display = mode !== designer.activeView ? 'none' : 'block';
// 云音乐移动端模式小屏幕适配,可能会溢出屏幕 // 移动端模式小屏幕适配,可能会溢出屏幕
const overflow = const overflow =
designer.simulator.name === 'phone' ? 'auto' : designer.isPreview ? 'auto' : 'hidden'; designer.simulator.name === 'phone' ? 'auto' : designer.isPreview ? 'auto' : 'hidden';
return ( return (
<Box <Box
className={`ViewPanel ${mode}`} className={cx('ViewPanel', mode, className)}
display={designer.activeView === 'dual' ? 'block' : display} display={designer.activeView === 'dual' ? 'block' : display}
flex="1" flex="1"
overflow={overflow} overflow={overflow}
position="relative" position="relative"
{...rest}
> >
{children} {children}
{/* 添加组件弹层 */} {/* 添加组件弹层 */}
{display === 'block' && <ComponentsPopover type="inner" isControlled />} {display === 'block' && <ComponentsPopover type="inner" isControlled />}
{designer.activeView === 'design' && <FileErrorsOverlay />}
</Box> </Box>
); );
}); });
+24
View File
@@ -3,6 +3,30 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.2.4](https://github.com/netease/tango/compare/@music163/tango-helpers@1.2.3...@music163/tango-helpers@1.2.4) (2024-09-09)
### Bug Fixes
- renderSetter with template ([#202](https://github.com/netease/tango/issues/202)) ([ea96872](https://github.com/netease/tango/commit/ea9687226123c839f7a9e001b4c01aa8315a135d))
## [1.2.3](https://github.com/netease/tango/compare/@music163/tango-helpers@1.2.2...@music163/tango-helpers@1.2.3) (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.2.2](https://github.com/netease/tango/compare/@music163/tango-helpers@1.2.1...@music163/tango-helpers@1.2.2) (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))
## [1.2.1](https://github.com/netease/tango/compare/@music163/tango-helpers@1.2.0...@music163/tango-helpers@1.2.1) (2024-08-01)
### Bug Fixes
- update types ([67d602a](https://github.com/netease/tango/commit/67d602a3ec6b7f74156bb78fda6f3b4bc2676e55))
# [1.2.0](https://github.com/netease/tango/compare/@music163/tango-helpers@1.1.1...@music163/tango-helpers@1.2.0) (2024-06-03) # [1.2.0](https://github.com/netease/tango/compare/@music163/tango-helpers@1.1.1...@music163/tango-helpers@1.2.0) (2024-06-03)
### Features ### Features
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@music163/tango-helpers", "name": "@music163/tango-helpers",
"version": "1.2.0", "version": "1.2.4",
"description": "Shared types, helpers, and hooks of tango-apps", "description": "Shared types, helpers, and hooks of tango-apps",
"keywords": [ "keywords": [
"shared", "shared",
+2 -2
View File
@@ -43,7 +43,7 @@ export function filterTreeData<T = unknown>(
childrenProp = 'children', childrenProp = 'children',
onlyLeaf = false, onlyLeaf = false,
) { ) {
const reducer = (result: T[], current: T) => { const reducer = (result: T[], current: any) => {
if (current[childrenProp]) { if (current[childrenProp]) {
const newChildren = current[childrenProp].reduce(reducer, []); const newChildren = current[childrenProp].reduce(reducer, []);
if (newChildren.length) { if (newChildren.length) {
@@ -70,7 +70,7 @@ export function mapTreeData<T = unknown>(
mapper: (node: T) => any, mapper: (node: T) => any,
childrenProp = 'children', childrenProp = 'children',
) { ) {
return treeData.map((node) => { return treeData.map((node: any) => {
const newNode = mapper(node); const newNode = mapper(node);
if (node[childrenProp]) { if (node[childrenProp]) {
newNode[childrenProp] = mapTreeData(node[childrenProp], mapper, childrenProp); newNode[childrenProp] = mapTreeData(node[childrenProp], mapper, childrenProp);
+1 -1
View File
@@ -32,7 +32,7 @@ export function isValidFunctionCode(str: string) {
} }
} }
const templatePattern = /^{{(.+)}}$/s; const templatePattern = /^{{([\s\S]+)}}$/;
/** /**
* 判断给定代码是否被双花括号包裹 * 判断给定代码是否被双花括号包裹

Some files were not shown because too many files have changed in this diff Show More