mirror of
https://github.com/NetEase/tango.git
synced 2026-08-31 20:06:59 +08:00
Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8fe57333a0 | |||
| 269e304d68 | |||
| 8c80255f81 | |||
| 359aa0b148 | |||
| 51ca4a72ce | |||
| b83d965955 | |||
| a1eea02f94 | |||
| 0d0479e3e5 | |||
| 67d602a3ec | |||
| 8dd289cd7d | |||
| 82bc5db4d8 | |||
| 85c053b3db | |||
| 32f5667cae | |||
| 1d0fb4f274 | |||
| 0011cdca0f | |||
| 0f3f0afdfa | |||
| b53a9f7eef | |||
| 5336c2ec28 | |||
| cdbd2351a2 | |||
| 1f03977f7d | |||
| a2cc418a73 | |||
| 239c0b0a28 | |||
| ae04850bb2 | |||
| ca740c83ae | |||
| 00bf52a411 | |||
| c1306e24e0 | |||
| 28040fc006 | |||
| 3f3981bb9d | |||
| 253ceca969 | |||
| a0ed57ff4f | |||
| f83c6bc0f6 |
@@ -9,3 +9,4 @@ coverage/
|
||||
.umi-production/
|
||||
.log
|
||||
.eslintcache
|
||||
*.pem
|
||||
|
||||
@@ -31,23 +31,17 @@ You can view the detailed usage guide through the following links:
|
||||
- Out-of-the-box front-end low-code designer, providing flexible and easy-to-use designer React components
|
||||
- Developed using TypeScript, providing complete type definition files
|
||||
|
||||
## 🎯 Milestone
|
||||
## 💡 Examples
|
||||
|
||||
```mermaid
|
||||
gantt
|
||||
dateFormat YYYY-MM-DD
|
||||
title Tango 1.0 Milestones
|
||||
excludes weekends
|
||||
%% (`excludes` accepts specific dates in YYYY-MM-DD format, days of the week ("sunday") or "weekends", but not the word "weekdays".)
|
||||
You can use tango to build lowcode builders, for example:
|
||||
|
||||
section Builder Release
|
||||
1.0 Alpha : m1, 2023-10-01, 2024-01-31
|
||||
1.0 RC : m2, 2024-02-01, 2024-04-30
|
||||
1.0 Stable : m3, 2024-04-30, 2024-12-31
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> Alpha versions will continue to optimize some implementation protocols and continuously integrate new features from the internal version of NetEase; RC versions will provide a stable parsing protocol and implementation; The 1.0 official version is planned to be released in Q2 2024. After the release of the 1.0 official version, we will continue to iterate on versions and gradually incorporate core capabilities such as process design, component library, and AIGC from the internal version.
|
||||
| Preview | Description |
|
||||
| ---------------------------------------------------------------------------------------------- | ----------------------- |
|
||||
|  | Internal App Builder |
|
||||
|  | Dashboard App Builder |
|
||||
|  | Mobile App Builder |
|
||||
|  | ReactNative App Builder |
|
||||
|  | Email Builder |
|
||||
|
||||
## 🌐 Compatibility
|
||||
|
||||
@@ -76,6 +70,23 @@ yarn
|
||||
yarn start
|
||||
```
|
||||
|
||||
### Local Https certificate
|
||||
|
||||
If you need to use https in the local development environment, you can use the following command to generate a certificate:
|
||||
|
||||
```bash
|
||||
brew install mkcert
|
||||
|
||||
# install the local certificate
|
||||
mkcert -install
|
||||
|
||||
# enter the playground app directory
|
||||
cd apps/playground
|
||||
|
||||
# generate a certificate
|
||||
mkcert local.netease.com # or change to your own domain name
|
||||
```
|
||||
|
||||
## 💬 Community
|
||||
|
||||
Join NetEase Tango Community to share your ideas, suggestions, or questions and connect with other users and contributors.
|
||||
|
||||
+30
-19
@@ -26,28 +26,22 @@
|
||||
## ✨ 特性
|
||||
|
||||
- 经历网易云音乐内网生产环境的实际检验,可灵活集成应用于低代码平台,本地开发工具等
|
||||
- 基于源码 AST 驱动,无私有 DSL 和协议
|
||||
- 提供实时出码能力,支持源码进,源码出
|
||||
- 开箱即用的前端低代码设计器,提供灵活易用的设计器 React 组件
|
||||
- 使用 TypeScript 开发,提供完整的类型定义文件
|
||||
- 提供基于源码 AST 驱动的低代码引擎,无私有 DSL 和协议
|
||||
- 提供实时出码能力,支持源码进,源码出,适配多种源码开发场景
|
||||
- 提供开箱即用的前端低代码设计器,提供灵活易用的设计器 React 组件
|
||||
- 使用 TypeScript 开发,提供完整的类型定义文件和完善的文档支持
|
||||
|
||||
## 🎯 里程碑
|
||||
## 💡 例子
|
||||
|
||||
```mermaid
|
||||
gantt
|
||||
dateFormat YYYY-MM-DD
|
||||
title Tango 1.0 Milestones
|
||||
excludes weekends
|
||||
%% (`excludes` accepts specific dates in YYYY-MM-DD format, days of the week ("sunday") or "weekends", but not the word "weekdays".)
|
||||
你可以使用 Tango 快速构建多种类型的基于源码的低代码搭建工具,例如:
|
||||
|
||||
section Builder Release
|
||||
1.0 Alpha : m1, 2023-10-01, 2024-01-31
|
||||
1.0 RC : m2, 2024-02-01, 2024-04-30
|
||||
1.0 Stable : m3, 2024-04-30, 2024-12-31
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> Alpha 版本会持续对一些实现协议进行优化,并持续融合网易内网版本中的新增特性;RC 版本将会提供基本稳定的解析协议和实现;1.0 正式版本计划在 2024 年Q2发布。1.0 正式版发布后我们将会持续进行版本迭代,并逐步融入内网版本中的流程设计、组件库、AIGC 等核心基础能力。
|
||||
| 预览图 | 说明 |
|
||||
| ---------------------------------------------------------------------------------------------- | -------------------- |
|
||||
|  | 中后台系统搭建 |
|
||||
|  | 仪表盘应用搭建 |
|
||||
|  | H5活动页面搭建 |
|
||||
|  | ReactNative 应用搭建 |
|
||||
|  | 营销类邮件搭建 |
|
||||
|
||||
## 🌐 兼容环境
|
||||
|
||||
@@ -76,6 +70,23 @@ yarn
|
||||
yarn start
|
||||
```
|
||||
|
||||
### 本地 https 证书
|
||||
|
||||
如果需要在本地开发环境中使用 https,可以使用以下命令生成证书:
|
||||
|
||||
```bash
|
||||
brew install mkcert
|
||||
|
||||
# 将 mkcert 添加到本地根 CA,仅在本地生效
|
||||
mkcert -install
|
||||
|
||||
# 进入 playground 应用目录
|
||||
cd apps/playground
|
||||
|
||||
# 为网站生成一个由 mkcert 签名的证书
|
||||
mkcert local.netease.com
|
||||
```
|
||||
|
||||
## 💬 社区讨论
|
||||
|
||||
参与 NetEase Tango 的社区,以分享您的想法、建议或问题,并与其他用户和贡献者建立联系。
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC4sAqDE3nhtSWD
|
||||
YqV4mlglnzIs2AioZr2uxfj/OKrpE1ZML/dflJyc5NGUpOZWV+W1vRc70wWVNFxB
|
||||
Si8nrYrrseoID7Ehxxe6+J1m/JsEbhZWHwV3M1iGW7JD7VhujaDpSLg67wMTMpSW
|
||||
3BUS+0NT+MNfCisp+u/ls4fXPCTgXiWcjMULm95oLlTeYwurPHfxwn9f9hh85I5I
|
||||
K3dI7w8MsQN7EFNrJz3UfrNLpmARMtSUBMwBYqrCitoUxRT0q7KYCru7kHxGPB6A
|
||||
SQrFpSpOWT79vp8GBSp9g3axhRsK+aEgzQ0QGrFPHBwBP/kWgmdB1URc/K7STSC9
|
||||
xyMNZOM7AgMBAAECggEATbyDYDKVbaRymr+tyHqmuYctdmSkGEXOdz8LFFoEzj/+
|
||||
ZekWpVuHJB7H/FoAPb2XiYyCuAKVafz1C+IuwPOx8o0bZbM9KmuDmIZGsm+GzDGO
|
||||
I0fBZC/vhfYYrbC8NSRV43thWCT8VVtH4cvW7vtllnWxvlIoYu4lhbiwZ68AEpO1
|
||||
8PlJsOhnagfkFpG8LehSDs9if5L+Ig0geQEcUY3pGUM9LKYsRoxFv6Vh8wQku1XD
|
||||
YOxLvLWDPqJ75m816t50RARe3e6eDJ+ukzDCoDjmLP8uhf1sk1FcL5aOgUNU3VOi
|
||||
P6sIH5uzuD0MA3FXx2mItYLOc44r4uaFJZiRUMEXGQKBgQDCjUYblE4rh0nKFkqx
|
||||
AhgfcRgpgLbP42OwFtD0u20RXdZAEJDRDQ43NdOgjFp2B50k75LBPIoFXYUZdzKj
|
||||
l7S2FH5O9Cc6hEgZGI4hCiYd+nQVI1mxv820YnzZJN4oooxyL+B5RlnW5cDT7LJN
|
||||
11Q5PRewkM+XpfbE3lKUjTNcDwKBgQDzBS+j8T9GJSzV2xgA+mpGHe1RjDq6yQtk
|
||||
Rl5GWpflW4Nr3/58BbmCWNA8WP0NOWutJhrkltwLCqJC8OZX/WHnmcmq2k+UAfQQ
|
||||
z98p+xcMWOTcl5XIbveuhYoZb2udt9WVJLcpRVxVhEWcBrtHsrpScZXsKYf/TaZ1
|
||||
JtCjPU5KFQKBgGXj/UqmYkYzxXZ8NEP998pHvcLGsXew/F2VpXv2yQlmXrYQtvd1
|
||||
YfOSUjJsL1hPZoKYogBoB1UGBJYwsimxYyhVFU2eWwkvjF7wWEd/fDUJHVCQwgHw
|
||||
lPga+I303BDmCK4o2uRI7vY4P8P92+gelPKhR03mmYIvnky+rvsz9CkLAoGAVerl
|
||||
du6Z0jDecxUsnVvrKrL17jjHorXlYyRFvFXXEe2SvxbIIAzeEylXJZ7IiF5epS6t
|
||||
n1d+oCC4UTZeGYmpjXofhFn+fHNcWH1FhtAQy/q5nwuwltY0luz7cBamU3Jk/n+m
|
||||
id7N0CYdfhYbLDQSD4e822ureyV2zkBUzwGjpikCgYEAoCVbhkxVrQHRmxLC2jDx
|
||||
pM94W1ij9TqblfGg3OMpPXjpTdlknatWjBw7JkoyxYiYJq5Gi2p4QV6HHUvS1osj
|
||||
BB+4A99/PD9A0ICoJglz8SXlGe/895zwHwpKT9f0zjizQrEd2KCPf75URa+t2FGY
|
||||
B2GkXsbChRWVrey5+uMzUD8=
|
||||
-----END PRIVATE KEY-----
|
||||
@@ -1,25 +0,0 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIEQjCCAqqgAwIBAgIRAIeLJ2jwt4MSKycHFmFXL6YwDQYJKoZIhvcNAQELBQAw
|
||||
fTEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMSkwJwYDVQQLDCB3d3N1
|
||||
bkBzdW4tbWJwLmxvY2FsIChTVU4gV2Vpd2VpKTEwMC4GA1UEAwwnbWtjZXJ0IHd3
|
||||
c3VuQHN1bi1tYnAubG9jYWwgKFNVTiBXZWl3ZWkpMB4XDTIyMDcyODAxNTIxMFoX
|
||||
DTI0MTAyODAxNTIxMFowVDEnMCUGA1UEChMebWtjZXJ0IGRldmVsb3BtZW50IGNl
|
||||
cnRpZmljYXRlMSkwJwYDVQQLDCB3d3N1bkBzdW4tbWJwLmxvY2FsIChTVU4gV2Vp
|
||||
d2VpKTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALiwCoMTeeG1JYNi
|
||||
pXiaWCWfMizYCKhmva7F+P84qukTVkwv91+UnJzk0ZSk5lZX5bW9FzvTBZU0XEFK
|
||||
Lyetiuux6ggPsSHHF7r4nWb8mwRuFlYfBXczWIZbskPtWG6NoOlIuDrvAxMylJbc
|
||||
FRL7Q1P4w18KKyn67+Wzh9c8JOBeJZyMxQub3mguVN5jC6s8d/HCf1/2GHzkjkgr
|
||||
d0jvDwyxA3sQU2snPdR+s0umYBEy1JQEzAFiqsKK2hTFFPSrspgKu7uQfEY8HoBJ
|
||||
CsWlKk5ZPv2+nwYFKn2DdrGFGwr5oSDNDRAasU8cHAE/+RaCZ0HVRFz8rtJNIL3H
|
||||
Iw1k4zsCAwEAAaNmMGQwDgYDVR0PAQH/BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUF
|
||||
BwMBMB8GA1UdIwQYMBaAFDILZR+SW32Ug0HkAKrH1KT2a1ttMBwGA1UdEQQVMBOC
|
||||
EWxvY2FsLm5ldGVhc2UuY29tMA0GCSqGSIb3DQEBCwUAA4IBgQCEdeV2Likl4MUt
|
||||
wWeN/X3tAR3L7mkl7e4f0CQ4v2lOs8cekOP7p3ZFk5nSyVbxda3usPB0OtbVTibw
|
||||
qZ1n/TMRb5epHroXIMleqngP82zFZNg+WVyHm/ZAml8gVD0VZjYM1Tb+U9+SrgGw
|
||||
lvZndbuUXa3h/d0irPH90KsdEeAR1SqwzAzvLO2JgvBuimHRceQWcTiiGWDZOyii
|
||||
tux4U+pEzh0Dia40uFozFAoYZPDUpZ+gsiQ3MR3HOrAu2OR0a+CfelDXZNLR3C4z
|
||||
cBKr53ajAbHstnpxKJh9IfeKph44PrbDo6HMe1XPprl7idoEesX3Ma3SBgm4lmId
|
||||
cfEOUWNIYOgtOT5bflXNNyzXOgYVjpLnjwg4EOASO/XIjhvqdjIyCu7HJMIcZ9oo
|
||||
Aob2exeluv5LVwic1UJQDl2McDBIgJ8R32oEN4skCKIBSbWlxWyjnxL5I5WkJF4y
|
||||
vlSx8bKAjVr8map0C3rWISuZdgheJkF96qjwhVSxOv++p6srq6M=
|
||||
-----END CERTIFICATE-----
|
||||
@@ -5,6 +5,7 @@ import { BranchesOutlined, MenuOutlined, QuestionCircleOutlined } from '@ant-des
|
||||
import { registerSetter } from '@music163/tango-designer';
|
||||
import type { IVariableTreeNode } from '@music163/tango-helpers';
|
||||
import { FooSetter } from '../components';
|
||||
import { ToggleButton } from '@music163/tango-ui';
|
||||
|
||||
export * from './mock-files';
|
||||
|
||||
@@ -46,8 +47,17 @@ registerSetter({
|
||||
// 平台 Logo
|
||||
export function Logo() {
|
||||
return (
|
||||
<Box width="50px" display="flex" alignItems="center" justifyContent="center" fontSize="20px">
|
||||
<MenuOutlined />
|
||||
<Box width="50px" display="flex" alignItems="center" justifyContent="center" fontSize="14px">
|
||||
<ToggleButton
|
||||
shape="text"
|
||||
items={[
|
||||
{ label: '回到首页', key: 'home' },
|
||||
{ label: '文档', key: 'doc' },
|
||||
{ label: '设置', key: 'setting' },
|
||||
]}
|
||||
>
|
||||
<MenuOutlined />
|
||||
</ToggleButton>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ const packageJson = {
|
||||
name: 'demo',
|
||||
private: true,
|
||||
dependencies: {
|
||||
'@music163/tango-mail': '0.1.1',
|
||||
'@music163/tango-mail': '0.2.4',
|
||||
'@music163/tango-boot': '0.2.5',
|
||||
react: '17.0.2',
|
||||
'react-dom': '17.0.2',
|
||||
@@ -44,7 +44,7 @@ const tangoConfigJson = {
|
||||
},
|
||||
'@music163/tango-mail': {
|
||||
description: 'TangoMail 基础物料',
|
||||
version: '0.1.1',
|
||||
version: '0.2.4',
|
||||
library: 'TangoMail',
|
||||
type: 'baseDependency',
|
||||
resources: ['https://unpkg.com/@music163/tango-mail@{{version}}/dist/index.js'],
|
||||
@@ -116,7 +116,6 @@ const WelcomeEmail = () => (
|
||||
</Section>
|
||||
<Text style={paragraph}>
|
||||
Best,
|
||||
<br />
|
||||
The Koala team
|
||||
</Text>
|
||||
<Hr style={hr} />
|
||||
|
||||
@@ -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'],
|
||||
@@ -56,7 +56,7 @@ const tangoConfigJson = {
|
||||
},
|
||||
'@music163/antd': {
|
||||
description: '云音乐低代码中后台应用基础物料',
|
||||
version: '0.2.5',
|
||||
version: '0.2.6',
|
||||
library: 'TangoAntd',
|
||||
type: 'baseDependency',
|
||||
resources: [
|
||||
@@ -166,7 +166,7 @@ import { OutButton } from "../components";
|
||||
class App extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<Page title={tango.stores.app.title} subTitle={111}>
|
||||
<Page title={tango.stores.app.title} subTitle={<><Button>hello</Button></>}>
|
||||
<Section tid="section0" />
|
||||
<Section tid="section1" title="Section Title">
|
||||
your input: <Input tid="input1" defaultValue="hello" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -79,6 +79,11 @@ const prototypeHasBasicProps: IComponentPrototype = {
|
||||
title: 'textAreaSetter',
|
||||
setter: 'textAreaSetter',
|
||||
},
|
||||
{
|
||||
name: 'src',
|
||||
title: 'imageSetter',
|
||||
setter: 'imageSetter',
|
||||
},
|
||||
{
|
||||
name: 'number',
|
||||
title: 'numberSetter',
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import React from 'react';
|
||||
import { DragPanel } from '@music163/tango-ui';
|
||||
import { Box, Text } from 'coral-system';
|
||||
import { Button } from 'antd';
|
||||
|
||||
export default {
|
||||
title: 'UI/DragPanel',
|
||||
};
|
||||
|
||||
export function Basic() {
|
||||
return (
|
||||
<>
|
||||
<DragPanel
|
||||
title="弹层标题"
|
||||
extra="右上角内容"
|
||||
width={350}
|
||||
body={<Box p="m">内容</Box>}
|
||||
footer={<Text>底部信息</Text>}
|
||||
>
|
||||
<Button>点击唤起浮层</Button>
|
||||
</DragPanel>
|
||||
<DragPanel
|
||||
title="弹层标题"
|
||||
extra="右上角内容"
|
||||
width={350}
|
||||
body={<Box p="m">内容</Box>}
|
||||
footer={<Text>底部信息</Text>}
|
||||
>
|
||||
<Button
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: 20,
|
||||
right: 20,
|
||||
}}
|
||||
>
|
||||
底部浮层自动修正弹出区域
|
||||
</Button>
|
||||
</DragPanel>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function FooterCustom() {
|
||||
return (
|
||||
<DragPanel
|
||||
title="弹层标题"
|
||||
extra="右上角内容"
|
||||
width={350}
|
||||
body={<Box p="m">内容</Box>}
|
||||
footer={(close) => (
|
||||
<Box display="flex" justifyContent="space-between">
|
||||
<Text>底部信息</Text>
|
||||
<Button size="small" onClick={() => close()}>
|
||||
点此关闭
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
>
|
||||
<Button>点击唤起浮层</Button>
|
||||
</DragPanel>
|
||||
);
|
||||
}
|
||||
|
||||
export function ResizeablePanel() {
|
||||
return (
|
||||
<DragPanel
|
||||
title="弹层标题"
|
||||
resizeable
|
||||
extra="右上角内容"
|
||||
width={350}
|
||||
height={400}
|
||||
body={
|
||||
<Box p="m" textAlign="center" background="#fa8484">
|
||||
内容
|
||||
</Box>
|
||||
}
|
||||
footer={<Text>底部信息</Text>}
|
||||
>
|
||||
<Button>点击唤起浮层</Button>
|
||||
</DragPanel>
|
||||
);
|
||||
}
|
||||
+4
-4
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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.1.8](https://github.com/netease/tango/compare/@music163/tango-context@1.1.7...@music163/tango-context@1.1.8) (2024-08-06)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
## [1.1.7](https://github.com/netease/tango/compare/@music163/tango-context@1.1.6...@music163/tango-context@1.1.7) (2024-08-05)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
## [1.1.6](https://github.com/netease/tango/compare/@music163/tango-context@1.1.5...@music163/tango-context@1.1.6) (2024-08-01)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
## [1.1.5](https://github.com/netease/tango/compare/@music163/tango-context@1.1.4...@music163/tango-context@1.1.5) (2024-07-12)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
## [1.1.4](https://github.com/netease/tango/compare/@music163/tango-context@1.1.3...@music163/tango-context@1.1.4) (2024-06-20)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
## [1.1.3](https://github.com/netease/tango/compare/@music163/tango-context@1.1.2...@music163/tango-context@1.1.3) (2024-06-05)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
## [1.1.2](https://github.com/netease/tango/compare/@music163/tango-context@1.1.1...@music163/tango-context@1.1.2) (2024-06-03)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
## [1.1.1](https://github.com/netease/tango/compare/@music163/tango-context@1.1.0...@music163/tango-context@1.1.1) (2024-05-21)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-context",
|
||||
"version": "1.1.1",
|
||||
"version": "1.1.8",
|
||||
"description": "react context for tango-apps",
|
||||
"keywords": [
|
||||
"react",
|
||||
@@ -31,8 +31,8 @@
|
||||
"react": ">= 16.8"
|
||||
},
|
||||
"dependencies": {
|
||||
"@music163/tango-core": "^1.2.0",
|
||||
"@music163/tango-helpers": "^1.1.1",
|
||||
"@music163/tango-core": "^1.4.2",
|
||||
"@music163/tango-helpers": "^1.2.2",
|
||||
"mobx-react-lite": "4.0.7"
|
||||
},
|
||||
"publishConfig": {
|
||||
|
||||
@@ -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.4.2](https://github.com/netease/tango/compare/@music163/tango-core@1.4.1...@music163/tango-core@1.4.2) (2024-08-06)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- view module \_nodesTree set init value ([#194](https://github.com/netease/tango/issues/194)) ([269e304](https://github.com/netease/tango/commit/269e304d685c4fcc80635b9d04220cda333b80c4))
|
||||
|
||||
## [1.4.1](https://github.com/netease/tango/compare/@music163/tango-core@1.4.0...@music163/tango-core@1.4.1) (2024-08-05)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-core
|
||||
|
||||
# [1.4.0](https://github.com/netease/tango/compare/@music163/tango-core@1.3.3...@music163/tango-core@1.4.0) (2024-08-01)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- update types ([67d602a](https://github.com/netease/tango/commit/67d602a3ec6b7f74156bb78fda6f3b4bc2676e55))
|
||||
|
||||
### Features
|
||||
|
||||
- add isError and errorMessage to File & add isAstSynced to Module ([#190](https://github.com/netease/tango/issues/190)) ([8dd289c](https://github.com/netease/tango/commit/8dd289cd7daba628e54dc6b8929f22a1e2245160))
|
||||
|
||||
## [1.3.3](https://github.com/netease/tango/compare/@music163/tango-core@1.3.2...@music163/tango-core@1.3.3) (2024-07-12)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- update designer config ([#186](https://github.com/netease/tango/issues/186)) ([85c053b](https://github.com/netease/tango/commit/85c053b3db6d652b41c9873dba1366315174833f))
|
||||
|
||||
## [1.3.2](https://github.com/netease/tango/compare/@music163/tango-core@1.3.1...@music163/tango-core@1.3.2) (2024-06-20)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- update designer style ([#176](https://github.com/netease/tango/issues/176)) ([0f3f0af](https://github.com/netease/tango/commit/0f3f0afdfa8aee2532a97c5c2e92ef4230397d86))
|
||||
|
||||
## [1.3.1](https://github.com/netease/tango/compare/@music163/tango-core@1.3.0...@music163/tango-core@1.3.1) (2024-06-05)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- set value as jsxElement children ([#173](https://github.com/netease/tango/issues/173)) ([ae04850](https://github.com/netease/tango/commit/ae04850bb251f392575a72ca5fd960249ea9d06b))
|
||||
|
||||
# [1.3.0](https://github.com/netease/tango/compare/@music163/tango-core@1.2.0...@music163/tango-core@1.3.0) (2024-06-03)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- parse app entry file & pass correct routerType to sandbox ([#168](https://github.com/netease/tango/issues/168)) ([3f3981b](https://github.com/netease/tango/commit/3f3981bb9db874b738a67fd449c579c35c58d08b))
|
||||
|
||||
### Features
|
||||
|
||||
- add context menu ([#161](https://github.com/netease/tango/issues/161)) ([28040fc](https://github.com/netease/tango/commit/28040fc00604339a40ad3216b76baf7de93a13e0))
|
||||
|
||||
# [1.2.0](https://github.com/netease/tango/compare/@music163/tango-core@1.1.0...@music163/tango-core@1.2.0) (2024-05-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-core",
|
||||
"version": "1.2.0",
|
||||
"version": "1.4.2",
|
||||
"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.1.1",
|
||||
"@music163/tango-helpers": "^1.2.2",
|
||||
"@types/babel__generator": "^7.6.7",
|
||||
"@types/babel__traverse": "^7.20.4",
|
||||
"mobx": "6.12.3",
|
||||
|
||||
@@ -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}`包裹
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)) {
|
||||
@@ -222,15 +218,16 @@ export function value2jsxAttributeValueNode(value: any) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
// TODO: 待校验
|
||||
export function value2jsxChildrenValueNode(value: any) {
|
||||
let ret: t.JSXElement | t.JSXFragment | t.JSXExpressionContainer | t.JSXSpreadChild | t.JSXText;
|
||||
switch (typeof value) {
|
||||
case 'string':
|
||||
if (isValidExpressionCode(value)) {
|
||||
if (isWrappedCode(value)) {
|
||||
// 代码模式
|
||||
const innerString = getVariableContent(value);
|
||||
ret = t.jsxExpressionContainer(code2expression(innerString));
|
||||
} else {
|
||||
// 普通的文本
|
||||
ret = t.jsxText(value);
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -391,8 +391,10 @@ export function updateJSXElementAttribute(node: t.JSXElement, attrName: string,
|
||||
isExist = true;
|
||||
// @ts-ignore
|
||||
if (t.isJSXAttribute(jsxAttributeNode)) {
|
||||
// 更新 jsx 属性值 <Foo bar="1" /> 的模式
|
||||
jsxAttributeNode.value = value2jsxAttributeValueNode(attrValue);
|
||||
} else if (name === 'children') {
|
||||
// 更新 jsx children 的情况 <Foo>children</Foo>
|
||||
node.children = value2jsxChildrenValueNode(attrValue);
|
||||
}
|
||||
}
|
||||
@@ -1007,7 +1009,7 @@ export function serviceConfig2Node(payload: object) {
|
||||
});
|
||||
}
|
||||
|
||||
export function updateServiceConfigToServiceFile(ast: t.File, config: Dict<object>) {
|
||||
export function updateServiceConfigToServiceFile(ast: t.File, config: Dict<Dict>) {
|
||||
traverse(ast, {
|
||||
CallExpression(path) {
|
||||
const calleeName = keyNode2value(path.node.callee) as string;
|
||||
@@ -1015,7 +1017,7 @@ export function updateServiceConfigToServiceFile(ast: t.File, config: Dict<objec
|
||||
const configNode = path.node.arguments[0];
|
||||
|
||||
if (t.isObjectExpression(configNode)) {
|
||||
const newPropertiesNodeMap = Object.keys(config).reduce((properties, key) => {
|
||||
const newPropertiesNodeMap = Object.keys(config).reduce<Dict>((properties, key) => {
|
||||
const serviceConfig = config[key];
|
||||
const property = t.objectProperty(t.identifier(key), serviceConfig2Node(serviceConfig));
|
||||
properties[key] = property;
|
||||
@@ -1425,3 +1427,22 @@ export function traverseFile(ast: t.File) {
|
||||
imports,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* app.js 入口文件解析
|
||||
* @param ast
|
||||
*/
|
||||
export function traverseEntryFile(ast: t.File) {
|
||||
let appConfig: any;
|
||||
traverse(ast, {
|
||||
CallExpression(path) {
|
||||
const { node } = path;
|
||||
const calleeName = keyNode2value(node.callee) as string;
|
||||
if (calleeName === 'runApp') {
|
||||
appConfig = node2value(node.arguments[0]);
|
||||
path.stop();
|
||||
}
|
||||
},
|
||||
});
|
||||
return appConfig;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ export function namesToImportDeclarations(
|
||||
names: string[],
|
||||
nameMap: Dict<IImportSpecifierSourceData>,
|
||||
) {
|
||||
const map = {};
|
||||
const map: Dict = {};
|
||||
names.forEach((name) => {
|
||||
const mod = nameMap[name];
|
||||
if (mod) {
|
||||
|
||||
@@ -1,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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -83,6 +83,16 @@ export class Designer {
|
||||
*/
|
||||
_addComponentPopoverPosition = { clientX: 0, clientY: 0 };
|
||||
|
||||
/**
|
||||
* 是否显示右键菜单
|
||||
*/
|
||||
_showContextMenu = false;
|
||||
|
||||
/**
|
||||
* 右键菜单在 iframe 上的位置
|
||||
*/
|
||||
_contextMenuPosition = { clientX: 0, clientY: 0 };
|
||||
|
||||
/**
|
||||
* 是否显示右侧面板
|
||||
*/
|
||||
@@ -136,6 +146,14 @@ export class Designer {
|
||||
return this._addComponentPopoverPosition;
|
||||
}
|
||||
|
||||
get showContextMenu() {
|
||||
return this._showContextMenu;
|
||||
}
|
||||
|
||||
get contextMenuPosition() {
|
||||
return this._contextMenuPosition;
|
||||
}
|
||||
|
||||
get menuData() {
|
||||
return this._menuData ?? ([] as MenuDataType);
|
||||
}
|
||||
@@ -178,6 +196,8 @@ export class Designer {
|
||||
_showRightPanel: observable,
|
||||
_showAddComponentPopover: observable,
|
||||
_addComponentPopoverPosition: observable,
|
||||
_showContextMenu: observable,
|
||||
_contextMenuPosition: observable,
|
||||
_menuData: observable,
|
||||
_isPreview: observable,
|
||||
simulator: computed,
|
||||
@@ -189,6 +209,8 @@ export class Designer {
|
||||
showSmartWizard: computed,
|
||||
showAddComponentPopover: computed,
|
||||
addComponentPopoverPosition: computed,
|
||||
showContextMenu: computed,
|
||||
contextMenuPosition: computed,
|
||||
menuData: computed,
|
||||
setSimulator: action,
|
||||
setViewport: action,
|
||||
@@ -199,6 +221,7 @@ export class Designer {
|
||||
toggleSmartWizard: action,
|
||||
toggleIsPreview: action,
|
||||
toggleAddComponentPopover: action,
|
||||
toggleContextMenu: action,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -216,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) {
|
||||
@@ -264,4 +294,15 @@ export class Designer {
|
||||
this.workspace.selectSource.clear();
|
||||
}
|
||||
}
|
||||
|
||||
toggleContextMenu(
|
||||
value: boolean,
|
||||
position: {
|
||||
clientX: number;
|
||||
clientY: number;
|
||||
} = this.contextMenuPosition,
|
||||
) {
|
||||
this._showContextMenu = value;
|
||||
this._contextMenuPosition = position;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { traverseEntryFile } from '../helpers';
|
||||
import { IFileConfig } from '../types';
|
||||
import { IWorkspace } from './interfaces';
|
||||
import { TangoModule } from './module';
|
||||
|
||||
export class AppEntryModule extends TangoModule {
|
||||
routerType: string;
|
||||
|
||||
constructor(workspace: IWorkspace, props: IFileConfig) {
|
||||
super(workspace, props, false);
|
||||
this.update(props.code, true, false);
|
||||
}
|
||||
|
||||
_analysisAst() {
|
||||
const config = traverseEntryFile(this.ast);
|
||||
this.routerType = config?.router?.type;
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,16 @@ export class TangoFile {
|
||||
*/
|
||||
lastModified: number;
|
||||
|
||||
/**
|
||||
* 文件解析是否出错
|
||||
*/
|
||||
isError: boolean;
|
||||
|
||||
/**
|
||||
* 文件解析错误消息
|
||||
*/
|
||||
errorMessage: string;
|
||||
|
||||
_code: string;
|
||||
_cleanCode: string;
|
||||
|
||||
@@ -40,6 +50,7 @@ export class TangoFile {
|
||||
this.filename = props.filename;
|
||||
this.type = props.type;
|
||||
this.lastModified = Date.now();
|
||||
this.isError = false;
|
||||
|
||||
// 这里主要是为了解决 umi ts 编译错误的问题,@see https://github.com/umijs/umi/issues/7594
|
||||
if (isSyncCode) {
|
||||
|
||||
@@ -7,6 +7,7 @@ export * from './drag-source';
|
||||
export * from './history';
|
||||
export * from './file';
|
||||
export * from './module';
|
||||
export * from './entry-module';
|
||||
export * from './route-module';
|
||||
export * from './service-module';
|
||||
export * from './store-module';
|
||||
|
||||
@@ -10,12 +10,14 @@ import {
|
||||
IPageConfigData,
|
||||
IImportSpecifierSourceData,
|
||||
IImportSpecifierData,
|
||||
IFileError,
|
||||
} from '../types';
|
||||
import { TangoFile, TangoJsonFile } from './file';
|
||||
import { TangoRouteModule } from './route-module';
|
||||
import { TangoStoreModule } from './store-module';
|
||||
import { TangoServiceModule } from './service-module';
|
||||
import { IdGenerator } from '../helpers';
|
||||
import { AppEntryModule } from './entry-module';
|
||||
|
||||
export interface IViewFile {
|
||||
readonly workspace: IWorkspace;
|
||||
@@ -152,6 +154,10 @@ export interface IWorkspace {
|
||||
* 解析后的 tango.config.json 文件,如果要获取项目配置,推荐使用 projectConfig 获取
|
||||
*/
|
||||
tangoConfigJson: TangoJsonFile;
|
||||
/**
|
||||
* app.js 入口文件解析后的模块
|
||||
*/
|
||||
appEntryModule: AppEntryModule;
|
||||
/**
|
||||
* 解析后的路由模块
|
||||
*/
|
||||
@@ -187,7 +193,18 @@ export interface IWorkspace {
|
||||
renameFile: (oldFilename: string, newFilename: string) => void;
|
||||
renameFolder: (oldFoldername: string, newFoldername: string) => void;
|
||||
|
||||
updateFile: (filename: string, code: string, shouldFormatCode?: boolean) => void;
|
||||
/**
|
||||
* 更新文件
|
||||
* @param filename 文件名
|
||||
* @param code 代码
|
||||
* @param isSyncAst 是否同步 ast
|
||||
*/
|
||||
updateFile: (filename: string, code: string, isSyncAst?: boolean) => void;
|
||||
|
||||
/**
|
||||
* 检查并同步文件的 ast
|
||||
*/
|
||||
syncFiles: () => void;
|
||||
|
||||
listFiles: () => Record<string, string>;
|
||||
getFile: (filename: string) => TangoFile;
|
||||
@@ -293,8 +310,12 @@ export interface IWorkspace {
|
||||
get bizComps(): string[];
|
||||
get baseComps(): string[];
|
||||
get localComps(): string[];
|
||||
get fileErrors(): IFileError[];
|
||||
/**
|
||||
* 是否是合法的项目
|
||||
* 是否是有效的项目
|
||||
* - 包含 tango.config.json
|
||||
* - 包含视图模块
|
||||
* - 没有文件错误
|
||||
*/
|
||||
get isValid(): boolean;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,11 @@ import { IWorkspace } from './interfaces';
|
||||
export class TangoModule extends TangoFile {
|
||||
ast: t.File;
|
||||
|
||||
/**
|
||||
* ast 是否与 code 保持同步
|
||||
*/
|
||||
isAstSynced: boolean;
|
||||
|
||||
/**
|
||||
* 导入的依赖列表
|
||||
*/
|
||||
@@ -33,23 +38,53 @@ export class TangoModule extends TangoFile {
|
||||
/**
|
||||
* 基于最新的 ast 进行同步
|
||||
* @param code 如果传入 code,则基于 code 进行同步
|
||||
* @param isFormatCode 是否格式化代码
|
||||
* @param refreshWorkspace 是否刷新 workspace
|
||||
* @param isSyncAst 是否同步 ast
|
||||
* @param isRefreshWorkspace 是否刷新 workspace
|
||||
*/
|
||||
update(code?: string, isFormatCode = true, refreshWorkspace = true) {
|
||||
update(code?: string, isSyncAst = true, isRefreshWorkspace = true) {
|
||||
this.lastModified = Date.now();
|
||||
if (isNil(code)) {
|
||||
this._syncByAst();
|
||||
} else {
|
||||
this._syncByCode(code, isFormatCode);
|
||||
|
||||
try {
|
||||
if (isNil(code)) {
|
||||
this._syncByAst();
|
||||
} else {
|
||||
this._syncByCode(code, isSyncAst);
|
||||
}
|
||||
|
||||
if (isSyncAst) {
|
||||
this._analysisAst();
|
||||
}
|
||||
|
||||
this.isAstSynced = isSyncAst;
|
||||
this.isError = false;
|
||||
this.errorMessage = undefined;
|
||||
|
||||
this.workspace.onFilesChange([this.filename]);
|
||||
if (isRefreshWorkspace) {
|
||||
this.workspace.refresh([this.filename]);
|
||||
}
|
||||
} catch (err: any) {
|
||||
this.isError = true;
|
||||
this.errorMessage = err.message;
|
||||
}
|
||||
}
|
||||
|
||||
this._analysisAst();
|
||||
|
||||
this.workspace.onFilesChange([this.filename]);
|
||||
|
||||
if (refreshWorkspace) {
|
||||
this.workspace.refresh([this.filename]);
|
||||
/**
|
||||
* 基于当前的代码重新生成 ast
|
||||
*/
|
||||
updateAst() {
|
||||
if (!this.isAstSynced) {
|
||||
try {
|
||||
this.ast = code2ast(this._code);
|
||||
this._analysisAst();
|
||||
this.isAstSynced = true;
|
||||
this.isError = false;
|
||||
this.errorMessage = undefined;
|
||||
} catch (err: any) {
|
||||
this.isAstSynced = false;
|
||||
this.isError = true;
|
||||
this.errorMessage = err.message;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,22 +110,26 @@ export class TangoModule extends TangoFile {
|
||||
/**
|
||||
* 基于输入的源码进行同步
|
||||
* @param code 源码
|
||||
* @param isFormatCode 是否格式化代码
|
||||
* @param isSyncAst 是否同步 ast
|
||||
* @returns
|
||||
*/
|
||||
_syncByCode(code: string, isFormatCode = true) {
|
||||
_syncByCode(code: string, isSyncAst = true) {
|
||||
if (code === this._code) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 提前格式化代码
|
||||
if (isFormatCode) {
|
||||
try {
|
||||
code = formatCode(code);
|
||||
} catch (err) {
|
||||
// err ignored, format code failed
|
||||
}
|
||||
|
||||
this._code = code;
|
||||
this._cleanCode = code;
|
||||
this.ast = code2ast(code);
|
||||
if (isSyncAst) {
|
||||
this.ast = code2ast(code);
|
||||
}
|
||||
}
|
||||
|
||||
_analysisAst() {
|
||||
@@ -105,14 +144,17 @@ export class TangoModule extends TangoFile {
|
||||
export class TangoJsModule extends TangoModule {
|
||||
constructor(workspace: IWorkspace, props: IFileConfig) {
|
||||
super(workspace, props, false);
|
||||
this.update(props.code, false, false);
|
||||
this.update(props.code, true, false);
|
||||
|
||||
makeObservable(this, {
|
||||
_code: observable,
|
||||
_cleanCode: observable,
|
||||
isError: observable,
|
||||
errorMessage: observable,
|
||||
code: computed,
|
||||
cleanCode: computed,
|
||||
update: action,
|
||||
updateAst: action,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { action, computed, makeObservable, observable, toJS } from 'mobx';
|
||||
import { logger } from '@music163/tango-helpers';
|
||||
import {
|
||||
traverseRouteFile,
|
||||
addRouteToRouteFile,
|
||||
@@ -27,10 +28,13 @@ export class TangoRouteModule extends TangoModule {
|
||||
_routes: observable,
|
||||
_code: observable,
|
||||
_cleanCode: observable,
|
||||
isError: observable,
|
||||
errorMessage: observable,
|
||||
routes: computed,
|
||||
code: computed,
|
||||
cleanCode: computed,
|
||||
update: action,
|
||||
updateAst: action,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -74,7 +78,7 @@ export class TangoRouteModule extends TangoModule {
|
||||
*/
|
||||
removeRoute(route: string) {
|
||||
if (route === '/') {
|
||||
console.warn('index route should not be removed!');
|
||||
logger.warn('index route should not be removed!');
|
||||
return;
|
||||
}
|
||||
const record = this.getRouteByRoutePath(route);
|
||||
|
||||
@@ -49,11 +49,14 @@ export class TangoServiceModule extends TangoModule {
|
||||
_baseConfig: observable,
|
||||
_code: observable,
|
||||
_cleanCode: observable,
|
||||
isError: observable,
|
||||
errorMessage: observable,
|
||||
serviceFunctions: computed,
|
||||
baseConfig: computed,
|
||||
cleanCode: computed,
|
||||
code: computed,
|
||||
update: action,
|
||||
updateAst: action,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -31,10 +31,13 @@ export class TangoStoreEntryModule extends TangoModule {
|
||||
_stores: observable,
|
||||
_code: observable,
|
||||
_cleanCode: observable,
|
||||
isError: observable,
|
||||
errorMessage: observable,
|
||||
stores: computed,
|
||||
code: computed,
|
||||
cleanCode: computed,
|
||||
update: action,
|
||||
updateAst: action,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@ function nodeListToTreeData(list: ITangoViewNodeData[]) {
|
||||
*/
|
||||
export class TangoViewModule extends TangoModule implements IViewFile {
|
||||
// 解析为树结构的 jsxNodes 数组
|
||||
_nodesTree: ITangoViewNodeData[];
|
||||
_nodesTree: ITangoViewNodeData[] = [];
|
||||
/**
|
||||
* 通过导入组件名查找组件来自的包
|
||||
*/
|
||||
@@ -138,10 +138,14 @@ export class TangoViewModule extends TangoModule implements IViewFile {
|
||||
_code: observable,
|
||||
_cleanCode: observable,
|
||||
|
||||
isError: observable,
|
||||
errorMessage: observable,
|
||||
|
||||
code: computed,
|
||||
cleanCode: computed,
|
||||
|
||||
update: action,
|
||||
updateAst: action,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -320,7 +324,7 @@ export class TangoViewModule extends TangoModule implements IViewFile {
|
||||
updateNodeAttributes(nodeId: string, config: Record<string, any>, relatedImports?: string[]) {
|
||||
if (relatedImports && relatedImports.length) {
|
||||
// 导入依赖的组件
|
||||
const newImportData = relatedImports.reduce((prev, name) => {
|
||||
const newImportData = relatedImports.reduce<Dict<IImportSpecifierData[]>>((prev, name) => {
|
||||
const proto = this.workspace.getPrototype(name);
|
||||
const { source, specifiers } = prototype2importDeclarationData(proto, this.filename);
|
||||
const existSpecifiers: IImportSpecifierData[] = prev[source];
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
parseServiceVariablePath,
|
||||
parseStoreVariablePath,
|
||||
uniq,
|
||||
setValue,
|
||||
} from '@music163/tango-helpers';
|
||||
import {
|
||||
prototype2jsxElement,
|
||||
@@ -24,10 +25,10 @@ import {
|
||||
import { DropMethod } from './drop-target';
|
||||
import { HistoryMessage, TangoHistory } from './history';
|
||||
import { TangoNode } from './node';
|
||||
import { TangoJsModule } from './module';
|
||||
import { TangoJsModule, TangoModule } from './module';
|
||||
import { TangoFile, TangoJsonFile, TangoLessFile } from './file';
|
||||
import { IWorkspace } from './interfaces';
|
||||
import { IFileConfig, FileType, ITangoConfigPackages, IPageConfigData } from '../types';
|
||||
import { IFileConfig, FileType, ITangoConfigPackages, IPageConfigData, IFileError } from '../types';
|
||||
import { SelectSource } from './select-source';
|
||||
import { DragSource } from './drag-source';
|
||||
import { TangoRouteModule } from './route-module';
|
||||
@@ -35,6 +36,7 @@ import { TangoStoreEntryModule, TangoStoreModule } from './store-module';
|
||||
import { TangoServiceModule } from './service-module';
|
||||
import { TangoViewModule } from './view-module';
|
||||
import { TangoComponentsEntryModule } from './component-module';
|
||||
import { AppEntryModule } from './entry-module';
|
||||
|
||||
export interface IWorkspaceOptions {
|
||||
/**
|
||||
@@ -97,6 +99,11 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
*/
|
||||
activeViewFile: string;
|
||||
|
||||
/**
|
||||
* 应用入口模块
|
||||
*/
|
||||
appEntryModule: AppEntryModule;
|
||||
|
||||
/**
|
||||
* 路由配置模块
|
||||
*/
|
||||
@@ -144,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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -216,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);
|
||||
@@ -249,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,
|
||||
@@ -329,6 +348,9 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
* @param fileType 模块类型
|
||||
*/
|
||||
addFile(filename: string, code: string, fileType?: FileType) {
|
||||
if (!fileType && filename === this.entry) {
|
||||
fileType = FileType.AppEntryModule;
|
||||
}
|
||||
const moduleType = fileType || inferFileType(filename);
|
||||
const props = {
|
||||
filename,
|
||||
@@ -338,6 +360,10 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
|
||||
let module;
|
||||
switch (moduleType) {
|
||||
case FileType.AppEntryModule:
|
||||
module = new AppEntryModule(this, props);
|
||||
this.appEntryModule = module;
|
||||
break;
|
||||
case FileType.StoreEntryModule:
|
||||
module = new TangoStoreEntryModule(this, props);
|
||||
this.storeEntryModule = module;
|
||||
@@ -425,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({
|
||||
@@ -439,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);
|
||||
@@ -500,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;
|
||||
});
|
||||
@@ -728,6 +763,7 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
* 更新依赖,没有就添加
|
||||
* @param name
|
||||
* @param version
|
||||
* FIXME: 参数3的设置需要重新考虑下
|
||||
*/
|
||||
updateDependency(
|
||||
name: string,
|
||||
@@ -750,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;
|
||||
})
|
||||
|
||||
@@ -8,6 +8,7 @@ export type SimulatorMode = 'desktop' | 'tablet' | 'phone';
|
||||
export enum FileType {
|
||||
// js 文件
|
||||
Module = 'module',
|
||||
AppEntryModule = 'appEntryModule',
|
||||
StoreEntryModule = 'storeEntryModule',
|
||||
RouteModule = 'routeModule',
|
||||
ServiceModule = 'serviceModule',
|
||||
@@ -56,6 +57,11 @@ export interface IFileConfig {
|
||||
type?: FileType;
|
||||
}
|
||||
|
||||
export interface IFileError {
|
||||
filename: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 视图节点数据类型
|
||||
*/
|
||||
|
||||
@@ -3,6 +3,64 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.4.2](https://github.com/netease/tango/compare/@music163/tango-designer@1.4.1...@music163/tango-designer@1.4.2) (2024-08-06)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-designer
|
||||
|
||||
## [1.4.1](https://github.com/netease/tango/compare/@music163/tango-designer@1.4.0...@music163/tango-designer@1.4.1) (2024-08-05)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- parse template code & update maxRow of TextAreaSetter ([#192](https://github.com/netease/tango/issues/192)) ([a1eea02](https://github.com/netease/tango/commit/a1eea02f9409db73f042c74d7903ad84258f25c2))
|
||||
- show right panel when design view select node & optimize editor css ([#193](https://github.com/netease/tango/issues/193)) ([b83d965](https://github.com/netease/tango/commit/b83d965955c8d33ed8d37063482c7abe079e62b1))
|
||||
|
||||
# [1.4.0](https://github.com/netease/tango/compare/@music163/tango-designer@1.3.3...@music163/tango-designer@1.4.0) (2024-08-01)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- update types ([67d602a](https://github.com/netease/tango/commit/67d602a3ec6b7f74156bb78fda6f3b4bc2676e55))
|
||||
|
||||
### Features
|
||||
|
||||
- add isError and errorMessage to File & add isAstSynced to Module ([#190](https://github.com/netease/tango/issues/190)) ([8dd289c](https://github.com/netease/tango/commit/8dd289cd7daba628e54dc6b8929f22a1e2245160))
|
||||
|
||||
## [1.3.3](https://github.com/netease/tango/compare/@music163/tango-designer@1.3.2...@music163/tango-designer@1.3.3) (2024-07-12)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- selection bug without prototype & update ToggleButton style ([#180](https://github.com/netease/tango/issues/180)) ([1d0fb4f](https://github.com/netease/tango/commit/1d0fb4f274a5b767962c54583ab5863329d5bcc5))
|
||||
- update designer config ([#186](https://github.com/netease/tango/issues/186)) ([85c053b](https://github.com/netease/tango/commit/85c053b3db6d652b41c9873dba1366315174833f))
|
||||
|
||||
## [1.3.2](https://github.com/netease/tango/compare/@music163/tango-designer@1.3.1...@music163/tango-designer@1.3.2) (2024-06-20)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- drag-panel add resizeable & height prop ([#175](https://github.com/netease/tango/issues/175)) ([b53a9f7](https://github.com/netease/tango/commit/b53a9f7eefc748ba8a7c94270c3a374b9d6ed211))
|
||||
- fix global hotkey triggering on content editable elements ([#174](https://github.com/netease/tango/issues/174)) ([a2cc418](https://github.com/netease/tango/commit/a2cc418a73edfd9b7fe8aad456ae775a49df2166))
|
||||
- update designer style ([#176](https://github.com/netease/tango/issues/176)) ([0f3f0af](https://github.com/netease/tango/commit/0f3f0afdfa8aee2532a97c5c2e92ef4230397d86))
|
||||
|
||||
## [1.3.1](https://github.com/netease/tango/compare/@music163/tango-designer@1.3.0...@music163/tango-designer@1.3.1) (2024-06-05)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- fix context menu error on macOS ([#170](https://github.com/netease/tango/issues/170)) ([ca740c8](https://github.com/netease/tango/commit/ca740c83ae7f72a75c7db90d2e137a7e9a032326))
|
||||
|
||||
# [1.3.0](https://github.com/netease/tango/compare/@music163/tango-designer@1.2.3...@music163/tango-designer@1.3.0) (2024-06-03)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- parse app entry file & pass correct routerType to sandbox ([#168](https://github.com/netease/tango/issues/168)) ([3f3981b](https://github.com/netease/tango/commit/3f3981bb9db874b738a67fd449c579c35c58d08b))
|
||||
|
||||
### Features
|
||||
|
||||
- add context menu ([#161](https://github.com/netease/tango/issues/161)) ([28040fc](https://github.com/netease/tango/commit/28040fc00604339a40ad3216b76baf7de93a13e0))
|
||||
|
||||
## [1.2.3](https://github.com/netease/tango/compare/@music163/tango-designer@1.2.2...@music163/tango-designer@1.2.3) (2024-05-30)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- optimize variable panel scroll ui & onCancel bug ([#166](https://github.com/netease/tango/issues/166)) ([a0ed57f](https://github.com/netease/tango/commit/a0ed57ff4f332bd88749b8b1925bdb29319e4d13))
|
||||
|
||||
## [1.2.2](https://github.com/netease/tango/compare/@music163/tango-designer@1.2.1...@music163/tango-designer@1.2.2) (2024-05-27)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-designer",
|
||||
"version": "1.2.2",
|
||||
"version": "1.4.2",
|
||||
"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.1",
|
||||
"@music163/tango-core": "^1.2.0",
|
||||
"@music163/tango-helpers": "^1.1.1",
|
||||
"@music163/tango-sandbox": "^1.0.4",
|
||||
"@music163/tango-setting-form": "^1.2.3",
|
||||
"@music163/tango-ui": "^1.2.2",
|
||||
"@music163/tango-context": "^1.1.8",
|
||||
"@music163/tango-core": "^1.4.2",
|
||||
"@music163/tango-helpers": "^1.2.2",
|
||||
"@music163/tango-sandbox": "^1.0.11",
|
||||
"@music163/tango-setting-form": "^1.2.11",
|
||||
"@music163/tango-ui": "^1.4.1",
|
||||
"antd": "^4.24.2",
|
||||
"cash-dom": "^8.1.2",
|
||||
"classnames": "^2.5.1",
|
||||
|
||||
@@ -125,6 +125,8 @@ export const ComponentsPopover = observer(
|
||||
}
|
||||
footer={tipsTextMap[type]}
|
||||
width="330px"
|
||||
height="450px"
|
||||
resizeable
|
||||
maskClosable
|
||||
body={
|
||||
<ComponentsPanel
|
||||
@@ -133,10 +135,6 @@ export const ComponentsPopover = observer(
|
||||
menuData={menuData}
|
||||
layout={layout}
|
||||
onItemSelect={handleSelect}
|
||||
style={{
|
||||
maxHeight: '400px',
|
||||
overflow: 'auto',
|
||||
}}
|
||||
/>
|
||||
}
|
||||
{...popoverProps}
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
import React from 'react';
|
||||
import { getWidget } from '../widgets';
|
||||
import { Menu, MenuProps } from 'antd';
|
||||
import { observer, useWorkspace } from '@music163/tango-context';
|
||||
import { ISelectedItemData, isString } from '@music163/tango-helpers';
|
||||
import { Box, css } from 'coral-system';
|
||||
import { IconFont } from '@music163/tango-ui';
|
||||
|
||||
const contextMenuStyle = css`
|
||||
.ant-dropdown-menu {
|
||||
width: 240px;
|
||||
}
|
||||
|
||||
.ant-dropdown-menu-title-content {
|
||||
padding-left: 22px;
|
||||
}
|
||||
|
||||
.ant-dropdown-menu-item-icon {
|
||||
font-size: 14px;
|
||||
width: 14px;
|
||||
}
|
||||
|
||||
.ant-dropdown-menu-item-icon + .ant-dropdown-menu-title-content {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.ant-dropdown-menu-submenu-popup {
|
||||
padding: 0;
|
||||
margin-top: -4px;
|
||||
}
|
||||
`;
|
||||
|
||||
const ParentNodesMenuItem = ({
|
||||
record,
|
||||
onClick,
|
||||
key,
|
||||
}: {
|
||||
record: ISelectedItemData;
|
||||
onClick: () => any;
|
||||
key?: string;
|
||||
}) => {
|
||||
const workspace = useWorkspace();
|
||||
const componentPrototype = workspace.componentPrototypes.get(record.name);
|
||||
const icon = componentPrototype?.icon || 'icon-placeholder';
|
||||
|
||||
const iconRender = icon.startsWith('icon-') ? (
|
||||
<IconFont className="material-icon" type={icon} />
|
||||
) : (
|
||||
<img className="material-icon" src={icon} alt={componentPrototype.name} />
|
||||
);
|
||||
|
||||
return (
|
||||
<Menu.Item key={key || record.id} icon={iconRender} onClick={onClick}>
|
||||
{record.name}
|
||||
{!!record.codeId && ` (${record.codeId})`}
|
||||
</Menu.Item>
|
||||
);
|
||||
};
|
||||
|
||||
const ParentNodesMenu = observer(() => {
|
||||
const workspace = useWorkspace();
|
||||
const selectSource = workspace.selectSource;
|
||||
const parents = selectSource.first?.parents;
|
||||
|
||||
if (!parents?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Menu.SubMenu key="parentNodes" title="选取父节点">
|
||||
{parents.map((item, index) => (
|
||||
<ParentNodesMenuItem
|
||||
key={item.id}
|
||||
record={item}
|
||||
onClick={() => {
|
||||
selectSource.select({
|
||||
...item,
|
||||
parents: parents.slice(index + 1),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Menu.SubMenu>
|
||||
);
|
||||
});
|
||||
|
||||
export interface ContextMenuProps extends MenuProps {
|
||||
/**
|
||||
* 动作列表,默认列出全部
|
||||
*/
|
||||
actions?: Array<string | React.ReactElement>;
|
||||
/**
|
||||
* 是否显示父节点选项
|
||||
*/
|
||||
showParents?: boolean;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
menuStyle?: React.CSSProperties;
|
||||
}
|
||||
|
||||
export const ContextMenu = observer(
|
||||
({
|
||||
showParents,
|
||||
actions: actionsProp,
|
||||
className,
|
||||
style,
|
||||
menuStyle,
|
||||
...rest
|
||||
}: ContextMenuProps) => {
|
||||
const actions = actionsProp || Object.keys(getWidget('contextMenu'));
|
||||
const menus = actions.map((item) => {
|
||||
if (isString(item)) {
|
||||
const widget = getWidget(['contextMenu', item].join('.'));
|
||||
return widget ? React.createElement(widget) : null;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
if (showParents) {
|
||||
menus.unshift(<ParentNodesMenu />);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box display="inline-block" css={contextMenuStyle} className={className} style={style}>
|
||||
<Menu activeKey={null} {...rest} style={menuStyle}>
|
||||
{menus}
|
||||
</Menu>
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,68 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Box, css } from 'coral-system';
|
||||
import { observer, useWorkspace } from '@music163/tango-context';
|
||||
import { CloseOutlined } from '@ant-design/icons';
|
||||
|
||||
const errorMessageStyle = css`
|
||||
padding: 24px;
|
||||
white-space: pre-wrap;
|
||||
`;
|
||||
|
||||
/**
|
||||
* 文件错误提示浮层
|
||||
*/
|
||||
export const FileErrorsOverlay = observer(() => {
|
||||
const [isVisible, setIsVisible] = useState<boolean>(true);
|
||||
const workspace = useWorkspace();
|
||||
|
||||
const handleClose = () => {
|
||||
setIsVisible(false);
|
||||
};
|
||||
|
||||
if (workspace.fileErrors.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
className="WorkspaceErrorOverlay"
|
||||
position="absolute"
|
||||
top="0"
|
||||
left="0"
|
||||
right="0"
|
||||
height="100%"
|
||||
bg="rgba(244, 244, 244, 0.9)"
|
||||
color="red"
|
||||
zIndex={1999}
|
||||
>
|
||||
<Box position="relative" p="4">
|
||||
<CloseOutlined
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 12,
|
||||
right: 12,
|
||||
color: '#000',
|
||||
}}
|
||||
onClick={handleClose}
|
||||
/>
|
||||
<Box css={errorMessageStyle}>
|
||||
<Box fontSize="20px" mb="l">
|
||||
代码解析错误,请回到源码模式检查并修改错误
|
||||
</Box>
|
||||
{workspace.fileErrors.map((fileError) => (
|
||||
<Box key={fileError.filename} borderBottom="solid" borderBottomColor="line2" py="l">
|
||||
<Box fontWeight="bold">{fileError.filename}</Box>
|
||||
<Box as="code" display="block">
|
||||
{fileError.message}
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
@@ -1,5 +1,7 @@
|
||||
export * from './context-menu';
|
||||
export * from './drag-box';
|
||||
export * from './input-kv';
|
||||
export * from './variable-tree';
|
||||
export * from './variable-tree-modal';
|
||||
export * from './components-popover';
|
||||
export * from './file-errors-overlay';
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { isValidFunctionCode, url2serviceName } from '@music163/tango-helpers';
|
||||
import { Dict, isValidFunctionCode, url2serviceName } from '@music163/tango-helpers';
|
||||
import { InputCode } from '@music163/tango-ui';
|
||||
import { Button, Dropdown, Form, FormProps, Input, Select, Space } from 'antd';
|
||||
import { Box } from 'coral-system';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
interface AddServiceFormProps extends FormProps {
|
||||
initialValues?: object;
|
||||
initialValues?: Dict;
|
||||
onSubmit: (values: object) => void;
|
||||
onCancel: () => void;
|
||||
serviceModules: object[];
|
||||
|
||||
@@ -166,7 +166,7 @@ export function VariableTree(props: VariableTreeProps) {
|
||||
|
||||
return (
|
||||
<Box display="flex" columnGap="l" className="VariableTree" css={varTreeStyle} {...rest}>
|
||||
<Box className="VariableList" width="40%">
|
||||
<Box className="VariableList" width="40%" overflow="auto">
|
||||
{renderHeaderExtra?.(props, state)}
|
||||
<Box mb="m" position="sticky" top="0" bg="white" zIndex={2}>
|
||||
<Search placeholder="请输入变量名" onChange={(val) => setKeyword(val?.trim())} />
|
||||
@@ -203,7 +203,11 @@ export function VariableTree(props: VariableTreeProps) {
|
||||
<Box flex="0 0 72px" textAlign="right">
|
||||
{node.showRemoveButton && (
|
||||
<Popconfirm
|
||||
zIndex={99999}
|
||||
title={`确认删除吗 ${node.title}?该操作会导致引用此模型的代码报错,请谨慎操作!`}
|
||||
onCancel={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
onConfirm={(e) => {
|
||||
e.stopPropagation();
|
||||
if (isStoreVariablePath(node.key)) {
|
||||
@@ -333,7 +337,14 @@ export function VariableTree(props: VariableTreeProps) {
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<ValueDefine data={activeNode} onSave={onUpdateStoreVariable} />
|
||||
<ValueDefine
|
||||
data={activeNode}
|
||||
onSave={(variableKey, code) => {
|
||||
onUpdateStoreVariable(variableKey, code);
|
||||
// 更新当前节点 code
|
||||
setActiveNode((pre) => ({ ...pre, raw: code }));
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</ValueDetail>
|
||||
|
||||
@@ -84,6 +84,8 @@ export function ValueDefine({ data, onSave = noop }: ValueDefineProps) {
|
||||
onClick={() => {
|
||||
off();
|
||||
setError('');
|
||||
// 重置到原始值
|
||||
setValue(data.raw);
|
||||
}}
|
||||
>
|
||||
取消
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
import { useWorkspace, observer } from '@music163/tango-context';
|
||||
import { ContextAction } from '@music163/tango-ui';
|
||||
import { CopyOutlined } from '@ant-design/icons';
|
||||
|
||||
export const CopyNodeContextAction = observer(() => {
|
||||
const workspace = useWorkspace();
|
||||
return (
|
||||
<ContextAction
|
||||
icon={<CopyOutlined />}
|
||||
hotkey="Command+C"
|
||||
onClick={() => {
|
||||
workspace.copySelectedNode();
|
||||
}}
|
||||
>
|
||||
复制节点
|
||||
</ContextAction>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
import { useWorkspace, observer } from '@music163/tango-context';
|
||||
import { ContextAction } from '@music163/tango-ui';
|
||||
import { DeleteOutlined } from '@ant-design/icons';
|
||||
|
||||
export const DeleteNodeContextAction = observer(() => {
|
||||
const workspace = useWorkspace();
|
||||
return (
|
||||
<ContextAction
|
||||
icon={<DeleteOutlined />}
|
||||
hotkey="Backspace"
|
||||
onClick={() => {
|
||||
workspace.removeSelectedNode();
|
||||
}}
|
||||
>
|
||||
删除节点
|
||||
</ContextAction>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from './copy-node';
|
||||
export * from './delete-node';
|
||||
export * from './paste-node';
|
||||
export * from './view-source';
|
||||
@@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
import { useWorkspace, observer } from '@music163/tango-context';
|
||||
import { ContextAction } from '@music163/tango-ui';
|
||||
import { SnippetsOutlined } from '@ant-design/icons';
|
||||
|
||||
export const PasteNodeContextAction = observer(() => {
|
||||
const workspace = useWorkspace();
|
||||
return (
|
||||
<ContextAction
|
||||
icon={<SnippetsOutlined />}
|
||||
hotkey="Command+V"
|
||||
onClick={() => {
|
||||
workspace.pasteSelectedNode();
|
||||
}}
|
||||
>
|
||||
粘贴节点
|
||||
</ContextAction>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import { useDesigner, observer } from '@music163/tango-context';
|
||||
import { ContextAction } from '@music163/tango-ui';
|
||||
import { CodeOutlined } from '@ant-design/icons';
|
||||
|
||||
export const ViewSourceContextAction = observer(() => {
|
||||
const designer = useDesigner();
|
||||
return (
|
||||
<ContextAction
|
||||
icon={<CodeOutlined />}
|
||||
onClick={() => {
|
||||
designer.setActiveView('code');
|
||||
}}
|
||||
>
|
||||
查看源码
|
||||
</ContextAction>
|
||||
);
|
||||
});
|
||||
@@ -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) => {
|
||||
|
||||
@@ -77,6 +77,9 @@ export function useDnd({
|
||||
if (designer.isPreview) {
|
||||
return;
|
||||
}
|
||||
if (designer.showContextMenu) {
|
||||
designer.toggleContextMenu(false);
|
||||
}
|
||||
|
||||
const point = sandboxQuery.getRelativePoint({ x: e.clientX, y: e.clientY });
|
||||
selectSource.setStart({
|
||||
@@ -128,9 +131,14 @@ export function useDnd({
|
||||
};
|
||||
|
||||
const onClick = (e: React.MouseEvent) => {
|
||||
if (designer.showContextMenu) {
|
||||
designer.toggleContextMenu(false);
|
||||
}
|
||||
const data = sandboxQuery.getDraggableParentsData(e.target as HTMLElement, true);
|
||||
if (data && data.id) {
|
||||
selectSource.select(data);
|
||||
// 画布选中节点时,自动唤起 right-panel
|
||||
designer.toggleRightPanel(true);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -380,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);
|
||||
@@ -388,6 +397,56 @@ export function useDnd({
|
||||
}
|
||||
};
|
||||
|
||||
const onContextMenu = (event: React.MouseEvent) => {
|
||||
if (designer.showContextMenu) {
|
||||
designer.toggleContextMenu(false);
|
||||
}
|
||||
// 按下其他按键时,视为用户有特殊操作,此时不展示右键菜单
|
||||
if (event.ctrlKey || event.altKey || event.metaKey || event.shiftKey) {
|
||||
return;
|
||||
}
|
||||
const { clientX, clientY } = event;
|
||||
let target;
|
||||
if (workspace.selectSource.isSelected) {
|
||||
for (const item of workspace.selectSource.selected) {
|
||||
if (
|
||||
// 如果选中的节点是页面根节点(无 parents),则忽略
|
||||
item.parents?.length &&
|
||||
item.bounding &&
|
||||
clientX >= item.bounding.left &&
|
||||
clientX <= item.bounding.left + item.bounding.width &&
|
||||
clientY >= item.bounding.top &&
|
||||
clientY <= item.bounding.top + item.bounding.height
|
||||
) {
|
||||
// 右键坐标已经在当前选中组件的选区内,直接展示右键菜单
|
||||
target = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 否则,根据右键的元素选中最接近的组件
|
||||
if (!target) {
|
||||
target = sandboxQuery.getDraggableParentsData(event.target as HTMLElement, true);
|
||||
}
|
||||
if (target && target.id) {
|
||||
if (!target.parents?.length) {
|
||||
// 页面根节点不展示右键菜单操作
|
||||
return;
|
||||
}
|
||||
// 右键时高亮选中当前元素
|
||||
// 以防之前选区有多个元素,即便已经是选中的元素也再选中一遍
|
||||
event.preventDefault();
|
||||
selectSource.select(target);
|
||||
// 在下一周期再展示右键菜单,以让先前的菜单先被销毁
|
||||
requestAnimationFrame(() => {
|
||||
designer.toggleContextMenu(true, {
|
||||
clientX,
|
||||
clientY,
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onTango = (e: CustomEvent) => {
|
||||
const detail = e.detail || {};
|
||||
|
||||
@@ -437,6 +496,7 @@ export function useDnd({
|
||||
onDragEnd,
|
||||
onScroll,
|
||||
onKeyDown,
|
||||
onContextMenu,
|
||||
onTango,
|
||||
...selectHandler,
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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';
|
||||
@@ -49,6 +49,10 @@ export interface CombinedSandboxRef {
|
||||
|
||||
const LANDING_PAGE_PATH = '/__background_landing_page__';
|
||||
|
||||
function fixRouterMode(routerType: string): CodeSandboxProps['routerMode'] {
|
||||
return routerType === 'hash' ? 'hash' : undefined;
|
||||
}
|
||||
|
||||
function useSandbox({
|
||||
isPreview: isPreviewProp,
|
||||
configFormatter,
|
||||
@@ -84,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 });
|
||||
@@ -105,23 +109,26 @@ function useSandbox({
|
||||
// 根据当前 workspace 状态与组件传入的状态是否一致,控制是否需要切换到空白路由
|
||||
const display = isActive ? 'block' : 'none';
|
||||
const routePath = isActive ? startRoute || workspace.activeRoute : LANDING_PAGE_PATH;
|
||||
const routerMode = fixRouterMode(workspace.appEntryModule?.routerType);
|
||||
|
||||
const sandboxProps = isPreview
|
||||
? {
|
||||
files,
|
||||
eventHandlers: pick(dndHandlers, ['onTango']),
|
||||
onMessage,
|
||||
onLoad,
|
||||
display,
|
||||
startRoute: routePath,
|
||||
onLoad,
|
||||
routerMode,
|
||||
}
|
||||
: {
|
||||
files,
|
||||
eventHandlers: dndHandlers as any,
|
||||
onMessage,
|
||||
onLoad,
|
||||
display,
|
||||
startRoute: routePath,
|
||||
onLoad,
|
||||
routerMode,
|
||||
};
|
||||
|
||||
return sandboxProps;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from './copy-node';
|
||||
export * from './delete-node';
|
||||
export * from './more-actions';
|
||||
export * from './select-parent-node';
|
||||
export * from './view-source';
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
import { observer } from '@music163/tango-context';
|
||||
import { SelectAction } from '@music163/tango-ui';
|
||||
import { MoreOutlined } from '@ant-design/icons';
|
||||
import { Dropdown } from 'antd';
|
||||
import { ContextMenu } from '../components';
|
||||
|
||||
export const MoreActionsAction = observer(() => {
|
||||
return (
|
||||
<Dropdown placement="bottomRight" overlay={<ContextMenu showParents />}>
|
||||
<SelectAction tooltip="更多">
|
||||
<MoreOutlined />
|
||||
</SelectAction>
|
||||
</Dropdown>
|
||||
);
|
||||
});
|
||||
@@ -4,7 +4,7 @@ import { AutoComplete } from 'antd';
|
||||
import { ActionSelect } from '@music163/tango-ui';
|
||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||
import { useWorkspace, useWorkspaceData } from '@music163/tango-context';
|
||||
import { wrapCode } from '@music163/tango-helpers';
|
||||
import { Dict, wrapCode } from '@music163/tango-helpers';
|
||||
import { ExpressionPopover } from './expression-setter';
|
||||
import { value2code } from '@music163/tango-core';
|
||||
|
||||
@@ -141,7 +141,7 @@ export function EventSetter(props: EventSetterProps) {
|
||||
);
|
||||
}
|
||||
|
||||
const handlerMap = {
|
||||
const handlerMap: Dict = {
|
||||
[EventAction.OpenModal]: 'openModal',
|
||||
[EventAction.CloseModal]: 'closeModal',
|
||||
[EventAction.NavigateTo]: 'navigateTo',
|
||||
|
||||
@@ -3,8 +3,8 @@ import { Box, Text, css } from 'coral-system';
|
||||
import { Dropdown, Button } from 'antd';
|
||||
import { isValidExpressionCode } from '@music163/tango-core';
|
||||
import { getValue, IVariableTreeNode, noop } from '@music163/tango-helpers';
|
||||
import { CloseCircleFilled, ExpandAltOutlined, MenuOutlined } from '@ant-design/icons';
|
||||
import { Panel, InputCode, Action, DragPanel } from '@music163/tango-ui';
|
||||
import { CloseCircleFilled, MenuOutlined } from '@ant-design/icons';
|
||||
import { Panel, InputCode, Action, DragPanel, PopOutOutlined } from '@music163/tango-ui';
|
||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||
import { useWorkspace, useWorkspaceData } from '@music163/tango-context';
|
||||
import { VariableTree } from '../components';
|
||||
@@ -182,7 +182,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 +195,65 @@ export function ExpressionPopover({
|
||||
return (
|
||||
<DragPanel
|
||||
width={700}
|
||||
title={`将 ${selectNodePath}/${title} 设置为引用变量或自定义表达式`}
|
||||
extra={subTitle}
|
||||
height={615}
|
||||
resizeable
|
||||
title={`使用代码设置 ${selectNodePath} 组件的${title}`}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setExp(undefined);
|
||||
setError('');
|
||||
}
|
||||
}}
|
||||
popoverStyle={{
|
||||
height: '615px',
|
||||
}}
|
||||
body={
|
||||
<>
|
||||
<Panel shape="solid">
|
||||
<Box display="flex" flexDirection="column">
|
||||
<Box p="m">
|
||||
<InputCode
|
||||
shape="inset"
|
||||
minHeight="56px"
|
||||
maxHeight="200px"
|
||||
maxHeight="160px"
|
||||
value={exp}
|
||||
placeholder={placeholder}
|
||||
onChange={handleExpInputChange}
|
||||
onBlur={() => {
|
||||
setError(expressionValueValidate(exp));
|
||||
if (exp) {
|
||||
setError(expressionValueValidate(exp));
|
||||
}
|
||||
}}
|
||||
autoCompleteContext={evaluateContext}
|
||||
autoCompleteOptions={autoCompleteOptions}
|
||||
/>
|
||||
{error ? (
|
||||
<Text color="red" fontSize="12px">
|
||||
<Text color="red" fontSize="12px" as="p">
|
||||
出错了!输入的表达式存在语法错误,请修改后再提交!
|
||||
</Text>
|
||||
) : (
|
||||
<Text fontSize="12px" color="text3">
|
||||
说明:你可以在上面的代码输入框里输入常规的 javascript 代码,还可以直接使用 jsx
|
||||
代码,但需要符合该属性的接受值定义。
|
||||
) : null}
|
||||
{subTitle && (
|
||||
<Text fontSize="12px" color="text3" as="p">
|
||||
{subTitle}
|
||||
</Text>
|
||||
)}
|
||||
</Panel>
|
||||
<Text fontSize="12px" color="text3" as="p">
|
||||
说明:你可以在上面的代码输入框里输入常规的 javascript 代码,还可以直接使用 jsx
|
||||
代码,但需要符合该属性的接受值定义。
|
||||
</Text>
|
||||
</Box>
|
||||
<Panel
|
||||
title="从变量列表中选中"
|
||||
shape="solid"
|
||||
borderTop="0"
|
||||
overflow="hidden"
|
||||
bodyProps={{ overflow: 'hidden' }}
|
||||
borderLeft="0"
|
||||
borderRight="0"
|
||||
flex={1}
|
||||
bodyProps={{
|
||||
style: {
|
||||
overflow: 'hidden',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<VariableTree
|
||||
height={380}
|
||||
height="100%"
|
||||
showViewButton
|
||||
dataSource={dataSource || expressionVariables}
|
||||
appContext={sandbox?.window['tango']}
|
||||
appContext={evaluateContext['tango']}
|
||||
getStoreNames={() => Object.keys(workspace.storeModules)}
|
||||
serviceModules={serviceModules}
|
||||
getServiceData={(serviceKey) => {
|
||||
@@ -307,10 +316,10 @@ export function ExpressionPopover({
|
||||
}}
|
||||
/>
|
||||
</Panel>
|
||||
</>
|
||||
</Box>
|
||||
}
|
||||
footer={(close) => (
|
||||
<Box display="flex" justifyContent="flex-end" gap="5px">
|
||||
<Box display="flex" width="100%" justifyContent="flex-end" gap="5px">
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
@@ -330,7 +339,12 @@ export function ExpressionPopover({
|
||||
)}
|
||||
>
|
||||
{children || (
|
||||
<Action tooltip="打开表达式变量选择面板" icon={<ExpandAltOutlined />} size="small" />
|
||||
<Action
|
||||
tooltip="弹出代码编辑和变量选择面板"
|
||||
tooltipProps={{ placement: 'topLeft' }}
|
||||
icon={<PopOutOutlined />}
|
||||
size="small"
|
||||
/>
|
||||
)}
|
||||
</DragPanel>
|
||||
);
|
||||
|
||||
@@ -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[] = [];
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { ActionSelect, InputCode } from '@music163/tango-ui';
|
||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||
import { Box } from 'coral-system';
|
||||
import { Dict } from '@music163/tango-helpers';
|
||||
|
||||
interface IRenderOption {
|
||||
label: string;
|
||||
@@ -37,7 +38,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;
|
||||
}, {});
|
||||
|
||||
@@ -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,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) {
|
||||
|
||||
@@ -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(
|
||||
() =>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { DropMethod, ITangoViewNodeData } from '@music163/tango-core';
|
||||
import { noop, parseDndId } from '@music163/tango-helpers';
|
||||
import { useSandboxQuery } from '../../context';
|
||||
import { buildQueryBySlotId } from '../../helpers';
|
||||
import { ContextMenu } from '../../components';
|
||||
|
||||
export interface ComponentsTreeProps {
|
||||
/**
|
||||
@@ -143,6 +144,18 @@ export const ComponentsTree: React.FC<ComponentsTreeProps> = observer(
|
||||
const file = workspace.activeViewModule;
|
||||
const nodesTree = (file?.nodesTree ?? []) as ITangoViewNodeData[];
|
||||
const [expandedKeys, setExpandedKeys] = useState(getNodeKeys(nodesTree));
|
||||
const [contextMenuOpen, setContextMenuOpen] = useState(false);
|
||||
|
||||
const handleSelect = (keys: React.Key[]) => {
|
||||
const slotKey = keys?.[0] as string;
|
||||
const data = sandboxQuery.getDraggableParentsData(buildQueryBySlotId(slotKey), true);
|
||||
if (data && data.id) {
|
||||
workspace.selectSource.select(data);
|
||||
}
|
||||
// export selected
|
||||
onSelect(slotKey);
|
||||
setSelectedKeys(keys);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedKeys(workspace.selectSource.selected.map((item) => item.id));
|
||||
@@ -164,64 +177,83 @@ export const ComponentsTree: React.FC<ComponentsTreeProps> = observer(
|
||||
|
||||
return (
|
||||
<Box css={outlineStyle}>
|
||||
<Tree
|
||||
selectedKeys={selectedKeys as string[]}
|
||||
fieldNames={filedNames}
|
||||
treeData={nodesTree as any[]}
|
||||
onSelect={(keys) => {
|
||||
const slotKey = keys?.[0] as string;
|
||||
const data = sandboxQuery.getDraggableParentsData(buildQueryBySlotId(slotKey), true);
|
||||
if (data && data.id) {
|
||||
workspace.selectSource.select(data);
|
||||
<Dropdown
|
||||
placement="bottomLeft"
|
||||
trigger={['contextMenu']}
|
||||
open={contextMenuOpen}
|
||||
onOpenChange={(val) => {
|
||||
if (!val) {
|
||||
setContextMenuOpen(val);
|
||||
}
|
||||
// export selected
|
||||
onSelect(slotKey);
|
||||
setSelectedKeys(keys);
|
||||
}}
|
||||
blockNode
|
||||
expandedKeys={expandedKeys}
|
||||
onExpand={(keys) => setExpandedKeys(keys as string[])}
|
||||
draggable
|
||||
onDragStart={(data) => {
|
||||
const prototype = workspace.componentPrototypes.get(data.node.component);
|
||||
if (!prototype) {
|
||||
return;
|
||||
}
|
||||
const { canDrag } = prototype.rules || {};
|
||||
if (canDrag && !canDrag()) {
|
||||
return;
|
||||
}
|
||||
workspace.dragSource.set({
|
||||
id: data.node.id,
|
||||
name: data.node.component,
|
||||
});
|
||||
}}
|
||||
onDrop={(data) => {
|
||||
const dropKey = data.node.key as string;
|
||||
let method;
|
||||
if (data.dropToGap) {
|
||||
// 插入节点的后面
|
||||
method = DropMethod.InsertAfter;
|
||||
} else {
|
||||
// 作为第一个子节点
|
||||
method = DropMethod.InsertFirstChild;
|
||||
}
|
||||
workspace.dragSource.dropTarget.set(
|
||||
{
|
||||
id: dropKey,
|
||||
},
|
||||
method,
|
||||
);
|
||||
workspace.dropNode();
|
||||
}}
|
||||
titleRender={(node) => (
|
||||
<OutlineTreeNode
|
||||
actionItems={actionItems}
|
||||
showToggleVisibleIcon={showToggleVisibleIcon}
|
||||
node={node}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
overlay={<ContextMenu onClick={() => setContextMenuOpen(false)} showParents={false} />}
|
||||
>
|
||||
<Tree
|
||||
selectedKeys={selectedKeys as string[]}
|
||||
fieldNames={filedNames}
|
||||
treeData={nodesTree as any[]}
|
||||
onSelect={handleSelect}
|
||||
blockNode
|
||||
expandedKeys={expandedKeys}
|
||||
onExpand={(keys) => setExpandedKeys(keys as string[])}
|
||||
draggable
|
||||
onDragStart={(data) => {
|
||||
const prototype = workspace.componentPrototypes.get(data.node.component);
|
||||
if (!prototype) {
|
||||
return;
|
||||
}
|
||||
const { canDrag } = prototype.rules || {};
|
||||
if (canDrag && !canDrag()) {
|
||||
return;
|
||||
}
|
||||
workspace.dragSource.set({
|
||||
id: data.node.id,
|
||||
name: data.node.component,
|
||||
});
|
||||
}}
|
||||
onDrop={(data) => {
|
||||
const dropKey = data.node.key as string;
|
||||
let method;
|
||||
if (data.dropToGap) {
|
||||
// 插入节点的后面
|
||||
method = DropMethod.InsertAfter;
|
||||
} else {
|
||||
// 作为第一个子节点
|
||||
method = DropMethod.InsertFirstChild;
|
||||
}
|
||||
workspace.dragSource.dropTarget.set(
|
||||
{
|
||||
id: dropKey,
|
||||
},
|
||||
method,
|
||||
);
|
||||
workspace.dropNode();
|
||||
}}
|
||||
onRightClick={({ event, node }) => {
|
||||
// 按下其他按键时,视为用户有特殊操作,此时不展示右键菜单
|
||||
if (event.ctrlKey || event.altKey || event.metaKey || event.shiftKey) {
|
||||
setContextMenuOpen(false);
|
||||
return;
|
||||
}
|
||||
// 顶层的 Page 组件不展示右键菜单
|
||||
if (node.component === 'Page') {
|
||||
setContextMenuOpen(false);
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
// 由于部分操作是基于 selectSource 实现的,因此右键时默认选中当前项
|
||||
handleSelect([node.key]);
|
||||
setContextMenuOpen(true);
|
||||
}}
|
||||
titleRender={(node) => (
|
||||
<OutlineTreeNode
|
||||
actionItems={actionItems}
|
||||
showToggleVisibleIcon={showToggleVisibleIcon}
|
||||
node={node}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Dropdown>
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -29,7 +29,7 @@ export const VariablePanel = observer(
|
||||
新建模型
|
||||
</Button>
|
||||
}
|
||||
bodyProps={{ p: 'm' }}
|
||||
bodyProps={{ p: 'm', height: '100%' }}
|
||||
>
|
||||
{isAdd ? (
|
||||
<AddStoreForm
|
||||
@@ -42,6 +42,7 @@ export const VariablePanel = observer(
|
||||
/>
|
||||
) : (
|
||||
<VariableTree
|
||||
height="100%"
|
||||
defaultValueDetailMode="define"
|
||||
dataSource={storeVariables}
|
||||
onAddStoreVariable={(storeName, data) => {
|
||||
|
||||
@@ -25,7 +25,13 @@ export interface SelectionToolsProps {
|
||||
|
||||
export const SelectionTools = observer(
|
||||
({
|
||||
actions: actionsProp = ['selectParentNode', 'viewSource', 'copyNode', 'deleteNode'],
|
||||
actions: actionsProp = [
|
||||
'selectParentNode',
|
||||
'viewSource',
|
||||
'copyNode',
|
||||
'deleteNode',
|
||||
'moreActions',
|
||||
],
|
||||
}: SelectionToolsProps) => {
|
||||
const workspace = useWorkspace();
|
||||
const selectSource = workspace.selectSource;
|
||||
@@ -149,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
|
||||
@@ -198,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 />} />
|
||||
|
||||
@@ -1,20 +1,61 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import cx from 'classnames';
|
||||
import { Box, HTMLCoralProps } from 'coral-system';
|
||||
import { useDesigner, useWorkspace } from '@music163/tango-context';
|
||||
import { observer, useDesigner, useWorkspace } from '@music163/tango-context';
|
||||
import { Ghost } from './ghost';
|
||||
import { useSandboxQuery } from '../context';
|
||||
import { SelectionTools, SelectionToolsProps } from './selection';
|
||||
import { InsertionPrompt } from './insertion';
|
||||
import { DraggingMask } from './mask';
|
||||
import { Dropdown, DropdownProps } from 'antd';
|
||||
import { ContextMenu } from '../components';
|
||||
|
||||
export interface ViewportProps extends HTMLCoralProps<'div'> {
|
||||
selectionTools?: SelectionToolsProps['actions'];
|
||||
}
|
||||
|
||||
const ViewportContextMenu = observer((props: DropdownProps) => {
|
||||
const designer = useDesigner();
|
||||
|
||||
// FIXME: 考虑优化定位实现?
|
||||
// 由于 iframe 内的 contextmenu 事件无法被外层感知到,所以无法自动展示与定位
|
||||
// 而手动指定 align 会干扰 contextmenu 溢出时的位置处理,会定位到完全相反的位置
|
||||
// 因此,给 dropdown 单独提供一个定位容器,将其定位至 iframe 内触发点击的位置
|
||||
// 并在每次 contextmenu 隐藏时销毁 dropdown,让 dropdown 能重新获取新的位置
|
||||
return (
|
||||
designer.showContextMenu && (
|
||||
<Dropdown
|
||||
autoAdjustOverflow
|
||||
trigger={['contextMenu']}
|
||||
placement="bottomLeft"
|
||||
open
|
||||
onOpenChange={(val) => {
|
||||
if (!val) {
|
||||
designer.toggleContextMenu(val);
|
||||
}
|
||||
}}
|
||||
overlay={<ContextMenu onClick={() => designer.toggleContextMenu(false)} showParents />}
|
||||
{...props}
|
||||
>
|
||||
<Box
|
||||
width={0}
|
||||
height={0}
|
||||
position="absolute"
|
||||
style={{
|
||||
left: designer.contextMenuPosition.clientX,
|
||||
top: designer.contextMenuPosition.clientY,
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
/>
|
||||
</Dropdown>
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
export function Viewport({ selectionTools, children, className, ...rest }: ViewportProps) {
|
||||
return (
|
||||
<Box position="relative" height="100%" className={cx('Viewport', className)} {...rest}>
|
||||
<ViewportContextMenu />
|
||||
<ViewportBody>{children}</ViewportBody>
|
||||
<div className="AuxTools">
|
||||
<SelectionTools actions={selectionTools} />
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
<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="双屏视图"
|
||||
>
|
||||
|
||||
@@ -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={() => {
|
||||
|
||||
@@ -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" />;
|
||||
|
||||
@@ -18,14 +18,21 @@ import {
|
||||
DeleteNodeAction,
|
||||
ViewSourceAction,
|
||||
SelectParentNodeAction,
|
||||
MoreActionsAction,
|
||||
} from './selection-menu';
|
||||
import {
|
||||
CopyNodeContextAction,
|
||||
DeleteNodeContextAction,
|
||||
PasteNodeContextAction,
|
||||
ViewSourceContextAction,
|
||||
} from './context-menu';
|
||||
|
||||
const widgets = {};
|
||||
|
||||
export function registerWidget(key: string, component: React.ComponentType<any>) {
|
||||
if (!/^(toolbar|sidebar|selectionMenu)\.[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, or selection-menu`,
|
||||
`Invalid widget key(${key}), should start with toolbar, sidebar, contextMenu or selectionMenu`,
|
||||
);
|
||||
}
|
||||
setValue(widgets, key, component);
|
||||
@@ -55,3 +62,9 @@ registerWidget('selectionMenu.copyNode', CopyNodeAction);
|
||||
registerWidget('selectionMenu.deleteNode', DeleteNodeAction);
|
||||
registerWidget('selectionMenu.selectParentNode', SelectParentNodeAction);
|
||||
registerWidget('selectionMenu.viewSource', ViewSourceAction);
|
||||
registerWidget('selectionMenu.moreActions', MoreActionsAction);
|
||||
|
||||
registerWidget('contextMenu.viewSource', ViewSourceContextAction);
|
||||
registerWidget('contextMenu.copyNode', CopyNodeContextAction);
|
||||
registerWidget('contextMenu.pasteNode', PasteNodeContextAction);
|
||||
registerWidget('contextMenu.deleteNode', DeleteNodeContextAction);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -3,6 +3,24 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.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
|
||||
|
||||
- add context menu ([#161](https://github.com/netease/tango/issues/161)) ([28040fc](https://github.com/netease/tango/commit/28040fc00604339a40ad3216b76baf7de93a13e0))
|
||||
|
||||
## [1.1.1](https://github.com/netease/tango/compare/@music163/tango-helpers@1.1.0...@music163/tango-helpers@1.1.1) (2024-05-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-helpers",
|
||||
"version": "1.1.1",
|
||||
"version": "1.2.2",
|
||||
"description": "Shared types, helpers, and hooks of tango-apps",
|
||||
"keywords": [
|
||||
"shared",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -68,3 +68,10 @@ export function isServiceVariablePath(key: string) {
|
||||
export function isInTangoDesignMode() {
|
||||
return !!(window as any).__TANGO_DESIGNER__;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否是 macOS 或 iOS like 设备
|
||||
*/
|
||||
export function isApplePlatform() {
|
||||
return /Mac|iPhone|iPad|iPod/i.test(navigator.platform);
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ export function isValidFunctionCode(str: string) {
|
||||
}
|
||||
}
|
||||
|
||||
const templatePattern = /^{{(.+)}}$/s;
|
||||
const templatePattern = /^{{([\s\S]+)}}$/;
|
||||
|
||||
/**
|
||||
* 判断给定代码是否被双花括号包裹
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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 作为追踪标记
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
});
|
||||
});
|
||||
@@ -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.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
|
||||
|
||||
## [1.0.5](https://github.com/netease/tango/compare/@music163/tango-sandbox@1.0.4...@music163/tango-sandbox@1.0.5) (2024-06-03)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- use addEventListener to bind iframe event handlers ([#169](https://github.com/netease/tango/issues/169)) ([c1306e2](https://github.com/netease/tango/commit/c1306e24e0c747d0f4bd845dc1714488ddd8cdac))
|
||||
|
||||
## [1.0.4](https://github.com/netease/tango/compare/@music163/tango-sandbox@1.0.3...@music163/tango-sandbox@1.0.4) (2024-05-21)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-sandbox
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-sandbox",
|
||||
"version": "1.0.4",
|
||||
"version": "1.0.11",
|
||||
"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.2.0",
|
||||
"@music163/tango-helpers": "^1.1.1",
|
||||
"@music163/tango-core": "^1.4.2",
|
||||
"@music163/tango-helpers": "^1.2.2",
|
||||
"crypto-js": "^4.1.1",
|
||||
"lodash.isequal": "4.5.0",
|
||||
"react-frame-component": "^5.2.4"
|
||||
|
||||
@@ -3,7 +3,13 @@ import { TangoEventName } from '@music163/tango-helpers';
|
||||
import Loading from './loading';
|
||||
import Manager from './manager';
|
||||
import { createMissingPackageJSON, changeRoute } from './helper';
|
||||
import { IFiles, CodeSandboxState, CodeSandboxProps, UnsubscribeFunction } from '../types';
|
||||
import {
|
||||
IFiles,
|
||||
CodeSandboxState,
|
||||
CodeSandboxProps,
|
||||
UnsubscribeFunction,
|
||||
EventHandlers,
|
||||
} from '../types';
|
||||
|
||||
// CodeSandbox 使用示例
|
||||
// <CodeSandbox files={files} entry={entry} template={template} bundlerURL={`${window.location.protocol}//codesandbox.fn.netease.com/`} />
|
||||
@@ -32,6 +38,8 @@ export class CodeSandbox extends React.Component<CodeSandboxProps, CodeSandboxSt
|
||||
|
||||
iframe: HTMLIFrameElement;
|
||||
|
||||
private eventHandlers: EventHandlers;
|
||||
|
||||
constructor(props: CodeSandboxProps) {
|
||||
super(props);
|
||||
|
||||
@@ -75,11 +83,8 @@ export class CodeSandbox extends React.Component<CodeSandboxProps, CodeSandboxSt
|
||||
this.manager.cleanup();
|
||||
|
||||
// clear tango event
|
||||
if (this.iframe && this.props?.eventHandlers?.onTango) {
|
||||
this.iframe.contentDocument?.removeEventListener(
|
||||
TangoEventName.DesignerAction,
|
||||
this.props.eventHandlers.onTango,
|
||||
);
|
||||
if (this.iframe && this.eventHandlers) {
|
||||
this.unbindEvents(this.eventHandlers);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,6 +121,29 @@ export class CodeSandbox extends React.Component<CodeSandboxProps, CodeSandboxSt
|
||||
externalResources: this.props.externalResources,
|
||||
});
|
||||
|
||||
normalizeEventType = (eventType: string) =>
|
||||
eventType === 'onTango'
|
||||
? TangoEventName.DesignerAction
|
||||
: eventType.replace(/^on/, '').toLowerCase();
|
||||
|
||||
bindEvents = (eventHandlers: EventHandlers) => {
|
||||
Object.keys(eventHandlers).forEach((eventType) => {
|
||||
this.iframe?.contentDocument?.addEventListener(
|
||||
this.normalizeEventType(eventType),
|
||||
eventHandlers[eventType],
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
unbindEvents = (eventHandlers: EventHandlers) => {
|
||||
Object.keys(eventHandlers).forEach((eventType) => {
|
||||
this.iframe?.contentDocument?.removeEventListener(
|
||||
this.normalizeEventType(eventType),
|
||||
eventHandlers[eventType],
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
// Manager 实例化,即开始构建页面
|
||||
// 将 el 传给 Manager 作为实例化参数,当构建完成后,会将构建的页面 dom 设置给 el
|
||||
// 注:Manager是一个管理者的角色,从大局上把控整个转译和执行的流程
|
||||
@@ -155,17 +183,11 @@ export class CodeSandbox extends React.Component<CodeSandboxProps, CodeSandboxSt
|
||||
|
||||
// 注册监听函数
|
||||
const { eventHandlers } = this.props;
|
||||
Object.keys(eventHandlers).forEach((eventType) => {
|
||||
if (el.contentDocument) {
|
||||
el.contentDocument[eventType.toLowerCase()] = eventHandlers[eventType];
|
||||
}
|
||||
if (eventType === 'onTango') {
|
||||
el.contentDocument?.addEventListener(
|
||||
TangoEventName.DesignerAction,
|
||||
eventHandlers[eventType],
|
||||
);
|
||||
}
|
||||
});
|
||||
if (eventHandlers) {
|
||||
this.bindEvents(eventHandlers);
|
||||
// 保存一份当前的 eventHandlers,避免 props 变更导致无法取消现在已注册的事件绑定
|
||||
this.eventHandlers = eventHandlers;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3,6 +3,44 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [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
|
||||
|
||||
## [1.2.5](https://github.com/netease/tango/compare/@music163/tango-setting-form@1.2.4...@music163/tango-setting-form@1.2.5) (2024-06-03)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-setting-form
|
||||
|
||||
## [1.2.4](https://github.com/netease/tango/compare/@music163/tango-setting-form@1.2.3...@music163/tango-setting-form@1.2.4) (2024-05-30)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-setting-form
|
||||
|
||||
## [1.2.3](https://github.com/netease/tango/compare/@music163/tango-setting-form@1.2.2...@music163/tango-setting-form@1.2.3) (2024-05-27)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-setting-form
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-setting-form",
|
||||
"version": "1.2.3",
|
||||
"version": "1.2.11",
|
||||
"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.2.0",
|
||||
"@music163/tango-helpers": "^1.1.1",
|
||||
"@music163/tango-ui": "^1.2.2",
|
||||
"@music163/tango-core": "^1.4.2",
|
||||
"@music163/tango-helpers": "^1.2.2",
|
||||
"@music163/tango-ui": "^1.4.1",
|
||||
"antd": "^4.24.2",
|
||||
"coral-system": "^1.0.5",
|
||||
"mobx": "6.12.3",
|
||||
|
||||
@@ -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;
|
||||
}, {});
|
||||
|
||||
+5
@@ -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({
|
||||
|
||||
@@ -3,6 +3,51 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [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
|
||||
|
||||
- fix context menu error on macOS ([#170](https://github.com/netease/tango/issues/170)) ([ca740c8](https://github.com/netease/tango/commit/ca740c83ae7f72a75c7db90d2e137a7e9a032326))
|
||||
|
||||
# [1.3.0](https://github.com/netease/tango/compare/@music163/tango-ui@1.2.3...@music163/tango-ui@1.3.0) (2024-06-03)
|
||||
|
||||
### Features
|
||||
|
||||
- add context menu ([#161](https://github.com/netease/tango/issues/161)) ([28040fc](https://github.com/netease/tango/commit/28040fc00604339a40ad3216b76baf7de93a13e0))
|
||||
|
||||
## [1.2.3](https://github.com/netease/tango/compare/@music163/tango-ui@1.2.2...@music163/tango-ui@1.2.3) (2024-05-30)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- popover re-click position bug & drag panel drag bug ([#165](https://github.com/netease/tango/issues/165)) ([f83c6bc](https://github.com/netease/tango/commit/f83c6bc0f69820582720512eefedc9ddf5db2975))
|
||||
|
||||
## [1.2.2](https://github.com/netease/tango/compare/@music163/tango-ui@1.2.1...@music163/tango-ui@1.2.2) (2024-05-27)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-ui",
|
||||
"version": "1.2.2",
|
||||
"version": "1.4.1",
|
||||
"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.1.1",
|
||||
"@music163/tango-helpers": "^1.2.2",
|
||||
"@uiw/react-codemirror": "^4.22.0",
|
||||
"antd": "^4.24.2",
|
||||
"classnames": "^2.5.1",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user