Compare commits

...

25 Commits

Author SHA1 Message Date
wwsun
54b3d4a823 fix: display file error source name 2024-07-26 13:50:40 +08:00
wwsun
2e03f21320 feat: check file errors & show errors overlay 2024-07-26 13:22:18 +08:00
wwsun
ed8a725ca9 fix: update 2024-07-25 20:52:57 +08:00
wwsun
08e6db6ce1 feat: add LiveCodeEditor 2024-07-25 19:34:35 +08:00
wwsun
14ddb53710 fix: update 2024-07-24 19:35:11 +08:00
wwsun
3e3ac59578 feat: move editorState to workspace 2024-07-24 18:22:39 +08:00
wwsun
820f67d98f fix: update code style 2024-07-23 20:05:13 +08:00
wwsun
c239bc367f feat: add editorHistory 2024-07-23 16:36:28 +08:00
wwsun
72f2c86619 feat: add editorState 2024-07-23 15:48:12 +08:00
wwsun
51b5107706 fix: hide left bar in code mode 2024-07-22 16:12:35 +08:00
wwsun
a1492dde15 fix: allow pass system props to WorkspaceView 2024-07-12 16:54:06 +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
Wells
0011cdca0f chore(release): publish
- @music163/tango-context@1.1.4
 - @music163/tango-core@1.3.2
 - @music163/tango-designer@1.3.2
 - @music163/tango-sandbox@1.0.7
 - @music163/tango-setting-form@1.2.7
 - @music163/tango-ui@1.3.2
