mirror of
https://github.com/NetEase/tango.git
synced 2026-09-01 15:03:03 +08:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 00bf52a411 | |||
| c1306e24e0 | |||
| 28040fc006 | |||
| 3f3981bb9d | |||
| 253ceca969 | |||
| a0ed57ff4f | |||
| f83c6bc0f6 | |||
| bb92210594 | |||
| 163ecedff3 |
@@ -56,7 +56,7 @@ const tangoConfigJson = {
|
|||||||
},
|
},
|
||||||
'@music163/antd': {
|
'@music163/antd': {
|
||||||
description: '云音乐低代码中后台应用基础物料',
|
description: '云音乐低代码中后台应用基础物料',
|
||||||
version: '0.2.5',
|
version: '0.2.6',
|
||||||
library: 'TangoAntd',
|
library: 'TangoAntd',
|
||||||
type: 'baseDependency',
|
type: 'baseDependency',
|
||||||
resources: [
|
resources: [
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,6 +3,10 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [1.1.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)
|
## [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
|
**Note:** Version bump only for package @music163/tango-context
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@music163/tango-context",
|
"name": "@music163/tango-context",
|
||||||
"version": "1.1.1",
|
"version": "1.1.2",
|
||||||
"description": "react context for tango-apps",
|
"description": "react context for tango-apps",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"react",
|
"react",
|
||||||
@@ -31,8 +31,8 @@
|
|||||||
"react": ">= 16.8"
|
"react": ">= 16.8"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@music163/tango-core": "^1.2.0",
|
"@music163/tango-core": "^1.3.0",
|
||||||
"@music163/tango-helpers": "^1.1.1",
|
"@music163/tango-helpers": "^1.2.0",
|
||||||
"mobx-react-lite": "4.0.7"
|
"mobx-react-lite": "4.0.7"
|
||||||
},
|
},
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
|
|||||||
@@ -3,6 +3,16 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
# [1.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)
|
# [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
|
### Bug Fixes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@music163/tango-core",
|
"name": "@music163/tango-core",
|
||||||
"version": "1.2.0",
|
"version": "1.3.0",
|
||||||
"description": "tango core",
|
"description": "tango core",
|
||||||
"author": "wwsun <ww.sun@outlook.com>",
|
"author": "wwsun <ww.sun@outlook.com>",
|
||||||
"homepage": "",
|
"homepage": "",
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
"@babel/parser": "^7.23.5",
|
"@babel/parser": "^7.23.5",
|
||||||
"@babel/traverse": "^7.23.5",
|
"@babel/traverse": "^7.23.5",
|
||||||
"@babel/types": "^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__generator": "^7.6.7",
|
||||||
"@types/babel__traverse": "^7.20.4",
|
"@types/babel__traverse": "^7.20.4",
|
||||||
"mobx": "6.12.3",
|
"mobx": "6.12.3",
|
||||||
|
|||||||
@@ -1425,3 +1425,22 @@ export function traverseFile(ast: t.File) {
|
|||||||
imports,
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -83,6 +83,16 @@ export class Designer {
|
|||||||
*/
|
*/
|
||||||
_addComponentPopoverPosition = { clientX: 0, clientY: 0 };
|
_addComponentPopoverPosition = { clientX: 0, clientY: 0 };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否显示右键菜单
|
||||||
|
*/
|
||||||
|
_showContextMenu = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 右键菜单在 iframe 上的位置
|
||||||
|
*/
|
||||||
|
_contextMenuPosition = { clientX: 0, clientY: 0 };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 是否显示右侧面板
|
* 是否显示右侧面板
|
||||||
*/
|
*/
|
||||||
@@ -136,6 +146,14 @@ export class Designer {
|
|||||||
return this._addComponentPopoverPosition;
|
return this._addComponentPopoverPosition;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get showContextMenu() {
|
||||||
|
return this._showContextMenu;
|
||||||
|
}
|
||||||
|
|
||||||
|
get contextMenuPosition() {
|
||||||
|
return this._contextMenuPosition;
|
||||||
|
}
|
||||||
|
|
||||||
get menuData() {
|
get menuData() {
|
||||||
return this._menuData ?? ([] as MenuDataType);
|
return this._menuData ?? ([] as MenuDataType);
|
||||||
}
|
}
|
||||||
@@ -178,6 +196,8 @@ export class Designer {
|
|||||||
_showRightPanel: observable,
|
_showRightPanel: observable,
|
||||||
_showAddComponentPopover: observable,
|
_showAddComponentPopover: observable,
|
||||||
_addComponentPopoverPosition: observable,
|
_addComponentPopoverPosition: observable,
|
||||||
|
_showContextMenu: observable,
|
||||||
|
_contextMenuPosition: observable,
|
||||||
_menuData: observable,
|
_menuData: observable,
|
||||||
_isPreview: observable,
|
_isPreview: observable,
|
||||||
simulator: computed,
|
simulator: computed,
|
||||||
@@ -189,6 +209,8 @@ export class Designer {
|
|||||||
showSmartWizard: computed,
|
showSmartWizard: computed,
|
||||||
showAddComponentPopover: computed,
|
showAddComponentPopover: computed,
|
||||||
addComponentPopoverPosition: computed,
|
addComponentPopoverPosition: computed,
|
||||||
|
showContextMenu: computed,
|
||||||
|
contextMenuPosition: computed,
|
||||||
menuData: computed,
|
menuData: computed,
|
||||||
setSimulator: action,
|
setSimulator: action,
|
||||||
setViewport: action,
|
setViewport: action,
|
||||||
@@ -199,6 +221,7 @@ export class Designer {
|
|||||||
toggleSmartWizard: action,
|
toggleSmartWizard: action,
|
||||||
toggleIsPreview: action,
|
toggleIsPreview: action,
|
||||||
toggleAddComponentPopover: action,
|
toggleAddComponentPopover: action,
|
||||||
|
toggleContextMenu: action,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,4 +287,15 @@ export class Designer {
|
|||||||
this.workspace.selectSource.clear();
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ export * from './drag-source';
|
|||||||
export * from './history';
|
export * from './history';
|
||||||
export * from './file';
|
export * from './file';
|
||||||
export * from './module';
|
export * from './module';
|
||||||
|
export * from './entry-module';
|
||||||
export * from './route-module';
|
export * from './route-module';
|
||||||
export * from './service-module';
|
export * from './service-module';
|
||||||
export * from './store-module';
|
export * from './store-module';
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { TangoRouteModule } from './route-module';
|
|||||||
import { TangoStoreModule } from './store-module';
|
import { TangoStoreModule } from './store-module';
|
||||||
import { TangoServiceModule } from './service-module';
|
import { TangoServiceModule } from './service-module';
|
||||||
import { IdGenerator } from '../helpers';
|
import { IdGenerator } from '../helpers';
|
||||||
|
import { AppEntryModule } from './entry-module';
|
||||||
|
|
||||||
export interface IViewFile {
|
export interface IViewFile {
|
||||||
readonly workspace: IWorkspace;
|
readonly workspace: IWorkspace;
|
||||||
@@ -152,6 +153,10 @@ export interface IWorkspace {
|
|||||||
* 解析后的 tango.config.json 文件,如果要获取项目配置,推荐使用 projectConfig 获取
|
* 解析后的 tango.config.json 文件,如果要获取项目配置,推荐使用 projectConfig 获取
|
||||||
*/
|
*/
|
||||||
tangoConfigJson: TangoJsonFile;
|
tangoConfigJson: TangoJsonFile;
|
||||||
|
/**
|
||||||
|
* app.js 入口文件解析后的模块
|
||||||
|
*/
|
||||||
|
appEntryModule: AppEntryModule;
|
||||||
/**
|
/**
|
||||||
* 解析后的路由模块
|
* 解析后的路由模块
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import { TangoStoreEntryModule, TangoStoreModule } from './store-module';
|
|||||||
import { TangoServiceModule } from './service-module';
|
import { TangoServiceModule } from './service-module';
|
||||||
import { TangoViewModule } from './view-module';
|
import { TangoViewModule } from './view-module';
|
||||||
import { TangoComponentsEntryModule } from './component-module';
|
import { TangoComponentsEntryModule } from './component-module';
|
||||||
|
import { AppEntryModule } from './entry-module';
|
||||||
|
|
||||||
export interface IWorkspaceOptions {
|
export interface IWorkspaceOptions {
|
||||||
/**
|
/**
|
||||||
@@ -97,6 +98,11 @@ export class Workspace extends EventTarget implements IWorkspace {
|
|||||||
*/
|
*/
|
||||||
activeViewFile: string;
|
activeViewFile: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用入口模块
|
||||||
|
*/
|
||||||
|
appEntryModule: AppEntryModule;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 路由配置模块
|
* 路由配置模块
|
||||||
*/
|
*/
|
||||||
@@ -329,6 +335,9 @@ export class Workspace extends EventTarget implements IWorkspace {
|
|||||||
* @param fileType 模块类型
|
* @param fileType 模块类型
|
||||||
*/
|
*/
|
||||||
addFile(filename: string, code: string, fileType?: FileType) {
|
addFile(filename: string, code: string, fileType?: FileType) {
|
||||||
|
if (!fileType && filename === this.entry) {
|
||||||
|
fileType = FileType.AppEntryModule;
|
||||||
|
}
|
||||||
const moduleType = fileType || inferFileType(filename);
|
const moduleType = fileType || inferFileType(filename);
|
||||||
const props = {
|
const props = {
|
||||||
filename,
|
filename,
|
||||||
@@ -338,6 +347,10 @@ export class Workspace extends EventTarget implements IWorkspace {
|
|||||||
|
|
||||||
let module;
|
let module;
|
||||||
switch (moduleType) {
|
switch (moduleType) {
|
||||||
|
case FileType.AppEntryModule:
|
||||||
|
module = new AppEntryModule(this, props);
|
||||||
|
this.appEntryModule = module;
|
||||||
|
break;
|
||||||
case FileType.StoreEntryModule:
|
case FileType.StoreEntryModule:
|
||||||
module = new TangoStoreEntryModule(this, props);
|
module = new TangoStoreEntryModule(this, props);
|
||||||
this.storeEntryModule = module;
|
this.storeEntryModule = module;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export type SimulatorMode = 'desktop' | 'tablet' | 'phone';
|
|||||||
export enum FileType {
|
export enum FileType {
|
||||||
// js 文件
|
// js 文件
|
||||||
Module = 'module',
|
Module = 'module',
|
||||||
|
AppEntryModule = 'appEntryModule',
|
||||||
StoreEntryModule = 'storeEntryModule',
|
StoreEntryModule = 'storeEntryModule',
|
||||||
RouteModule = 'routeModule',
|
RouteModule = 'routeModule',
|
||||||
ServiceModule = 'serviceModule',
|
ServiceModule = 'serviceModule',
|
||||||
|
|||||||
@@ -3,6 +3,28 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
# [1.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
|
||||||
|
|
||||||
|
- export usePreviewSandboxQuery & add builtin sandboxQuery ([#163](https://github.com/netease/tango/issues/163)) ([163eced](https://github.com/netease/tango/commit/163ecedff3ed7baee59e200a8cc60dcc63a24e48))
|
||||||
|
|
||||||
## [1.2.1](https://github.com/netease/tango/compare/@music163/tango-designer@1.2.0...@music163/tango-designer@1.2.1) (2024-05-22)
|
## [1.2.1](https://github.com/netease/tango/compare/@music163/tango-designer@1.2.0...@music163/tango-designer@1.2.1) (2024-05-22)
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@music163/tango-designer",
|
"name": "@music163/tango-designer",
|
||||||
"version": "1.2.1",
|
"version": "1.3.0",
|
||||||
"description": "lowcode designer",
|
"description": "lowcode designer",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"react"
|
"react"
|
||||||
@@ -33,12 +33,12 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/icons": "^4.8.0",
|
"@ant-design/icons": "^4.8.0",
|
||||||
"@music163/request": "^0.2.0",
|
"@music163/request": "^0.2.0",
|
||||||
"@music163/tango-context": "^1.1.1",
|
"@music163/tango-context": "^1.1.2",
|
||||||
"@music163/tango-core": "^1.2.0",
|
"@music163/tango-core": "^1.3.0",
|
||||||
"@music163/tango-helpers": "^1.1.1",
|
"@music163/tango-helpers": "^1.2.0",
|
||||||
"@music163/tango-sandbox": "^1.0.4",
|
"@music163/tango-sandbox": "^1.0.5",
|
||||||
"@music163/tango-setting-form": "^1.2.2",
|
"@music163/tango-setting-form": "^1.2.5",
|
||||||
"@music163/tango-ui": "^1.2.1",
|
"@music163/tango-ui": "^1.3.0",
|
||||||
"antd": "^4.24.2",
|
"antd": "^4.24.2",
|
||||||
"cash-dom": "^8.1.2",
|
"cash-dom": "^8.1.2",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
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: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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 './drag-box';
|
||||||
export * from './input-kv';
|
export * from './input-kv';
|
||||||
export * from './variable-tree';
|
export * from './variable-tree';
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ export function VariableTree(props: VariableTreeProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box display="flex" columnGap="l" className="VariableTree" css={varTreeStyle} {...rest}>
|
<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)}
|
{renderHeaderExtra?.(props, state)}
|
||||||
<Box mb="m" position="sticky" top="0" bg="white" zIndex={2}>
|
<Box mb="m" position="sticky" top="0" bg="white" zIndex={2}>
|
||||||
<Search placeholder="请输入变量名" onChange={(val) => setKeyword(val?.trim())} />
|
<Search placeholder="请输入变量名" onChange={(val) => setKeyword(val?.trim())} />
|
||||||
@@ -203,7 +203,11 @@ export function VariableTree(props: VariableTreeProps) {
|
|||||||
<Box flex="0 0 72px" textAlign="right">
|
<Box flex="0 0 72px" textAlign="right">
|
||||||
{node.showRemoveButton && (
|
{node.showRemoveButton && (
|
||||||
<Popconfirm
|
<Popconfirm
|
||||||
|
zIndex={99999}
|
||||||
title={`确认删除吗 ${node.title}?该操作会导致引用此模型的代码报错,请谨慎操作!`}
|
title={`确认删除吗 ${node.title}?该操作会导致引用此模型的代码报错,请谨慎操作!`}
|
||||||
|
onCancel={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
onConfirm={(e) => {
|
onConfirm={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (isStoreVariablePath(node.key)) {
|
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>
|
</ValueDetail>
|
||||||
|
|||||||
@@ -84,6 +84,8 @@ export function ValueDefine({ data, onSave = noop }: ValueDefineProps) {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
off();
|
off();
|
||||||
setError('');
|
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>
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -6,10 +6,14 @@ export interface IDesignerContext<S = any> {
|
|||||||
* 沙箱查询实例
|
* 沙箱查询实例
|
||||||
*/
|
*/
|
||||||
sandboxQuery: DndQuery;
|
sandboxQuery: DndQuery;
|
||||||
|
/**
|
||||||
|
* 预览沙箱查询实例
|
||||||
|
*/
|
||||||
|
previewSandboxQuery: DndQuery;
|
||||||
/**
|
/**
|
||||||
* 远程服务
|
* 远程服务
|
||||||
*/
|
*/
|
||||||
remoteServices?: Record<string, S>;
|
remoteServices: Record<string, S>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [DesignerProvider, useDesigner] = createContext<IDesignerContext>({
|
const [DesignerProvider, useDesigner] = createContext<IDesignerContext>({
|
||||||
@@ -19,9 +23,13 @@ const [DesignerProvider, useDesigner] = createContext<IDesignerContext>({
|
|||||||
export { DesignerProvider };
|
export { DesignerProvider };
|
||||||
|
|
||||||
export const useSandboxQuery = () => {
|
export const useSandboxQuery = () => {
|
||||||
return useDesigner()?.sandboxQuery;
|
return useDesigner().sandboxQuery;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const usePreviewSandboxQuery = () => {
|
||||||
|
return useDesigner().previewSandboxQuery;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useRemoteServices = () => {
|
export const useRemoteServices = () => {
|
||||||
return useDesigner()?.remoteServices;
|
return useDesigner().remoteServices;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,8 +5,18 @@ import { TangoEngineProvider, ITangoEngineContext } from '@music163/tango-contex
|
|||||||
import zhCN from 'antd/lib/locale/zh_CN';
|
import zhCN from 'antd/lib/locale/zh_CN';
|
||||||
import { DesignerProvider, IDesignerContext } from './context';
|
import { DesignerProvider, IDesignerContext } from './context';
|
||||||
import defaultTheme from './themes/default';
|
import defaultTheme from './themes/default';
|
||||||
|
import { DndQuery } from './dnd';
|
||||||
|
import { DESIGN_SANDBOX_ID, PREVIEW_SANDBOX_ID } from './helpers';
|
||||||
|
|
||||||
export interface DesignerProps extends IDesignerContext, ITangoEngineContext {
|
const builtinSandboxQuery = new DndQuery({
|
||||||
|
context: `#${DESIGN_SANDBOX_ID}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const builtinPreviewSandboxQuery = new DndQuery({
|
||||||
|
context: `#${PREVIEW_SANDBOX_ID}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface DesignerProps extends Partial<IDesignerContext>, ITangoEngineContext {
|
||||||
/**
|
/**
|
||||||
* 主题包
|
* 主题包
|
||||||
*/
|
*/
|
||||||
@@ -20,13 +30,23 @@ export interface DesignerProps extends IDesignerContext, ITangoEngineContext {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function Designer(props: DesignerProps) {
|
export function Designer(props: DesignerProps) {
|
||||||
const { engine, config, theme: themeProp, sandboxQuery, remoteServices = {}, children } = props;
|
const {
|
||||||
|
engine,
|
||||||
|
config,
|
||||||
|
theme: themeProp,
|
||||||
|
sandboxQuery = builtinSandboxQuery,
|
||||||
|
previewSandboxQuery = builtinPreviewSandboxQuery,
|
||||||
|
remoteServices = {},
|
||||||
|
children,
|
||||||
|
} = props;
|
||||||
const theme = useMemo(() => extendTheme(themeProp, defaultTheme), [themeProp]);
|
const theme = useMemo(() => extendTheme(themeProp, defaultTheme), [themeProp]);
|
||||||
return (
|
return (
|
||||||
<SystemProvider theme={theme} prefix="--tango">
|
<SystemProvider theme={theme} prefix="--tango">
|
||||||
<ConfigProvider locale={zhCN}>
|
<ConfigProvider locale={zhCN}>
|
||||||
<TangoEngineProvider value={{ engine, config }}>
|
<TangoEngineProvider value={{ engine, config }}>
|
||||||
<DesignerProvider value={{ sandboxQuery, remoteServices }}>{children}</DesignerProvider>
|
<DesignerProvider value={{ sandboxQuery, previewSandboxQuery, remoteServices }}>
|
||||||
|
{children}
|
||||||
|
</DesignerProvider>
|
||||||
</TangoEngineProvider>
|
</TangoEngineProvider>
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
</SystemProvider>
|
</SystemProvider>
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ export const DRAGGABLE_SELECTOR = `[${SLOT.dnd}]`;
|
|||||||
interface DndQueryOptions {
|
interface DndQueryOptions {
|
||||||
/**
|
/**
|
||||||
* DOM 查询上下文选择器
|
* DOM 查询上下文选择器
|
||||||
* TODO: 是不是可以合并成一个 API
|
|
||||||
*/
|
*/
|
||||||
context?: string;
|
context?: string;
|
||||||
/**
|
/**
|
||||||
@@ -63,6 +62,9 @@ export class DndQuery {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 沙箱内的 window 对象
|
||||||
|
*/
|
||||||
get window() {
|
get window() {
|
||||||
if (this.context && 'defaultView' in this.context) {
|
if (this.context && 'defaultView' in this.context) {
|
||||||
return (this.context as unknown as Document).defaultView;
|
return (this.context as unknown as Document).defaultView;
|
||||||
@@ -71,6 +73,9 @@ export class DndQuery {
|
|||||||
return window;
|
return window;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 沙箱内的全局滚动偏移
|
||||||
|
*/
|
||||||
get scrollTop() {
|
get scrollTop() {
|
||||||
if (this.context && 'documentElement' in this.context) {
|
if (this.context && 'documentElement' in this.context) {
|
||||||
return (this.context as unknown as Document).documentElement.scrollTop;
|
return (this.context as unknown as Document).documentElement.scrollTop;
|
||||||
|
|||||||
@@ -77,6 +77,9 @@ export function useDnd({
|
|||||||
if (designer.isPreview) {
|
if (designer.isPreview) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (designer.showContextMenu) {
|
||||||
|
designer.toggleContextMenu(false);
|
||||||
|
}
|
||||||
|
|
||||||
const point = sandboxQuery.getRelativePoint({ x: e.clientX, y: e.clientY });
|
const point = sandboxQuery.getRelativePoint({ x: e.clientX, y: e.clientY });
|
||||||
selectSource.setStart({
|
selectSource.setStart({
|
||||||
@@ -128,6 +131,9 @@ export function useDnd({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onClick = (e: React.MouseEvent) => {
|
const onClick = (e: React.MouseEvent) => {
|
||||||
|
if (designer.showContextMenu) {
|
||||||
|
designer.toggleContextMenu(false);
|
||||||
|
}
|
||||||
const data = sandboxQuery.getDraggableParentsData(e.target as HTMLElement, true);
|
const data = sandboxQuery.getDraggableParentsData(e.target as HTMLElement, true);
|
||||||
if (data && data.id) {
|
if (data && data.id) {
|
||||||
selectSource.select(data);
|
selectSource.select(data);
|
||||||
@@ -388,6 +394,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 onTango = (e: CustomEvent) => {
|
||||||
const detail = e.detail || {};
|
const detail = e.detail || {};
|
||||||
|
|
||||||
@@ -437,6 +493,7 @@ export function useDnd({
|
|||||||
onDragEnd,
|
onDragEnd,
|
||||||
onScroll,
|
onScroll,
|
||||||
onKeyDown,
|
onKeyDown,
|
||||||
|
onContextMenu,
|
||||||
onTango,
|
onTango,
|
||||||
...selectHandler,
|
...selectHandler,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,6 +7,20 @@ import {
|
|||||||
parseDndId,
|
parseDndId,
|
||||||
} from '@music163/tango-helpers';
|
} from '@music163/tango-helpers';
|
||||||
|
|
||||||
|
// -----------
|
||||||
|
// CONSTANTS
|
||||||
|
// -----------
|
||||||
|
|
||||||
|
export const DRAG_GHOST_ID = 'dragGhost';
|
||||||
|
|
||||||
|
export const DESIGN_SANDBOX_ID = 'sandbox-container';
|
||||||
|
|
||||||
|
export const PREVIEW_SANDBOX_ID = 'preview-sandbox-container';
|
||||||
|
|
||||||
|
// -----------
|
||||||
|
// DOM Helpers
|
||||||
|
// -----------
|
||||||
|
|
||||||
export function buildQueryBySlotId(id: string) {
|
export function buildQueryBySlotId(id: string) {
|
||||||
return `[${SLOT.dnd}="${id}"]`;
|
return `[${SLOT.dnd}="${id}"]`;
|
||||||
}
|
}
|
||||||
@@ -15,8 +29,6 @@ export function getElement(selector: Selector) {
|
|||||||
return $(selector).get(0);
|
return $(selector).get(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DRAG_GHOST_ID = 'dragGhost';
|
|
||||||
|
|
||||||
export const getDragGhostElement = () => getElement(`#${DRAG_GHOST_ID}`);
|
export const getDragGhostElement = () => getElement(`#${DRAG_GHOST_ID}`);
|
||||||
|
|
||||||
export function setElementStyle(selector: Selector, style: any) {
|
export function setElementStyle(selector: Selector, style: any) {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { useSandboxQuery } from '../context';
|
|||||||
import { DndQuery, useDnd } from '../dnd';
|
import { DndQuery, useDnd } from '../dnd';
|
||||||
import { Navigator } from './navigator';
|
import { Navigator } from './navigator';
|
||||||
import { SelectionToolsProps } from '../simulator/selection';
|
import { SelectionToolsProps } from '../simulator/selection';
|
||||||
|
import { DESIGN_SANDBOX_ID, PREVIEW_SANDBOX_ID } from '../helpers';
|
||||||
|
|
||||||
interface ISandboxEventHandlerConfig {
|
interface ISandboxEventHandlerConfig {
|
||||||
sandboxQuery?: DndQuery;
|
sandboxQuery?: DndQuery;
|
||||||
@@ -48,6 +49,10 @@ export interface CombinedSandboxRef {
|
|||||||
|
|
||||||
const LANDING_PAGE_PATH = '/__background_landing_page__';
|
const LANDING_PAGE_PATH = '/__background_landing_page__';
|
||||||
|
|
||||||
|
function fixRouterMode(routerType: string): CodeSandboxProps['routerMode'] {
|
||||||
|
return routerType === 'hash' ? 'hash' : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
function useSandbox({
|
function useSandbox({
|
||||||
isPreview: isPreviewProp,
|
isPreview: isPreviewProp,
|
||||||
configFormatter,
|
configFormatter,
|
||||||
@@ -62,6 +67,7 @@ function useSandbox({
|
|||||||
const workspace = useWorkspace();
|
const workspace = useWorkspace();
|
||||||
const designer = useDesigner();
|
const designer = useDesigner();
|
||||||
const sandboxQuery = useSandboxQuery();
|
const sandboxQuery = useSandboxQuery();
|
||||||
|
|
||||||
const isPreview = isPreviewProp ?? designer.isPreview;
|
const isPreview = isPreviewProp ?? designer.isPreview;
|
||||||
|
|
||||||
// 组件不一定会立即刷新,因此 isActive 需要实时获取
|
// 组件不一定会立即刷新,因此 isActive 需要实时获取
|
||||||
@@ -103,23 +109,26 @@ function useSandbox({
|
|||||||
// 根据当前 workspace 状态与组件传入的状态是否一致,控制是否需要切换到空白路由
|
// 根据当前 workspace 状态与组件传入的状态是否一致,控制是否需要切换到空白路由
|
||||||
const display = isActive ? 'block' : 'none';
|
const display = isActive ? 'block' : 'none';
|
||||||
const routePath = isActive ? startRoute || workspace.activeRoute : LANDING_PAGE_PATH;
|
const routePath = isActive ? startRoute || workspace.activeRoute : LANDING_PAGE_PATH;
|
||||||
|
const routerMode = fixRouterMode(workspace.appEntryModule?.routerType);
|
||||||
|
|
||||||
const sandboxProps = isPreview
|
const sandboxProps = isPreview
|
||||||
? {
|
? {
|
||||||
files,
|
files,
|
||||||
eventHandlers: pick(dndHandlers, ['onTango']),
|
eventHandlers: pick(dndHandlers, ['onTango']),
|
||||||
onMessage,
|
onMessage,
|
||||||
|
onLoad,
|
||||||
display,
|
display,
|
||||||
startRoute: routePath,
|
startRoute: routePath,
|
||||||
onLoad,
|
routerMode,
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
files,
|
files,
|
||||||
eventHandlers: dndHandlers as any,
|
eventHandlers: dndHandlers as any,
|
||||||
onMessage,
|
onMessage,
|
||||||
|
onLoad,
|
||||||
display,
|
display,
|
||||||
startRoute: routePath,
|
startRoute: routePath,
|
||||||
onLoad,
|
routerMode,
|
||||||
};
|
};
|
||||||
|
|
||||||
return sandboxProps;
|
return sandboxProps;
|
||||||
@@ -140,7 +149,7 @@ const PreviewSandbox = observer(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box display={display} width="100%" height="100%">
|
<Box display={display} width="100%" height="100%">
|
||||||
<CodeSandbox ref={ref} iframeId="preview-sandbox-container" {...sandboxProps} {...rest} />
|
<CodeSandbox ref={ref} iframeId={PREVIEW_SANDBOX_ID} {...sandboxProps} {...rest} />
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -188,7 +197,7 @@ const DesignSandbox = observer(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box display={display} width="100%" height="100%">
|
<Box display={display} width="100%" height="100%">
|
||||||
<CodeSandbox ref={ref} {...sandboxProps} {...rest} />
|
<CodeSandbox ref={ref} iframeId={DESIGN_SANDBOX_ID} {...sandboxProps} {...rest} />
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export * from './copy-node';
|
export * from './copy-node';
|
||||||
export * from './delete-node';
|
export * from './delete-node';
|
||||||
|
export * from './more-actions';
|
||||||
export * from './select-parent-node';
|
export * from './select-parent-node';
|
||||||
export * from './view-source';
|
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>
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -8,6 +8,7 @@ import { DropMethod, ITangoViewNodeData } from '@music163/tango-core';
|
|||||||
import { noop, parseDndId } from '@music163/tango-helpers';
|
import { noop, parseDndId } from '@music163/tango-helpers';
|
||||||
import { useSandboxQuery } from '../../context';
|
import { useSandboxQuery } from '../../context';
|
||||||
import { buildQueryBySlotId } from '../../helpers';
|
import { buildQueryBySlotId } from '../../helpers';
|
||||||
|
import { ContextMenu } from '../../components';
|
||||||
|
|
||||||
export interface ComponentsTreeProps {
|
export interface ComponentsTreeProps {
|
||||||
/**
|
/**
|
||||||
@@ -143,6 +144,18 @@ export const ComponentsTree: React.FC<ComponentsTreeProps> = observer(
|
|||||||
const file = workspace.activeViewModule;
|
const file = workspace.activeViewModule;
|
||||||
const nodesTree = (file?.nodesTree ?? []) as ITangoViewNodeData[];
|
const nodesTree = (file?.nodesTree ?? []) as ITangoViewNodeData[];
|
||||||
const [expandedKeys, setExpandedKeys] = useState(getNodeKeys(nodesTree));
|
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(() => {
|
useEffect(() => {
|
||||||
setSelectedKeys(workspace.selectSource.selected.map((item) => item.id));
|
setSelectedKeys(workspace.selectSource.selected.map((item) => item.id));
|
||||||
@@ -164,64 +177,83 @@ export const ComponentsTree: React.FC<ComponentsTreeProps> = observer(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box css={outlineStyle}>
|
<Box css={outlineStyle}>
|
||||||
<Tree
|
<Dropdown
|
||||||
selectedKeys={selectedKeys as string[]}
|
placement="bottomLeft"
|
||||||
fieldNames={filedNames}
|
trigger={['contextMenu']}
|
||||||
treeData={nodesTree as any[]}
|
open={contextMenuOpen}
|
||||||
onSelect={(keys) => {
|
onOpenChange={(val) => {
|
||||||
const slotKey = keys?.[0] as string;
|
if (!val) {
|
||||||
const data = sandboxQuery.getDraggableParentsData(buildQueryBySlotId(slotKey), true);
|
setContextMenuOpen(val);
|
||||||
if (data && data.id) {
|
|
||||||
workspace.selectSource.select(data);
|
|
||||||
}
|
}
|
||||||
// export selected
|
|
||||||
onSelect(slotKey);
|
|
||||||
setSelectedKeys(keys);
|
|
||||||
}}
|
}}
|
||||||
blockNode
|
overlay={<ContextMenu onClick={() => setContextMenuOpen(false)} showParents={false} />}
|
||||||
expandedKeys={expandedKeys}
|
>
|
||||||
onExpand={(keys) => setExpandedKeys(keys as string[])}
|
<Tree
|
||||||
draggable
|
selectedKeys={selectedKeys as string[]}
|
||||||
onDragStart={(data) => {
|
fieldNames={filedNames}
|
||||||
const prototype = workspace.componentPrototypes.get(data.node.component);
|
treeData={nodesTree as any[]}
|
||||||
if (!prototype) {
|
onSelect={handleSelect}
|
||||||
return;
|
blockNode
|
||||||
}
|
expandedKeys={expandedKeys}
|
||||||
const { canDrag } = prototype.rules || {};
|
onExpand={(keys) => setExpandedKeys(keys as string[])}
|
||||||
if (canDrag && !canDrag()) {
|
draggable
|
||||||
return;
|
onDragStart={(data) => {
|
||||||
}
|
const prototype = workspace.componentPrototypes.get(data.node.component);
|
||||||
workspace.dragSource.set({
|
if (!prototype) {
|
||||||
id: data.node.id,
|
return;
|
||||||
name: data.node.component,
|
}
|
||||||
});
|
const { canDrag } = prototype.rules || {};
|
||||||
}}
|
if (canDrag && !canDrag()) {
|
||||||
onDrop={(data) => {
|
return;
|
||||||
const dropKey = data.node.key as string;
|
}
|
||||||
let method;
|
workspace.dragSource.set({
|
||||||
if (data.dropToGap) {
|
id: data.node.id,
|
||||||
// 插入节点的后面
|
name: data.node.component,
|
||||||
method = DropMethod.InsertAfter;
|
});
|
||||||
} else {
|
}}
|
||||||
// 作为第一个子节点
|
onDrop={(data) => {
|
||||||
method = DropMethod.InsertFirstChild;
|
const dropKey = data.node.key as string;
|
||||||
}
|
let method;
|
||||||
workspace.dragSource.dropTarget.set(
|
if (data.dropToGap) {
|
||||||
{
|
// 插入节点的后面
|
||||||
id: dropKey,
|
method = DropMethod.InsertAfter;
|
||||||
},
|
} else {
|
||||||
method,
|
// 作为第一个子节点
|
||||||
);
|
method = DropMethod.InsertFirstChild;
|
||||||
workspace.dropNode();
|
}
|
||||||
}}
|
workspace.dragSource.dropTarget.set(
|
||||||
titleRender={(node) => (
|
{
|
||||||
<OutlineTreeNode
|
id: dropKey,
|
||||||
actionItems={actionItems}
|
},
|
||||||
showToggleVisibleIcon={showToggleVisibleIcon}
|
method,
|
||||||
node={node}
|
);
|
||||||
/>
|
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>
|
</Box>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export const VariablePanel = observer(
|
|||||||
新建模型
|
新建模型
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
bodyProps={{ p: 'm' }}
|
bodyProps={{ p: 'm', height: '100%' }}
|
||||||
>
|
>
|
||||||
{isAdd ? (
|
{isAdd ? (
|
||||||
<AddStoreForm
|
<AddStoreForm
|
||||||
@@ -42,6 +42,7 @@ export const VariablePanel = observer(
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<VariableTree
|
<VariableTree
|
||||||
|
height="100%"
|
||||||
defaultValueDetailMode="define"
|
defaultValueDetailMode="define"
|
||||||
dataSource={storeVariables}
|
dataSource={storeVariables}
|
||||||
onAddStoreVariable={(storeName, data) => {
|
onAddStoreVariable={(storeName, data) => {
|
||||||
|
|||||||
@@ -25,7 +25,13 @@ export interface SelectionToolsProps {
|
|||||||
|
|
||||||
export const SelectionTools = observer(
|
export const SelectionTools = observer(
|
||||||
({
|
({
|
||||||
actions: actionsProp = ['selectParentNode', 'viewSource', 'copyNode', 'deleteNode'],
|
actions: actionsProp = [
|
||||||
|
'selectParentNode',
|
||||||
|
'viewSource',
|
||||||
|
'copyNode',
|
||||||
|
'deleteNode',
|
||||||
|
'moreActions',
|
||||||
|
],
|
||||||
}: SelectionToolsProps) => {
|
}: SelectionToolsProps) => {
|
||||||
const workspace = useWorkspace();
|
const workspace = useWorkspace();
|
||||||
const selectSource = workspace.selectSource;
|
const selectSource = workspace.selectSource;
|
||||||
|
|||||||
@@ -1,20 +1,61 @@
|
|||||||
import React, { useEffect, useRef } from 'react';
|
import React, { useEffect, useRef } from 'react';
|
||||||
import cx from 'classnames';
|
import cx from 'classnames';
|
||||||
import { Box, HTMLCoralProps } from 'coral-system';
|
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 { Ghost } from './ghost';
|
||||||
import { useSandboxQuery } from '../context';
|
import { useSandboxQuery } from '../context';
|
||||||
import { SelectionTools, SelectionToolsProps } from './selection';
|
import { SelectionTools, SelectionToolsProps } from './selection';
|
||||||
import { InsertionPrompt } from './insertion';
|
import { InsertionPrompt } from './insertion';
|
||||||
import { DraggingMask } from './mask';
|
import { DraggingMask } from './mask';
|
||||||
|
import { Dropdown, DropdownProps } from 'antd';
|
||||||
|
import { ContextMenu } from '../components';
|
||||||
|
|
||||||
export interface ViewportProps extends HTMLCoralProps<'div'> {
|
export interface ViewportProps extends HTMLCoralProps<'div'> {
|
||||||
selectionTools?: SelectionToolsProps['actions'];
|
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) {
|
export function Viewport({ selectionTools, children, className, ...rest }: ViewportProps) {
|
||||||
return (
|
return (
|
||||||
<Box position="relative" height="100%" className={cx('Viewport', className)} {...rest}>
|
<Box position="relative" height="100%" className={cx('Viewport', className)} {...rest}>
|
||||||
|
<ViewportContextMenu />
|
||||||
<ViewportBody>{children}</ViewportBody>
|
<ViewportBody>{children}</ViewportBody>
|
||||||
<div className="AuxTools">
|
<div className="AuxTools">
|
||||||
<SelectionTools actions={selectionTools} />
|
<SelectionTools actions={selectionTools} />
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export const PreviewTool = observer(() => {
|
|||||||
designer.setActiveView('design');
|
designer.setActiveView('design');
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
tooltip="预览"
|
tooltip={designer.isPreview ? '切换到设计模式' : '切换到预览模式'}
|
||||||
>
|
>
|
||||||
<EyeOutlined />
|
<EyeOutlined />
|
||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ export function Toolbar({ children }: ToolbarProps) {
|
|||||||
<Group display="flex" alignItems="center" gap="m">
|
<Group display="flex" alignItems="center" gap="m">
|
||||||
{centerTools}
|
{centerTools}
|
||||||
</Group>
|
</Group>
|
||||||
<Group display="flex" alignItems="center" gap="m">
|
<Group display="flex" alignItems="center" justifyContent="flex-end" gap="m">
|
||||||
{rightTools}
|
{rightTools}
|
||||||
</Group>
|
</Group>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -18,14 +18,21 @@ import {
|
|||||||
DeleteNodeAction,
|
DeleteNodeAction,
|
||||||
ViewSourceAction,
|
ViewSourceAction,
|
||||||
SelectParentNodeAction,
|
SelectParentNodeAction,
|
||||||
|
MoreActionsAction,
|
||||||
} from './selection-menu';
|
} from './selection-menu';
|
||||||
|
import {
|
||||||
|
CopyNodeContextAction,
|
||||||
|
DeleteNodeContextAction,
|
||||||
|
PasteNodeContextAction,
|
||||||
|
ViewSourceContextAction,
|
||||||
|
} from './context-menu';
|
||||||
|
|
||||||
const widgets = {};
|
const widgets = {};
|
||||||
|
|
||||||
export function registerWidget(key: string, component: React.ComponentType<any>) {
|
export function registerWidget(key: string, component: React.ComponentType<any>) {
|
||||||
if (!/^(toolbar|sidebar|selectionMenu)\.[a-zA-z]+$/.test(key)) {
|
if (!/^(toolbar|sidebar|selectionMenu|contextMenu)\.[a-zA-z]+$/.test(key)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Invalid widget key(${key}), should start with toolbar, sidebar, or selection-menu`,
|
`Invalid widget key(${key}), should start with toolbar, sidebar, contextMenu or selectionMenu`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
setValue(widgets, key, component);
|
setValue(widgets, key, component);
|
||||||
@@ -55,3 +62,9 @@ registerWidget('selectionMenu.copyNode', CopyNodeAction);
|
|||||||
registerWidget('selectionMenu.deleteNode', DeleteNodeAction);
|
registerWidget('selectionMenu.deleteNode', DeleteNodeAction);
|
||||||
registerWidget('selectionMenu.selectParentNode', SelectParentNodeAction);
|
registerWidget('selectionMenu.selectParentNode', SelectParentNodeAction);
|
||||||
registerWidget('selectionMenu.viewSource', ViewSourceAction);
|
registerWidget('selectionMenu.viewSource', ViewSourceAction);
|
||||||
|
registerWidget('selectionMenu.moreActions', MoreActionsAction);
|
||||||
|
|
||||||
|
registerWidget('contextMenu.viewSource', ViewSourceContextAction);
|
||||||
|
registerWidget('contextMenu.copyNode', CopyNodeContextAction);
|
||||||
|
registerWidget('contextMenu.pasteNode', PasteNodeContextAction);
|
||||||
|
registerWidget('contextMenu.deleteNode', DeleteNodeContextAction);
|
||||||
|
|||||||
@@ -3,6 +3,12 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
# [1.2.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)
|
## [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
|
### Bug Fixes
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@music163/tango-helpers",
|
"name": "@music163/tango-helpers",
|
||||||
"version": "1.1.1",
|
"version": "1.2.0",
|
||||||
"description": "Shared types, helpers, and hooks of tango-apps",
|
"description": "Shared types, helpers, and hooks of tango-apps",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"shared",
|
"shared",
|
||||||
|
|||||||
@@ -68,3 +68,10 @@ export function isServiceVariablePath(key: string) {
|
|||||||
export function isInTangoDesignMode() {
|
export function isInTangoDesignMode() {
|
||||||
return !!(window as any).__TANGO_DESIGNER__;
|
return !!(window as any).__TANGO_DESIGNER__;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否是 macOS 或 iOS like 设备
|
||||||
|
*/
|
||||||
|
export function isApplePlatform() {
|
||||||
|
return /Mac|iPhone|iPad|iPod/i.test(navigator.platform);
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,12 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [1.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)
|
## [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
|
**Note:** Version bump only for package @music163/tango-sandbox
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@music163/tango-sandbox",
|
"name": "@music163/tango-sandbox",
|
||||||
"version": "1.0.4",
|
"version": "1.0.5",
|
||||||
"description": "sandbox of tango apps",
|
"description": "sandbox of tango apps",
|
||||||
"author": "wwsun <ww.sun@outlook.com>",
|
"author": "wwsun <ww.sun@outlook.com>",
|
||||||
"homepage": "",
|
"homepage": "",
|
||||||
@@ -29,8 +29,8 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/icons": "^4.8.0",
|
"@ant-design/icons": "^4.8.0",
|
||||||
"@music163/tango-core": "^1.2.0",
|
"@music163/tango-core": "^1.3.0",
|
||||||
"@music163/tango-helpers": "^1.1.1",
|
"@music163/tango-helpers": "^1.2.0",
|
||||||
"crypto-js": "^4.1.1",
|
"crypto-js": "^4.1.1",
|
||||||
"lodash.isequal": "4.5.0",
|
"lodash.isequal": "4.5.0",
|
||||||
"react-frame-component": "^5.2.4"
|
"react-frame-component": "^5.2.4"
|
||||||
|
|||||||
@@ -3,7 +3,13 @@ import { TangoEventName } from '@music163/tango-helpers';
|
|||||||
import Loading from './loading';
|
import Loading from './loading';
|
||||||
import Manager from './manager';
|
import Manager from './manager';
|
||||||
import { createMissingPackageJSON, changeRoute } from './helper';
|
import { createMissingPackageJSON, changeRoute } from './helper';
|
||||||
import { IFiles, CodeSandboxState, CodeSandboxProps, UnsubscribeFunction } from '../types';
|
import {
|
||||||
|
IFiles,
|
||||||
|
CodeSandboxState,
|
||||||
|
CodeSandboxProps,
|
||||||
|
UnsubscribeFunction,
|
||||||
|
EventHandlers,
|
||||||
|
} from '../types';
|
||||||
|
|
||||||
// CodeSandbox 使用示例
|
// CodeSandbox 使用示例
|
||||||
// <CodeSandbox files={files} entry={entry} template={template} bundlerURL={`${window.location.protocol}//codesandbox.fn.netease.com/`} />
|
// <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;
|
iframe: HTMLIFrameElement;
|
||||||
|
|
||||||
|
private eventHandlers: EventHandlers;
|
||||||
|
|
||||||
constructor(props: CodeSandboxProps) {
|
constructor(props: CodeSandboxProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
@@ -75,11 +83,8 @@ export class CodeSandbox extends React.Component<CodeSandboxProps, CodeSandboxSt
|
|||||||
this.manager.cleanup();
|
this.manager.cleanup();
|
||||||
|
|
||||||
// clear tango event
|
// clear tango event
|
||||||
if (this.iframe && this.props?.eventHandlers?.onTango) {
|
if (this.iframe && this.eventHandlers) {
|
||||||
this.iframe.contentDocument?.removeEventListener(
|
this.unbindEvents(this.eventHandlers);
|
||||||
TangoEventName.DesignerAction,
|
|
||||||
this.props.eventHandlers.onTango,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,6 +121,29 @@ export class CodeSandbox extends React.Component<CodeSandboxProps, CodeSandboxSt
|
|||||||
externalResources: this.props.externalResources,
|
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 实例化,即开始构建页面
|
// Manager 实例化,即开始构建页面
|
||||||
// 将 el 传给 Manager 作为实例化参数,当构建完成后,会将构建的页面 dom 设置给 el
|
// 将 el 传给 Manager 作为实例化参数,当构建完成后,会将构建的页面 dom 设置给 el
|
||||||
// 注:Manager是一个管理者的角色,从大局上把控整个转译和执行的流程
|
// 注:Manager是一个管理者的角色,从大局上把控整个转译和执行的流程
|
||||||
@@ -155,17 +183,11 @@ export class CodeSandbox extends React.Component<CodeSandboxProps, CodeSandboxSt
|
|||||||
|
|
||||||
// 注册监听函数
|
// 注册监听函数
|
||||||
const { eventHandlers } = this.props;
|
const { eventHandlers } = this.props;
|
||||||
Object.keys(eventHandlers).forEach((eventType) => {
|
if (eventHandlers) {
|
||||||
if (el.contentDocument) {
|
this.bindEvents(eventHandlers);
|
||||||
el.contentDocument[eventType.toLowerCase()] = eventHandlers[eventType];
|
// 保存一份当前的 eventHandlers,避免 props 变更导致无法取消现在已注册的事件绑定
|
||||||
}
|
this.eventHandlers = eventHandlers;
|
||||||
if (eventType === 'onTango') {
|
}
|
||||||
el.contentDocument?.addEventListener(
|
|
||||||
TangoEventName.DesignerAction,
|
|
||||||
eventHandlers[eventType],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,6 +3,18 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
## [1.2.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.2.2](https://github.com/netease/tango/compare/@music163/tango-setting-form@1.2.1...@music163/tango-setting-form@1.2.2) (2024-05-22)
|
## [1.2.2](https://github.com/netease/tango/compare/@music163/tango-setting-form@1.2.1...@music163/tango-setting-form@1.2.2) (2024-05-22)
|
||||||
|
|
||||||
**Note:** Version bump only for package @music163/tango-setting-form
|
**Note:** Version bump only for package @music163/tango-setting-form
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@music163/tango-setting-form",
|
"name": "@music163/tango-setting-form",
|
||||||
"version": "1.2.2",
|
"version": "1.2.5",
|
||||||
"description": "setting form of tango-apps",
|
"description": "setting form of tango-apps",
|
||||||
"author": "wwsun <ww.sun@outlook.com>",
|
"author": "wwsun <ww.sun@outlook.com>",
|
||||||
"homepage": "",
|
"homepage": "",
|
||||||
@@ -28,9 +28,9 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/icons": "^4.8.0",
|
"@ant-design/icons": "^4.8.0",
|
||||||
"@music163/tango-core": "^1.2.0",
|
"@music163/tango-core": "^1.3.0",
|
||||||
"@music163/tango-helpers": "^1.1.1",
|
"@music163/tango-helpers": "^1.2.0",
|
||||||
"@music163/tango-ui": "^1.2.1",
|
"@music163/tango-ui": "^1.3.0",
|
||||||
"antd": "^4.24.2",
|
"antd": "^4.24.2",
|
||||||
"coral-system": "^1.0.5",
|
"coral-system": "^1.0.5",
|
||||||
"mobx": "6.12.3",
|
"mobx": "6.12.3",
|
||||||
|
|||||||
@@ -3,6 +3,24 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||||
|
|
||||||
|
# [1.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
|
||||||
|
|
||||||
|
- export usePreviewSandboxQuery & add builtin sandboxQuery ([#163](https://github.com/netease/tango/issues/163)) ([163eced](https://github.com/netease/tango/commit/163ecedff3ed7baee59e200a8cc60dcc63a24e48))
|
||||||
|
|
||||||
## [1.2.1](https://github.com/netease/tango/compare/@music163/tango-ui@1.2.0...@music163/tango-ui@1.2.1) (2024-05-22)
|
## [1.2.1](https://github.com/netease/tango/compare/@music163/tango-ui@1.2.0...@music163/tango-ui@1.2.1) (2024-05-22)
|
||||||
|
|
||||||
**Note:** Version bump only for package @music163/tango-ui
|
**Note:** Version bump only for package @music163/tango-ui
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@music163/tango-ui",
|
"name": "@music163/tango-ui",
|
||||||
"version": "1.2.1",
|
"version": "1.3.0",
|
||||||
"description": "ui widgets of tango",
|
"description": "ui widgets of tango",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"react",
|
"react",
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
"@codemirror/lang-javascript": "^6.2.2",
|
"@codemirror/lang-javascript": "^6.2.2",
|
||||||
"@codemirror/lint": "^6.7.1",
|
"@codemirror/lint": "^6.7.1",
|
||||||
"@codemirror/search": "^6.5.6",
|
"@codemirror/search": "^6.5.6",
|
||||||
"@music163/tango-helpers": "^1.1.1",
|
"@music163/tango-helpers": "^1.2.0",
|
||||||
"@uiw/react-codemirror": "^4.22.0",
|
"@uiw/react-codemirror": "^4.22.0",
|
||||||
"antd": "^4.24.2",
|
"antd": "^4.24.2",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ const actionStyle = css`
|
|||||||
border-radius: var(--tango-radii-s);
|
border-radius: var(--tango-radii-s);
|
||||||
color: var(--tango-colors-text2);
|
color: var(--tango-colors-text2);
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--tango-colors-fill2);
|
background-color: var(--tango-colors-fill2);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -5,6 +5,24 @@ import Draggable from 'react-draggable';
|
|||||||
import { CloseOutlined } from '@ant-design/icons';
|
import { CloseOutlined } from '@ant-design/icons';
|
||||||
import { noop } from '@music163/tango-helpers';
|
import { noop } from '@music163/tango-helpers';
|
||||||
|
|
||||||
|
// 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)`
|
const CloseIcon = styled(CloseOutlined)`
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
@@ -17,7 +35,7 @@ const CloseIcon = styled(CloseOutlined)`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
interface DragPanelProps extends Omit<PopoverProps, 'overlay'> {
|
interface DragPanelProps extends Omit<PopoverProps, 'overlay' | 'open'> {
|
||||||
// 标题
|
// 标题
|
||||||
title?: React.ReactNode | string;
|
title?: React.ReactNode | string;
|
||||||
// 内容
|
// 内容
|
||||||
@@ -59,7 +77,12 @@ export function DragPanel({
|
|||||||
onOpenChange(innerOpen);
|
onOpenChange(innerOpen);
|
||||||
}}
|
}}
|
||||||
overlay={
|
overlay={
|
||||||
<Draggable handle=".selection-drag-bar">
|
<Draggable
|
||||||
|
handle=".selection-drag-bar"
|
||||||
|
onStart={() => {
|
||||||
|
injectStyleToBody();
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Box
|
<Box
|
||||||
bg="#FFF"
|
bg="#FFF"
|
||||||
borderRadius="m"
|
borderRadius="m"
|
||||||
|
|||||||
@@ -24,3 +24,4 @@ export * from './copy-clipboard';
|
|||||||
export * from './tag-select';
|
export * from './tag-select';
|
||||||
export * from './popover';
|
export * from './popover';
|
||||||
export * from './drag-panel';
|
export * from './drag-panel';
|
||||||
|
export * from './context-action';
|
||||||
|
|||||||
@@ -97,6 +97,11 @@ export const Popover: React.FC<PopoverProps> = ({
|
|||||||
const handleClick = useCallback(
|
const handleClick = useCallback(
|
||||||
(e: React.MouseEvent) => {
|
(e: React.MouseEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
if (visible) {
|
||||||
|
setVisible(false);
|
||||||
|
onOpenChange(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const x = e.clientX;
|
const x = e.clientX;
|
||||||
const y = e.clientY;
|
const y = e.clientY;
|
||||||
setLeft(x);
|
setLeft(x);
|
||||||
@@ -104,7 +109,7 @@ export const Popover: React.FC<PopoverProps> = ({
|
|||||||
setVisible(true);
|
setVisible(true);
|
||||||
onOpenChange(true);
|
onOpenChange(true);
|
||||||
},
|
},
|
||||||
[onOpenChange],
|
[visible, onOpenChange],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ const buttonStyle = css`
|
|||||||
background-color: var(--tango-colors-custom-toolbarButtonBg, rgba(223, 223, 223, 0.08));
|
background-color: var(--tango-colors-custom-toolbarButtonBg, rgba(223, 223, 223, 0.08));
|
||||||
color: var(--tango-colors-custom-toolbarButtonTextColor, #999);
|
color: var(--tango-colors-custom-toolbarButtonTextColor, #999);
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: var(--tango-radii-m);
|
border-radius: var(--tango-radii-s);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: var(--tango-colors-custom-toolbarButtonTextColorHover, #fff);
|
color: var(--tango-colors-custom-toolbarButtonTextColorHover, #fff);
|
||||||
|
|||||||
Reference in New Issue
Block a user