Compare commits

..

17 Commits

Author SHA1 Message Date
Wells 0011cdca0f chore(release): publish
- @music163/tango-context@1.1.4
 - @music163/tango-core@1.3.2
 - @music163/tango-designer@1.3.2
 - @music163/tango-sandbox@1.0.7
 - @music163/tango-setting-form@1.2.7
 - @music163/tango-ui@1.3.2
2024-06-20 15:45:08 +08:00
Wells 0f3f0afdfa fix: update designer style (#176)
* fix: remove useless codes

* fix: show insert children icon only for hasChildren node

* fix: update dependency

* fix: update styles

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

* fix: update ToggleButton style

* fix: add showDropdownIcon to ToggleButton

* fix: update codeSetter style

* fix: add builtin imageSetter

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

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

* fix: adjust context menu icon style
2024-06-03 15:57:58 +08:00
Wells 00bf52a411 chore(release): publish
- @music163/tango-context@1.1.2
 - @music163/tango-core@1.3.0
 - @music163/tango-designer@1.3.0
 - @music163/tango-helpers@1.2.0
 - @music163/tango-sandbox@1.0.5
 - @music163/tango-setting-form@1.2.5
 - @music163/tango-ui@1.3.0
2024-06-03 09:51:00 +08:00
864907600cc c1306e24e0 fix: use addEventListener to bind iframe event handlers (#169)
* feat: use addEventListener to bind iframe event handlers

* fix: types
2024-05-31 17:44:55 +08:00
864907600cc 28040fc006 feat: add context menu (#161)
* feat: add more actions menu on SelectionTools

* feat: add view source context action

* feat: normalize hotkey by platform

* feat: add context menu to components tree

* feat: add context menu to viewport
2024-05-31 17:31:58 +08:00
Wells 3f3981bb9d fix: parse app entry file & pass correct routerType to sandbox (#168)
* fix: parse app entry file & pass correct routerType to sandbox

* fix: export entry module
2024-05-31 14:54:12 +08:00
Wells 253ceca969 chore(release): publish
- @music163/tango-designer@1.2.3
 - @music163/tango-setting-form@1.2.4
 - @music163/tango-ui@1.2.3
2024-05-30 17:05:52 +08:00
BoBoooooo a0ed57ff4f fix: optimize variable panel scroll ui & onCancel bug (#166) 2024-05-30 14:06:12 +08:00
BoBoooooo f83c6bc0f6 fix: popover re-click position bug & drag panel drag bug (#165)
* fix: popover reClick position bug

* fix: drag event lose when drag over iframe
2024-05-29 15:26:00 +08:00
66 changed files with 1197 additions and 260 deletions
+9 -15
View File
@@ -31,23 +31,17 @@ You can view the detailed usage guide through the following links:
- Out-of-the-box front-end low-code designer, providing flexible and easy-to-use designer React components
- Developed using TypeScript, providing complete type definition files
## 🎯 Milestone
## 💡 Examples
```mermaid
gantt
dateFormat YYYY-MM-DD
title Tango 1.0 Milestones
excludes weekends
%% (`excludes` accepts specific dates in YYYY-MM-DD format, days of the week ("sunday") or "weekends", but not the word "weekdays".)
You can use tango to build lowcode builders, for example:
section Builder Release
1.0 Alpha : m1, 2023-10-01, 2024-01-31
1.0 RC : m2, 2024-02-01, 2024-04-30
1.0 Stable : m3, 2024-04-30, 2024-12-31
```
> [!NOTE]
> Alpha versions will continue to optimize some implementation protocols and continuously integrate new features from the internal version of NetEase; RC versions will provide a stable parsing protocol and implementation; The 1.0 official version is planned to be released in Q2 2024. After the release of the 1.0 official version, we will continue to iterate on versions and gradually incorporate core capabilities such as process design, component library, and AIGC from the internal version.
| Preview | Description |
|---------|-------------------------|
| ![image](https://github.com/NetEase/tango/assets/6984035/56a71741-4536-4336-803a-d81d51128a76) | Internal App Builder |
| ![image](https://github.com/NetEase/tango/assets/6984035/7f817e59-f7aa-4476-bc29-ac3e33fadbf7) | Dashboard App Builder |
| ![image](https://github.com/NetEase/tango/assets/6984035/790d88e1-d716-4f99-868a-31dda25e9fb1) | Mobile App Builder |
| ![rn app](public/rn-builder.png) | ReactNative App Builder |
| ![mail app](public/mail-builder.png) | Email Builder |
## 🌐 Compatibility
+13 -18
View File
@@ -26,28 +26,23 @@
## ✨ 特性
- 经历网易云音乐内网生产环境的实际检验,可灵活集成应用于低代码平台,本地开发工具等
- 基于源码 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
```
| 预览图 | 说明 |
|---------|-------------------------|
| ![image](https://github.com/NetEase/tango/assets/6984035/56a71741-4536-4336-803a-d81d51128a76) | 中后台系统搭建 |
| ![image](https://github.com/NetEase/tango/assets/6984035/7f817e59-f7aa-4476-bc29-ac3e33fadbf7) | 仪表盘应用搭建 |
| ![image](https://github.com/NetEase/tango/assets/6984035/790d88e1-d716-4f99-868a-31dda25e9fb1) | H5活动页面搭建 |
| ![rn app](public/rn-builder.png) | ReactNative 应用搭建 |
| ![mail app](public/mail-builder.png) | 营销类邮件搭建 |
> [!NOTE]
> Alpha 版本会持续对一些实现协议进行优化,并持续融合网易内网版本中的新增特性;RC 版本将会提供基本稳定的解析协议和实现;1.0 正式版本计划在 2024 年Q2发布。1.0 正式版发布后我们将会持续进行版本迭代,并逐步融入内网版本中的流程设计、组件库、AIGC 等核心基础能力。
## 🌐 兼容环境
+12 -2
View File
@@ -5,6 +5,7 @@ import { BranchesOutlined, MenuOutlined, QuestionCircleOutlined } from '@ant-des
import { registerSetter } from '@music163/tango-designer';
import type { IVariableTreeNode } from '@music163/tango-helpers';
import { FooSetter } from '../components';
import { ToggleButton } from '@music163/tango-ui';
export * from './mock-files';
@@ -46,8 +47,17 @@ registerSetter({
// 平台 Logo
export function Logo() {
return (
<Box width="50px" display="flex" alignItems="center" justifyContent="center" fontSize="20px">
<MenuOutlined />
<Box width="50px" display="flex" alignItems="center" justifyContent="center" fontSize="14px">
<ToggleButton
shape="text"
items={[
{ label: '回到首页', key: 'home' },
{ label: '文档', key: 'doc' },
{ label: '设置', key: 'setting' },
]}
>
<MenuOutlined />
</ToggleButton>
</Box>
);
}
+2 -3
View File
@@ -2,7 +2,7 @@ const packageJson = {
name: 'demo',
private: true,
dependencies: {
'@music163/tango-mail': '0.1.1',
'@music163/tango-mail': '0.2.4',
'@music163/tango-boot': '0.2.5',
react: '17.0.2',
'react-dom': '17.0.2',
@@ -44,7 +44,7 @@ const tangoConfigJson = {
},
'@music163/tango-mail': {
description: 'TangoMail 基础物料',
version: '0.1.1',
version: '0.2.4',
library: 'TangoMail',
type: 'baseDependency',
resources: ['https://unpkg.com/@music163/tango-mail@{{version}}/dist/index.js'],
@@ -116,7 +116,6 @@ const WelcomeEmail = () => (
</Section>
<Text style={paragraph}>
Best,
<br />
The Koala team
</Text>
<Hr style={hr} />
+1 -1
View File
@@ -56,7 +56,7 @@ const tangoConfigJson = {
},
'@music163/antd': {
description: '云音乐低代码中后台应用基础物料',
version: '0.2.5',
version: '0.2.6',
library: 'TangoAntd',
type: 'baseDependency',
resources: [
+2 -3
View File
@@ -20,12 +20,11 @@ import {
ApiOutlined,
AppstoreAddOutlined,
BuildOutlined,
ClusterOutlined,
FunctionOutlined,
PlusOutlined,
createFromIconfontCN,
} from '@ant-design/icons';
import { Action } from '@music163/tango-ui';
import { Action, PackageOutlined } from '@music163/tango-ui';
import { useState } from 'react';
const menuData = {
@@ -182,7 +181,7 @@ export default function App() {
<Sidebar.Item
key="dependency"
label="依赖"
icon={<ClusterOutlined />}
icon={<PackageOutlined />}
isFloat
width={800}
/>
@@ -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>
);
}
+12
View File
@@ -3,6 +3,18 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.1.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
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@music163/tango-context",
"version": "1.1.1",
"version": "1.1.4",
"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.3.2",
"@music163/tango-helpers": "^1.2.0",
"mobx-react-lite": "4.0.7"
},
"publishConfig": {
+22
View File
@@ -3,6 +3,28 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [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
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@music163/tango-core",
"version": "1.2.0",
"version": "1.3.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.0",
"@types/babel__generator": "^7.6.7",
"@types/babel__traverse": "^7.20.4",
"mobx": "6.12.3",
+3 -2
View File
@@ -222,15 +222,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;
+21
View File
@@ -391,8 +391,10 @@ export function updateJSXElementAttribute(node: t.JSXElement, attrName: string,
isExist = true;
// @ts-ignore
if (t.isJSXAttribute(jsxAttributeNode)) {
// 更新 jsx 属性值 <Foo bar="1" /> 的模式
jsxAttributeNode.value = value2jsxAttributeValueNode(attrValue);
} else if (name === 'children') {
// 更新 jsx children 的情况 <Foo>children</Foo>
node.children = value2jsxChildrenValueNode(attrValue);
}
}
@@ -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;
}
+34
View File
@@ -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,
});
}
@@ -264,4 +287,15 @@ export class Designer {
this.workspace.selectSource.clear();
}
}
toggleContextMenu(
value: boolean,
position: {
clientX: number;
clientY: number;
} = this.contextMenuPosition,
) {
this._showContextMenu = value;
this._contextMenuPosition = position;
}
}
+18
View File
@@ -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;
}
}
+1
View File
@@ -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';
+5
View File
@@ -16,6 +16,7 @@ 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 +153,10 @@ export interface IWorkspace {
* 解析后的 tango.config.json 文件,如果要获取项目配置,推荐使用 projectConfig 获取
*/
tangoConfigJson: TangoJsonFile;
/**
* app.js 入口文件解析后的模块
*/
appEntryModule: AppEntryModule;
/**
* 解析后的路由模块
*/
+21 -9
View File
@@ -11,6 +11,7 @@ import {
parseServiceVariablePath,
parseStoreVariablePath,
uniq,
setValue,
} from '@music163/tango-helpers';
import {
prototype2jsxElement,
@@ -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;
/**
* 路由配置模块
*/
@@ -329,6 +336,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 +348,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;
@@ -728,6 +742,7 @@ export class Workspace extends EventTarget implements IWorkspace {
* 更新依赖,没有就添加
* @param name
* @param version
* FIXME: 参数3的设置需要重新考虑下
*/
updateDependency(
name: string,
@@ -750,15 +765,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;
})
+1
View File
@@ -8,6 +8,7 @@ export type SimulatorMode = 'desktop' | 'tablet' | 'phone';
export enum FileType {
// js 文件
Module = 'module',
AppEntryModule = 'appEntryModule',
StoreEntryModule = 'storeEntryModule',
RouteModule = 'routeModule',
ServiceModule = 'serviceModule',
+30
View File
@@ -3,6 +3,36 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.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
+7 -7
View File
@@ -1,6 +1,6 @@
{
"name": "@music163/tango-designer",
"version": "1.2.2",
"version": "1.3.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.4",
"@music163/tango-core": "^1.3.2",
"@music163/tango-helpers": "^1.2.0",
"@music163/tango-sandbox": "^1.0.7",
"@music163/tango-setting-form": "^1.2.7",
"@music163/tango-ui": "^1.3.2",
"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>
);
},
);
@@ -1,3 +1,4 @@
export * from './context-menu';
export * from './drag-box';
export * from './input-kv';
export * from './variable-tree';
@@ -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>
);
});
+59 -1
View File
@@ -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,6 +131,9 @@ 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);
@@ -380,7 +386,8 @@ export function useDnd({
};
const onKeyDown = (e: React.KeyboardEvent) => {
const isInputElement = isInputElements((e.target as HTMLElement).nodeName);
const target = e.target as HTMLElement;
const isInputElement = target.isContentEditable || isInputElements(target.nodeName);
// 禁用输入事件的触发的快捷键逻辑
if (!isInputElement) {
const key = getHotkey(e);
@@ -388,6 +395,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 +494,7 @@ export function useDnd({
onDragEnd,
onScroll,
onKeyDown,
onContextMenu,
onTango,
...selectHandler,
};
+9 -2
View File
@@ -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,
@@ -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>
);
});
@@ -3,8 +3,8 @@ import { Box, Text, css } from 'coral-system';
import { Dropdown, Button } from 'antd';
import { isValidExpressionCode } from '@music163/tango-core';
import { getValue, IVariableTreeNode, noop } from '@music163/tango-helpers';
import { CloseCircleFilled, ExpandAltOutlined, MenuOutlined } from '@ant-design/icons';
import { Panel, InputCode, Action, DragPanel } from '@music163/tango-ui';
import { CloseCircleFilled, MenuOutlined } from '@ant-design/icons';
import { Panel, InputCode, Action, DragPanel, PopOutOutlined } from '@music163/tango-ui';
import { FormItemComponentProps } from '@music163/tango-setting-form';
import { useWorkspace, useWorkspaceData } from '@music163/tango-context';
import { VariableTree } from '../components';
@@ -195,53 +195,62 @@ export function ExpressionPopover({
return (
<DragPanel
width={700}
title={`${selectNodePath}/${title} 设置为引用变量或自定义表达式`}
extra={subTitle}
height={615}
resizeable
title={`使用代码设置 ${selectNodePath} 组件的${title}`}
onOpenChange={(open) => {
if (!open) {
setExp(undefined);
setError('');
}
}}
popoverStyle={{
height: '615px',
}}
body={
<>
<Panel shape="solid">
<Box display="flex" flexDirection="column">
<Box p="m">
<InputCode
shape="inset"
minHeight="56px"
maxHeight="200px"
maxHeight="160px"
value={exp}
placeholder={placeholder}
onChange={handleExpInputChange}
onBlur={() => {
setError(expressionValueValidate(exp));
if (exp) {
setError(expressionValueValidate(exp));
}
}}
autoCompleteContext={evaluateContext}
autoCompleteOptions={autoCompleteOptions}
/>
{error ? (
<Text color="red" fontSize="12px">
<Text color="red" fontSize="12px" as="p">
</Text>
) : (
<Text fontSize="12px" color="text3">
javascript 使 jsx
) : null}
{subTitle && (
<Text fontSize="12px" color="text3" as="p">
{subTitle}
</Text>
)}
</Panel>
<Text fontSize="12px" color="text3" as="p">
javascript 使 jsx
</Text>
</Box>
<Panel
title="从变量列表中选中"
shape="solid"
borderTop="0"
overflow="hidden"
bodyProps={{ overflow: 'hidden' }}
borderLeft="0"
borderRight="0"
flex={1}
bodyProps={{
style: {
overflow: 'hidden',
},
}}
>
<VariableTree
height={380}
height="100%"
showViewButton
dataSource={dataSource || expressionVariables}
appContext={sandbox?.window['tango']}
@@ -307,10 +316,10 @@ export function ExpressionPopover({
}}
/>
</Panel>
</>
</Box>
}
footer={(close) => (
<Box display="flex" justifyContent="flex-end" gap="5px">
<Box display="flex" width="100%" justifyContent="flex-end" gap="5px">
<Button
type="primary"
size="small"
@@ -330,7 +339,12 @@ export function ExpressionPopover({
)}
>
{children || (
<Action tooltip="打开表达式变量选择面板" icon={<ExpandAltOutlined />} size="small" />
<Action
tooltip="弹出代码编辑和变量选择面板"
tooltipProps={{ placement: 'topLeft' }}
icon={<PopOutOutlined />}
size="small"
/>
)}
</DragPanel>
);
@@ -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>
);
},
@@ -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) => {
+22 -14
View File
@@ -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 />} />
+42 -1
View File
@@ -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} />
+15 -2
View File
@@ -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);
+6
View File
@@ -3,6 +3,12 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [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 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@music163/tango-helpers",
"version": "1.1.1",
"version": "1.2.0",
"description": "Shared types, helpers, and hooks of tango-apps",
"keywords": [
"shared",
+7
View File
@@ -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);
}
+14
View File
@@ -3,6 +3,20 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.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
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@music163/tango-sandbox",
"version": "1.0.4",
"version": "1.0.7",
"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.3.2",
"@music163/tango-helpers": "^1.2.0",
"crypto-js": "^4.1.1",
"lodash.isequal": "4.5.0",
"react-frame-component": "^5.2.4"
+39 -17
View File
@@ -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;
}
};
}
};
+18
View File
@@ -3,6 +3,24 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.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
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@music163/tango-setting-form",
"version": "1.2.3",
"version": "1.2.7",
"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.3.2",
"@music163/tango-helpers": "^1.2.0",
"@music163/tango-ui": "^1.3.2",
"antd": "^4.24.2",
"coral-system": "^1.0.5",
"mobx": "6.12.3",
@@ -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;
+25
View File
@@ -3,6 +3,31 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [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
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@music163/tango-ui",
"version": "1.2.2",
"version": "1.3.2",
"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.0",
"@uiw/react-codemirror": "^4.22.0",
"antd": "^4.24.2",
"classnames": "^2.5.1",
+11 -2
View File
@@ -1,4 +1,4 @@
import { Tooltip } from 'antd';
import { Tooltip, TooltipProps } from 'antd';
import React from 'react';
import { css, Button, Link, HTMLCoralProps } from 'coral-system';
import cx from 'classnames';
@@ -48,6 +48,10 @@ export interface ActionProps extends HTMLCoralProps<any> {
* 提示文本
*/
tooltip?: string;
/**
* Tooltip属性透传
*/
tooltipProps?: TooltipProps;
/**
* 超链接
*/
@@ -75,6 +79,7 @@ export interface ActionProps extends HTMLCoralProps<any> {
export function Action({
icon,
tooltip,
tooltipProps,
href,
size,
shape,
@@ -110,7 +115,11 @@ export function Action({
}
if (tooltip) {
return <Tooltip title={tooltip}>{ret}</Tooltip>;
return (
<Tooltip title={tooltip} {...tooltipProps}>
{ret}
</Tooltip>
);
}
return ret;
}
+7 -5
View File
@@ -1,6 +1,6 @@
import React from 'react';
import { Box, HTMLCoralProps, css } from 'coral-system';
import { UpOutlined } from '@ant-design/icons';
import { CaretRightOutlined } from '@ant-design/icons';
import { useControllableState } from '@music163/tango-helpers';
export interface CollapsePanelProps extends Omit<HTMLCoralProps<'div'>, 'title'> {
@@ -39,8 +39,10 @@ export interface CollapsePanelProps extends Omit<HTMLCoralProps<'div'>, 'title'>
const headerStyle = css`
user-select: none;
.anticon-up {
margin-right: 8px;
.anticon-caret-right {
width: 10px;
height: 10px;
margin-right: 4px;
color: var(--tango-colors-text2);
transition: transform 0.2s ease 0s;
}
@@ -67,7 +69,7 @@ export function CollapsePanel(props: CollapsePanelProps) {
});
const iconStyle = {
transform: collapsed ? undefined : 'rotate(-180deg)',
transform: collapsed ? undefined : 'rotate(90deg)',
};
const stickHeaderProps: HTMLCoralProps<'div'> = stickyHeader
@@ -104,7 +106,7 @@ export function CollapsePanel(props: CollapsePanelProps) {
css={headerStyle}
>
<Box display="flex" alignItems="center" fontSize="14px" fontWeight="500">
<UpOutlined style={iconStyle} />
<CaretRightOutlined style={iconStyle} />
{title}
</Box>
<Box>{extra}</Box>
+86
View File
@@ -0,0 +1,86 @@
import React, { useMemo } from 'react';
import { Menu, Space } from 'antd';
import { Box, css } from 'coral-system';
import { isApplePlatform } from '@music163/tango-helpers';
const contextActionStyle = css`
display: flex;
gap: 8px;
align-items: center;
.ContextActionContent {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.ContextActionExtra {
flex: none;
color: var(--tango-colors-gray-40);
}
`;
interface ContextActionProps {
icon?: React.ReactNode;
children?: React.ReactNode;
hotkey?: string;
extra?: React.ReactNode;
onClick?: () => void;
className?: string;
disabled?: boolean;
key?: string;
}
export function ContextAction({
icon,
children,
hotkey,
extra,
disabled,
onClick,
className,
key,
}: ContextActionProps) {
const normalizedHotKey = useMemo(() => {
if (!hotkey) {
return null;
}
const keyMap = isApplePlatform()
? {
command: '⌘',
meta: '⌘',
ctrl: '^',
control: '^',
alt: '⌥',
option: '⌥',
shift: '⇧',
'+': '',
}
: {
command: 'Ctrl',
meta: 'Win',
ctrl: 'Ctrl',
control: 'Ctrl',
alt: 'Alt',
option: 'Alt',
shift: 'Shift',
};
const regexp = new RegExp(Object.keys(keyMap).join('|').replace(/\+/, '\\+'), 'ig');
return hotkey.replace(regexp, (match) => keyMap[match.toLowerCase()]);
}, [hotkey]);
return (
<Menu.Item key={key} onClick={onClick} icon={icon} className={className} disabled={disabled}>
<Box className="ContextAction" css={contextActionStyle}>
<Box className="ContextActionContent">{children}</Box>
<Box className="ContextActionExtra">
<Space>
{normalizedHotKey}
{extra}
</Space>
</Box>
</Box>
</Menu.Item>
);
}
+89 -40
View File
@@ -1,9 +1,27 @@
import React, { useState } from 'react';
import { Box, Text, styled } from 'coral-system';
import { Popover, PopoverProps, IconFont } from './';
import Draggable from 'react-draggable';
import { CloseOutlined } from '@ant-design/icons';
import { CloseOutlined, HolderOutlined } from '@ant-design/icons';
import { noop } from '@music163/tango-helpers';
import { Popover, PopoverProps } from './popover';
// Dragging over an iframe stops dragging when moving the mouse too fast #613
// https://github.com/react-grid-layout/react-draggable/issues/613
const injectStyleToBody = () => {
const id = 'react-draggable-transparent-selection';
if (document.getElementById(id)) {
return;
}
const style = document.createElement('style');
style.id = id;
style.innerHTML = `
/* Prevent iframes from stealing drag events */
.react-draggable-transparent-selection iframe {
pointer-events: none;
}
`;
document.head.appendChild(style);
};
const CloseIcon = styled(CloseOutlined)`
cursor: pointer;
@@ -17,7 +35,53 @@ const CloseIcon = styled(CloseOutlined)`
}
`;
interface DragPanelProps extends Omit<PopoverProps, 'overlay'> {
const DragPanelContainer = styled(Box)`
display: flex;
flex-direction: column;
justify-content: space-between;
background-color: #fff;
border-radius: 8px;
box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.1);
border: 1px solid var(--tango-colors-line2);
width: ${(props) => props.width}px;
height: ${(props) => props.height}px;
resize: ${(props) => (props.resizeable ? 'both' : 'none')};
overflow: ${(props) => (props.resizeable ? 'auto' : 'hidden')};
`;
const DragPanelHeader = styled(Box)`
padding: 8px 12px;
border-bottom: 1px solid var(--tango-colors-line2);
cursor: move;
display: flex;
justify-content: space-between;
align-items: center;
min-height: 36px;
`;
const DragPanelBody = styled(Box)`
flex: 1;
min-height: 0;
> * {
height: 100%;
}
`;
const DragPanelFooter = styled(Box)`
padding: 8px 12px;
min-height: 36px;
display: flex;
align-items: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
background: var(--tango-colors-line1);
font-size: 12px;
font-weight: 400;
border-top: 1px solid var(--tango-colors-line2);
`;
interface DragPanelProps extends Omit<PopoverProps, 'overlay' | 'open'> {
// 标题
title?: React.ReactNode | string;
// 内容
@@ -26,8 +90,12 @@ interface DragPanelProps extends Omit<PopoverProps, 'overlay'> {
footer?: ((close: () => void) => React.ReactNode) | React.ReactNode | string;
// 宽度
width?: number | string;
// 高度
height?: number | string;
// 右上角区域
extra?: React.ReactNode | string;
// 是否可以缩放
resizeable?: boolean;
children?: React.ReactNode;
}
@@ -37,8 +105,10 @@ export function DragPanel({
body,
children,
width = 330,
height = 330,
extra,
onOpenChange = noop,
resizeable = false,
...props
}: DragPanelProps) {
const [open, setOpen] = useState(false);
@@ -59,28 +129,21 @@ export function DragPanel({
onOpenChange(innerOpen);
}}
overlay={
<Draggable handle=".selection-drag-bar">
<Box
bg="#FFF"
borderRadius="m"
boxShadow="lowDown"
border="solid"
borderColor="line2"
overflow="hidden"
<Draggable
handle=".drag-panel-header"
onStart={() => {
injectStyleToBody();
}}
>
<DragPanelContainer
className="drag-panel"
width={width}
height={height}
resizeable={resizeable}
>
{/* 头部区域 */}
<Box
px="l"
py="m"
className="selection-drag-bar"
borderBottom="1px solid var(--tango-colors-line2)"
cursor="move"
display="flex"
justifyContent="space-between"
>
<DragPanelHeader className="drag-panel-header">
<Box fontSize="12px" color="text2">
<IconFont type="icon-applications" />
<HolderOutlined />
<Text marginLeft={'5px'}>{title}</Text>
</Box>
<Box color="text2" fontSize="12px" display="flex" alignItems="center">
@@ -92,26 +155,12 @@ export function DragPanel({
}}
/>
</Box>
</Box>
{/* 主体区域 */}
{body}
{/* 底部 */}
</DragPanelHeader>
<DragPanelBody className="drag-panel-body">{body}</DragPanelBody>
{footer && (
<Box
px="l"
py="m"
whiteSpace="nowrap"
overflow="hidden"
textOverflow="ellipsis"
background="var(--tango-colors-line1)"
fontSize="12px"
fontWeight={400}
borderTop="1px solid var(--tango-colors-line2)"
>
{footerNode}
</Box>
<DragPanelFooter className="drag-panel-footer">{footerNode}</DragPanelFooter>
)}
</Box>
</DragPanelContainer>
</Draggable>
}
{...props}
+2
View File
@@ -2,10 +2,12 @@ export * from './create-icon';
export * from './code-outlined';
export * from './line-dashed-outlined';
export * from './line-solid-outlined';
export * from './package-outlined';
export * from './open-panel-filled-left-outlined';
export * from './open-panel-filled-right-outlined';
export * from './open-panel-left-outlined';
export * from './open-panel-right-outlined';
export * from './undo-outlined';
export * from './pop-out-outlined';
export * from './redo-outlined';
export * from './dual-outlined';
@@ -0,0 +1,16 @@
import React from 'react';
import { createIcon } from './create-icon';
const PackageOutlinedSvg = () => (
<svg
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
>
<path d="M915.2 284.8c0-3.2 0-3.2-3.2-6.4 0-3.2-3.2-3.2-6.4-6.4-9.6-12.8-22.4-25.6-38.4-35.2l-297.6-169.6c-35.2-22.4-80-22.4-118.4 0L313.6 147.2c-6.4 3.2-12.8 6.4-16 12.8L153.6 240c-12.8 9.6-25.6 22.4-38.4 35.2 0 3.2-3.2 3.2-3.2 6.4s0 3.2-3.2 6.4c-6.4 16-12.8 35.2-12.8 54.4v342.4c0 41.6 22.4 80 57.6 102.4l297.6 169.6c12.8 6.4 28.8 12.8 41.6 16 3.2 3.2 9.6 3.2 16 3.2s9.6-3.2 16-3.2c16-3.2 28.8-6.4 41.6-16l297.6-169.6c35.2-22.4 57.6-60.8 57.6-102.4V342.4c6.4-19.2 0-38.4-6.4-57.6z m-428.8-160c9.6-3.2 16-6.4 25.6-6.4 9.6 0 19.2 3.2 25.6 6.4L832 291.2l-128 73.6-316.8-182.4 99.2-57.6z m25.6 352L192 291.2l128-73.6 316.8 182.4-124.8 76.8zM185.6 729.6c-16-9.6-25.6-28.8-25.6-48V345.6l320 185.6V896l-294.4-166.4z m652.8 0L544 896V531.2l320-185.6v336c0 19.2-9.6 38.4-25.6 48z" />
</svg>
);
export const PackageOutlined = createIcon(PackageOutlinedSvg, 'PackageOutlined');
@@ -0,0 +1,17 @@
import React from 'react';
import { createIcon } from './create-icon';
const Svg = () => (
<svg
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
fill="currentColor"
>
<path d="M832 544a32 32 0 0 1 64 0V832a128 128 0 0 1-128 128H192a128 128 0 0 1-128-128V256a128 128 0 0 1 128-128h288a32 32 0 0 1 0 64H192a64 64 0 0 0-64 64v576a64 64 0 0 0 64 64h576a64 64 0 0 0 64-64V544z m-327.68 21.696a32 32 0 1 1-45.184-45.248L851.52 128H672a32 32 0 0 1 0-64H896a64 64 0 0 1 64 64v224a32 32 0 0 1-64 0V174.08L504.32 565.632z" />
</svg>
);
export const PopOutOutlined = createIcon(Svg, 'PopOutOutlined');
+1
View File
@@ -24,3 +24,4 @@ export * from './copy-clipboard';
export * from './tag-select';
export * from './popover';
export * from './drag-panel';
export * from './context-action';
+6 -1
View File
@@ -97,6 +97,11 @@ export const Popover: React.FC<PopoverProps> = ({
const handleClick = useCallback(
(e: React.MouseEvent) => {
e.preventDefault();
if (visible) {
setVisible(false);
onOpenChange(false);
return;
}
const x = e.clientX;
const y = e.clientY;
setLeft(x);
@@ -104,7 +109,7 @@ export const Popover: React.FC<PopoverProps> = ({
setVisible(true);
onOpenChange(true);
},
[onOpenChange],
[visible, onOpenChange],
);
useEffect(() => {
+13 -2
View File
@@ -14,8 +14,10 @@ const buttonStyle = css`
padding: 0 8px;
.anticon-down {
font-size: 12px !important;
width: 10px;
height: 10px;
margin-left: 4px;
transition: transform 0.1s ease-in;
}
&.size-s {
@@ -57,6 +59,10 @@ const buttonStyle = css`
&:hover {
background-color: rgba(197, 197, 197, 0.16);
.anticon-down {
transform: translateY(2px);
}
}
&.disabled {
@@ -154,6 +160,10 @@ export interface ToggleButtonProps extends Omit<HTMLCoralProps<'button'>, 'type'
* 下拉菜单的属性
*/
dropdownProps?: DropDownProps;
/**
* 是否显示下拉图标
*/
showDropdownIcon?: boolean;
}
/**
@@ -176,6 +186,7 @@ export function ToggleButton(props: ToggleButtonProps) {
overlayStyle,
onItemClick,
dropdownProps = {},
showDropdownIcon = true,
...rest
} = props;
const clazz = cx({
@@ -198,7 +209,7 @@ export function ToggleButton(props: ToggleButtonProps) {
)}
{children}
</Box>
{showDropdown ? <DownOutlined /> : null}
{showDropdown && showDropdownIcon ? <DownOutlined /> : null}
</Button>
);
Binary file not shown.

After

Width:  |  Height:  |  Size: 899 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB