Compare commits

...

34 Commits

Author SHA1 Message Date
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
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
114 changed files with 2595 additions and 740 deletions
+1
View File
@@ -9,3 +9,4 @@ coverage/
.umi-production/
.log
.eslintcache
*.pem
+26 -15
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.
+30 -19
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 的社区,以分享您的想法、建议或问题,并与其他用户和贡献者建立联系。
-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-----
+17 -7
View File
@@ -1,10 +1,11 @@
import React from 'react';
import { Box, Group } from 'coral-system';
import { Box, Group, Text } from 'coral-system';
import { Avatar, Space, Switch } from 'antd';
import { BranchesOutlined, MenuOutlined, QuestionCircleOutlined } from '@ant-design/icons';
import { registerSetter } from '@music163/tango-designer';
import type { 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>
);
}
@@ -56,12 +66,12 @@ export function Logo() {
export function ProjectDetail() {
return (
<Box display="flex" alignItems="center" columnGap="l">
<Box className="ProjectName" fontSize="18px" fontWeight="bold">
<Text className="ProjectName" fontSize="18px" fontWeight="bold" truncated>
community-test
</Box>
<Box className="BranchName" as="code" fontSize="14px">
</Text>
<Text className="BranchName" as="code" fontSize="14px" truncated>
<BranchesOutlined /> feature/list
</Box>
</Text>
</Box>
);
}
+10 -4
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'],
@@ -153,6 +153,7 @@ import { definePage } from "@music163/tango-boot";
import {
Page,
Section,
Box,
Button,
Input,
FormilyForm,
@@ -166,8 +167,10 @@ import { OutButton } from "../components";
class App extends React.Component {
render() {
return (
<Page title={tango.stores.app.title} subTitle={111}>
<Section tid="section0" />
<Page title={tango.stores.app.title} subTitle={<><Button>hello</Button></>}>
<Section tid="section0">
<Box></Box>
</Section>
<Section tid="section1" title="Section Title">
your input: <Input tid="input1" defaultValue="hello" />
copy input: <Input value={tango.page.input1?.value} />
@@ -374,13 +377,16 @@ export default defineServices({
add: {
url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/api/users',
method: 'post',
description: '新增用户'
},
update: {
url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/api/users',
method: 'post',
description: '更新用户'
},
delete: {
url: 'https://nei.hz.netease.com/api/apimock-v2/c45109399a1d33d83e32a59984b25b00/api/users?id=1',
description: '删除用户'
},
});
`;
+55 -1
View File
@@ -48,7 +48,6 @@ const Snippet3ColumnLayout: IComponentPrototype = {
icon: 'icon-column-3',
type: 'snippet',
package: '@music163/antd',
// FIXME: Column 组件 需要更新为 defineComponent
initChildren: `
<Columns columns={12}>
<Column colSpan={4}>
@@ -132,6 +131,9 @@ export const nativeDomPrototypes = () => {
title: '样式',
group: 'style',
setter: 'expressionSetter',
setterProps: {
expressionType: 'cssObject',
},
},
{
name: 'className',
@@ -166,6 +168,32 @@ const prototypes: Dict<IComponentPrototype> = {
Snippet2ColumnLayout,
Snippet3ColumnLayout,
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}',
},
],
},
Box: {
name: 'Box',
title: '盒子',
@@ -197,6 +225,32 @@ const prototypes: Dict<IComponentPrototype> = {
title: 'd',
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'],
},
],
},
],
},
Columns: {
+10 -11
View File
@@ -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 = {
@@ -114,8 +113,8 @@ export default function App() {
actions={
<Box px="l">
<Toolbar>
<Toolbar.Item key="routeSwitch" placement="left" />
<Toolbar.Item key="addPage" placement="left">
<Toolbar.Item key="routeSwitch" placement="left" activeViews={['design']} />
<Toolbar.Item key="addPage" placement="left" activeViews={['design']}>
<Action
tooltip="添加页面"
shape="outline"
@@ -123,10 +122,10 @@ export default function App() {
onClick={() => setShowNewPageModal(true)}
/>
</Toolbar.Item>
<Toolbar.Item key="history" placement="left" />
<Toolbar.Item key="preview" placement="left" />
<Toolbar.Item key="history" placement="left" activeViews={['design']} />
<Toolbar.Item key="preview" placement="left" activeViews={['design']} />
<Toolbar.Item key="togglePanel" placement="right" activeViews={['design']} />
<Toolbar.Item key="modeSwitch" placement="right" />
<Toolbar.Item key="togglePanel" placement="right" />
<Toolbar.Separator />
<Toolbar.Item placement="right">
<Space>
@@ -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">
<CodeEditor />
</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>
+1 -1
View File
@@ -3,7 +3,7 @@ import { SystemProvider } from 'coral-system';
import 'antd/dist/antd.css';
export const parameters = {
actions: { argTypesRegex: '^on[A-Z].*' },
actions: { argTypesRegex: '^on.*' },
controls: {
matchers: {
color: /(background|color)$/i,
@@ -69,6 +69,14 @@ const prototypeHasBasicProps: IComponentPrototype = {
title: 'codeSetter',
setter: 'codeSetter',
},
{
name: 'style',
title: 'codeSetter(cssObject)',
setter: 'codeSetter',
setterProps: {
expressionType: 'cssObject',
},
},
{
name: 'text',
title: 'textSetter',
@@ -79,6 +87,11 @@ const prototypeHasBasicProps: IComponentPrototype = {
title: 'textAreaSetter',
setter: 'textAreaSetter',
},
{
name: 'src',
title: 'imageSetter',
setter: 'imageSetter',
},
{
name: 'number',
title: 'numberSetter',
@@ -263,6 +276,9 @@ export function InitValues() {
name: 'style',
title: 'codeSetter',
setter: 'codeSetter',
setterProps: {
expressionType: 'cssObject',
},
},
{
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} />;
}
+48 -9
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>
);
}
+5 -1
View File
@@ -1,5 +1,5 @@
import React from 'react';
import { Action, InputCode } from '@music163/tango-ui';
import { Action, InputCode, InputStyleCode } from '@music163/tango-ui';
import { BlockOutlined } from '@ant-design/icons';
export default {
@@ -28,6 +28,10 @@ export function Basic() {
);
}
export function CSS() {
return <InputStyleCode enableESLint suffix={<Action icon={<BlockOutlined />} size="small" />} />;
}
const code = `
function foo() {
console.log("test");
+4 -4
View File
@@ -15,10 +15,10 @@
"test:watch": "jest --watch",
"eslint": "eslint packages/**/src/*.{ts,tsx} --cache",
"publish": "lerna publish",
"ver": "lerna version --no-private",
"release": "yarn eslint && yarn build && lerna version --no-private --conventional-commits && lerna publish from-git",
"release:npm": "yarn eslint && yarn build && yarn run ver && lerna publish from-git",
"release:beta": "yarn eslint && yarn build && yarn run ver && lerna publish from-package --dist-tag beta",
"ver": "lerna version --no-private --conventional-commits",
"release": "yarn eslint && yarn build && yarn ver && lerna publish from-git",
"release:npm": "yarn eslint && yarn build && yarn ver && lerna publish from-package",
"release:beta": "yarn eslint && yarn build && yarn ver && lerna publish from-package --dist-tag beta",
"up": "yarn upgrade-interactive --latest",
"prepare": "husky install"
},
+30
View File
@@ -3,6 +3,36 @@
All notable changes to this project will be documented in this file.
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)
**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
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@music163/tango-context",
"version": "1.1.3",
"version": "1.1.10",
"description": "react context for tango-apps",
"keywords": [
"react",
@@ -31,8 +31,8 @@
"react": ">= 16.8"
},
"dependencies": {
"@music163/tango-core": "^1.3.1",
"@music163/tango-helpers": "^1.2.0",
"@music163/tango-core": "^1.4.4",
"@music163/tango-helpers": "^1.2.4",
"mobx-react-lite": "4.0.7"
},
"publishConfig": {
+9 -6
View File
@@ -90,12 +90,15 @@ export const useWorkspaceData = () => {
key: prefix,
selectable: false,
showAddButton: true,
children: Object.keys(file.serviceFunctions || {}).map((key) => ({
title: key,
key: [prefix, key].join('.'),
type: 'function',
showRemoveButton: true,
})),
children: Object.entries(file.serviceFunctions || {}).map(([key, value]) => {
const title = value.description ? `${key}(${value.description})` : key;
return {
title,
key: [prefix, key].join('.'),
type: 'function',
showRemoveButton: true,
};
}),
});
});
+40
View File
@@ -3,6 +3,46 @@
All notable changes to this project will be documented in this file.
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)
### 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
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@music163/tango-core",
"version": "1.3.1",
"version": "1.4.4",
"description": "tango core",
"author": "wwsun <ww.sun@outlook.com>",
"homepage": "",
@@ -28,7 +28,7 @@
"@babel/parser": "^7.23.5",
"@babel/traverse": "^7.23.5",
"@babel/types": "^7.23.5",
"@music163/tango-helpers": "^1.2.0",
"@music163/tango-helpers": "^1.2.4",
"@types/babel__generator": "^7.6.7",
"@types/babel__traverse": "^7.20.4",
"mobx": "6.12.3",
+1 -1
View File
@@ -35,7 +35,7 @@ export function isTangoVariable(name: string) {
return /^tango\??\.(stores|services)\??\./.test(name) && name.split('.').length > 2;
}
const templatePattern = /^{(.+)}$/s;
const templatePattern = /^{(.+)}$/;
/**
* 判断给定字符串是否被表达式容器`{expCode}`包裹
+4 -16
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,7 @@ export function ast2code(ast: t.Node, options: GeneratorOptions = defaultGenerat
return code;
}
const bracketPattern = /^\(.+\)$/s;
/**
* 是否被 () 包裹
*
* @example ({ foo: 'foo' }) -> true
* @example { foo: 'foo' } -> false
*
* @param str 目标字符串
*/
function isWrappingWithBrackets(str: string) {
return bracketPattern.test(str);
}
const bracketPattern = /^\(.+\)$/;
/**
* 将表达式生成为块级代码
@@ -54,7 +42,7 @@ export function expression2code(node: t.Expression) {
const isWrappingExpression = t.isObjectExpression(node) || t.isFunctionExpression(node);
if (isWrappingExpression && isWrappingWithBrackets(ret)) {
if (isWrappingExpression && bracketPattern.test(ret)) {
// 如果是对象,输出包含 ({}),则去掉首尾的括号
ret = ret.slice(1, -1);
}
@@ -195,7 +183,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);
+2 -6
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,
getValueNode: (value: any, key?: string) => t.Expression = value2node,
) {
if (!isPlainObject(obj)) {
+2 -2
View File
@@ -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, {
CallExpression(path) {
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];
if (t.isObjectExpression(configNode)) {
const newPropertiesNodeMap = Object.keys(config).reduce((properties, key) => {
const newPropertiesNodeMap = Object.keys(config).reduce<Dict>((properties, key) => {
const serviceConfig = config[key];
const property = t.objectProperty(t.identifier(key), serviceConfig2Node(serviceConfig));
properties[key] = property;
+1 -1
View File
@@ -11,7 +11,7 @@ export function namesToImportDeclarations(
names: string[],
nameMap: Dict<IImportSpecifierSourceData>,
) {
const map = {};
const map: Dict = {};
names.forEach((name) => {
const mod = nameMap[name];
if (mod) {
+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 } ] }
export function deepCloneNode(obj: any, component?: string): any {
function deepCloneObject() {
const target = {};
const target: Dict = {};
for (const key in obj) {
if (Object.hasOwn(obj, key)) {
target[key] = deepCloneNode(obj[key], component);
+1 -1
View File
@@ -13,7 +13,7 @@ export class TangoComponentsEntryModule extends TangoModule {
constructor(workspace: IWorkspace, props: IFileConfig) {
super(workspace, props, false);
this.update(props.code, false, false);
this.update(props.code, true, false);
makeObservable(this, {
_code: observable,
_cleanCode: observable,
+7
View File
@@ -239,6 +239,13 @@ export class Designer {
setActiveView(view: DesignerViewType) {
this._activeView = view;
if (view === 'dual') {
this.setActiveSidebarPanel('');
this.toggleRightPanel(false);
this.toggleIsPreview(true);
} else if (view === 'design') {
this.toggleIsPreview(false);
}
}
setActiveSidebarPanel(panel: string) {
+11
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) {
+18 -2
View File
@@ -10,6 +10,7 @@ import {
IPageConfigData,
IImportSpecifierSourceData,
IImportSpecifierData,
IFileError,
} from '../types';
import { TangoFile, TangoJsonFile } from './file';
import { TangoRouteModule } from './route-module';
@@ -192,7 +193,18 @@ export interface IWorkspace {
renameFile: (oldFilename: string, newFilename: string) => void;
renameFolder: (oldFoldername: string, newFoldername: string) => void;
updateFile: (filename: string, code: string, shouldFormatCode?: boolean) => void;
/**
* 更新文件
* @param filename 文件名
* @param code 代码
* @param isSyncAst 是否同步 ast
*/
updateFile: (filename: string, code: string, isSyncAst?: boolean) => void;
/**
* 检查并同步文件的 ast
*/
syncFiles: () => void;
listFiles: () => Record<string, string>;
getFile: (filename: string) => TangoFile;
@@ -298,8 +310,12 @@ export interface IWorkspace {
get bizComps(): string[];
get baseComps(): string[];
get localComps(): string[];
get fileErrors(): IFileError[];
/**
* 是否是合法的项目
* 是否是有效的项目
* - 包含 tango.config.json
* - 包含视图模块
* - 没有文件错误
*/
get isValid(): boolean;
}
+60 -18
View File
@@ -21,6 +21,11 @@ import { IWorkspace } from './interfaces';
export class TangoModule extends TangoFile {
ast: t.File;
/**
* ast 是否与 code 保持同步
*/
isAstSynced: boolean;
/**
* 导入的依赖列表
*/
@@ -33,23 +38,53 @@ export class TangoModule extends TangoFile {
/**
* 基于最新的 ast 进行同步
* @param code 如果传入 code,则基于 code 进行同步
* @param isFormatCode 是否格式化代码
* @param refreshWorkspace 是否刷新 workspace
* @param isSyncAst 是否同步 ast
* @param isRefreshWorkspace 是否刷新 workspace
*/
update(code?: string, isFormatCode = true, refreshWorkspace = true) {
update(code?: string, isSyncAst = true, isRefreshWorkspace = true) {
this.lastModified = Date.now();
if (isNil(code)) {
this._syncByAst();
} else {
this._syncByCode(code, isFormatCode);
try {
if (isNil(code)) {
this._syncByAst();
} else {
this._syncByCode(code, isSyncAst);
}
if (isSyncAst) {
this._analysisAst();
}
this.isAstSynced = isSyncAst;
this.isError = false;
this.errorMessage = undefined;
this.workspace.onFilesChange([this.filename]);
if (isRefreshWorkspace) {
this.workspace.refresh([this.filename]);
}
} catch (err: any) {
this.isError = true;
this.errorMessage = err.message;
}
}
this._analysisAst();
this.workspace.onFilesChange([this.filename]);
if (refreshWorkspace) {
this.workspace.refresh([this.filename]);
/**
* 基于当前的代码重新生成 ast
*/
updateAst() {
if (!this.isAstSynced) {
try {
this.ast = code2ast(this._code);
this._analysisAst();
this.isAstSynced = true;
this.isError = false;
this.errorMessage = undefined;
} catch (err: any) {
this.isAstSynced = false;
this.isError = true;
this.errorMessage = err.message;
}
}
}
@@ -75,22 +110,26 @@ export class TangoModule extends TangoFile {
/**
* 基于输入的源码进行同步
* @param code 源码
* @param isFormatCode 是否格式化代码
* @param isSyncAst 是否同步 ast
* @returns
*/
_syncByCode(code: string, isFormatCode = true) {
_syncByCode(code: string, isSyncAst = true) {
if (code === this._code) {
return;
}
// 提前格式化代码
if (isFormatCode) {
try {
code = formatCode(code);
} catch (err) {
// err ignored, format code failed
}
this._code = code;
this._cleanCode = code;
this.ast = code2ast(code);
if (isSyncAst) {
this.ast = code2ast(code);
}
}
_analysisAst() {
@@ -105,14 +144,17 @@ export class TangoModule extends TangoFile {
export class TangoJsModule extends TangoModule {
constructor(workspace: IWorkspace, props: IFileConfig) {
super(workspace, props, false);
this.update(props.code, false, false);
this.update(props.code, true, false);
makeObservable(this, {
_code: observable,
_cleanCode: observable,
isError: observable,
errorMessage: observable,
code: computed,
cleanCode: computed,
update: action,
updateAst: action,
});
}
}
+5 -1
View File
@@ -1,4 +1,5 @@
import { action, computed, makeObservable, observable, toJS } from 'mobx';
import { logger } from '@music163/tango-helpers';
import {
traverseRouteFile,
addRouteToRouteFile,
@@ -27,10 +28,13 @@ export class TangoRouteModule extends TangoModule {
_routes: observable,
_code: observable,
_cleanCode: observable,
isError: observable,
errorMessage: observable,
routes: computed,
code: computed,
cleanCode: computed,
update: action,
updateAst: action,
});
}
@@ -74,7 +78,7 @@ export class TangoRouteModule extends TangoModule {
*/
removeRoute(route: string) {
if (route === '/') {
console.warn('index route should not be removed!');
logger.warn('index route should not be removed!');
return;
}
const record = this.getRouteByRoutePath(route);
@@ -49,11 +49,14 @@ export class TangoServiceModule extends TangoModule {
_baseConfig: observable,
_code: observable,
_cleanCode: observable,
isError: observable,
errorMessage: observable,
serviceFunctions: computed,
baseConfig: computed,
cleanCode: computed,
code: computed,
update: action,
updateAst: action,
});
}
+3
View File
@@ -31,10 +31,13 @@ export class TangoStoreEntryModule extends TangoModule {
_stores: observable,
_code: observable,
_cleanCode: observable,
isError: observable,
errorMessage: observable,
stores: computed,
code: computed,
cleanCode: computed,
update: action,
updateAst: action,
});
}
+6 -2
View File
@@ -84,7 +84,7 @@ function nodeListToTreeData(list: ITangoViewNodeData[]) {
*/
export class TangoViewModule extends TangoModule implements IViewFile {
// 解析为树结构的 jsxNodes 数组
_nodesTree: ITangoViewNodeData[];
_nodesTree: ITangoViewNodeData[] = [];
/**
* 通过导入组件名查找组件来自的包
*/
@@ -138,10 +138,14 @@ export class TangoViewModule extends TangoModule implements IViewFile {
_code: observable,
_cleanCode: observable,
isError: observable,
errorMessage: observable,
code: computed,
cleanCode: computed,
update: action,
updateAst: action,
});
}
@@ -320,7 +324,7 @@ export class TangoViewModule extends TangoModule implements IViewFile {
updateNodeAttributes(nodeId: string, config: Record<string, any>, relatedImports?: string[]) {
if (relatedImports && relatedImports.length) {
// 导入依赖的组件
const newImportData = relatedImports.reduce((prev, name) => {
const newImportData = relatedImports.reduce<Dict<IImportSpecifierData[]>>((prev, name) => {
const proto = this.workspace.getPrototype(name);
const { source, specifiers } = prototype2importDeclarationData(proto, this.filename);
const existSpecifiers: IImportSpecifierData[] = prev[source];
+44 -24
View File
@@ -11,6 +11,7 @@ import {
parseServiceVariablePath,
parseStoreVariablePath,
uniq,
setValue,
} from '@music163/tango-helpers';
import {
prototype2jsxElement,
@@ -24,10 +25,10 @@ import {
import { DropMethod } from './drop-target';
import { HistoryMessage, TangoHistory } from './history';
import { TangoNode } from './node';
import { TangoJsModule } from './module';
import { TangoJsModule, TangoModule } from './module';
import { TangoFile, TangoJsonFile, TangoLessFile } from './file';
import { IWorkspace } from './interfaces';
import { IFileConfig, FileType, ITangoConfigPackages, IPageConfigData } from '../types';
import { IFileConfig, FileType, ITangoConfigPackages, IPageConfigData, IFileError } from '../types';
import { SelectSource } from './select-source';
import { DragSource } from './drag-source';
import { TangoRouteModule } from './route-module';
@@ -150,11 +151,8 @@ export class Workspace extends EventTarget implements IWorkspace {
*/
private copyTempNodes: TangoNode[];
/**
* 是否是合法的项目
*/
get isValid() {
return !!this.tangoConfigJson && !!this.activeViewModule;
return !!this.tangoConfigJson && !!this.activeViewModule && this.fileErrors.length === 0;
}
/**
@@ -222,6 +220,19 @@ export class Workspace extends EventTarget implements IWorkspace {
return Object.keys(this.componentsEntryModule?.exportList || {});
}
get fileErrors() {
const errors: IFileError[] = [];
this.files.forEach((file) => {
if (file.isError) {
errors.push({
filename: file.filename,
message: file.errorMessage,
});
}
});
return errors;
}
constructor(options?: IWorkspaceOptions) {
super();
this.history = new TangoHistory(this);
@@ -255,6 +266,8 @@ export class Workspace extends EventTarget implements IWorkspace {
activeViewFile: observable,
pages: computed,
bizComps: computed,
fileErrors: computed,
isValid: computed,
setActiveRoute: action,
setActiveFile: action,
addFile: action,
@@ -438,10 +451,16 @@ export class Workspace extends EventTarget implements IWorkspace {
);
}
updateFile(filename: string, code: string, shouldFormatCode = false) {
updateFile(filename: string, code: string, isSyncAst = true) {
const file = this.getFile(filename);
file.update(code);
if (shouldFormatCode && file instanceof TangoViewModule) {
if (file instanceof TangoModule) {
file.update(code, isSyncAst);
} else {
file.update(code);
}
const shouldFormat = this.projectConfig?.designerConfig?.autoFormatCode;
if (shouldFormat && file instanceof TangoViewModule) {
file.removeUnusedImportSpecifiers().update();
}
this.history.push({
@@ -452,12 +471,15 @@ export class Workspace extends EventTarget implements IWorkspace {
});
}
/**
* 删除工作区的文件
* @param filename
*/
syncFiles() {
this.files.forEach((file) => {
if (file instanceof TangoModule) {
file.updateAst();
}
});
}
removeFile(filename: string) {
// TODO: refactor visitFile to share this logic
if (this.files.get(filename)) {
// 如果是文件,直接删除
this.files.delete(filename);
@@ -513,7 +535,7 @@ export class Workspace extends EventTarget implements IWorkspace {
* @returns { [filename]: fileCode }
*/
listFiles() {
const ret = {};
const ret: Dict<string> = {};
this.files.forEach((file) => {
ret[file.filename] = file.cleanCode;
});
@@ -741,6 +763,7 @@ export class Workspace extends EventTarget implements IWorkspace {
* 更新依赖,没有就添加
* @param name
* @param version
* FIXME: 参数3的设置需要重新考虑下
*/
updateDependency(
name: string,
@@ -763,15 +786,12 @@ export class Workspace extends EventTarget implements IWorkspace {
if (!packages) {
return undefined;
}
if (options?.package || packages[name]) {
packages[name] = {
type: packages[name]?.type,
// 如果没有传入 package 信息,则沿用 tango.config.json 中已记录的数据
// 如果传入了,则全量使用传入的信息,适配可能在某个版本中如 resources 等字段被删除的情况
...(options?.package || packages[name]),
version,
};
}
setValue(packages, name, {
...packages[name], // 保留原有的配置
version, // 更新版本号
...options?.package, // 更新 package 配置
});
return packages;
})
+5
View File
@@ -57,6 +57,11 @@ export interface IFileConfig {
type?: FileType;
}
export interface IFileError {
filename: string;
message: string;
}
/**
* 视图节点数据类型
*/
+64
View File
@@ -3,6 +3,70 @@
All notable changes to this project will be documented in this file.
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)
### 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
+7 -7
View File
@@ -1,6 +1,6 @@
{
"name": "@music163/tango-designer",
"version": "1.3.1",
"version": "1.4.6",
"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.3",
"@music163/tango-core": "^1.3.1",
"@music163/tango-helpers": "^1.2.0",
"@music163/tango-sandbox": "^1.0.6",
"@music163/tango-setting-form": "^1.2.6",
"@music163/tango-ui": "^1.3.1",
"@music163/tango-context": "^1.1.10",
"@music163/tango-core": "^1.4.4",
"@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",
"antd": "^4.24.2",
"cash-dom": "^8.1.2",
"classnames": "^2.5.1",
@@ -34,17 +34,17 @@ export const ComponentsPopover = observer(
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
workspace.componentPrototypes.get(selectedNode?.name) ?? ({} as IComponentPrototype);
// 推荐使用的子组件
const insertedList = useMemo(
() =>
Array.isArray(prototype?.childrenName)
? prototype?.childrenName
: [prototype?.childrenName].filter(Boolean),
[prototype?.childrenName],
);
// 推荐使用的代码片段
const siblingList = useMemo(() => prototype?.siblingNames ?? [], [prototype.siblingNames]);
const recommendedList = useMemo(() => {
if (type === 'inner') {
return prototype?.childrenName
? Array.isArray(prototype?.childrenName)
? prototype.childrenName
: [prototype.childrenName]
: [];
}
// 默认推荐使用相同类型的组件作为兄弟节点
return prototype.siblingNames || [prototype.name];
}, [prototype.childrenName, prototype.siblingNames, prototype.name, type]);
const tipsTextMap = useMemo(
() => ({
@@ -82,22 +82,15 @@ export const ComponentsPopover = observer(
const menuList = JSON.parse(JSON.stringify(designer.menuData));
const commonList = menuList['common'] ?? [];
if (commonList?.length && siblingList?.length) {
commonList.unshift({
title: '代码片段',
items: siblingList,
});
}
if (commonList?.length && insertedList?.length) {
if (commonList?.length && recommendedList.length) {
commonList.unshift({
title: '推荐使用',
items: insertedList,
items: recommendedList,
});
}
return menuList;
}, [insertedList, siblingList, designer.menuData]);
}, [recommendedList, designer.menuData]);
const innerTypeProps =
// 手动触发 适用于 点击添加组件
@@ -125,6 +118,8 @@ export const ComponentsPopover = observer(
}
footer={tipsTextMap[type]}
width="330px"
height="450px"
resizeable
maskClosable
body={
<ComponentsPanel
@@ -133,10 +128,6 @@ export const ComponentsPopover = observer(
menuData={menuData}
layout={layout}
onItemSelect={handleSelect}
style={{
maxHeight: '400px',
overflow: 'auto',
}}
/>
}
{...popoverProps}
@@ -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-modal';
export * from './components-popover';
export * from './file-errors-overlay';
@@ -1,11 +1,11 @@
import { isValidFunctionCode, url2serviceName } from '@music163/tango-helpers';
import { Dict, isValidFunctionCode, url2serviceName } from '@music163/tango-helpers';
import { InputCode } from '@music163/tango-ui';
import { Button, Dropdown, Form, FormProps, Input, Select, Space } from 'antd';
import { Box } from 'coral-system';
import React, { useState } from 'react';
interface AddServiceFormProps extends FormProps {
initialValues?: object;
initialValues?: Dict;
onSubmit: (values: object) => void;
onCancel: () => void;
serviceModules: object[];
@@ -88,6 +88,10 @@ export function AddServiceForm({
>
<Input placeholder="请输入数据服务调用名称" disabled={disabled || isModifyMode} />
</Form.Item>
{/* 备注 */}
<Form.Item label="备注" name="description">
<Input placeholder="请输入备注信息" />
</Form.Item>
<Form.Item label="路径" name="url" rules={[{ required: true, type: 'url' }]}>
<Input placeholder="请输入数据服务调用名称" disabled={disabled || isModifyMode} />
</Form.Item>
+3 -1
View File
@@ -1,8 +1,10 @@
import { Dict } from '@music163/tango-helpers';
/**
* 快捷键
*/
export class Hotkey {
private readonly hotkeyMap = {};
private readonly hotkeyMap: Dict = {};
constructor(hotkeys: Record<string, Function>) {
Object.keys(hotkeys).forEach((hotkey) => {
+4 -1
View File
@@ -137,6 +137,8 @@ export function useDnd({
const data = sandboxQuery.getDraggableParentsData(e.target as HTMLElement, true);
if (data && data.id) {
selectSource.select(data);
// 画布选中节点时,自动唤起 right-panel
designer.toggleRightPanel(true);
}
};
@@ -386,7 +388,8 @@ export function useDnd({
};
const onKeyDown = (e: React.KeyboardEvent) => {
const isInputElement = isInputElements((e.target as HTMLElement).nodeName);
const target = e.target as HTMLElement;
const isInputElement = target.isContentEditable || isInputElements(target.nodeName);
// 禁用输入事件的触发的快捷键逻辑
if (!isInputElement) {
const key = getHotkey(e);
+80 -109
View File
@@ -2,8 +2,6 @@ import React, { useRef, useEffect, useCallback } from 'react';
import { Box } 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';
const ideConfig = {
// disableFileOps: {
@@ -21,116 +19,89 @@ const ideConfig = {
// disableSetting: true,
};
export interface CodeEditorProps extends Partial<MultiEditorProps> {
/**
* 是否自动清楚未使用的导入
*/
autoRemoveUnusedImports?: boolean;
}
export type CodeEditorProps = Partial<MultiEditorProps>;
export const CodeEditor = observer(
({ autoRemoveUnusedImports = true, ...rest }: CodeEditorProps) => {
const editorRef = useRef(null);
const workspace = useWorkspace();
const designer = useDesigner();
const files = workspace.listFiles();
const activeFile = workspace.activeFile;
export const CodeEditor = observer((props: CodeEditorProps) => {
const editorRef = useRef(null);
const workspace = useWorkspace();
const designer = useDesigner();
const files = workspace.listFiles();
const activeFile = workspace.activeFile;
let loc: any; // 记录视图代码的选中位置
const selectNode = workspace.selectSource.firstNode;
if (selectNode && activeFile === workspace.activeViewFile) {
loc = selectNode.loc;
}
let loc: any; // 记录视图代码的选中位置
const selectNode = workspace.selectSource.firstNode;
if (selectNode && activeFile === workspace.activeViewFile) {
loc = selectNode.loc;
}
useEffect(() => {
editorRef.current?.refresh(files, activeFile, loc);
}, [files, activeFile, loc]);
useEffect(() => {
editorRef.current?.refresh(files, activeFile, loc);
}, [files, activeFile, loc]);
const fileSave = useCallback(
(path: string, value: string) => {
if (!isJsFile(path)) {
// 非 js 文件直接保存
workspace.updateFile(path, value, autoRemoveUnusedImports);
return;
const fileSave = useCallback(
(path: string, value: string) => {
workspace.updateFile(path, value, false);
},
[workspace],
);
const handleFileChange = useCallback(
(type: string, info: any) => {
switch (type) {
case 'addFile':
workspace.addFile(info.path, info.value);
break;
case 'deleteFile':
case 'deleteFolder':
workspace.removeFile(info.path);
break;
case 'renameFile':
workspace.renameFile(info.path, info.newpath);
workspace.setActiveFile(info.newpath);
break;
case 'renameFolder':
workspace.renameFolder(info.path, info.newpath);
break;
case 'addFolder':
default:
break;
}
},
[workspace],
);
const handlePathChange = useCallback(
(path: string) => {
workspace.setActiveFile(path);
},
[workspace],
);
const borderStyle =
designer.activeView === 'dual'
? {
borderLeft: 'solid 1px var(--tango-colors-line2)',
borderRight: 'solid 1px var(--tango-colors-line2)',
}
: {};
// js 文件需要先检查语法,只有语法正确才会保存
if (isValidCode(value)) {
workspace.updateFile(path, value, autoRemoveUnusedImports);
} else {
Modal.confirm({
title: '检测到代码中存在语法错误,暂时无法将代码同步给设计器,是否回退到安全代码?',
onOk: () => {
editorRef.current?.refresh(files, activeFile);
},
onCancel: () => {},
});
}
},
[workspace, autoRemoveUnusedImports, activeFile, files],
);
const handleFileChange = useCallback(
(type: string, info: any) => {
switch (type) {
case 'addFile':
workspace.addFile(info.path, info.value);
break;
case 'deleteFile':
case 'deleteFolder':
workspace.removeFile(info.path);
break;
case 'renameFile':
workspace.renameFile(info.path, info.newpath);
workspace.setActiveFile(info.newpath);
break;
case 'renameFolder':
workspace.renameFolder(info.path, info.newpath);
break;
case 'addFolder':
default:
break;
}
},
[workspace],
);
const handlePathChange = useCallback(
(path: string) => {
workspace.setActiveFile(path);
},
[workspace],
);
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);
}
return (
<Box display="flex" flexDirection="row" height="100%" bg="white" {...borderStyle}>
<MultiEditor
ref={editorRef}
options={{
fontSize: 14,
automaticLayout: true,
}}
ideConfig={ideConfig}
onFileSave={fileSave}
onPathChange={handlePathChange}
onFileChange={handleFileChange}
defaultPath={activeFile}
defaultTheme="GithubLightDefault"
defaultFiles={files}
{...props}
/>
</Box>
);
});
+2 -2
View File
@@ -1,7 +1,7 @@
import React, { useImperativeHandle, ForwardedRef, useRef, useState, useEffect } from 'react';
import { Box } from 'coral-system';
import { CodeSandbox, CodeSandboxProps } from '@music163/tango-sandbox';
import { getValue, isFunction, logger, pick, setValue } from '@music163/tango-helpers';
import { Dict, getValue, isFunction, logger, pick, setValue } from '@music163/tango-helpers';
import { observer, useWorkspace, useDesigner } from '@music163/tango-context';
import { Simulator, Viewport } from '../simulator';
import { useSandboxQuery } from '../context';
@@ -88,7 +88,7 @@ function useSandbox({
onViewChange: (data) => onViewChange && onViewChange(data, getSandboxConfig()),
});
let files = Array.from(workspace.files.keys()).reduce((prev, filename) => {
let files = Array.from(workspace.files.keys()).reduce<Dict>((prev, filename) => {
let code = workspace.getFile(filename).code;
if (filename === '/tango.config.json') {
code = mergeTangoConfigJson(code, { isPreview, injectScript, formatter: configFormatter });
@@ -1,11 +1,11 @@
import React, { useCallback } from 'react';
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 { IconFont } from '@music163/tango-ui';
interface ChoiceSetterProps {
options?: OptionType[];
options?: IOptionItem[];
}
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 { 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 { getValue, interpolate, IVariableTreeNode, noop } from '@music163/tango-helpers';
import { CloseCircleFilled, MenuOutlined } from '@ant-design/icons';
import {
Panel,
InputCode,
Action,
DragPanel,
PopOutOutlined,
InputCodeProps,
InputStyleCode,
} from '@music163/tango-ui';
import { FormItemComponentProps } from '@music163/tango-setting-form';
import { useWorkspace, useWorkspaceData } from '@music163/tango-context';
import { VariableTree } from '../components';
@@ -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`
display: flex;
align-items: center;
@@ -37,26 +58,32 @@ const suffixStyle = css`
}
`;
export interface ExpressionSetterProps extends FormItemComponentProps<string> {
export interface CodeSetterProps extends FormItemComponentProps<string> {
modalTitle?: string;
modalTip?: string;
autoCompleteOptions?: string[];
allowClear?: boolean;
showOptionsDropDown?: boolean;
/**
* css object
*/
expressionType?: 'cssObject';
}
export function ExpressionSetter(props: ExpressionSetterProps) {
export function CodeSetter(props: CodeSetterProps) {
const {
onChange,
modalTitle,
modalTip,
autoCompleteOptions,
placeholder = '在这里输入JS代码',
template,
placeholder = '在这里输入代码',
value: valueProp,
status,
allowClear = true,
newStoreTemplate,
showOptionsDropDown = true,
expressionType,
} = props;
// const codeValue = getCodeOfWrappedCode(valueProp);
const [inputValue, setInputValue] = useState(valueProp);
@@ -79,10 +106,12 @@ export function ExpressionSetter(props: ExpressionSetterProps) {
const sandbox = useSandboxQuery();
const evaluateContext = sandbox.window;
const InputCodeComponent = expressionType === 'cssObject' ? InputStyleCode : InputCode;
return (
<Box className="ExpressionSetter">
{/* 同时支持下拉框展示 */}
<InputCode
<InputCodeComponent
placeholder={placeholder}
suffix={
<Box css={suffixStyle}>
@@ -90,7 +119,7 @@ export function ExpressionSetter(props: ExpressionSetterProps) {
<CloseCircleFilled
title="清空"
onClick={() => {
change('');
change(undefined);
}}
/>
)}
@@ -119,8 +148,10 @@ export function ExpressionSetter(props: ExpressionSetterProps) {
subTitle={modalTip}
placeholder={placeholder}
autoCompleteOptions={autoCompleteOptions}
template={template}
newStoreTemplate={newStoreTemplate}
value={inputValue}
expressionType={expressionType}
onOk={(value) => {
change(value);
}}
@@ -141,7 +172,7 @@ export function ExpressionSetter(props: ExpressionSetterProps) {
);
}
export interface ExpressionPopoverProps {
export interface ExpressionPopoverProps extends InputCodeProps {
title?: string;
subTitle?: string;
placeholder?: string;
@@ -150,10 +181,18 @@ export interface ExpressionPopoverProps {
onOk?: (value: string) => void;
dataSource?: IVariableTreeNode[];
autoCompleteOptions?: string[];
/**
*
*/
template?: string;
/**
* store
*/
newStoreTemplate?: string;
/**
* css object
*/
expressionType?: 'cssObject';
children?: React.ReactNode;
}
@@ -166,12 +205,15 @@ export function ExpressionPopover({
value,
dataSource,
autoCompleteOptions,
template,
newStoreTemplate = CODE_TEMPLATES.newStoreTemplate,
children,
expressionType,
}: ExpressionPopoverProps) {
const [exp, setExp] = useState(value ?? defaultValue);
const [error, setError] = useState('');
const workspace = useWorkspace();
const InputCodeComponent = expressionType === 'cssObject' ? InputStyleCode : InputCode;
const selectNodePath = workspace.selectSource.selected[0]?.codeId;
@@ -182,7 +224,7 @@ export function ExpressionPopover({
}));
const sandbox = useSandboxQuery();
const evaluateContext = sandbox.window;
const evaluateContext: any = sandbox.window;
const handleExpInputChange = (val: string) => {
setExp(val?.trim());
@@ -195,56 +237,64 @@ 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">
<InputCode
<Box display="flex" flexDirection="column">
<Box p="m">
<InputCodeComponent
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="div">
</Text>
) : (
<Text fontSize="12px" color="text3">
javascript 使 jsx
) : null}
<Box fontSize="12px" color="text2">
<Text display="block"></Text>
{subTitle && <Text display="block">{subTitle}</Text>}
<Text display="block">
javascript 使 jsx
</Text>
)}
</Panel>
</Box>
</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']}
appContext={evaluateContext['tango']}
getStoreNames={() => Object.keys(workspace.storeModules)}
serviceModules={serviceModules}
getServiceData={(serviceKey) => {
@@ -265,7 +315,11 @@ export function ExpressionPopover({
}
let str;
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 {
str = `${node.key}`;
}
@@ -307,10 +361,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 +384,12 @@ export function ExpressionPopover({
)}
>
{children || (
<Action tooltip="打开表达式变量选择面板" icon={<ExpandAltOutlined />} size="small" />
<Action
tooltip="弹出代码编辑和变量选择面板"
tooltipProps={{ placement: 'topLeft' }}
icon={<PopOutOutlined />}
size="small"
/>
)}
</DragPanel>
);
+29 -33
View File
@@ -1,11 +1,11 @@
import React, { useCallback, useMemo, useState } from 'react';
import { css, Box, Text } from 'coral-system';
import { AutoComplete } from 'antd';
import { AutoComplete, Input } from 'antd';
import { ActionSelect } from '@music163/tango-ui';
import { FormItemComponentProps } from '@music163/tango-setting-form';
import { useWorkspace, useWorkspaceData } from '@music163/tango-context';
import { wrapCode } from '@music163/tango-helpers';
import { ExpressionPopover } from './expression-setter';
import { Dict, wrapCode } from '@music163/tango-helpers';
import { ExpressionPopover, getCallbackValue } from './code-setter';
import { value2code } from '@music163/tango-core';
enum EventAction {
@@ -31,7 +31,7 @@ export type EventSetterProps = FormItemComponentProps<string>;
* 事件监听函数绑定器
*/
export function EventSetter(props: EventSetterProps) {
const { value, onChange, modalTitle } = props;
const { value, onChange, modalTitle, modalTip, template } = props;
const [type, setType] = useState<EventAction>(); // 事件类型
const [temp, setTemp] = useState(''); // 二级暂存值
const { actionVariables, routeOptions } = useWorkspaceData();
@@ -59,7 +59,9 @@ export function EventSetter(props: EventSetterProps) {
label: (
<ExpressionPopover
title={modalTitle}
subTitle={modalTip}
value={value}
template={template}
onOk={(nextValue) => {
handleChange(nextValue);
}}
@@ -74,30 +76,34 @@ export function EventSetter(props: EventSetterProps) {
{ label: '打开弹窗', value: EventAction.OpenModal },
{ label: '关闭弹窗', value: EventAction.CloseModal },
],
[modalTitle, value, actionVariables, handleChange],
[modalTitle, value, actionVariables, template, handleChange],
);
const onAction = (key: string) => {
setType(key as EventAction); // 记录事件类型
setTemp(''); // 重置二级选项值
switch (key) {
case EventAction.ConsoleLog:
handleChange('(...args) => console.log(...args)');
break;
case EventAction.NoAction:
handleChange(undefined);
break;
default:
break;
if (key === EventAction.NoAction) {
handleChange(undefined);
return;
}
};
const actionText = getActionText(type, temp, code);
return (
<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 && (
<AutoComplete
placeholder="选择或输入页面路由"
@@ -141,26 +147,16 @@ export function EventSetter(props: EventSetterProps) {
);
}
const handlerMap = {
[EventAction.OpenModal]: 'openModal',
[EventAction.CloseModal]: 'closeModal',
[EventAction.NavigateTo]: 'navigateTo',
const handlerMap: Dict = {
[EventAction.ConsoleLog]: 'console.log',
[EventAction.OpenModal]: 'tango.openModal',
[EventAction.CloseModal]: 'tango.closeModal',
[EventAction.NavigateTo]: 'tango.navigateTo',
};
function getActionText(type: EventAction, temp: string, 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 = '') {
const handler = handlerMap[type];
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 { EnumSetter } from './enum-setter';
import { EventSetter } from './event-setter';
import { ExpressionSetter } from './expression-setter';
import { CodeSetter } from './code-setter';
import { JSONSetter } from './json-setter';
import { JsxSetter } from './jsx-setter';
import { ListSetter } from './list-setter';
import { ModelSetter } from './model-setter';
import { OptionSetter } from './option-setter';
import { PickerSetter } from './picker-setter';
import { RenderSetter, TableCellSetter, TableExpandableSetter } from './render-setter';
import { RenderSetter } from './render-setter';
import { RouterSetter } from './router-setter';
import {
SpacingSetter,
@@ -26,6 +26,7 @@ import {
FlexDirectionSetter,
} from './style-setter';
import { ChoiceSetter } from './choice-setter';
import { ClassNameSetter } from './classname-setter';
import { isValidExpressionCode } from '@music163/tango-core';
const codeValidate: IFormItemCreateOptions['validate'] = (value, field) => {
@@ -49,10 +50,14 @@ export const BUILT_IN_SETTERS: IFormItemCreateOptions[] = [
{
name: 'codeSetter',
alias: ['expSetter', 'expressionSetter'],
component: ExpressionSetter,
component: CodeSetter,
type: 'code',
validate: codeValidate,
},
{
name: 'classNameSetter',
component: ClassNameSetter,
},
{
name: 'radioGroupSetter',
alias: ['choiceSetter'],
@@ -126,22 +131,12 @@ export const BUILT_IN_SETTERS: IFormItemCreateOptions[] = [
component: PickerSetter,
},
{
name: 'renderPropsSetter',
name: 'renderSetter',
alias: ['renderPropsSetter'],
component: RenderSetter,
type: 'code',
validate: codeValidate,
},
{
name: 'tableCellSetter',
component: TableCellSetter,
type: 'code',
},
{
name: 'tableExpandableSetter',
component: TableExpandableSetter,
type: 'code',
validate: codeValidate,
},
{
name: 'routerSetter',
component: RouterSetter,
@@ -3,10 +3,11 @@ import { css, Box } from 'coral-system';
import { Form, Input, Button, Popover } from 'antd';
import { DeleteOutlined, EditOutlined, HolderOutlined } from '@ant-design/icons';
import { FormItemComponentProps } from '@music163/tango-setting-form';
import { Dict } from '@music163/tango-helpers';
import { DragBox } from '../components';
function copyWithoutUndefined(source: Record<string, any>) {
const ret = {};
function copyWithoutUndefined(source: Dict) {
const ret: Dict = {};
for (const key in source) {
if (source[key] !== undefined) {
ret[key] = source[key];
@@ -60,7 +60,7 @@ export function ModelSetter({
}: FormItemComponentProps) {
const [inputValue, setInputValue] = useState(value);
const { modelVariables } = useWorkspaceData();
const evaluateContext = useSandboxQuery().window || {};
const evaluateContext: any = useSandboxQuery().window || {};
const workspace = useWorkspace();
const definedVariables = useMemo(() => {
const list: string[] = [];
+11 -80
View File
@@ -2,21 +2,16 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { ActionSelect, InputCode } from '@music163/tango-ui';
import { FormItemComponentProps } from '@music163/tango-setting-form';
import { Box } from 'coral-system';
interface IRenderOption {
label: string;
value: string;
render?: string;
relatedImports?: string[];
}
import { Dict, IOptionItem } from '@music163/tango-helpers';
import { getCallbackValue } from './code-setter';
export interface RenderSetterProps {
text?: string;
options?: IRenderOption[];
fallbackOption?: IRenderOption;
options?: IOptionItem[];
fallbackOption?: IOptionItem;
}
const defaultOptions: IRenderOption[] = [
const defaultOptions: IOptionItem[] = [
{ label: '取消自定义', value: '' },
{ label: '自定义渲染', value: 'Box', render: '() => <Box></Box>' },
];
@@ -29,6 +24,7 @@ export function RenderSetter({
onChange,
text = '自定义渲染为',
options = defaultOptions,
template = `() => {{content}}`,
fallbackOption,
}: FormItemComponentProps & RenderSetterProps) {
const [inputValue, setInputValue] = useState(value || '');
@@ -37,7 +33,7 @@ export function RenderSetter({
}, [value]);
const optionsMap = useMemo(() => {
return options.reduce((prev, cur) => {
return options.reduce<Dict>((prev, cur) => {
prev[cur.value] = cur;
return prev;
}, {});
@@ -46,13 +42,14 @@ export function RenderSetter({
const onSelect = useCallback(
(key: string) => {
const option = optionsMap[key] || fallbackOption;
if (option?.render) {
onChange(option.render, { relatedImports: option.relatedImports });
const next = option?.render || getCallbackValue(option.renderBody, template);
if (next) {
onChange(next, { relatedImports: option.relatedImports });
} else {
onChange(undefined);
}
},
[optionsMap, fallbackOption, onChange],
[optionsMap, fallbackOption, onChange, template],
);
return (
<Box>
@@ -67,69 +64,3 @@ export function RenderSetter({
</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 { Box } from 'coral-system';
import { Input, Radio } from 'antd';
import { isValidUrl } from '@music163/tango-helpers';
import { Dict, isValidUrl } from '@music163/tango-helpers';
import { FormItemComponentProps } from '@music163/tango-setting-form';
import { useWorkspaceData } from '@music163/tango-context';
import { PickerSetter } from './picker-setter';
const routerModeMap: Dict = {
link: '链接',
router: '路由',
both: '',
};
export function RouterSetter(props: FormItemComponentProps) {
const [type, setType] = useState<'input' | 'select'>(() => {
return isValidUrl(props.value) ? 'input' : 'select';
@@ -37,7 +43,7 @@ export function RouterSetter(props: FormItemComponentProps) {
)}
{type === 'input' && (
<Input
placeholder={`请输入任意${{ link: '链接', router: '路由', both: '' }[displayType]}地址`}
placeholder={`请输入任意${routerModeMap[displayType]}地址`}
{...props}
value={input}
onChange={(e) => setInput(e.target.value)}
+2 -2
View File
@@ -2,7 +2,7 @@ import React from 'react';
import { Box } from 'coral-system';
import { SettingForm, FormModel, SettingFormProps } from '@music163/tango-setting-form';
import { Panel } from '@music163/tango-ui';
import { clone, parseDndId } from '@music163/tango-helpers';
import { clone, Dict, parseDndId } from '@music163/tango-helpers';
import { observer, useDesigner, useWorkspace } from '@music163/tango-context';
import { registerBuiltinSetters } from './setters';
@@ -34,7 +34,7 @@ export const SettingPanel = observer((props: SettingPanelProps) => {
}
const getSettingValue = (attributes: Record<string, string>) => {
const ret = {};
const ret: Dict = {};
const keys = Object.keys(attributes);
for (const key of keys) {
if (/^data-/.test(key)) {
@@ -2,6 +2,7 @@ import React, { useMemo, useState } from 'react';
import { Box, Grid, Text } from 'coral-system';
import styled, { css } from 'styled-components';
import {
Dict,
IComponentPrototype,
MenuDataType,
MenuValueType,
@@ -66,7 +67,7 @@ export interface ComponentsPanelProps {
layout?: 'grid' | 'line';
}
const localeMap = {
const localeMap: Dict = {
common: '基础组件',
atom: '原子组件',
snippet: '组合',
@@ -124,7 +125,7 @@ export const ComponentsPanel = observer(
const tabs = Object.keys(menuData).map((key) => ({
key,
label: localeMap[key],
children: <MaterialList data={menuData[key]} />,
children: <MaterialList data={(menuData as any)[key]} />,
}));
if (showBizComps) {
@@ -364,7 +365,7 @@ function MaterialGrid({ data }: MaterialProps) {
<IconFont className="material-icon" type={data.icon || 'icon-placeholder'} />
) : (
<Box className="material-icon">
<img src={icon} alt={data.name} />
<img src={icon} alt={data.name} height="32" />
</Box>
);
@@ -24,7 +24,7 @@ const DataSourceView = observer(() => {
<Box className="ServiceFunctionList" p="m">
<VariableTree
dataSource={serviceVariables}
appContext={sandbox?.window['tango']}
appContext={(sandbox?.window as any)['tango']}
serviceModules={serviceModules}
onRemoveService={(variableKey) => {
workspace.removeServiceFunction(variableKey);
@@ -4,6 +4,7 @@ import { ConfigGroup, ConfigItem } from '@music163/tango-ui';
import { Box, css } from 'coral-system';
import { Input, Switch, Form, Button, message, FormProps } from 'antd';
import { CloseCircleOutlined, CloseOutlined } from '@ant-design/icons';
import { Dict } from '@music163/tango-helpers';
export default observer(() => {
const workspace = useWorkspace();
@@ -63,7 +64,7 @@ function ProxyManage({
initialValues: initialValuesProp = {},
onSave,
}: {
initialValues?: object;
initialValues?: Dict;
onSave?: (values: any) => void;
}) {
const initialValues = useMemo(() => {
@@ -84,7 +85,7 @@ function ProxyManage({
{...proxyManageFormStyle}
initialValues={initialValues}
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;
return prev;
}, {});
@@ -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(
() =>
@@ -1,12 +1,12 @@
import React, { useEffect, useReducer } from 'react';
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 { ReloadOutlined } from '@ant-design/icons';
import { useSandboxQuery } from '../../context';
function processTango(obj: any = {}, index = 0) {
const ret = {};
function processTango(obj: Dict = {}, index = 0) {
const ret: Dict = {};
Object.keys(obj).forEach((key) => {
const val = obj[key];
if (index > 2) {
@@ -25,11 +25,12 @@ function processTango(obj: any = {}, index = 0) {
export const StateTree = observer(() => {
const sandboxQuery = useSandboxQuery();
const [, forceUpdate] = useReducer((x) => x + 1, 0);
const tangoContext = pick(sandboxQuery.window['tango'] || {}, [
const tangoContext = pick((sandboxQuery.window as any)['tango'] || {}, [
'stores',
'pageStore',
'services',
'config',
'routeData',
]);
const callback = () => {
forceUpdate();
@@ -19,7 +19,7 @@ const resizeHandleStyle = (axis: ResizableBoxProps['axis'], barStyle: HTMLCoralP
cursor: ${axis === 'x' ? 'col-resize' : 'row-resize'};
${barStyle &&
Object.keys(barStyle)
.map((key) => `${key}: ${barStyle[key]};`)
.map((key) => `${key}: ${(barStyle as any)[key]};`)
.join('')}
&:hover,
&:active {
+28 -18
View File
@@ -3,7 +3,7 @@ import cx from 'classnames';
import { Box, Text, css, HTMLCoralProps } from 'coral-system';
import { Badge } from 'antd';
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 { getWidget } from '../widgets';
@@ -19,15 +19,22 @@ const sidebarStyle = css`
list-style: none;
user-select: none;
transition: all 0.3s ease;
&.disabled {
pointer-events: none;
}
}
.SidebarPanelBarListItem {
color: var(--tango-colors-text2);
&.active {
background-color: var(--tango-colors-custom-sidebarItemActiveBg);
color: var(--tango-colors-brand);
background-color: var(--tango-colors-custom-sidebarItemActiveBg, --tango-colors-fill2);
}
&: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) {
const designer = useDesigner();
const workspace = useWorkspace();
const items = useMemo(() => {
const ret: Record<string, SidebarPanelItemProps> = {};
@@ -92,10 +100,23 @@ function BaseSidebarPanel({ panelWidth: defaultPanelWidth = 266, footer, childre
return ret;
}, [children]);
if (designer.activeView !== 'design') {
return null;
}
return (
<Box display="flex" flexShrink={0} css={sidebarStyle} className="SidebarPanel">
<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) => {
const item = items[key];
const isActive = key === designer.activeSidebarPanel;
@@ -112,7 +133,6 @@ function BaseSidebarPanel({ panelWidth: defaultPanelWidth = 266, footer, childre
icon={item.icon}
label={item.label}
showBadge={item.showBadge}
isActive={isActive}
/>
</li>
);
@@ -183,10 +203,6 @@ BaseSidebarPanel.Item = function ({
export const Sidebar = observer(BaseSidebarPanel);
interface SidebarPanelBarItemProps {
/**
* 是否选中
*/
isActive?: boolean;
/**
* 侧边栏图标
*/
@@ -212,15 +228,9 @@ interface SidebarPanelBarItemProps {
};
}
function SidebarPanelBarItem({
isActive,
icon: iconProp,
label,
showBadge,
}: SidebarPanelBarItemProps) {
const color = isActive ? 'brand' : 'text.body';
function SidebarPanelBarItem({ icon: iconProp, label, showBadge }: SidebarPanelBarItemProps) {
let icon = (
<Text fontSize="24px" lineHeight={1} color={color}>
<Text fontSize="24px" lineHeight={1}>
{iconProp}
</Text>
);
@@ -241,7 +251,7 @@ function SidebarPanelBarItem({
py="12px"
>
{icon}
<Text fontSize="12px" mt="s" color={color}>
<Text fontSize="12px" mt="s">
{label}
</Text>
</Box>
+15 -13
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 />} />
+35 -12
View File
@@ -25,22 +25,45 @@ function DesktopSimulator({ children }: HTMLCoralProps<'div'>) {
}
const mobileStyle = css`
--device-viewport-width: 375px;
--device-viewport-height: 812px;
--device-frame-width: 13px;
background-image: url(https://p5.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/21897838692/b1c5/2665/56db/10558c1bb2030d55ceabbcc74431de99.png);
background-repeat: no-repeat;
background-size: 100% 100%;
box-shadow: 0 2px 8px rgb(2 8 20 / 10%), 0 8px 16px rgb(2 8 20 / 8%);
border-radius: 62px;
overflow: hidden;
--device-viewport-width: 360px;
--device-viewport-height: 640px;
position: relative;
margin: auto;
border: 16px black solid;
border-top-width: 60px;
border-bottom-width: 60px;
border-radius: 36px;
&::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 {
border-radius: 50px;
width: var(--device-viewport-width);
height: var(--device-viewport-height);
margin: var(--device-frame-width);
overflow: hidden;
background: white;
}
`;
+3 -2
View File
@@ -5,11 +5,12 @@ import { observer, useWorkspace } from '@music163/tango-context';
export const HistoryTool = observer(() => {
const workspace = useWorkspace();
const disabled = !workspace.isValid;
return (
<Box display="flex" columnGap="m">
<ToggleButton
shape="ghost"
disabled={!workspace.history.couldBack || workspace.history.index === 0}
disabled={disabled || !workspace.history.couldBack || workspace.history.index === 0}
tooltip="撤销"
onClick={() => {
workspace.history.back();
@@ -20,7 +21,7 @@ export const HistoryTool = observer(() => {
</ToggleButton>
<ToggleButton
shape="ghost"
disabled={!workspace.history.couldForward}
disabled={disabled || !workspace.history.couldForward}
tooltip="重做"
onClick={() => {
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 { Modal } from 'antd';
import { ToggleButton, CodeOutlined, DualOutlined } from '@music163/tango-ui';
import { observer, useDesigner, useWorkspace } from '@music163/tango-context';
import { BorderOutlined } from '@ant-design/icons';
@@ -9,23 +8,13 @@ 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>
<Group attached whiteSpace="nowrap">
<ToggleButton
shape="ghost"
selected={designer.activeView === 'design'}
onClick={() => {
workspace.syncFiles(); // 保证 ast 与 code 是同步的
designer.setActiveView('design');
}}
tooltip="设计视图"
@@ -38,7 +27,6 @@ export const ModeSwitchTool = observer(() => {
onClick={() => {
designer.setActiveView('code'); // 切换到源码视图
designer.setActiveSidebarPanel(''); // 关闭左侧面板
activeFileCheck();
}}
tooltip="源码视图"
>
@@ -50,7 +38,6 @@ export const ModeSwitchTool = observer(() => {
onClick={() => {
designer.setActiveView('dual'); // 切换到双屏视图
designer.setActiveSidebarPanel(''); // 关闭左侧面板
activeFileCheck();
}}
tooltip="双屏视图"
>
+3 -1
View File
@@ -1,13 +1,15 @@
import React from 'react';
import { ToggleButton } from '@music163/tango-ui';
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(() => {
const designer = useDesigner();
const workspace = useWorkspace();
return (
<ToggleButton
disabled={!workspace.isValid}
shape="ghost"
selected={designer.isPreview}
onClick={() => {
@@ -47,11 +47,14 @@ function PageSelect({ value, onSelect, onRemove, onUpdate, onCopy, options }: Pa
setVisible(false);
};
const current = optionMap[value];
const pathname = value?.split?.('?')[0];
const current = optionMap[pathname];
return (
<ToggleButton
shape="ghost"
minWidth={100}
maxWidth={200}
textAlign="left"
dropdownProps={{
open: visible,
onOpenChange: setVisible,
+46 -16
View File
@@ -1,6 +1,8 @@
import React, { useMemo } from 'react';
import { Box, Group } from 'coral-system';
import { ReactComponentProps } from '@music163/tango-helpers';
import { DesignerViewType } from '@music163/tango-core';
import { observer, useDesigner } from '@music163/tango-context';
import { getWidget } from '../widgets';
export interface ToolbarProps {
@@ -15,25 +17,23 @@ export function Toolbar({ children }: ToolbarProps) {
let prevPlacement: string;
React.Children.forEach(children, (child: React.ReactElement, index) => {
let fallbackNode;
const childProps = child.props;
let fallbackChildren;
if (child.key) {
const Widget = getWidget(['toolbar', child.key].join('.'));
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;
node = (
<div key={key} className="ToolbarPanelItem" data-key={key}>
{node}
</div>
);
const placement = child.props.placement || prevPlacement || 'center';
const node = React.cloneElement(child, {
key,
'data-key': key,
children: childProps.children || fallbackChildren,
});
const placement = childProps.placement || prevPlacement || 'center';
switch (placement) {
case 'left':
left.push(node);
@@ -68,17 +68,47 @@ export function Toolbar({ children }: ToolbarProps) {
}
export interface ToolbarItemProps extends ReactComponentProps {
/**
* 类型
*/
type?: 'divider';
placement?: 'left' | 'center' | 'right';
children?: React.ReactElement;
/**
* 如果 key 匹配到内置组件的话,传递给子节点的属性
*/
widgetProps?: object;
/**
* 工具箱展示的视图
*/
activeViews?: DesignerViewType[];
}
function ToolbarItem({ placement, widgetProps, children }: ToolbarItemProps) {
return <div>{children}</div>;
}
const ToolbarItem = observer(
({
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() {
return <Box className="Separator" width={1} height={16} bg="colors.custom.toolbarDividerColor" />;
+1 -1
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`,
);
+9 -7
View File
@@ -1,35 +1,37 @@
import React from 'react';
import { Box } from 'coral-system';
import { Box, HTMLCoralProps } from 'coral-system';
import { observer, useDesigner } from '@music163/tango-context';
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 的模式
*/
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';
// 云音乐移动端模式小屏幕适配,可能会溢出屏幕
// 移动端模式小屏幕适配,可能会溢出屏幕
const overflow =
designer.simulator.name === 'phone' ? 'auto' : designer.isPreview ? 'auto' : 'hidden';
return (
<Box
className={`ViewPanel ${mode}`}
className={cx('ViewPanel', mode, className)}
display={designer.activeView === 'dual' ? 'block' : display}
flex="1"
overflow={overflow}
position="relative"
{...rest}
>
{children}
{/* 添加组件弹层 */}
{display === 'block' && <ComponentsPopover type="inner" isControlled />}
{designer.activeView === 'design' && <FileErrorsOverlay />}
</Box>
);
});
+24
View File
@@ -3,6 +3,30 @@
All notable changes to this project will be documented in this file.
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)
### Features
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@music163/tango-helpers",
"version": "1.2.0",
"version": "1.2.4",
"description": "Shared types, helpers, and hooks of tango-apps",
"keywords": [
"shared",
+2 -2
View File
@@ -43,7 +43,7 @@ export function filterTreeData<T = unknown>(
childrenProp = 'children',
onlyLeaf = false,
) {
const reducer = (result: T[], current: T) => {
const reducer = (result: T[], current: any) => {
if (current[childrenProp]) {
const newChildren = current[childrenProp].reduce(reducer, []);
if (newChildren.length) {
@@ -70,7 +70,7 @@ export function mapTreeData<T = unknown>(
mapper: (node: T) => any,
childrenProp = 'children',
) {
return treeData.map((node) => {
return treeData.map((node: any) => {
const newNode = mapper(node);
if (node[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]+)}}$/;
/**
* 判断给定代码是否被双花括号包裹
+3 -2
View File
@@ -1,5 +1,6 @@
import get from 'lodash.get';
import set from 'lodash.set';
import { Dict } from '../types';
/**
* 合并 target 和 source 对象,source 对象的优先级更高,如存在重名,覆盖 target 中的 key
@@ -45,7 +46,7 @@ export function clone(obj: any, withUndefined = true) {
return obj;
}
const target = {};
const target: Dict = {};
for (const key in obj) {
if (withUndefined) {
target[key] = obj[key];
@@ -65,7 +66,7 @@ export function omit(obj: any, keys: string[]) {
}
export function pick(obj: any, keys: string[]) {
const target = {};
const target: Dict = {};
for (const key of keys) {
target[key] = obj[key];
}
+14
View File
@@ -125,3 +125,17 @@ export function parseDndId(str: string): DndIdParsedType {
id: str,
};
}
/**
* 替换模板中的变量
* @example interpolate('hello {{name}}', { name: 'world' }) -> 'hello world'
*
* @param template 带模板变量的字符串
* @param props 变量字典
* @returns 返回替换后的字符串
*/
export function interpolate(template: string, props: Record<string, any>) {
return template.replace(/\{\{(\w+)\}\}/g, (_match, key) => {
return props[key];
});
}
+1 -1
View File
@@ -57,7 +57,7 @@ export function withDnd(options: IWithDndOptions<any>) {
const override = options.overrideProps || {};
const renderFooter = options.renderFooter || renderEmpty;
const Component = forwardRef<unknown, P & DraggableComponentProps>((props, refProp) => {
const Component = forwardRef<unknown, P & DraggableComponentProps>((props: any, refProp) => {
const dndProps = {
[SLOT.id]: props.tid, // id 作为唯一标记
[SLOT.dnd]: props[SLOT.dnd], // dnd 作为追踪标记
+3 -2
View File
@@ -1,13 +1,14 @@
import { warning } from '../helpers';
import { Dict } from '../types';
type ListStoreOptionsType<T = object> = {
type ListStoreOptionsType<T extends Dict = Dict> = {
data: T[];
keyProp?: string;
labelProp?: string;
childrenProp?: string;
};
export class ListStore<T = object> {
export class ListStore<T extends Dict = Dict> {
private nodeMap: Map<string, T>;
private keyProp: string;
private childrenProp: string;
+20 -4
View File
@@ -10,11 +10,19 @@ export type MousePoint = {
y: number;
};
export type OptionType = {
/**
* @deprecated 使用 IOptionItem 代替
*/
export type OptionType = IOptionItem<any>;
/**
* 列表项
*/
export interface IOptionItem<T = string> {
/**
* 值
*/
value: any;
value: T;
/**
* 显示的文本
*/
@@ -28,10 +36,18 @@ export type OptionType = {
*/
tip?: string;
/**
* 关联的导入
* 自定义渲染函数,仅用于 renderSetter
*/
render?: string;
/**
* 自定义渲染函数的 body(可以用来和 template 配合),仅用于 renderSetter
*/
renderBody?: string;
/**
* 关联的导入,仅用于 renderSetter
*/
relatedImports?: string[];
};
}
/**
* 变量树节点类型
+7 -2
View File
@@ -2,7 +2,7 @@
* 物料描述类型
*/
import { OptionType } from './advanced';
import { IOptionItem } from './advanced';
import { PartialRecord } from './base';
/**
@@ -88,6 +88,11 @@ export interface IComponentProp<T = any> {
* 自动补全的提示值,仅对 ExpressionSetter 有效
*/
autoCompleteOptions?: string[];
/**
* value 的模板,一般用于函数类型的属性,便于 setter 用来拼装返回值
* @example "(arg1, arg2, arg3) => { {{content}}}"
*/
template?: string;
/**
* 设置器
*/
@@ -108,7 +113,7 @@ export interface IComponentProp<T = any> {
/**
* 配置项的可选值,setterProps.options 的简写
*/
options?: OptionType[];
options?: IOptionItem[];
/**
* 是否禁用变量绑定
*/
@@ -0,0 +1,13 @@
import { isWrappedCode } from '../src/helpers';
describe('helpers/codeHelper', () => {
it('isWrappedCode', () => {
expect(isWrappedCode('{{[]}}')).toBeTruthy(); // true
expect(isWrappedCode('{{{}}}')).toBeTruthy(); // true
expect(isWrappedCode('{{this.foo}}')).toBeTruthy(); // true
expect(isWrappedCode('{{123}}')).toBeTruthy(); // true
expect(isWrappedCode('{{() => {}}}')).toBeTruthy(); // true
expect(isWrappedCode('{{<Button>button</Button>}}')).toBeTruthy(); // true
expect(isWrappedCode('{{<>\n<Button>hello</Button>\n</>}}')).toBeTruthy(); // true
});
});
+28
View File
@@ -3,6 +3,34 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.0.13](https://github.com/netease/tango/compare/@music163/tango-sandbox@1.0.12...@music163/tango-sandbox@1.0.13) (2024-09-09)
**Note:** Version bump only for package @music163/tango-sandbox
## [1.0.12](https://github.com/netease/tango/compare/@music163/tango-sandbox@1.0.11...@music163/tango-sandbox@1.0.12) (2024-09-02)
**Note:** Version bump only for package @music163/tango-sandbox
## [1.0.11](https://github.com/netease/tango/compare/@music163/tango-sandbox@1.0.10...@music163/tango-sandbox@1.0.11) (2024-08-06)
**Note:** Version bump only for package @music163/tango-sandbox
## [1.0.10](https://github.com/netease/tango/compare/@music163/tango-sandbox@1.0.9...@music163/tango-sandbox@1.0.10) (2024-08-05)
**Note:** Version bump only for package @music163/tango-sandbox
## [1.0.9](https://github.com/netease/tango/compare/@music163/tango-sandbox@1.0.8...@music163/tango-sandbox@1.0.9) (2024-08-01)
**Note:** Version bump only for package @music163/tango-sandbox
## [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
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@music163/tango-sandbox",
"version": "1.0.6",
"version": "1.0.13",
"description": "sandbox of tango apps",
"author": "wwsun <ww.sun@outlook.com>",
"homepage": "",
@@ -29,8 +29,8 @@
},
"dependencies": {
"@ant-design/icons": "^4.8.0",
"@music163/tango-core": "^1.3.1",
"@music163/tango-helpers": "^1.2.0",
"@music163/tango-core": "^1.4.4",
"@music163/tango-helpers": "^1.2.4",
"crypto-js": "^4.1.1",
"lodash.isequal": "4.5.0",
"react-frame-component": "^5.2.4"
+48
View File
@@ -3,6 +3,54 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.2.15](https://github.com/netease/tango/compare/@music163/tango-setting-form@1.2.14...@music163/tango-setting-form@1.2.15) (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.14](https://github.com/netease/tango/compare/@music163/tango-setting-form@1.2.13...@music163/tango-setting-form@1.2.14) (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.13](https://github.com/netease/tango/compare/@music163/tango-setting-form@1.2.12...@music163/tango-setting-form@1.2.13) (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.2.12](https://github.com/netease/tango/compare/@music163/tango-setting-form@1.2.11...@music163/tango-setting-form@1.2.12) (2024-08-09)
**Note:** Version bump only for package @music163/tango-setting-form
## [1.2.11](https://github.com/netease/tango/compare/@music163/tango-setting-form@1.2.10...@music163/tango-setting-form@1.2.11) (2024-08-06)
**Note:** Version bump only for package @music163/tango-setting-form
## [1.2.10](https://github.com/netease/tango/compare/@music163/tango-setting-form@1.2.9...@music163/tango-setting-form@1.2.10) (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.9](https://github.com/netease/tango/compare/@music163/tango-setting-form@1.2.8...@music163/tango-setting-form@1.2.9) (2024-08-01)
### Bug Fixes
- update types ([67d602a](https://github.com/netease/tango/commit/67d602a3ec6b7f74156bb78fda6f3b4bc2676e55))
## [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
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@music163/tango-setting-form",
"version": "1.2.6",
"version": "1.2.15",
"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.1",
"@music163/tango-helpers": "^1.2.0",
"@music163/tango-ui": "^1.3.1",
"@music163/tango-core": "^1.4.4",
"@music163/tango-helpers": "^1.2.4",
"@music163/tango-ui": "^1.4.5",
"antd": "^4.24.2",
"coral-system": "^1.0.5",
"mobx": "6.12.3",
+11 -4
View File
@@ -182,6 +182,7 @@ export function createFormItem(options: IFormItemCreateOptions) {
placeholder,
docs,
autoCompleteOptions,
template,
setter: setterProp,
setterProps,
defaultValue,
@@ -230,14 +231,20 @@ export function createFormItem(options: IFormItemCreateOptions) {
// FIXME: 重新考虑这段代码的位置,外置这个逻辑
if (
['codeSetter', 'expressionSetter', 'expSetter', 'actionSetter', 'eventSetter'].includes(
setter,
)
[
'codeSetter',
'expressionSetter',
'expSetter',
'actionSetter',
'eventSetter',
'renderSetter',
].includes(setter)
) {
expProps = {
modalTitle: title,
modalTip: tip,
autoCompleteOptions,
template,
};
}
@@ -246,7 +253,7 @@ export function createFormItem(options: IFormItemCreateOptions) {
const CodeSetter = REGISTERED_FORM_ITEM_MAP['codeSetter']?.config?.component;
const setterNode = isCodeSetter ? (
<CodeSetter {...expProps} {...baseComponentProps} />
<CodeSetter {...expProps} {...baseComponentProps} {...setterProps} />
) : (
renderSetter({
...expProps,
+2 -1
View File
@@ -7,6 +7,7 @@ import {
noop,
filterTreeData,
mapTreeData,
Dict,
} from '@music163/tango-helpers';
import { Action, Search, Tabs } from '@music163/tango-ui';
import { SettingFormItem, register } from './form-item';
@@ -24,7 +25,7 @@ function normalizeComponentProps(
props: IComponentPrototype['props'],
groupOptions: IFormTabsGroupOption[],
) {
const groups: Record<string, IComponentProp[]> = groupOptions.reduce((prev, cur) => {
const groups: Record<string, IComponentProp[]> = groupOptions.reduce<Dict>((prev, cur) => {
prev[cur.value] = [];
return prev;
}, {});
@@ -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;
@@ -34,7 +34,7 @@ export function TextSetter({
const autoSize = {
minRows: 2,
maxRows: 5,
maxRows: 6,
};
export function TextAreaSetter({
+52
View File
@@ -3,6 +3,58 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.4.5](https://github.com/netease/tango/compare/@music163/tango-ui@1.4.4...@music163/tango-ui@1.4.5) (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))
## [1.4.4](https://github.com/netease/tango/compare/@music163/tango-ui@1.4.3...@music163/tango-ui@1.4.4) (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.3](https://github.com/netease/tango/compare/@music163/tango-ui@1.4.2...@music163/tango-ui@1.4.3) (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.2](https://github.com/netease/tango/compare/@music163/tango-ui@1.4.1...@music163/tango-ui@1.4.2) (2024-08-09)
### Bug Fixes
- 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.1](https://github.com/netease/tango/compare/@music163/tango-ui@1.4.0...@music163/tango-ui@1.4.1) (2024-08-05)
**Note:** Version bump only for package @music163/tango-ui
# [1.4.0](https://github.com/netease/tango/compare/@music163/tango-ui@1.3.3...@music163/tango-ui@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-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
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@music163/tango-ui",
"version": "1.3.1",
"version": "1.4.5",
"description": "ui widgets of tango",
"keywords": [
"react",
@@ -38,7 +38,7 @@
"@codemirror/lang-javascript": "^6.2.2",
"@codemirror/lint": "^6.7.1",
"@codemirror/search": "^6.5.6",
"@music163/tango-helpers": "^1.2.0",
"@music163/tango-helpers": "^1.2.4",
"@uiw/react-codemirror": "^4.22.0",
"antd": "^4.24.2",
"classnames": "^2.5.1",
@@ -46,7 +46,7 @@
"eslint-linter-browserify": "^8.51.0",
"react-draggable": "^4.4.5",
"react-json-view": "^1.21.3",
"react-monaco-editor-lite": "^1.3.11"
"react-monaco-editor-lite": "^1.3.16"
},
"publishConfig": {
"access": "public",
+21 -12
View File
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useState } from 'react';
import { Box, css, Text } from 'coral-system';
import { Button, Dropdown, Input, Menu } from 'antd';
import { DownOutlined, PlusSquareOutlined } from '@ant-design/icons';
@@ -23,9 +23,13 @@ interface ActionSelectProps {
*/
onInputChange?: (value: string) => void;
/**
* 提示文本
* 受控的提示文本
*/
text?: string;
/**
* 默认的提示文本
*/
defaultText?: string;
/**
* 选择菜单时的回调
*/
@@ -50,11 +54,6 @@ const actionInputStyle = css`
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--tango-colors-text3);
&:hover {
color: var(--tango-colors-text2);
}
}
.anticon-down {
@@ -71,15 +70,25 @@ const inputStyle = { width: 'calc(100% - 82px)' };
export function ActionSelect({
showInput = false,
defaultInputValue,
text,
text: textProp,
defaultText,
options = [],
onSelect,
onInputChange,
}: ActionSelectProps) {
const [text, setText] = useState<string>(defaultText);
const menu = (
<Menu onClick={({ key }) => onSelect(key)}>
<Menu>
{options.map((item) => (
<Menu.Item key={item.value}>
<Menu.Item
key={item.value}
onClick={() => {
onSelect?.(item.value);
if (!textProp) {
setText(item.label as string);
}
}}
>
<Box display="flex" alignItems="center" minWidth={200} columnGap="m">
<Box flex="1">{item.label}</Box>
<Text fontSize="12px" color="text.note">
@@ -122,8 +131,8 @@ export function ActionSelect({
mb="m"
css={actionInputStyle}
>
<Box className="action-input-text" color="text.body">
{text}
<Box className="action-input-text" color="text2">
{textProp ?? text}
</Box>
<DownOutlined />
</Box>
+11 -2
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;
}
+312
View File
@@ -0,0 +1,312 @@
import React, { useState, useRef, KeyboardEvent, ChangeEvent, useEffect } from 'react';
import { Dropdown, Menu, Tag } from 'antd';
import styled from 'styled-components';
const InputWrapper = styled.div`
display: flex;
flex-wrap: wrap;
align-items: center;
padding: 4px 11px;
border: 1px solid #d9d9d9;
border-radius: 2px;
min-height: 32px;
&:hover {
border-color: #40a9ff;
}
&:focus-within {
border-color: #40a9ff;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
`;
const Input = styled.input`
flex: 1;
border: none;
outline: none;
padding: 0;
font-size: 14px;
min-width: 50px;
height: 24px;
line-height: 24px;
`;
const StyledTag = styled(Tag)`
margin: 2px 4px 2px 0;
`;
// Tailwind CSS 基础类名列表
const tailwindClasses = [
// 布局
'container',
'flex',
'grid',
'block',
'inline',
'inline-block',
'hidden',
// 弹性布局
'flex-row',
'flex-col',
'flex-wrap',
'flex-nowrap',
'justify-start',
'justify-end',
'justify-center',
'justify-between',
'justify-around',
'items-start',
'items-end',
'items-center',
'items-baseline',
'items-stretch',
// 网格布局
'grid-cols-1',
'grid-cols-2',
'grid-cols-3',
'grid-cols-4',
'grid-cols-5',
'grid-cols-6',
'grid-cols-12',
// 间距
'p-0',
'p-1',
'p-2',
'p-3',
'p-4',
'p-5',
'p-6',
'p-8',
'p-10',
'p-12',
'p-16',
'p-20',
'm-0',
'm-1',
'm-2',
'm-3',
'm-4',
'm-5',
'm-6',
'm-8',
'm-10',
'm-12',
'm-16',
'm-20',
// 尺寸
'w-full',
'w-auto',
'w-1/2',
'w-1/3',
'w-2/3',
'w-1/4',
'w-3/4',
'h-full',
'h-auto',
'h-screen',
// 字体
'text-xs',
'text-sm',
'text-base',
'text-lg',
'text-xl',
'text-2xl',
'text-3xl',
'text-4xl',
'text-5xl',
'font-thin',
'font-light',
'font-normal',
'font-medium',
'font-semibold',
'font-bold',
'font-extrabold',
// 文本颜色
'text-black',
'text-white',
'text-gray-100',
'text-gray-200',
'text-gray-300',
'text-gray-400',
'text-gray-500',
'text-red-500',
'text-blue-500',
'text-green-500',
'text-yellow-500',
'text-purple-500',
'text-pink-500',
// 背景颜色
'bg-transparent',
'bg-black',
'bg-white',
'bg-gray-100',
'bg-gray-200',
'bg-gray-300',
'bg-gray-400',
'bg-gray-500',
'bg-red-500',
'bg-blue-500',
'bg-green-500',
'bg-yellow-500',
'bg-purple-500',
'bg-pink-500',
// 边框
'border',
'border-0',
'border-2',
'border-4',
'border-8',
'border-black',
'border-white',
'border-gray-300',
'border-gray-400',
'border-gray-500',
// 圆角
'rounded-none',
'rounded-sm',
'rounded',
'rounded-lg',
'rounded-full',
// 阴影
'shadow-sm',
'shadow',
'shadow-md',
'shadow-lg',
'shadow-xl',
'shadow-2xl',
'shadow-none',
// 不透明度
'opacity-0',
'opacity-25',
'opacity-50',
'opacity-75',
'opacity-100',
];
interface ClassNameInputProps {
value?: string;
defaultValue?: string;
onChange?: (value: string) => void;
}
export function ClassNameInput({ value, defaultValue, onChange }: ClassNameInputProps) {
const [inputValue, setInputValue] = useState('');
const [suggestions, setSuggestions] = useState<string[]>([]);
const [highlightedIndex, setHighlightedIndex] = useState(-1);
const inputRef = useRef<HTMLInputElement>(null);
const [internalValue, setInternalValue] = useState(defaultValue || '');
const isControlled = value !== undefined;
const classNames = (isControlled ? value : internalValue).split(' ').filter(Boolean);
useEffect(() => {
if (isControlled) {
setInternalValue(value);
}
}, [isControlled, value]);
const isValidClassName = (className: string) => {
return /^[a-zA-Z0-9_-]+(?::[a-zA-Z0-9_-]+)*$/.test(className);
};
const updateValue = (newClassNames: string[]) => {
const newValue = newClassNames.join(' ');
if (isControlled) {
onChange?.(newValue);
} else {
setInternalValue(newValue);
onChange?.(newValue);
}
};
const handleInputKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
if (event.key === 'Enter') {
event.preventDefault();
if (highlightedIndex >= 0 && highlightedIndex < suggestions.length) {
addClassName(suggestions[highlightedIndex]);
} else {
addClassName(inputValue.trim());
}
} else if (event.key === 'ArrowDown') {
event.preventDefault();
setHighlightedIndex((prev) => (prev < suggestions.length - 1 ? prev + 1 : 0));
} else if (event.key === 'ArrowUp') {
event.preventDefault();
setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : suggestions.length - 1));
} else if (event.key === 'Backspace' && inputValue === '' && classNames.length > 0) {
event.preventDefault();
const newClassNames = [...classNames];
newClassNames.pop();
updateValue(newClassNames);
}
};
const handleInputChange = (event: ChangeEvent<HTMLInputElement>) => {
const input = event.target.value;
setInputValue(input);
if (input) {
const matchedSuggestions = tailwindClasses
.filter(
(className) =>
className.toLowerCase().includes(input.toLowerCase()) &&
!classNames.includes(className),
)
.slice(0, 10);
setSuggestions(matchedSuggestions);
setHighlightedIndex(-1);
} else {
setSuggestions([]);
}
};
const addClassName = (className: string) => {
if (className && !classNames.includes(className) && isValidClassName(className)) {
updateValue([...classNames, className]);
setInputValue('');
setSuggestions([]);
setHighlightedIndex(-1);
}
};
const removeClassName = (removedTag: string) => {
const newClassNames = classNames.filter((tag) => tag !== removedTag);
updateValue(newClassNames);
};
const handleSuggestionClick = (suggestion: string) => {
addClassName(suggestion);
};
const menu = (
<Menu>
{suggestions.map((suggestion, index) => (
<Menu.Item
key={index}
onClick={() => handleSuggestionClick(suggestion)}
className={index === highlightedIndex ? 'ant-dropdown-menu-item-active' : ''}
>
{suggestion}
</Menu.Item>
))}
</Menu>
);
return (
<Dropdown overlay={menu} visible={suggestions.length > 0} placement="bottomLeft">
<InputWrapper onClick={() => inputRef.current?.focus()}>
{classNames.map((className) => (
<StyledTag key={className} closable onClose={() => removeClassName(className)}>
{className}
</StyledTag>
))}
<Input
ref={inputRef}
type="text"
value={inputValue}
onKeyDown={handleInputKeyDown}
onChange={handleInputChange}
placeholder={classNames.length === 0 ? '输入 class 名称' : ''}
/>
</InputWrapper>
</Dropdown>
);
}
+7 -5
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>
+2 -2
View File
@@ -1,7 +1,7 @@
import React, { useMemo } from 'react';
import { Menu, Space } from 'antd';
import { Box, css } from 'coral-system';
import { isApplePlatform } from '@music163/tango-helpers';
import { Dict, isApplePlatform } from '@music163/tango-helpers';
const contextActionStyle = css`
display: flex;
@@ -46,7 +46,7 @@ export function ContextAction({
if (!hotkey) {
return null;
}
const keyMap = isApplePlatform()
const keyMap: Dict = isApplePlatform()
? {
command: '⌘',
meta: '⌘',

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