2024-06-20 15:45:08 +08:00
Wells
0f3f0afdfa fix: update designer style (#176)
* fix: remove useless codes

* fix: show insert children icon only for hasChildren node

* fix: update dependency

* fix: update styles

* fix: show sibling action for nodes except page & update drag icon for DragPanel

* fix: update ToggleButton style

* fix: add showDropdownIcon to ToggleButton

* fix: update codeSetter style

* fix: add builtin imageSetter

* fix: update PopOut icon
2024-06-20 15:42:49 +08:00
BoBoooooo
b53a9f7eef fix: drag-panel add resizeable & height prop (#175)
* fix: drag-panel add resizeable & height prop

* fix: add className
2024-06-18 16:56:00 +08:00
Wells
5336c2ec28 docs: update 2024-06-11 09:17:36 +00:00
Wells
cdbd2351a2 docs: update README.zh-CN.md 2024-06-11 17:04:28 +08:00
Wells
1f03977f7d docs: add some examples 2024-06-11 17:01:41 +08:00
864907600cc
a2cc418a73 fix: fix global hotkey triggering on content editable elements (#174) 2024-06-06 14:57:41 +08:00
Wells
239c0b0a28 chore(release): publish
- @music163/tango-context@1.1.3
 - @music163/tango-core@1.3.1
 - @music163/tango-designer@1.3.1
 - @music163/tango-sandbox@1.0.6
 - @music163/tango-setting-form@1.2.6
 - @music163/tango-ui@1.3.1
2024-06-05 18:17:02 +08:00
Wells
ae04850bb2 fix: set value as jsxElement children (#173) 2024-06-05 18:11:57 +08:00
864907600cc
ca740c83ae fix: fix context menu error on macOS (#170)
* fix: fix context menu error on macOS

* fix: adjust context menu icon style
2024-06-03 15:57:58 +08:00
68 changed files with 1277 additions and 497 deletions

1
.gitignore vendored
View File

@@ -9,3 +9,4 @@ coverage/
.umi-production/
.log
.eslintcache
*.pem

View File

@@ -31,23 +31,17 @@ You can view the detailed usage guide through the following links:
- Out-of-the-box front-end low-code designer, providing flexible and easy-to-use designer React components
- Developed using TypeScript, providing complete type definition files
## 🎯 Milestone
## 💡 Examples
```mermaid
gantt
dateFormat YYYY-MM-DD
title Tango 1.0 Milestones
excludes weekends
%% (`excludes` accepts specific dates in YYYY-MM-DD format, days of the week ("sunday") or "weekends", but not the word "weekdays".)
You can use tango to build lowcode builders, for example:
section Builder Release
1.0 Alpha : m1, 2023-10-01, 2024-01-31
1.0 RC : m2, 2024-02-01, 2024-04-30
1.0 Stable : m3, 2024-04-30, 2024-12-31
```
> [!NOTE]
> Alpha versions will continue to optimize some implementation protocols and continuously integrate new features from the internal version of NetEase; RC versions will provide a stable parsing protocol and implementation; The 1.0 official version is planned to be released in Q2 2024. After the release of the 1.0 official version, we will continue to iterate on versions and gradually incorporate core capabilities such as process design, component library, and AIGC from the internal version.
| Preview | Description |
| ---------------------------------------------------------------------------------------------- | ----------------------- |
| ![image](https://github.com/NetEase/tango/assets/6984035/56a71741-4536-4336-803a-d81d51128a76) | Internal 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 |
| ![rn app](public/rn-builder.png) | ReactNative App Builder |
| ![mail app](public/mail-builder.png) | Email Builder |
## 🌐 Compatibility
@@ -76,6 +70,23 @@ yarn
yarn start
```
### Local Https certificate
If you need to use https in the local development environment, you can use the following command to generate a certificate:
```bash
brew install mkcert
# install the local certificate
mkcert -install
# enter the playground app directory
cd apps/playground
# generate a certificate
mkcert local.netease.com # or change to your own domain name
```
## 💬 Community
Join NetEase Tango Community to share your ideas, suggestions, or questions and connect with other users and contributors.

View File

@@ -26,28 +26,22 @@
## ✨ 特性
- 经历网易云音乐内网生产环境的实际检验,可灵活集成应用于低代码平台,本地开发工具等
- 基于源码 AST 驱动,无私有 DSL 和协议
- 提供实时出码能力,支持源码进,源码出
- 开箱即用的前端低代码设计器,提供灵活易用的设计器 React 组件
- 使用 TypeScript 开发,提供完整的类型定义文件
- 提供基于源码 AST 驱动的低代码引擎,无私有 DSL 和协议
- 提供实时出码能力,支持源码进,源码出,适配多种源码开发场景
- 提供开箱即用的前端低代码设计器,提供灵活易用的设计器 React 组件
- 使用 TypeScript 开发,提供完整的类型定义文件和完善的文档支持
## 🎯 里程碑
## 💡 例子
```mermaid
gantt
dateFormat YYYY-MM-DD
title Tango 1.0 Milestones
excludes weekends
%% (`excludes` accepts specific dates in YYYY-MM-DD format, days of the week ("sunday") or "weekends", but not the word "weekdays".)
你可以使用 Tango 快速构建多种类型的基于源码的低代码搭建工具,例如:
section Builder Release
1.0 Alpha : m1, 2023-10-01, 2024-01-31
1.0 RC : m2, 2024-02-01, 2024-04-30
1.0 Stable : m3, 2024-04-30, 2024-12-31
```
> [!NOTE]
> Alpha 版本会持续对一些实现协议进行优化并持续融合网易内网版本中的新增特性RC 版本将会提供基本稳定的解析协议和实现1.0 正式版本计划在 2024 年Q2发布。1.0 正式版发布后我们将会持续进行版本迭代并逐步融入内网版本中的流程设计、组件库、AIGC 等核心基础能力。
| 预览图 | 说明 |
| ---------------------------------------------------------------------------------------------- | -------------------- |
| ![image](https://github.com/NetEase/tango/assets/6984035/56a71741-4536-4336-803a-d81d51128a76) | 中后台系统搭建 |
| ![dashboard app](public/dashboard-builder.png) | 仪表盘应用搭建 |
| ![image](https://github.com/NetEase/tango/assets/6984035/790d88e1-d716-4f99-868a-31dda25e9fb1) | H5活动页面搭建 |
| ![rn app](public/rn-builder.png) | ReactNative 应用搭建 |
| ![mail app](public/mail-builder.png) | 营销类邮件搭建 |
## 🌐 兼容环境
@@ -76,6 +70,23 @@ yarn
yarn start
```
### 本地 https 证书
如果需要在本地开发环境中使用 https可以使用以下命令生成证书
```bash
brew install mkcert
# 将 mkcert 添加到本地根 CA仅在本地生效
mkcert -install
# 进入 playground 应用目录
cd apps/playground
# 为网站生成一个由 mkcert 签名的证书
mkcert local.netease.com
```
## 💬 社区讨论
参与 NetEase Tango 的社区,以分享您的想法、建议或问题,并与其他用户和贡献者建立联系。

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

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

View File

@@ -5,6 +5,7 @@ import { BranchesOutlined, MenuOutlined, QuestionCircleOutlined } from '@ant-des
import { registerSetter } from '@music163/tango-designer';
import type { IVariableTreeNode } from '@music163/tango-helpers';
import { FooSetter } from '../components';
import { ToggleButton } from '@music163/tango-ui';
export * from './mock-files';
@@ -46,8 +47,17 @@ registerSetter({
// 平台 Logo
export function Logo() {
return (
<Box width="50px" display="flex" alignItems="center" justifyContent="center" fontSize="20px">
<MenuOutlined />
<Box width="50px" display="flex" alignItems="center" justifyContent="center" fontSize="14px">
<ToggleButton
shape="text"
items={[
{ label: '回到首页', key: 'home' },
{ label: '文档', key: 'doc' },
{ label: '设置', key: 'setting' },
]}
>
<MenuOutlined />
</ToggleButton>
</Box>
);
}

View File

@@ -2,7 +2,7 @@ const packageJson = {
name: 'demo',
private: true,
dependencies: {
'@music163/tango-mail': '0.1.1',
'@music163/tango-mail': '0.2.4',
'@music163/tango-boot': '0.2.5',
react: '17.0.2',
'react-dom': '17.0.2',
@@ -44,7 +44,7 @@ const tangoConfigJson = {
},
'@music163/tango-mail': {
description: 'TangoMail 基础物料',
version: '0.1.1',
version: '0.2.4',
library: 'TangoMail',
type: 'baseDependency',
resources: ['https://unpkg.com/@music163/tango-mail@{{version}}/dist/index.js'],
@@ -116,7 +116,6 @@ const WelcomeEmail = () => (
</Section>
<Text style={paragraph}>
Best,
<br />
The Koala team
</Text>
<Hr style={hr} />

View File

@@ -3,7 +3,7 @@ const packageJson = {
private: true,
dependencies: {
'@music163/antd': '0.2.5',
'@music163/tango-boot': '0.2.5',
'@music163/tango-boot': '0.3.5',
react: '17.0.2',
'react-dom': '17.0.2',
'prop-types': '15.7.2',
@@ -48,7 +48,7 @@ const tangoConfigJson = {
},
'@music163/tango-boot': {
description: '云音乐低代码运行时框架',
version: '0.2.5',
version: '0.3.5',
library: 'TangoBoot',
type: 'baseDependency',
resources: ['https://unpkg.com/@music163/tango-boot@{{version}}/dist/boot.js'],

View File

@@ -8,7 +8,7 @@ import {
Toolbar,
WorkspacePanel,
WorkspaceView,
CodeEditor,
LiveCodeEditor,
Sandbox,
DndQuery,
themeLight,
@@ -20,12 +20,11 @@ import {
ApiOutlined,
AppstoreAddOutlined,
BuildOutlined,
ClusterOutlined,
FunctionOutlined,
PlusOutlined,
createFromIconfontCN,
} from '@ant-design/icons';
import { Action } from '@music163/tango-ui';
import { Action, PackageOutlined } from '@music163/tango-ui';
import { useState } from 'react';
const menuData = {
@@ -182,12 +181,15 @@ export default function App() {
<Sidebar.Item
key="dependency"
label="依赖"
icon={<ClusterOutlined />}
icon={<PackageOutlined />}
isFloat
width={800}
/>
</Sidebar>
<WorkspacePanel>
<WorkspaceView mode="code">
<LiveCodeEditor />
</WorkspaceView>
<WorkspaceView mode="design">
<Sandbox
onMessage={(e) => {
@@ -209,9 +211,6 @@ export default function App() {
navigatorExtra={<Button size="small">hello world</Button>}
/>
</WorkspaceView>
<WorkspaceView mode="code">
<CodeEditor />
</WorkspaceView>
</WorkspacePanel>
<SettingPanel />
</DesignerPanel>

View File

@@ -79,6 +79,11 @@ const prototypeHasBasicProps: IComponentPrototype = {
title: 'textAreaSetter',
setter: 'textAreaSetter',
},
{
name: 'src',
title: 'imageSetter',
setter: 'imageSetter',
},
{
name: 'number',
title: 'numberSetter',

View File

@@ -9,15 +9,34 @@ export default {
export function Basic() {
return (
<DragPanel
title="弹层标题"
extra="右上角内容"
width={350}
body={<Box p="m"></Box>}
footer={<Text></Text>}
>
<Button></Button>
</DragPanel>
<>
<DragPanel
title="弹层标题"
extra="右上角内容"
width={350}
body={<Box p="m"></Box>}
footer={<Text></Text>}
>
<Button></Button>
</DragPanel>
<DragPanel
title="弹层标题"
extra="右上角内容"
width={350}
body={<Box p="m"></Box>}
footer={<Text></Text>}
>
<Button
style={{
position: 'absolute',
bottom: 20,
right: 20,
}}
>
</Button>
</DragPanel>
</>
);
}
@@ -41,3 +60,23 @@ export function FooterCustom() {
</DragPanel>
);
}
export function ResizeablePanel() {
return (
<DragPanel
title="弹层标题"
resizeable
extra="右上角内容"
width={350}
height={400}
body={
<Box p="m" textAlign="center" background="#fa8484">
</Box>
}
footer={<Text></Text>}
>
<Button></Button>
</DragPanel>
);
}

View File

@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.1.5](https://github.com/netease/tango/compare/@music163/tango-context@1.1.4...@music163/tango-context@1.1.5) (2024-07-12)
**Note:** Version bump only for package @music163/tango-context
## [1.1.4](https://github.com/netease/tango/compare/@music163/tango-context@1.1.3...@music163/tango-context@1.1.4) (2024-06-20)
**Note:** Version bump only for package @music163/tango-context
## [1.1.3](https://github.com/netease/tango/compare/@music163/tango-context@1.1.2...@music163/tango-context@1.1.3) (2024-06-05)
**Note:** Version bump only for package @music163/tango-context
## [1.1.2](https://github.com/netease/tango/compare/@music163/tango-context@1.1.1...@music163/tango-context@1.1.2) (2024-06-03)
**Note:** Version bump only for package @music163/tango-context

View File

@@ -1,6 +1,6 @@
{
"name": "@music163/tango-context",
"version": "1.1.2",
"version": "1.1.5",
"description": "react context for tango-apps",
"keywords": [
"react",
@@ -31,7 +31,7 @@
"react": ">= 16.8"
},
"dependencies": {
"@music163/tango-core": "^1.3.0",
"@music163/tango-core": "^1.3.3",
"@music163/tango-helpers": "^1.2.0",
"mobx-react-lite": "4.0.7"
},

View File

@@ -29,6 +29,10 @@ export const useDesigner = () => {
return useTangoEngine()?.engine.designer;
};
export const useEditorState = () => {
return useTangoEngine()?.engine.workspace.editorState;
};
export const useWorkspaceData = () => {
const ctx = useTangoEngine();
const workspace = useWorkspace();

View File

@@ -3,6 +3,24 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.3.3](https://github.com/netease/tango/compare/@music163/tango-core@1.3.2...@music163/tango-core@1.3.3) (2024-07-12)
### Bug Fixes
- update designer config ([#186](https://github.com/netease/tango/issues/186)) ([85c053b](https://github.com/netease/tango/commit/85c053b3db6d652b41c9873dba1366315174833f))
## [1.3.2](https://github.com/netease/tango/compare/@music163/tango-core@1.3.1...@music163/tango-core@1.3.2) (2024-06-20)
### Bug Fixes
- update designer style ([#176](https://github.com/netease/tango/issues/176)) ([0f3f0af](https://github.com/netease/tango/commit/0f3f0afdfa8aee2532a97c5c2e92ef4230397d86))
## [1.3.1](https://github.com/netease/tango/compare/@music163/tango-core@1.3.0...@music163/tango-core@1.3.1) (2024-06-05)
### Bug Fixes
- set value as jsxElement children ([#173](https://github.com/netease/tango/issues/173)) ([ae04850](https://github.com/netease/tango/commit/ae04850bb251f392575a72ca5fd960249ea9d06b))
# [1.3.0](https://github.com/netease/tango/compare/@music163/tango-core@1.2.0...@music163/tango-core@1.3.0) (2024-06-03)
### Bug Fixes

View File

@@ -1,6 +1,6 @@
{
"name": "@music163/tango-core",
"version": "1.3.0",
"version": "1.3.3",
"description": "tango core",
"author": "wwsun <ww.sun@outlook.com>",
"homepage": "",

View File

@@ -3,7 +3,7 @@
*/
import generator, { GeneratorOptions } from '@babel/generator';
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';
const defaultGeneratorOptions: GeneratorOptions = {
@@ -25,19 +25,12 @@ export function ast2code(ast: t.Node, options: GeneratorOptions = defaultGenerat
return code;
}
const bracketPattern = /^\(.+\)$/s;
/**
* 是否被 () 包裹
*
* @example ({ foo: 'foo' }) -> true
* @example { foo: 'foo' } -> false
*
* @param str 目标字符串
*/
function isWrappingWithBrackets(str: string) {
return bracketPattern.test(str);
}
const bracketPattern = /^\(.+\)$/;
/**
* 将表达式生成为块级代码
@@ -54,7 +47,7 @@ export function expression2code(node: t.Expression) {
const isWrappingExpression = t.isObjectExpression(node) || t.isFunctionExpression(node);
if (isWrappingExpression && isWrappingWithBrackets(ret)) {
if (isWrappingExpression && bracketPattern.test(ret)) {
// 如果是对象,输出包含 ({}),则去掉首尾的括号
ret = ret.slice(1, -1);
}
@@ -195,7 +188,7 @@ export function node2value(node: t.Node, isWrapCode = true): any {
);
if (isSimpleObject) {
// simple object: { key1, key2, key3 }
ret = node.properties.reduce((prev, propertyNode) => {
ret = node.properties.reduce<Dict>((prev, propertyNode) => {
if (propertyNode.type === 'ObjectProperty') {
const key = keyNode2value(propertyNode.key);
const value = node2value(propertyNode.value, isWrapCode);

View File

@@ -68,11 +68,7 @@ export function isValidExpressionCode(code: string) {
* @returns
*/
export function code2ast(code: string): t.File {
try {
return parse(code, babelParserConfig);
} catch (err) {
logger.error('[code2ast failed!]', err);
}
return parse(code, babelParserConfig);
}
/**
@@ -175,7 +171,7 @@ export function value2node(
* 将 js 普通对象解析为 t.Node
*/
export function object2node(
obj: object,
obj: Dict<any>,
getValueNode: (value: any, key?: string) => t.Expression = value2node,
) {
if (!isPlainObject(obj)) {
@@ -222,15 +218,16 @@ export function value2jsxAttributeValueNode(value: any) {
return ret;
}
// TODO: 待校验
export function value2jsxChildrenValueNode(value: any) {
let ret: t.JSXElement | t.JSXFragment | t.JSXExpressionContainer | t.JSXSpreadChild | t.JSXText;
switch (typeof value) {
case 'string':
if (isValidExpressionCode(value)) {
if (isWrappedCode(value)) {
// 代码模式
const innerString = getVariableContent(value);
ret = t.jsxExpressionContainer(code2expression(innerString));
} else {
// 普通的文本
ret = t.jsxText(value);
}
break;

View File

@@ -391,8 +391,10 @@ export function updateJSXElementAttribute(node: t.JSXElement, attrName: string,
isExist = true;
// @ts-ignore
if (t.isJSXAttribute(jsxAttributeNode)) {
// 更新 jsx 属性值 <Foo bar="1" /> 的模式
jsxAttributeNode.value = value2jsxAttributeValueNode(attrValue);
} else if (name === 'children') {
// 更新 jsx children 的情况 <Foo>children</Foo>
node.children = value2jsxChildrenValueNode(attrValue);
}
}
@@ -1007,7 +1009,7 @@ export function serviceConfig2Node(payload: object) {
});
}
export function updateServiceConfigToServiceFile(ast: t.File, config: Dict<object>) {
export function updateServiceConfigToServiceFile(ast: t.File, config: Dict<Dict>) {
traverse(ast, {
CallExpression(path) {
const calleeName = keyNode2value(path.node.callee) as string;
@@ -1015,7 +1017,7 @@ export function updateServiceConfigToServiceFile(ast: t.File, config: Dict<objec
const configNode = path.node.arguments[0];
if (t.isObjectExpression(configNode)) {
const newPropertiesNodeMap = Object.keys(config).reduce((properties, key) => {
const newPropertiesNodeMap: Dict = Object.keys(config).reduce<Dict>((properties, key) => {
const serviceConfig = config[key];
const property = t.objectProperty(t.identifier(key), serviceConfig2Node(serviceConfig));
properties[key] = property;

View File

@@ -1,6 +1,6 @@
import { action, computed, makeObservable, observable, toJS } from 'mobx';
import { IWorkspace } from './interfaces';
import { MenuDataType } from '@music163/tango-helpers';
import { IWorkspace } from './interfaces';
export type SimulatorNameType = 'desktop' | 'phone';
@@ -185,6 +185,7 @@ export class Designer {
// 默认激活的视图
if (defaultActiveView) {
this.setActiveView(defaultActiveView);
this.workspace.setMode(defaultActiveView === 'design' ? 'design' : 'code');
}
makeObservable(this, {
@@ -238,6 +239,23 @@ export class Designer {
}
setActiveView(view: DesignerViewType) {
const prevView = this._activeView;
if (view === prevView) {
return;
}
if (view === 'dual') {
this.setActiveSidebarPanel('');
this.toggleRightPanel(false);
this.toggleIsPreview(true);
this.workspace.setMode('code');
} else if (view === 'design') {
this.toggleIsPreview(false);
this.workspace.setMode('design');
} else if (view === 'code') {
this.workspace.setMode('code');
}
this._activeView = view;
}

View File

@@ -0,0 +1,253 @@
import { action, computed, makeObservable, observable } from 'mobx';
import { FileType, IFileConfig } from '../types';
interface EditorActionPayload {
filename: string;
newFilename?: string;
code: string;
type?: FileType;
}
type AddFilePayload = Omit<EditorActionPayload, 'newFileName'>;
type DeleteFilePayload = Pick<EditorActionPayload, 'filename'>;
type RenameFilePayload = Pick<EditorActionPayload, 'filename' | 'newFilename'>;
type UpdateFilePayload = Pick<EditorActionPayload, 'filename' | 'code'>;
interface EditorStateConfig {
defaultActiveFile: string;
files: IFileConfig[];
}
export class EditorState {
_files: Map<string, Pick<IFileConfig, 'code' | 'type'>> = new Map();
_activeFile: string;
_history: EditorHistory;
get activeFile() {
return this._activeFile;
}
/**
* 文件是否存在变更记录
*/
get isFilesChanged() {
return this._history.size > 1;
}
constructor(options?: EditorStateConfig) {
this._history = new EditorHistory();
this._activeFile = options?.defaultActiveFile;
this.addFiles(options?.files);
makeObservable(this, {
_files: observable,
_activeFile: observable,
activeFile: computed,
setActiveFile: action,
updateFile: action,
addFile: action,
addFiles: action,
deleteFile: action,
deleteFolder: action,
_renameFile: action,
});
}
clear() {
this._files.clear();
this._history.clear();
}
setActiveFile(filename: string) {
this._activeFile = filename;
}
/**
* 返回文件列表数组
* @returns
*/
listFileData() {
const list: IFileConfig[] = [];
this._files.forEach((value, filename) => {
list.push({
...value,
filename,
});
});
return list;
}
/**
* 返回文件列表对象
* @returns 文件列表对象 { [filename]: code }
*/
getFiles(): Record<string, string> {
const fileCodes: Record<string, string> = {};
this._files.forEach((fileData, filename) => {
fileCodes[filename] = fileData.code;
});
return fileCodes;
}
addFile({ filename, ...rest }: AddFilePayload) {
this._files.set(filename, rest);
this._history.push({
message: 'addFile',
data: [
{
filename,
...rest,
},
],
});
}
addFiles(fileList: AddFilePayload[]) {
if (!fileList || !fileList.length) {
return;
}
fileList.forEach(({ filename, ...rest }) => {
this._files.set(filename, rest);
});
this._history.push({
message: 'addFiles',
data: [...fileList],
});
}
deleteFile({ filename }: DeleteFilePayload) {
const file = this._files.get(filename);
this._files.delete(filename);
this._history.push({
message: 'deleteFile',
data: [
{
filename,
...file,
},
],
});
}
deleteFolder(payload: DeleteFilePayload) {
const deleteFiles: AddFilePayload[] = [];
const fileNames = Object.keys(this._files);
for (const filename of fileNames) {
if (filename.startsWith(payload.filename)) {
const file = this._files.get(filename);
deleteFiles.push({
filename,
...file,
});
this._files.delete(filename);
}
}
this._history.push({
message: 'deleteFolder',
data: deleteFiles,
});
}
_renameFile({ filename, newFilename }: RenameFilePayload) {
const fileData = this._files.get(filename);
this._files.set(newFilename, { ...fileData });
this._files.delete(filename);
}
renameFile(payload: RenameFilePayload) {
this._renameFile(payload);
this._history.push({
message: 'renameFile',
data: [payload],
});
}
renameFolder({ filename: oldFolderName, newFilename: newFolderName }: RenameFilePayload) {
const fileNames = Object.keys(this._files);
for (const filename of fileNames) {
if (filename.startsWith(oldFolderName)) {
const newFilename = filename.replace(oldFolderName, newFolderName);
this._renameFile({ filename, newFilename });
}
}
this._history.push({
message: 'renameFolder',
data: [
{
filename: oldFolderName,
newFilename: newFolderName,
},
],
});
}
updateFile({ filename, code }: UpdateFilePayload) {
const fileData = this._files.get(filename);
if (fileData.code === code) {
// 代码未发生变化
return;
}
fileData.code = code;
this._files.set(filename, fileData);
this._history.push({
message: 'updateFile',
data: [
{
filename,
code: fileData.code,
},
],
});
}
}
interface EditorHistoryRecord {
data: Array<Partial<AddFilePayload>>;
time: number;
message: string;
}
/**
* TODO: 实现上需要要 TangoHistory 合并
*/
class EditorHistory {
_records: EditorHistoryRecord[] = [];
_index = 0;
_maxSize = 100;
get size() {
return this._records.length;
}
push(payload: Omit<EditorHistoryRecord, 'time'>) {
if (this._index < this.size - 1) {
this._records = this._records.slice(0, this._index + 1);
}
this._index = this.size;
this._records.push({
time: Date.now(),
...payload,
});
if (this.size > this._maxSize) {
this._records.splice(0, this.size - this._maxSize);
this._index = this.size - 1;
}
}
clear() {
this._records = [];
this._index = 0;
}
}

View File

@@ -24,6 +24,16 @@ export class TangoFile {
*/
lastModified: number;
/**
* 文件是否存在错误
*/
isError: boolean;
/**
* 文件的错误消息
*/
errorMessage: string;
_code: string;
_cleanCode: string;
@@ -40,6 +50,7 @@ export class TangoFile {
this.filename = props.filename;
this.type = props.type;
this.lastModified = Date.now();
this.isError = false;
// 这里主要是为了解决 umi ts 编译错误的问题,@see https://github.com/umijs/umi/issues/7594
if (isSyncCode) {

View File

@@ -10,6 +10,7 @@ import {
IPageConfigData,
IImportSpecifierSourceData,
IImportSpecifierData,
IFileError,
} from '../types';
import { TangoFile, TangoJsonFile } from './file';
import { TangoRouteModule } from './route-module';
@@ -17,6 +18,7 @@ import { TangoStoreModule } from './store-module';
import { TangoServiceModule } from './service-module';
import { IdGenerator } from '../helpers';
import { AppEntryModule } from './entry-module';
import { EditorState } from './editor-state';
export interface IViewFile {
readonly workspace: IWorkspace;
@@ -136,16 +138,26 @@ export interface IViewNode {
get loc(): unknown;
}
export type WorkspaceMode = 'design' | 'code';
export interface IWorkspace {
history: TangoHistory;
selectSource: SelectSource;
dragSource: DragSource;
editorState: EditorState;
/**
* 工作区模式: design-设计模式, code-源码模式
*/
mode: WorkspaceMode;
files: Map<string, TangoFile>;
componentPrototypes: Map<string, IComponentPrototype>;
entry: string;
activeFile: string;
/**
* @deprecated 已废弃,使用 editor.activeFile 代替
*/
activeFile?: string;
activeViewFile: string;
activeRoute: string;
@@ -173,8 +185,16 @@ export interface IWorkspace {
ready: () => void;
refresh: (names: string[]) => void;
setMode: (mode: WorkspaceMode) => void;
setActiveRoute: (path: string) => void;
setActiveFile: (filename: string) => void;
/**
* @deprecated 已废弃,使用 editor.setActiveFile 代替
* @param filename
* @returns
*/
setActiveFile?: (filename: string) => void;
setComponentPrototypes: (prototypes: Record<string, IComponentPrototype>) => void;
getPrototype: (name: string | IComponentPrototype) => IComponentPrototype;
@@ -188,12 +208,14 @@ export interface IWorkspace {
addViewFile: (viewName: string, code: string) => void;
removeFile: (filename: string) => void;
clearFiles: () => void;
renameFile: (oldFilename: string, newFilename: string) => void;
renameFolder: (oldFoldername: string, newFoldername: string) => void;
updateFile: (filename: string, code: string, shouldFormatCode?: boolean) => void;
listFileData: () => IFileConfig[];
listFiles: () => Record<string, string>;
getFile: (filename: string) => TangoFile;
@@ -298,6 +320,10 @@ export interface IWorkspace {
get bizComps(): string[];
get baseComps(): string[];
get localComps(): string[];
/**
* 文件错误列表
*/
get fileErrors(): IFileError[];
/**
* 是否是合法的项目
*/

View File

@@ -38,18 +38,26 @@ export class TangoModule extends TangoFile {
*/
update(code?: string, isFormatCode = true, refreshWorkspace = true) {
this.lastModified = Date.now();
if (isNil(code)) {
this._syncByAst();
} else {
this._syncByCode(code, isFormatCode);
}
this._analysisAst();
try {
if (isNil(code)) {
this._syncByAst();
} else {
this._syncByCode(code, isFormatCode);
}
this._analysisAst();
this.workspace.onFilesChange([this.filename]);
this.isError = false;
this.errorMessage = undefined;
if (refreshWorkspace) {
this.workspace.refresh([this.filename]);
this.workspace.onFilesChange([this.filename]);
if (refreshWorkspace) {
this.workspace.refresh([this.filename]);
}
} catch (err: any) {
this.isError = true;
this.errorMessage = err.message;
}
}
@@ -110,6 +118,8 @@ export class TangoJsModule extends TangoModule {
makeObservable(this, {
_code: observable,
_cleanCode: observable,
isError: observable,
errorMessage: observable,
code: computed,
cleanCode: computed,
update: action,

View File

@@ -1,4 +1,5 @@
import { action, computed, makeObservable, observable, toJS } from 'mobx';
import { logger } from '@music163/tango-helpers';
import {
traverseRouteFile,
addRouteToRouteFile,
@@ -27,6 +28,8 @@ export class TangoRouteModule extends TangoModule {
_routes: observable,
_code: observable,
_cleanCode: observable,
isError: observable,
errorMessage: observable,
routes: computed,
code: computed,
cleanCode: computed,
@@ -74,7 +77,7 @@ export class TangoRouteModule extends TangoModule {
*/
removeRoute(route: string) {
if (route === '/') {
console.warn('index route should not be removed!');
logger.warn('index route should not be removed!');
return;
}
const record = this.getRouteByRoutePath(route);

View File

@@ -49,6 +49,8 @@ export class TangoServiceModule extends TangoModule {
_baseConfig: observable,
_code: observable,
_cleanCode: observable,
isError: observable,
errorMessage: observable,
serviceFunctions: computed,
baseConfig: computed,
cleanCode: computed,

View File

@@ -31,6 +31,8 @@ export class TangoStoreEntryModule extends TangoModule {
_stores: observable,
_code: observable,
_cleanCode: observable,
isError: observable,
errorMessage: observable,
stores: computed,
code: computed,
cleanCode: computed,

View File

@@ -138,6 +138,9 @@ export class TangoViewModule extends TangoModule implements IViewFile {
_code: observable,
_cleanCode: observable,
isError: observable,
errorMessage: observable,
code: computed,
cleanCode: computed,
@@ -320,7 +323,7 @@ export class TangoViewModule extends TangoModule implements IViewFile {
updateNodeAttributes(nodeId: string, config: Record<string, any>, relatedImports?: string[]) {
if (relatedImports && relatedImports.length) {
// 导入依赖的组件
const newImportData = relatedImports.reduce((prev, name) => {
const newImportData = relatedImports.reduce<Dict<IImportSpecifierData[]>>((prev, name) => {
const proto = this.workspace.getPrototype(name);
const { source, specifiers } = prototype2importDeclarationData(proto, this.filename);
const existSpecifiers: IImportSpecifierData[] = prev[source];

View File

@@ -11,6 +11,7 @@ import {
parseServiceVariablePath,
parseStoreVariablePath,
uniq,
setValue,
} from '@music163/tango-helpers';
import {
prototype2jsxElement,
@@ -26,8 +27,8 @@ import { HistoryMessage, TangoHistory } from './history';
import { TangoNode } from './node';
import { TangoJsModule } from './module';
import { TangoFile, TangoJsonFile, TangoLessFile } from './file';
import { IWorkspace } from './interfaces';
import { IFileConfig, FileType, ITangoConfigPackages, IPageConfigData } from '../types';
import { IWorkspace, WorkspaceMode } from './interfaces';
import { IFileConfig, FileType, ITangoConfigPackages, IPageConfigData, IFileError } from '../types';
import { SelectSource } from './select-source';
import { DragSource } from './drag-source';
import { TangoRouteModule } from './route-module';
@@ -36,6 +37,7 @@ import { TangoServiceModule } from './service-module';
import { TangoViewModule } from './view-module';
import { TangoComponentsEntryModule } from './component-module';
import { AppEntryModule } from './entry-module';
import { EditorState } from './editor-state';
export interface IWorkspaceOptions {
/**
@@ -64,9 +66,27 @@ export interface IWorkspaceOptions {
* 工作区
*/
export class Workspace extends EventTarget implements IWorkspace {
/**
* 历史记录
*/
history: TangoHistory;
/**
* 选中源
*/
selectSource: SelectSource;
/**
* 拖拽源
*/
dragSource: DragSource;
/**
* 编辑器状态
*/
editorState: EditorState;
/**
* 工作区模式
*/
mode: WorkspaceMode;
/**
* 工作区的文件列表
@@ -88,11 +108,6 @@ export class Workspace extends EventTarget implements IWorkspace {
*/
activeRoute: string;
/**
* 当前选中的文件
*/
activeFile: string;
/**
* 当前选中的视图文件
*/
@@ -222,15 +237,32 @@ export class Workspace extends EventTarget implements IWorkspace {
return Object.keys(this.componentsEntryModule?.exportList || {});
}
get fileErrors() {
const list: IFileError[] = [];
this.files.forEach((file) => {
if (file.isError) {
list.push({
filename: file.filename,
errorMessage: file.errorMessage,
});
}
});
return list;
}
constructor(options?: IWorkspaceOptions) {
super();
this.history = new TangoHistory(this);
this.selectSource = new SelectSource(this);
this.dragSource = new DragSource(this);
this.editorState = new EditorState({
defaultActiveFile: options?.entry,
files: options?.files,
});
this.mode = 'design';
this.componentPrototypes = new Map();
this.entry = options?.entry;
this.activeRoute = options?.defaultActiveRoute || '/';
this.activeFile = options?.entry;
this.activeViewFile = '';
this.files = new Map();
this.isReady = false;
@@ -249,19 +281,50 @@ export class Workspace extends EventTarget implements IWorkspace {
}
makeObservable(this, {
mode: observable,
files: observable,
activeRoute: observable,
activeFile: observable,
activeViewFile: observable,
pages: computed,
bizComps: computed,
fileErrors: computed,
setActiveRoute: action,
setActiveFile: action,
addFile: action,
removeFile: action,
setMode: action,
});
}
/**
* 设置工作区模式
*/
setMode(mode: WorkspaceMode) {
const prevMode = this.mode;
if (prevMode === mode) {
return;
}
this.mode = mode;
if (mode === 'design') {
// 从源码切换到设计模式
if (this.editorState.isFilesChanged) {
this.clearFiles();
this.addFiles(this.editorState.listFileData());
}
} else {
// 从设计切换到源码模式
if (this.fileErrors.length) {
// 工作区文件存在语法错误,不同步
return;
}
this.editorState.clear();
this.editorState.addFiles(this.listFileData());
}
}
getPrototype(name: string | IComponentPrototype) {
if (isString(name)) {
return this.componentPrototypes.get(name);
@@ -282,17 +345,6 @@ export class Workspace extends EventTarget implements IWorkspace {
this.setActiveViewFile(routePath);
}
/**
* 设置当前选中的文件
* @param filename
*/
setActiveFile(filename: string, isViewFile = false) {
this.activeFile = filename;
if (isViewFile) {
this.activeViewFile = filename;
}
}
/**
* 根据当前的路由计算当前的视图模块
*/
@@ -308,7 +360,7 @@ export class Workspace extends EventTarget implements IWorkspace {
}
}
if (filename) {
this.setActiveFile(filename, true);
this.editorState.setActiveFile(filename);
}
}
@@ -441,7 +493,9 @@ export class Workspace extends EventTarget implements IWorkspace {
updateFile(filename: string, code: string, shouldFormatCode = false) {
const file = this.getFile(filename);
file.update(code);
if (shouldFormatCode && file instanceof TangoViewModule) {
const shouldFormat = shouldFormatCode ?? this.projectConfig?.designerConfig?.autoFormatCode;
if (shouldFormat && file instanceof TangoViewModule) {
file.removeUnusedImportSpecifiers().update();
}
this.history.push({
@@ -472,6 +526,10 @@ export class Workspace extends EventTarget implements IWorkspace {
}
}
clearFiles() {
this.files.clear();
}
/**
* 重命名文件
* @param oldFilename
@@ -513,13 +571,29 @@ export class Workspace extends EventTarget implements IWorkspace {
* @returns { [filename]: fileCode }
*/
listFiles() {
const ret = {};
const ret: Dict<string> = {};
this.files.forEach((file) => {
ret[file.filename] = file.cleanCode;
});
return ret;
}
/**
* 获取文件列表FileData 参数形式)
* @returns Array<{ filename, code, type }>
*/
listFileData() {
const ret: IFileConfig[] = [];
this.files.forEach((file) => {
ret.push({
filename: file.filename,
code: file.cleanCode,
type: file.type,
});
});
return ret;
}
/**
* 删除视图模块
* @param route 路由名称
@@ -741,6 +815,7 @@ export class Workspace extends EventTarget implements IWorkspace {
* 更新依赖,没有就添加
* @param name
* @param version
* FIXME: 参数3的设置需要重新考虑下
*/
updateDependency(
name: string,
@@ -763,15 +838,12 @@ export class Workspace extends EventTarget implements IWorkspace {
if (!packages) {
return undefined;
}
if (options?.package || packages[name]) {
packages[name] = {
type: packages[name]?.type,
// 如果没有传入 package 信息,则沿用 tango.config.json 中已记录的数据
// 如果传入了,则全量使用传入的信息,适配可能在某个版本中如 resources 等字段被删除的情况
...(options?.package || packages[name]),
version,
};
}
setValue(packages, name, {
...packages[name], // 保留原有的配置
version, // 更新版本号
...options?.package, // 更新 package 配置
});
return packages;
})

View File

@@ -57,6 +57,11 @@ export interface IFileConfig {
type?: FileType;
}
export interface IFileError {
filename: string;
errorMessage: string;
}
/**
* 视图节点数据类型
*/

View File

@@ -71,7 +71,7 @@ describe('helpers', () => {
});
it('expression2code: arrowFunctionExpression', () => {
expect(expression2code(code2expression('() => {}'))).toEqual('() => {}');
expect(expression2code(code2expression('() => {\n\n}\n'))).toEqual('() => {\n\n}');
});
it('expression2code: memberExpression', () => {

View File

@@ -3,6 +3,27 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.3.3](https://github.com/netease/tango/compare/@music163/tango-designer@1.3.2...@music163/tango-designer@1.3.3) (2024-07-12)
### Bug Fixes
- selection bug without prototype & update ToggleButton style ([#180](https://github.com/netease/tango/issues/180)) ([1d0fb4f](https://github.com/netease/tango/commit/1d0fb4f274a5b767962c54583ab5863329d5bcc5))
- update designer config ([#186](https://github.com/netease/tango/issues/186)) ([85c053b](https://github.com/netease/tango/commit/85c053b3db6d652b41c9873dba1366315174833f))
## [1.3.2](https://github.com/netease/tango/compare/@music163/tango-designer@1.3.1...@music163/tango-designer@1.3.2) (2024-06-20)
### Bug Fixes
- drag-panel add resizeable & height prop ([#175](https://github.com/netease/tango/issues/175)) ([b53a9f7](https://github.com/netease/tango/commit/b53a9f7eefc748ba8a7c94270c3a374b9d6ed211))
- fix global hotkey triggering on content editable elements ([#174](https://github.com/netease/tango/issues/174)) ([a2cc418](https://github.com/netease/tango/commit/a2cc418a73edfd9b7fe8aad456ae775a49df2166))
- update designer style ([#176](https://github.com/netease/tango/issues/176)) ([0f3f0af](https://github.com/netease/tango/commit/0f3f0afdfa8aee2532a97c5c2e92ef4230397d86))
## [1.3.1](https://github.com/netease/tango/compare/@music163/tango-designer@1.3.0...@music163/tango-designer@1.3.1) (2024-06-05)
### Bug Fixes
- fix context menu error on macOS ([#170](https://github.com/netease/tango/issues/170)) ([ca740c8](https://github.com/netease/tango/commit/ca740c83ae7f72a75c7db90d2e137a7e9a032326))
# [1.3.0](https://github.com/netease/tango/compare/@music163/tango-designer@1.2.3...@music163/tango-designer@1.3.0) (2024-06-03)
### Bug Fixes

View File

@@ -1,6 +1,6 @@
{
"name": "@music163/tango-designer",
"version": "1.3.0",
"version": "1.3.3",
"description": "lowcode designer",
"keywords": [
"react"
@@ -33,12 +33,12 @@
"dependencies": {
"@ant-design/icons": "^4.8.0",
"@music163/request": "^0.2.0",
"@music163/tango-context": "^1.1.2",
"@music163/tango-core": "^1.3.0",
"@music163/tango-context": "^1.1.5",
"@music163/tango-core": "^1.3.3",
"@music163/tango-helpers": "^1.2.0",
"@music163/tango-sandbox": "^1.0.5",
"@music163/tango-setting-form": "^1.2.5",
"@music163/tango-ui": "^1.3.0",
"@music163/tango-sandbox": "^1.0.8",
"@music163/tango-setting-form": "^1.2.8",
"@music163/tango-ui": "^1.3.3",
"antd": "^4.24.2",
"cash-dom": "^8.1.2",
"classnames": "^2.5.1",

View File

@@ -125,6 +125,8 @@ export const ComponentsPopover = observer(
}
footer={tipsTextMap[type]}
width="330px"
height="450px"
resizeable
maskClosable
body={
<ComponentsPanel
@@ -133,10 +135,6 @@ export const ComponentsPopover = observer(
menuData={menuData}
layout={layout}
onItemSelect={handleSelect}
style={{
maxHeight: '400px',
overflow: 'auto',
}}
/>
}
{...popoverProps}

View File

@@ -12,7 +12,12 @@ const contextMenuStyle = css`
}
.ant-dropdown-menu-title-content {
padding-left: 20px;
padding-left: 22px;
}
.ant-dropdown-menu-item-icon {
font-size: 14px;
width: 14px;
}
.ant-dropdown-menu-item-icon + .ant-dropdown-menu-title-content {

View File

@@ -0,0 +1,65 @@
import React, { useState } from 'react';
import { Box, css } from 'coral-system';
import { observer, useWorkspace } from '@music163/tango-context';
import { CloseOutlined } from '@ant-design/icons';
const errorMessageStyle = css`
padding: 12px;
white-space: pre-wrap;
`;
/**
* 文件错误提示浮层
*/
export const FilesErrorOverlay = observer(() => {
const [isVisible, setIsVisible] = useState<boolean>(true);
const workspace = useWorkspace();
const handleClose = () => {
setIsVisible(false);
};
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="rgba(244, 244, 244, 0.9)"
color="red"
zIndex={1999}
>
<Box position="relative" p="4">
<CloseOutlined
style={{
position: 'absolute',
top: 12,
right: 12,
color: '#000',
}}
onClick={handleClose}
/>
<Box css={errorMessageStyle}>
{workspace.fileErrors.map((fileError) => (
<Box key={fileError.filename}>
<Box fontWeight="bold">{fileError.filename}</Box>
<Box as="code" display="block">
{fileError.errorMessage}
</Box>
</Box>
))}
</Box>
</Box>
</Box>
);
});

View File

@@ -4,3 +4,4 @@ export * from './input-kv';
export * from './variable-tree';
export * from './variable-tree-modal';
export * from './components-popover';
export * from './files-error-overlay';

View File

@@ -386,7 +386,8 @@ export function useDnd({
};
const onKeyDown = (e: React.KeyboardEvent) => {
const isInputElement = isInputElements((e.target as HTMLElement).nodeName);
const target = e.target as HTMLElement;
const isInputElement = target.isContentEditable || isInputElements(target.nodeName);
// 禁用输入事件的触发的快捷键逻辑
if (!isInputElement) {
const key = getHotkey(e);

View File

@@ -1,9 +1,8 @@
import React, { useRef, useEffect, useCallback } from 'react';
import { Box } from 'coral-system';
import { Box, css } from 'coral-system';
import { MultiEditor, MultiEditorProps } from '@music163/tango-ui';
import { observer, useDesigner, useWorkspace } from '@music163/tango-context';
import { isValidCode } from '@music163/tango-core';
import { Modal } from 'antd';
import { observer, useDesigner, useEditorState, useWorkspace } from '@music163/tango-context';
import { LiveEditorSandbox } from './sandbox';
const ideConfig = {
// disableFileOps: {
@@ -23,114 +22,125 @@ const ideConfig = {
export interface CodeEditorProps extends Partial<MultiEditorProps> {
/**
* 是否自动清未使用的导入
* 是否自动清未使用的导入
* @deprecated 已废弃,不再支持
*/
autoRemoveUnusedImports?: boolean;
}
export const CodeEditor = observer(
({ autoRemoveUnusedImports = true, ...rest }: CodeEditorProps) => {
const editorRef = useRef(null);
const workspace = useWorkspace();
const designer = useDesigner();
const files = workspace.listFiles();
const activeFile = workspace.activeFile;
export const CodeEditor = observer(({ autoRemoveUnusedImports, ...rest }: CodeEditorProps) => {
const editorRef = useRef(null);
const workspace = useWorkspace();
const designer = useDesigner();
const editorState = useEditorState();
const files = editorState.getFiles();
let loc: any; // 记录视图代码的选中位置
const selectNode = workspace.selectSource.firstNode;
if (selectNode && activeFile === workspace.activeViewFile) {
loc = selectNode.loc;
}
const activeFile = editorState.activeFile;
useEffect(() => {
editorRef.current?.refresh(files, activeFile, loc);
}, [files, activeFile, loc]);
let loc: any; // 记录视图代码的选中位置
const selectNode = workspace.selectSource.firstNode;
if (selectNode && activeFile === workspace.activeViewFile) {
loc = selectNode.loc;
}
const fileSave = useCallback(
(path: string, value: string) => {
if (!isJsFile(path)) {
// 非 js 文件直接保存
workspace.updateFile(path, value, autoRemoveUnusedImports);
return;
useEffect(() => {
editorRef.current?.refresh(files, activeFile, loc);
}, [files, activeFile, loc]);
const fileSave = useCallback(
(path: string, value: string) => {
editorState.updateFile({ filename: path, code: value });
},
[editorState],
);
const handleFileChange = useCallback(
(type: string, info: any) => {
switch (type) {
case 'addFile':
editorState.addFile({ filename: info.path, code: info.value });
break;
case 'deleteFile':
editorState.deleteFile({ filename: info.path });
break;
case 'deleteFolder':
editorState.deleteFolder({ filename: info.path });
break;
case 'renameFile':
editorState.renameFile({ filename: info.path, newFilename: info.newpath });
break;
case 'renameFolder':
editorState.renameFolder({ filename: info.path, newFilename: info.newpath });
break;
case 'addFolder':
default:
break;
}
},
[editorState],
);
const handlePathChange = useCallback(
(path: string) => {
editorState.setActiveFile(path);
},
[editorState],
);
const borderStyle =
designer.activeView === 'dual'
? {
borderLeft: 'solid 1px var(--tango-colors-line2)',
}
: {};
// js 文件需要先检查语法,只有语法正确才会保存
if (isValidCode(value)) {
workspace.updateFile(path, value, autoRemoveUnusedImports);
} else {
Modal.confirm({
title: '检测到代码中存在语法错误,暂时无法将代码同步给设计器,是否回退到安全代码?',
onOk: () => {
editorRef.current?.refresh(files, activeFile);
},
onCancel: () => {},
});
}
},
[workspace, autoRemoveUnusedImports, activeFile, files],
);
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>
);
});
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 liveEditorWrapperStyle = css`
.LiveCodeEditorLeft,
.LiveCodeEditorRight {
overflow: hidden;
transition: flex 100ms ease-in-out 0s;
}
`;
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}
/>
export const LiveCodeEditor = observer(() => {
const designer = useDesigner();
return (
<Box display="flex" className="LiveCodeEditor" height="100%" css={liveEditorWrapperStyle}>
<Box
flex={designer.activeView === 'dual' ? '55 1 0px' : '100 1 0px'}
className="LiveCodeEditorLeft"
>
<CodeEditor />
</Box>
);
},
);
function isJsFile(path: string) {
return /.jsx?$/.test(path);
}
<Box
flex={designer.activeView === 'dual' ? '45 1 0px' : '0 1 0px'}
borderLeft="solid"
borderLeftColor="line2"
className="LiveCodeEditorRight"
>
<LiveEditorSandbox />
</Box>
</Box>
);
});

View File

@@ -26,7 +26,7 @@ const navigatorStyle = css`
}
`;
interface NavigatorProps {
export interface NavigatorProps {
disabled?: boolean;
startRoute?: string;
onBack?: () => void;

View File

@@ -6,7 +6,7 @@ import { observer, useWorkspace, useDesigner } from '@music163/tango-context';
import { Simulator, Viewport } from '../simulator';
import { useSandboxQuery } from '../context';
import { DndQuery, useDnd } from '../dnd';
import { Navigator } from './navigator';
import { Navigator, NavigatorProps } from './navigator';
import { SelectionToolsProps } from '../simulator/selection';
import { DESIGN_SANDBOX_ID, PREVIEW_SANDBOX_ID } from '../helpers';
@@ -18,6 +18,7 @@ interface ISandboxEventHandlerConfig {
}
export type SandboxProps = Omit<CodeSandboxProps, 'files' | 'eventHandlers' | 'onMessage'> & {
files?: CodeSandboxProps['files'];
isPreview?: boolean;
selectionTools?: SelectionToolsProps['actions'];
/**
@@ -54,6 +55,7 @@ function fixRouterMode(routerType: string): CodeSandboxProps['routerMode'] {
}
function useSandbox({
files: filesProp,
isPreview: isPreviewProp,
configFormatter,
filesFormatter,
@@ -88,14 +90,29 @@ function useSandbox({
onViewChange: (data) => onViewChange && onViewChange(data, getSandboxConfig()),
});
let files = Array.from(workspace.files.keys()).reduce((prev, filename) => {
let code = workspace.getFile(filename).code;
if (filename === '/tango.config.json') {
code = mergeTangoConfigJson(code, { isPreview, injectScript, formatter: configFormatter });
}
prev[filename] = { code };
return prev;
}, {});
let files = filesProp;
if (!files) {
files = Array.from(workspace.files.keys()).reduce<CodeSandboxProps['files']>(
(prev, filename) => {
const code = workspace.getFile(filename).code;
prev[filename] = { code };
return prev;
},
{},
);
}
// fix tangoConfigJson
const tangoConfigJson = files['/tango.config.json'];
if (tangoConfigJson) {
files['/tango.config.json'] = {
code: mergeTangoConfigJson(tangoConfigJson.code, {
isPreview,
injectScript,
formatter: configFormatter,
}),
};
}
files = fixSandboxFiles(files, workspace.entry);
if (filesFormatter) {
@@ -303,16 +320,60 @@ export const CombinedSandbox = observer(
},
);
const FALLBACK_SANDBOX_BUNDLER_URL = 'https://codesandbox.fn.netease.com';
interface UseSandboxNavigatorProps {
sandbox?: any;
bundlerUrl?: string;
}
function useSandboxNavigator({ sandbox, bundlerUrl }: UseSandboxNavigatorProps): NavigatorProps {
const workspace = useWorkspace();
return {
disabled: !sandbox?.iframe,
startRoute: workspace.activeRoute,
onInputEnter: (newUrl) => {
if (newUrl?.split('?')[0] === workspace.activeRoute) {
// 只是输入了路由参数,默认为调试模式,直接更新沙箱内的路由地址
sandbox?.updateRoute(newUrl);
} else {
// 如果是输入了其他路由,则直接跳转
workspace.setActiveRoute(newUrl);
}
},
onBack: () => {
sandbox?.manager.iframeProtocol.dispatch({ type: 'urlback' });
},
onForward: () => {
sandbox?.manager.iframeProtocol.dispatch({ type: 'urlforward' });
},
onRefresh: () => {
if (!sandbox) {
return;
}
if (
!canAccessIFrame(sandbox.iframe) ||
sandbox.iframe.contentWindow.origin !== sandbox.props.bundlerURL
) {
sandbox.iframe.src = `${bundlerUrl}${workspace.activeRoute}`;
} else {
sandbox.manager.iframeProtocol.dispatch({ type: 'refresh' });
}
workspace.selectSource.clear();
},
};
}
export const Sandbox = observer(
({
isPreview: isPreviewProp,
selectionTools,
bundlerURL,
bundlerURL = FALLBACK_SANDBOX_BUNDLER_URL,
mode = 'combined',
navigatorExtra,
...props
}: SandboxProps) => {
const bundlerUrl = bundlerURL ?? 'https://codesandbox.fn.netease.com';
const lastScrollTopRef = useRef<number>();
const sandboxRef = useRef<CodeSandbox>(null);
const combinedSandboxRef = useRef<CombinedSandboxRef>(null);
@@ -320,13 +381,6 @@ export const Sandbox = observer(
const workspace = useWorkspace();
const designer = useDesigner();
let sandbox = sandboxRef.current;
if (mode === 'combined') {
sandbox = designer.isPreview
? combinedSandboxRef.current?.previewSandbox
: combinedSandboxRef.current?.designSandbox;
}
const onViewChange = (data: any, { isActive }: ISandboxEventHandlerConfig) => {
if (isActive) {
navigatorRef.current.changeRelativeUrl(data?.pathname + data?.search);
@@ -340,51 +394,24 @@ export const Sandbox = observer(
}
};
let sandbox = sandboxRef.current;
if (mode === 'combined') {
sandbox = designer.isPreview
? combinedSandboxRef.current?.previewSandbox
: combinedSandboxRef.current?.designSandbox;
}
const navigatorProps = useSandboxNavigator({ sandbox, bundlerUrl: bundlerURL });
return (
<Box className="SandboxContainer" height="100%">
<Navigator
ref={navigatorRef}
disabled={!sandbox?.iframe}
startRoute={workspace.activeRoute}
onInputEnter={(newUrl) => {
if (newUrl?.split('?')[0] === workspace.activeRoute) {
// 只是输入了路由参数,默认为调试模式,直接更新沙箱内的路由地址
sandbox?.updateRoute(newUrl);
} else {
// 如果是输入了其他路由,则直接跳转
workspace.setActiveRoute(newUrl);
}
}}
onBack={() => {
sandbox?.manager.iframeProtocol.dispatch({ type: 'urlback' });
}}
onForward={() => {
sandbox?.manager.iframeProtocol.dispatch({ type: 'urlforward' });
}}
onRefresh={() => {
if (!sandbox) {
return;
}
if (
!canAccessIFrame(sandbox.iframe) ||
sandbox.iframe.contentWindow.origin !== sandbox.props.bundlerURL
) {
sandbox.iframe.src = `${bundlerUrl}${workspace.activeRoute}`;
} else {
sandbox.manager.iframeProtocol.dispatch({ type: 'refresh' });
}
workspace.selectSource.clear();
}}
extra={navigatorExtra}
/>
<Navigator ref={navigatorRef} extra={navigatorExtra} {...navigatorProps} />
<Simulator>
<Viewport selectionTools={selectionTools}>
{mode === 'single' && (
<DesignSandbox
ref={sandboxRef}
template="create-react-app"
bundlerURL={bundlerUrl}
bundlerURL={bundlerURL}
entry={workspace.entry}
onViewChange={onViewChange}
onMessage={onMessage}
@@ -397,7 +424,7 @@ export const Sandbox = observer(
<CombinedSandbox
ref={combinedSandboxRef}
template="create-react-app"
bundlerURL={bundlerUrl}
bundlerURL={bundlerURL}
entry={workspace.entry}
onViewChange={onViewChange}
onMessage={onMessage}
@@ -411,6 +438,54 @@ export const Sandbox = observer(
},
);
export const LiveEditorSandbox = observer(
({ bundlerURL = FALLBACK_SANDBOX_BUNDLER_URL, navigatorExtra, ...props }: SandboxProps) => {
const sandboxRef = useRef<CodeSandbox>(null);
const workspace = useWorkspace();
const { display, ...sandboxProps } = useSandbox({
files: normalizeFiles(workspace.editorState.getFiles()),
isPreview: true,
sandboxType: 'preview',
});
const navigatorProps = useSandboxNavigator({
sandbox: sandboxRef.current,
bundlerUrl: bundlerURL,
});
return (
<Box className="LiveEditorSandbox" height="100%">
<Navigator extra={navigatorExtra} {...navigatorProps} />
<Simulator>
<CodeSandbox
ref={sandboxRef}
iframeId="live-editor-sandbox"
template="create-react-app"
bundlerURL={bundlerURL}
entry={workspace.entry}
startRoute={workspace.activeRoute}
{...sandboxProps}
{...props}
/>
</Simulator>
</Box>
);
},
);
function normalizeFiles(files: Record<string, any>) {
if (!files) {
return;
}
const result: CodeSandboxProps['files'] = {};
Object.keys(files).forEach((filename) => {
const file = files[filename];
result[filename] = {
code: typeof file === 'string' ? file : file.code,
};
});
return result;
}
// 兼容 tango.config.json转成 sandbox.config.json
function fixSandboxFiles(files: Record<string, { code: string }>, entry = '/src/index.js') {
if (files['/tango.config.json']) {

View File

@@ -3,8 +3,8 @@ import { Box, Text, css } from 'coral-system';
import { Dropdown, Button } from 'antd';
import { isValidExpressionCode } from '@music163/tango-core';
import { getValue, IVariableTreeNode, noop } from '@music163/tango-helpers';
import { CloseCircleFilled, ExpandAltOutlined, MenuOutlined } from '@ant-design/icons';
import { Panel, InputCode, Action, DragPanel } from '@music163/tango-ui';
import { CloseCircleFilled, MenuOutlined } from '@ant-design/icons';
import { Panel, InputCode, Action, DragPanel, PopOutOutlined } from '@music163/tango-ui';
import { FormItemComponentProps } from '@music163/tango-setting-form';
import { useWorkspace, useWorkspaceData } from '@music163/tango-context';
import { VariableTree } from '../components';
@@ -195,53 +195,62 @@ export function ExpressionPopover({
return (
<DragPanel
width={700}
title={`${selectNodePath}/${title} 设置为引用变量或自定义表达式`}
extra={subTitle}
height={615}
resizeable
title={`使用代码设置 ${selectNodePath} 组件的${title}`}
onOpenChange={(open) => {
if (!open) {
setExp(undefined);
setError('');
}
}}
popoverStyle={{
height: '615px',
}}
body={
<>
<Panel shape="solid">
<Box display="flex" flexDirection="column">
<Box p="m">
<InputCode
shape="inset"
minHeight="56px"
maxHeight="200px"
maxHeight="160px"
value={exp}
placeholder={placeholder}
onChange={handleExpInputChange}
onBlur={() => {
setError(expressionValueValidate(exp));
if (exp) {
setError(expressionValueValidate(exp));
}
}}
autoCompleteContext={evaluateContext}
autoCompleteOptions={autoCompleteOptions}
/>
{error ? (
<Text color="red" fontSize="12px">
<Text color="red" fontSize="12px" as="p">
</Text>
) : (
<Text fontSize="12px" color="text3">
javascript 使 jsx
) : null}
{subTitle && (
<Text fontSize="12px" color="text3" as="p">
{subTitle}
</Text>
)}
</Panel>
<Text fontSize="12px" color="text3" as="p">
javascript 使 jsx
</Text>
</Box>
<Panel
title="从变量列表中选中"
shape="solid"
borderTop="0"
overflow="hidden"
bodyProps={{ overflow: 'hidden' }}
borderLeft="0"
borderRight="0"
flex={1}
bodyProps={{
style: {
overflow: 'hidden',
},
}}
>
<VariableTree
height={380}
height="100%"
showViewButton
dataSource={dataSource || expressionVariables}
appContext={sandbox?.window['tango']}
@@ -307,10 +316,10 @@ export function ExpressionPopover({
}}
/>
</Panel>
</>
</Box>
}
footer={(close) => (
<Box display="flex" justifyContent="flex-end" gap="5px">
<Box display="flex" width="100%" justifyContent="flex-end" gap="5px">
<Button
type="primary"
size="small"
@@ -330,7 +339,12 @@ export function ExpressionPopover({
)}
>
{children || (
<Action tooltip="打开表达式变量选择面板" icon={<ExpandAltOutlined />} size="small" />
<Action
tooltip="弹出代码编辑和变量选择面板"
tooltipProps={{ placement: 'topLeft' }}
icon={<PopOutOutlined />}
size="small"
/>
)}
</DragPanel>
);

View File

@@ -68,17 +68,6 @@ export const DependencyPanel = observer(
[dependencies, baseDependencies, bizDependencies, packages, packageNames],
);
// 业务组件依赖
const bizDeps = useMemo(
() =>
bizDependencies?.map((name: string) => ({
name,
version: dependencies[name] ?? packages?.[name]?.version,
package: packages?.[name],
})) || [],
[bizDependencies, dependencies, packages],
);
// 基础包
const baseDeps = useMemo(
() =>

View File

@@ -30,6 +30,7 @@ export const StateTree = observer(() => {
'pageStore',
'services',
'config',
'routeData',
]);
const callback = () => {
forceUpdate();

View File

@@ -92,6 +92,10 @@ function BaseSidebarPanel({ panelWidth: defaultPanelWidth = 266, footer, childre
return ret;
}, [children]);
if (designer.activeView !== 'design') {
return <></>;
}
return (
<Box display="flex" flexShrink={0} css={sidebarStyle} className="SidebarPanel">
<Box className="SidebarPanelBar" display="flex" flexDirection="column" position="relative">

View File

@@ -155,18 +155,20 @@ function SelectionBox({ showActions, actions, data }: SelectionBoxProps) {
css={selectionBoxStyle}
style={style}
>
<>
<ComponentsPopover type="before">
<Tooltip title={`${selectedNodeName} 的前方添加兄弟节点`}>
<SelectionHelper icon={<PlusOutlined />} css={topAddSiblingBtnStyle} />
</Tooltip>
</ComponentsPopover>
<ComponentsPopover type="after">
<Tooltip title={`${selectedNodeName} 的后方添加兄弟节点`}>
<SelectionHelper icon={<PlusOutlined />} css={bottomAddSiblingBtnStyle} />
</Tooltip>
</ComponentsPopover>
</>
{!isPage ? (
<>
<ComponentsPopover type="before">
<Tooltip title={`${selectedNodeName} 的前方添加兄弟节点`}>
<SelectionHelper icon={<PlusOutlined />} css={topAddSiblingBtnStyle} />
</Tooltip>
</ComponentsPopover>
<ComponentsPopover type="after">
<Tooltip title={`${selectedNodeName} 的后方添加兄弟节点`}>
<SelectionHelper icon={<PlusOutlined />} css={bottomAddSiblingBtnStyle} />
</Tooltip>
</ComponentsPopover>
</>
) : null}
{showActions && (
<SelectionHelpers align={selectionHelpersAlign}>
<SelectionHelper
@@ -204,7 +206,7 @@ function SelectionBox({ showActions, actions, data }: SelectionBoxProps) {
}
/>
<SelectionToolSet>{!isPage && actions}</SelectionToolSet>
{isFromCurrentFile && prototype?.hasChildren !== false && (
{isFromCurrentFile && prototype?.hasChildren && (
<ComponentsPopover>
<Tooltip title="快捷添加子元素">
<SelectionHelper icon={<PlusOutlined />} />

View File

@@ -1,25 +1,11 @@
import React, { useCallback } from 'react';
import React from 'react';
import { Group } from 'coral-system';
import { Modal } from 'antd';
import { ToggleButton, CodeOutlined, DualOutlined } from '@music163/tango-ui';
import { observer, useDesigner, useWorkspace } from '@music163/tango-context';
import { observer, useDesigner } from '@music163/tango-context';
import { BorderOutlined } from '@ant-design/icons';
export const ModeSwitchTool = observer(() => {
const workspace = useWorkspace();
const designer = useDesigner();
const activeFileCheck = useCallback(() => {
if (workspace.activeFile !== workspace.activeViewFile) {
Modal.confirm({
title: '当前打开的文件与视图不匹配,是否切换到当前视图对应的文件?',
onOk: () => {
workspace.setActiveFile(workspace.activeViewFile);
},
});
}
}, [workspace]);
return (
<Group attached>
<ToggleButton
@@ -38,7 +24,6 @@ export const ModeSwitchTool = observer(() => {
onClick={() => {
designer.setActiveView('code'); // 切换到源码视图
designer.setActiveSidebarPanel(''); // 关闭左侧面板
activeFileCheck();
}}
tooltip="源码视图"
>
@@ -50,7 +35,6 @@ export const ModeSwitchTool = observer(() => {
onClick={() => {
designer.setActiveView('dual'); // 切换到双屏视图
designer.setActiveSidebarPanel(''); // 关闭左侧面板
activeFileCheck();
}}
tooltip="双屏视图"
>

View File

@@ -15,25 +15,32 @@ export function Toolbar({ children }: ToolbarProps) {
let prevPlacement: string;
React.Children.forEach(children, (child: React.ReactElement, index) => {
const childProps = child.props;
let fallbackNode;
if (child.key) {
const Widget = getWidget(['toolbar', child.key].join('.'));
if (Widget) {
fallbackNode = React.createElement(Widget, child.props.widgetProps);
fallbackNode = (
<ToolbarItem>
<Widget {...childProps.widgetProps} />
</ToolbarItem>
);
}
}
let node = child.props.children ?? fallbackNode ?? null;
if (!node) {
node = child; // separator
let node;
if (childProps.children) {
// 自定义子节点
node = child;
} else {
// 预注册的子节点
node = fallbackNode || child;
}
const key = child.key || index;
node = (
<div key={key} className="ToolbarPanelItem" data-key={key}>
{node}
</div>
);
const placement = child.props.placement || prevPlacement || 'center';
node = React.cloneElement(node, { key, 'data-key': key });
const placement = childProps.placement || prevPlacement || 'center';
switch (placement) {
case 'left':
left.push(node);
@@ -68,6 +75,10 @@ export function Toolbar({ children }: ToolbarProps) {
}
export interface ToolbarItemProps extends ReactComponentProps {
/**
* 类型
*/
type?: 'divider';
placement?: 'left' | 'center' | 'right';
children?: React.ReactElement;
/**
@@ -76,8 +87,15 @@ export interface ToolbarItemProps extends ReactComponentProps {
widgetProps?: object;
}
function ToolbarItem({ placement, widgetProps, children }: ToolbarItemProps) {
return <div>{children}</div>;
function ToolbarItem({ type, placement, widgetProps, children, ...rest }: ToolbarItemProps) {
if (type === 'divider') {
return <Separator />;
}
return (
<div className="ToolbarPanelItem" {...rest}>
{children}
</div>
);
}
function Separator() {

View File

@@ -30,7 +30,7 @@ import {
const widgets = {};
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(
`Invalid widget key(${key}), should start with toolbar, sidebar, contextMenu or selectionMenu`,
);

View File

@@ -1,35 +1,41 @@
import React from 'react';
import { Box } from 'coral-system';
import cx from 'classnames';
import { Box, HTMLCoralProps } from 'coral-system';
import { observer, useDesigner } from '@music163/tango-context';
import { DesignerViewType } from '@music163/tango-core';
import { ComponentsPopover } from './components';
import { ComponentsPopover, FilesErrorOverlay } from './components';
export interface WorkspaceViewProps {
export interface WorkspaceViewProps extends HTMLCoralProps<'div'> {
/**
* 视图面板模式,对应 Workspace 的模式
*/
mode?: DesignerViewType;
children: React.ReactNode;
}
export const WorkspaceView = observer((props: WorkspaceViewProps) => {
const { mode = 'design', children } = props;
const { mode = 'design', children, className, ...rest } = props;
const designer = useDesigner();
const display = mode !== designer.activeView ? 'none' : 'block';
let activeView = designer.activeView;
if (designer.activeView === 'dual') {
activeView = 'code';
}
const display = mode !== activeView ? 'none' : 'block';
// 云音乐移动端模式小屏幕适配,可能会溢出屏幕
const overflow =
designer.simulator.name === 'phone' ? 'auto' : designer.isPreview ? 'auto' : 'hidden';
return (
<Box
className={`ViewPanel ${mode}`}
display={designer.activeView === 'dual' ? 'block' : display}
flex="1"
className={cx('ViewPanel', mode, className)}
display={activeView === mode ? 'block' : display}
overflow={overflow}
flex="1"
position="relative"
{...rest}
>
{children}
{/* 添加组件弹层 */}
{display === 'block' && <ComponentsPopover type="inner" isControlled />}
{mode === 'design' && <FilesErrorOverlay />}
</Box>
);
});

View File

@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.0.8](https://github.com/netease/tango/compare/@music163/tango-sandbox@1.0.7...@music163/tango-sandbox@1.0.8) (2024-07-12)
**Note:** Version bump only for package @music163/tango-sandbox
## [1.0.7](https://github.com/netease/tango/compare/@music163/tango-sandbox@1.0.6...@music163/tango-sandbox@1.0.7) (2024-06-20)
**Note:** Version bump only for package @music163/tango-sandbox
## [1.0.6](https://github.com/netease/tango/compare/@music163/tango-sandbox@1.0.5...@music163/tango-sandbox@1.0.6) (2024-06-05)
**Note:** Version bump only for package @music163/tango-sandbox
## [1.0.5](https://github.com/netease/tango/compare/@music163/tango-sandbox@1.0.4...@music163/tango-sandbox@1.0.5) (2024-06-03)
### Bug Fixes

View File

@@ -1,6 +1,6 @@
{
"name": "@music163/tango-sandbox",
"version": "1.0.5",
"version": "1.0.8",
"description": "sandbox of tango apps",
"author": "wwsun <ww.sun@outlook.com>",
"homepage": "",
@@ -29,7 +29,7 @@
},
"dependencies": {
"@ant-design/icons": "^4.8.0",
"@music163/tango-core": "^1.3.0",
"@music163/tango-core": "^1.3.3",
"@music163/tango-helpers": "^1.2.0",
"crypto-js": "^4.1.1",
"lodash.isequal": "4.5.0",

View File

@@ -3,6 +3,20 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.2.8](https://github.com/netease/tango/compare/@music163/tango-setting-form@1.2.7...@music163/tango-setting-form@1.2.8) (2024-07-12)
**Note:** Version bump only for package @music163/tango-setting-form
## [1.2.7](https://github.com/netease/tango/compare/@music163/tango-setting-form@1.2.6...@music163/tango-setting-form@1.2.7) (2024-06-20)
### Bug Fixes
- update designer style ([#176](https://github.com/netease/tango/issues/176)) ([0f3f0af](https://github.com/netease/tango/commit/0f3f0afdfa8aee2532a97c5c2e92ef4230397d86))
## [1.2.6](https://github.com/netease/tango/compare/@music163/tango-setting-form@1.2.5...@music163/tango-setting-form@1.2.6) (2024-06-05)
**Note:** Version bump only for package @music163/tango-setting-form
## [1.2.5](https://github.com/netease/tango/compare/@music163/tango-setting-form@1.2.4...@music163/tango-setting-form@1.2.5) (2024-06-03)
**Note:** Version bump only for package @music163/tango-setting-form

View File

@@ -1,6 +1,6 @@
{
"name": "@music163/tango-setting-form",
"version": "1.2.5",
"version": "1.2.8",
"description": "setting form of tango-apps",
"author": "wwsun <ww.sun@outlook.com>",
"homepage": "",
@@ -28,9 +28,9 @@
},
"dependencies": {
"@ant-design/icons": "^4.8.0",
"@music163/tango-core": "^1.3.0",
"@music163/tango-core": "^1.3.3",
"@music163/tango-helpers": "^1.2.0",
"@music163/tango-ui": "^1.3.0",
"@music163/tango-ui": "^1.3.3",
"antd": "^4.24.2",
"coral-system": "^1.0.5",
"mobx": "6.12.3",

View File

@@ -1,3 +1,4 @@
import React from 'react';
import { CodeSetter } from './code-setter';
import { BoolSetter } from './bool-setter';
import { IdSetter } from './id-setter';
@@ -36,6 +37,10 @@ const BASIC_SETTERS: IFormItemCreateOptions[] = [
name: 'idSetter',
component: IdSetter,
},
{
name: 'imageSetter',
render: (props) => <TextSetter placeholder="请提供图片地址" {...props} />,
},
];
let registered = false;

View File

@@ -3,6 +3,25 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.3.3](https://github.com/netease/tango/compare/@music163/tango-ui@1.3.2...@music163/tango-ui@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))
## [1.3.2](https://github.com/netease/tango/compare/@music163/tango-ui@1.3.1...@music163/tango-ui@1.3.2) (2024-06-20)
### Bug Fixes
- drag-panel add resizeable & height prop ([#175](https://github.com/netease/tango/issues/175)) ([b53a9f7](https://github.com/netease/tango/commit/b53a9f7eefc748ba8a7c94270c3a374b9d6ed211))
- update designer style ([#176](https://github.com/netease/tango/issues/176)) ([0f3f0af](https://github.com/netease/tango/commit/0f3f0afdfa8aee2532a97c5c2e92ef4230397d86))
## [1.3.1](https://github.com/netease/tango/compare/@music163/tango-ui@1.3.0...@music163/tango-ui@1.3.1) (2024-06-05)
### Bug Fixes
- fix context menu error on macOS ([#170](https://github.com/netease/tango/issues/170)) ([ca740c8](https://github.com/netease/tango/commit/ca740c83ae7f72a75c7db90d2e137a7e9a032326))
# [1.3.0](https://github.com/netease/tango/compare/@music163/tango-ui@1.2.3...@music163/tango-ui@1.3.0) (2024-06-03)
### Features

View File

@@ -1,6 +1,6 @@
{
"name": "@music163/tango-ui",
"version": "1.3.0",
"version": "1.3.3",
"description": "ui widgets of tango",
"keywords": [
"react",

View File

@@ -1,4 +1,4 @@
import { Tooltip } from 'antd';
import { Tooltip, TooltipProps } from 'antd';
import React from 'react';
import { css, Button, Link, HTMLCoralProps } from 'coral-system';
import cx from 'classnames';
@@ -48,6 +48,10 @@ export interface ActionProps extends HTMLCoralProps<any> {
* 提示文本
*/
tooltip?: string;
/**
* Tooltip属性透传
*/
tooltipProps?: TooltipProps;
/**
* 超链接
*/
@@ -75,6 +79,7 @@ export interface ActionProps extends HTMLCoralProps<any> {
export function Action({
icon,
tooltip,
tooltipProps,
href,
size,
shape,
@@ -110,7 +115,11 @@ export function Action({
}
if (tooltip) {
return <Tooltip title={tooltip}>{ret}</Tooltip>;
return (
<Tooltip title={tooltip} {...tooltipProps}>
{ret}
</Tooltip>
);
}
return ret;
}

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { Box, HTMLCoralProps, css } from 'coral-system';
import { UpOutlined } from '@ant-design/icons';
import { CaretRightOutlined } from '@ant-design/icons';
import { useControllableState } from '@music163/tango-helpers';
export interface CollapsePanelProps extends Omit<HTMLCoralProps<'div'>, 'title'> {
@@ -39,8 +39,10 @@ export interface CollapsePanelProps extends Omit<HTMLCoralProps<'div'>, 'title'>
const headerStyle = css`
user-select: none;
.anticon-up {
margin-right: 8px;
.anticon-caret-right {
width: 10px;
height: 10px;
margin-right: 4px;
color: var(--tango-colors-text2);
transition: transform 0.2s ease 0s;
}
@@ -67,7 +69,7 @@ export function CollapsePanel(props: CollapsePanelProps) {
});
const iconStyle = {
transform: collapsed ? undefined : 'rotate(-180deg)',
transform: collapsed ? undefined : 'rotate(90deg)',
};
const stickHeaderProps: HTMLCoralProps<'div'> = stickyHeader
@@ -104,7 +106,7 @@ export function CollapsePanel(props: CollapsePanelProps) {
css={headerStyle}
>
<Box display="flex" alignItems="center" fontSize="14px" fontWeight="500">
<UpOutlined style={iconStyle} />
<CaretRightOutlined style={iconStyle} />
{title}
</Box>
<Box>{extra}</Box>

View File

@@ -66,7 +66,7 @@ export function ContextAction({
option: 'Alt',
shift: 'Shift',
};
const regexp = new RegExp(Object.keys(keyMap).join('|').replace(/\\+/, '\\+'), 'ig');
const regexp = new RegExp(Object.keys(keyMap).join('|').replace(/\+/, '\\+'), 'ig');
return hotkey.replace(regexp, (match) => keyMap[match.toLowerCase()]);
}, [hotkey]);

View File

@@ -1,9 +1,9 @@
import React, { useState } from 'react';
import { Box, Text, styled } from 'coral-system';
import { Popover, PopoverProps, IconFont } from './';
import Draggable from 'react-draggable';
import { CloseOutlined } from '@ant-design/icons';
import { CloseOutlined, HolderOutlined } from '@ant-design/icons';
import { noop } from '@music163/tango-helpers';
import { Popover, PopoverProps } from './popover';
// Dragging over an iframe stops dragging when moving the mouse too fast #613
// https://github.com/react-grid-layout/react-draggable/issues/613
@@ -35,6 +35,52 @@ const CloseIcon = styled(CloseOutlined)`
}
`;
const DragPanelContainer = styled(Box)`
display: flex;
flex-direction: column;
justify-content: space-between;
background-color: #fff;
border-radius: 8px;
box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.1);
border: 1px solid var(--tango-colors-line2);
width: ${(props) => props.width}px;
height: ${(props) => props.height}px;
resize: ${(props) => (props.resizeable ? 'both' : 'none')};
overflow: ${(props) => (props.resizeable ? 'auto' : 'hidden')};
`;
const DragPanelHeader = styled(Box)`
padding: 8px 12px;
border-bottom: 1px solid var(--tango-colors-line2);
cursor: move;
display: flex;
justify-content: space-between;
align-items: center;
min-height: 36px;
`;
const DragPanelBody = styled(Box)`
flex: 1;
min-height: 0;
> * {
height: 100%;
}
`;
const DragPanelFooter = styled(Box)`
padding: 8px 12px;
min-height: 36px;
display: flex;
align-items: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
background: var(--tango-colors-line1);
font-size: 12px;
font-weight: 400;
border-top: 1px solid var(--tango-colors-line2);
`;
interface DragPanelProps extends Omit<PopoverProps, 'overlay' | 'open'> {
// 标题
title?: React.ReactNode | string;
@@ -44,8 +90,12 @@ interface DragPanelProps extends Omit<PopoverProps, 'overlay' | 'open'> {
footer?: ((close: () => void) => React.ReactNode) | React.ReactNode | string;
// 宽度
width?: number | string;
// 高度
height?: number | string;
// 右上角区域
extra?: React.ReactNode | string;
// 是否可以缩放
resizeable?: boolean;
children?: React.ReactNode;
}
@@ -55,8 +105,10 @@ export function DragPanel({
body,
children,
width = 330,
height = 330,
extra,
onOpenChange = noop,
resizeable = false,
...props
}: DragPanelProps) {
const [open, setOpen] = useState(false);
@@ -78,32 +130,20 @@ export function DragPanel({
}}
overlay={
<Draggable
handle=".selection-drag-bar"
handle=".drag-panel-header"
onStart={() => {
injectStyleToBody();
}}
>
<Box
bg="#FFF"
borderRadius="m"
boxShadow="lowDown"
border="solid"
borderColor="line2"
overflow="hidden"
<DragPanelContainer
className="drag-panel"
width={width}
height={height}
resizeable={resizeable}
>
{/* 头部区域 */}
<Box
px="l"
py="m"
className="selection-drag-bar"
borderBottom="1px solid var(--tango-colors-line2)"
cursor="move"
display="flex"
justifyContent="space-between"
>
<DragPanelHeader className="drag-panel-header">
<Box fontSize="12px" color="text2">
<IconFont type="icon-applications" />
<HolderOutlined />
<Text marginLeft={'5px'}>{title}</Text>
</Box>
<Box color="text2" fontSize="12px" display="flex" alignItems="center">
@@ -115,26 +155,12 @@ export function DragPanel({
}}
/>
</Box>
</Box>
{/* 主体区域 */}
{body}
{/* 底部 */}
</DragPanelHeader>
<DragPanelBody className="drag-panel-body">{body}</DragPanelBody>
{footer && (
<Box
px="l"
py="m"
whiteSpace="nowrap"
overflow="hidden"
textOverflow="ellipsis"
background="var(--tango-colors-line1)"
fontSize="12px"
fontWeight={400}
borderTop="1px solid var(--tango-colors-line2)"
>
{footerNode}
</Box>
<DragPanelFooter className="drag-panel-footer">{footerNode}</DragPanelFooter>
)}
</Box>
</DragPanelContainer>
</Draggable>
}
{...props}

View File

@@ -2,10 +2,12 @@ export * from './create-icon';
export * from './code-outlined';
export * from './line-dashed-outlined';
export * from './line-solid-outlined';
export * from './package-outlined';
export * from './open-panel-filled-left-outlined';
export * from './open-panel-filled-right-outlined';
export * from './open-panel-left-outlined';
export * from './open-panel-right-outlined';
export * from './undo-outlined';
export * from './pop-out-outlined';
export * from './redo-outlined';
export * from './dual-outlined';

View File

@@ -0,0 +1,16 @@
import React from 'react';
import { createIcon } from './create-icon';
const PackageOutlinedSvg = () => (
<svg
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
>
<path d="M915.2 284.8c0-3.2 0-3.2-3.2-6.4 0-3.2-3.2-3.2-6.4-6.4-9.6-12.8-22.4-25.6-38.4-35.2l-297.6-169.6c-35.2-22.4-80-22.4-118.4 0L313.6 147.2c-6.4 3.2-12.8 6.4-16 12.8L153.6 240c-12.8 9.6-25.6 22.4-38.4 35.2 0 3.2-3.2 3.2-3.2 6.4s0 3.2-3.2 6.4c-6.4 16-12.8 35.2-12.8 54.4v342.4c0 41.6 22.4 80 57.6 102.4l297.6 169.6c12.8 6.4 28.8 12.8 41.6 16 3.2 3.2 9.6 3.2 16 3.2s9.6-3.2 16-3.2c16-3.2 28.8-6.4 41.6-16l297.6-169.6c35.2-22.4 57.6-60.8 57.6-102.4V342.4c6.4-19.2 0-38.4-6.4-57.6z m-428.8-160c9.6-3.2 16-6.4 25.6-6.4 9.6 0 19.2 3.2 25.6 6.4L832 291.2l-128 73.6-316.8-182.4 99.2-57.6z m25.6 352L192 291.2l128-73.6 316.8 182.4-124.8 76.8zM185.6 729.6c-16-9.6-25.6-28.8-25.6-48V345.6l320 185.6V896l-294.4-166.4z m652.8 0L544 896V531.2l320-185.6v336c0 19.2-9.6 38.4-25.6 48z" />
</svg>
);
export const PackageOutlined = createIcon(PackageOutlinedSvg, 'PackageOutlined');

View File

@@ -0,0 +1,17 @@
import React from 'react';
import { createIcon } from './create-icon';
const Svg = () => (
<svg
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="currentColor"
>
<path d="M832 544a32 32 0 0 1 64 0V832a128 128 0 0 1-128 128H192a128 128 0 0 1-128-128V256a128 128 0 0 1 128-128h288a32 32 0 0 1 0 64H192a64 64 0 0 0-64 64v576a64 64 0 0 0 64 64h576a64 64 0 0 0 64-64V544z m-327.68 21.696a32 32 0 1 1-45.184-45.248L851.52 128H672a32 32 0 0 1 0-64H896a64 64 0 0 1 64 64v224a32 32 0 0 1-64 0V174.08L504.32 565.632z" />
</svg>
);
export const PopOutOutlined = createIcon(Svg, 'PopOutOutlined');

View File

@@ -14,8 +14,14 @@ const buttonStyle = css`
padding: 0 8px;
.anticon-down {
font-size: 12px !important;
width: 10px;
height: 10px;
margin-left: 4px;
transition: transform 0.1s ease-in;
}
&:hover .anticon-down {
transform: translateY(2px);
}
&.size-s {
@@ -154,6 +160,10 @@ export interface ToggleButtonProps extends Omit<HTMLCoralProps<'button'>, 'type'
* 下拉菜单的属性
*/
dropdownProps?: DropDownProps;
/**
* 是否显示下拉图标
*/
showDropdownIcon?: boolean;
}
/**
@@ -176,6 +186,7 @@ export function ToggleButton(props: ToggleButtonProps) {
overlayStyle,
onItemClick,
dropdownProps = {},
showDropdownIcon = true,
...rest
} = props;
const clazz = cx({
@@ -198,7 +209,7 @@ export function ToggleButton(props: ToggleButtonProps) {
)}
{children}
</Box>
{showDropdown ? <DownOutlined /> : null}
{showDropdown && showDropdownIcon ? <DownOutlined /> : null}
</Button>
);

Binary file not shown.

After

Width:  |  Height:  |  Size: 422 KiB

BIN
public/mail-builder.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 899 KiB

BIN
public/rn-builder.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB