Compare commits

...

16 Commits

Author SHA1 Message Date
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
Wells bb92210594 chore(release): publish
- @music163/tango-designer@1.2.2
 - @music163/tango-setting-form@1.2.3
 - @music163/tango-ui@1.2.2
2024-05-27 10:18:03 +08:00
Wells 163ecedff3 fix: export usePreviewSandboxQuery & add builtin sandboxQuery (#163)
* fix: export usePreviewSandboxQuery

* fix: update ui

* fix: update ui
2024-05-24 18:28:04 +08:00
Wells 22a2e9bdd1 chore(release): publish
- @music163/tango-designer@1.2.1
 - @music163/tango-setting-form@1.2.2
 - @music163/tango-ui@1.2.1
2024-05-22 14:53:57 +08:00
Wells 696f82bd26 style: update 2024-05-22 14:52:44 +08:00
BoBoooooo a0484e8f64 fix: check local component prototypes (#160)
* fix: prototype undefined bug

* fix: logic bug

* fix: components grid item ui

* fix: ui bug

* fix: update local file
2024-05-22 11:56:52 +08:00
Wells cf31ef19f4 chore(release): publish
- @music163/tango-context@1.1.1
 - @music163/tango-core@1.2.0
 - @music163/tango-designer@1.2.0
 - @music163/tango-helpers@1.1.1
 - @music163/tango-sandbox@1.0.4
 - @music163/tango-setting-form@1.2.1
 - @music163/tango-ui@1.2.0
2024-05-21 15:06:42 +08:00
864907600cc fe31246483 feat: add select parent node of selected node (#158)
* feat: add select parent node of selected node

* feat: move select parent node to SelectSource

* fix: change icon & order

---------

Co-authored-by: ccloli <8115912+ccloli@users.noreply.github.com>
2024-05-20 20:15:28 +08:00
BoBoooooo 9daf3872e7 feat: event-setter & expression-setter use popover (#159) 2024-05-20 16:57:37 +08:00
BoBoooooo f17ccbb7f6 fix: support add components with popover (#155)
* feat: add drag panel & popover component

* feat: update web ide version to 1.3.11

* feat: add components-popover

* fix: update sidebar default width

* feat: add global components popover

* fix: _menuData default value

* feat: add addComponent event

* revert: remove isCollapsed prop

* fix: update local icon font url
2024-05-20 16:13:34 +08:00
BoBoooooo f854f75b8a feat: supoort set default active view (#157) 2024-05-20 15:27:32 +08:00
BoBoooooo df3021f75e feat: add history forward & back shortcut key (#154) 2024-05-20 15:26:46 +08:00
BoBoooooo 8bf53a76f8 fix: prototype2code & go back history error (#156)
* fix: snippet import bug

* fix: history could back logic bug

* fix: editor add left border when in dual mode

* fix: setting-form title overflow bug
2024-05-20 15:26:07 +08:00
Wells 791fbb162a fix: enhance code value validate in SettingForm (#152)
* fix: update register setter strategy & add getSetter

* fix: handle invalid code to prop value

* fix: add validate logic to code setters

* fix: update code validate

* fix: update ui of settingForm header
2024-05-17 18:20:08 +08:00
67 changed files with 1497 additions and 541 deletions
+16 -2
View File
@@ -56,7 +56,7 @@ const tangoConfigJson = {
},
'@music163/antd': {
description: '云音乐低代码中后台应用基础物料',
version: '0.2.5',
version: '0.2.6',
library: 'TangoAntd',
type: 'baseDependency',
resources: [
@@ -161,10 +161,13 @@ import {
} from "@music163/antd";
import { Space } from "@music163/antd";
import { LocalButton } from "../components";
import { OutButton } from "../components";
class App extends React.Component {
render() {
return (
<Page title={tango.stores.app.title} subTitle={111}>
<Section tid="section0" />
<Section tid="section1" title="Section Title">
your input: <Input tid="input1" defaultValue="hello" />
copy input: <Input value={tango.page.input1?.value} />
@@ -180,6 +183,7 @@ class App extends React.Component {
<Section tid="section2">
<Space tid="space1">
<LocalButton />
<OutButton />
<Button tid="button1">button</Button>
</Space>
</Section>
@@ -191,7 +195,7 @@ class App extends React.Component {
</Section>
<Section title="原生 DOM" tid="section4">
<h1 style={{ ...{ color: "red" }, fontSize: 64 }}>
hello world
hello world
</h1>
<div
style={{
@@ -289,6 +293,14 @@ registerComponentPrototype({
});
`;
const outButtonCode = `
import React from 'react';
export default function OutButton(props) {
return <button {...props}>Out button (from other file)</button>
}
`;
const componentsInputCode = `
import React from 'react';
import { registerComponentPrototype } from '../utils';
@@ -311,6 +323,7 @@ registerComponentPrototype({
const componentsEntryCode = `
export { default as LocalButton } from './button';
export { default as LocalInput } from './input';
export { default as OutButton } from './out-button';
`;
const storeApp = `
@@ -411,6 +424,7 @@ export const sampleFiles = [
{ filename: '/src/pages/list.js', code: viewHomeCode },
{ filename: '/src/pages/detail.js', code: emptyPageCode },
{ filename: '/src/components/button.js', code: componentsButtonCode },
{ filename: '/src/components/out-button.js', code: outButtonCode },
{ filename: '/src/components/input.js', code: componentsInputCode },
{ filename: '/src/components/index.js', code: componentsEntryCode },
{ filename: '/src/routes.js', code: routesCode },
+11 -11
View File
@@ -45,14 +45,18 @@ const Snippet2ColumnLayout: IComponentPrototype = {
const Snippet3ColumnLayout: IComponentPrototype = {
name: 'Snippet3ColumnLayout',
title: '三列布局',
icon: 'icon-column3',
icon: 'icon-column-3',
type: 'snippet',
package: '@music163/antd',
// FIXME: Column 组件 需要更新为 defineComponent
initChildren: `
<Columns columns={12}>
<Column colSpan={4}></Column>
<Column colSpan={4}></Column>
<Column colSpan={4}></Column>
<Column colSpan={4}>
</Column>
<Column colSpan={4}>
</Column>
<Column colSpan={4}>
</Column>
</Columns>
`,
relatedImports: ['Columns', 'Column'],
@@ -76,13 +80,9 @@ const SnippetButtonGroup: IComponentPrototype = {
// hack some prototypes
basePrototypes['Section'].siblingNames = [
'SnippetButtonGroup',
'Section',
'Section',
'Section',
'Section',
'Section',
'Section',
'Section',
'Snippet2ColumnLayout',
'Snippet3ColumnLayout',
'SnippetSuccessResult',
];
export const nativeDomPrototypes = () => {
+30 -28
View File
@@ -28,34 +28,6 @@ import {
import { Action } from '@music163/tango-ui';
import { useState } from 'react';
// 1. 实例化工作区
const workspace = new Workspace({
entry: '/src/index.js',
files: sampleFiles,
prototypes,
});
// inject workspace to window for debug
(window as any).__workspace__ = workspace;
// 2. 引擎初始化
const engine = createEngine({
workspace,
});
// @ts-ignore
window.__workspace__ = workspace;
// 3. 沙箱初始化
const sandboxQuery = new DndQuery({
context: 'iframe',
});
// 4. 图标库初始化(物料面板和组件树使用了 iconfont 里的图标)
createFromIconfontCN({
scriptUrl: '//at.alicdn.com/t/c/font_2891794_6d4hj5u0bjx.js',
});
const menuData = {
common: [
{
@@ -90,6 +62,36 @@ const menuData = {
],
};
// 1. 实例化工作区
const workspace = new Workspace({
entry: '/src/index.js',
files: sampleFiles,
prototypes,
});
// inject workspace to window for debug
(window as any).__workspace__ = workspace;
// 2. 引擎初始化
const engine = createEngine({
workspace,
menuData,
defaultActiveView: 'design', // dual code design
});
// @ts-ignore
window.__workspace__ = workspace;
// 3. 沙箱初始化
const sandboxQuery = new DndQuery({
context: 'iframe',
});
// 4. 图标库初始化(物料面板和组件树使用了 iconfont 里的图标)
createFromIconfontCN({
scriptUrl: '//at.alicdn.com/t/c/font_2891794_151xsllxqd7.js',
});
/**
* 5. 平台初始化,访问 https://local.netease.com:6006/
*/
+2 -1
View File
@@ -45,7 +45,7 @@ const sandboxQuery = new DndQuery({
// 4. 图标库初始化(物料面板和组件树使用了 iconfont 里的图标)
createFromIconfontCN({
scriptUrl: '//at.alicdn.com/t/c/font_2891794_cou9i7556tl.js',
scriptUrl: '//at.alicdn.com/t/c/font_2891794_151xsllxqd7.js',
});
/**
@@ -112,6 +112,7 @@ export default function App() {
if (sandboxWindow.TangoMail) {
if (sandboxWindow.TangoMail.menuData) {
setMenuData(sandboxWindow.TangoMail.menuData);
engine.designer.setMenuData(sandboxWindow.TangoMail.menuData);
}
if (sandboxWindow.TangoMail.prototypes) {
workspace.setComponentPrototypes(sandboxWindow.TangoMail.prototypes);
+4 -2
View File
@@ -14,7 +14,7 @@ const BLACK_LIST = ['codeSetter', 'eventSetter', 'modelSetter', 'routerSetter'];
BUILT_IN_SETTERS.filter((setter) => !BLACK_LIST.includes(setter.name)).forEach(register);
createFromIconfontCN({
scriptUrl: '//at.alicdn.com/t/c/font_2891794_cou9i7556tl.js',
scriptUrl: '//at.alicdn.com/t/c/font_2891794_151xsllxqd7.js',
});
export default {
@@ -38,7 +38,7 @@ function SettingFormDemo({ initValues, prototype }: SettingFormDemoProps) {
const model = new FormModel(initValues, { onChange: console.log });
return (
<Box display="flex">
<Box flex="0 0 400px" overflow="hidden">
<Box flex="0 0 320px" overflow="hidden">
<SettingForm
model={model}
prototype={prototype}
@@ -59,8 +59,10 @@ function SettingFormDemo({ initValues, prototype }: SettingFormDemoProps) {
const prototypeHasBasicProps: IComponentPrototype = {
name: 'Sample',
title: '演示组件',
package: 'sample-pkg',
type: 'element',
docs: 'https://4x-ant-design.antgroup.com/components/slider-cn',
props: [
{
name: 'code',
@@ -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>
);
}
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.1.1](https://github.com/netease/tango/compare/@music163/tango-context@1.1.0...@music163/tango-context@1.1.1) (2024-05-21)
**Note:** Version bump only for package @music163/tango-context
# [1.1.0](https://github.com/netease/tango/compare/@music163/tango-context@1.0.2...@music163/tango-context@1.1.0) (2024-05-17)
### Features
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@music163/tango-context",
"version": "1.1.0",
"version": "1.1.1",
"description": "react context for tango-apps",
"keywords": [
"react",
@@ -31,8 +31,8 @@
"react": ">= 16.8"
},
"dependencies": {
"@music163/tango-core": "^1.1.0",
"@music163/tango-helpers": "^1.1.0",
"@music163/tango-core": "^1.2.0",
"@music163/tango-helpers": "^1.1.1",
"mobx-react-lite": "4.0.7"
},
"publishConfig": {
+13
View File
@@ -3,6 +3,19 @@
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-core@1.1.0...@music163/tango-core@1.2.0) (2024-05-21)
### Bug Fixes
- enhance code value validate in SettingForm ([#152](https://github.com/netease/tango/issues/152)) ([791fbb1](https://github.com/netease/tango/commit/791fbb162a7147243924e01f54e9c0b586f14438))
- prototype2code & go back history error ([#156](https://github.com/netease/tango/issues/156)) ([8bf53a7](https://github.com/netease/tango/commit/8bf53a76f8a71eaf261ea68b9ee44e5bf19893aa))
- support add components with popover ([#155](https://github.com/netease/tango/issues/155)) ([f17ccbb](https://github.com/netease/tango/commit/f17ccbb7f645f8047ecd96d9f3f2185048a3b726))
### Features
- add select parent node of selected node ([#158](https://github.com/netease/tango/issues/158)) ([fe31246](https://github.com/netease/tango/commit/fe3124648325e72abfc58da8b2f8ff83301d40b8))
- supoort set default active view ([#157](https://github.com/netease/tango/issues/157)) ([f854f75](https://github.com/netease/tango/commit/f854f75b8a8c25384d290bd76d6b8bb6fb69f22d))
# [1.1.0](https://github.com/netease/tango/compare/@music163/tango-core@1.0.2...@music163/tango-core@1.1.0) (2024-05-17)
### Bug Fixes
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@music163/tango-core",
"version": "1.1.0",
"version": "1.2.0",
"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.0",
"@music163/tango-helpers": "^1.1.1",
"@types/babel__generator": "^7.6.7",
"@types/babel__traverse": "^7.20.4",
"mobx": "6.12.3",
+14 -1
View File
@@ -1,4 +1,5 @@
import { Designer, Engine, SimulatorNameType } from './models';
import { MenuDataType } from '@music163/tango-helpers';
import { Designer, DesignerViewType, Engine, SimulatorNameType } from './models';
import { IWorkspace } from './models/interfaces';
interface ICreateEngineOptions {
@@ -6,6 +7,10 @@ interface ICreateEngineOptions {
* 自定义工作区
*/
workspace?: IWorkspace;
/**
* 菜单信息
*/
menuData?: MenuDataType;
/**
* 默认的模拟器模式
*/
@@ -14,6 +19,10 @@ interface ICreateEngineOptions {
* 默认激活的侧边栏
*/
defaultActiveSidebarPanel?: string;
/**
* 默认激活的视图
*/
defaultActiveView?: DesignerViewType;
}
/**
@@ -23,15 +32,19 @@ interface ICreateEngineOptions {
*/
export function createEngine({
workspace,
defaultActiveView = 'design',
defaultSimulatorMode = 'desktop',
defaultActiveSidebarPanel = '',
menuData,
}: ICreateEngineOptions) {
const engine = new Engine({
workspace,
designer: new Designer({
workspace,
simulator: defaultSimulatorMode,
activeView: defaultActiveView,
activeSidebarPanel: defaultActiveSidebarPanel,
menuData,
}),
});
+10 -4
View File
@@ -97,8 +97,7 @@ export function code2expression(code: string) {
try {
expNode = t.cloneNode(parseExpression(code, babelParserConfig), false, true);
} catch (err) {
logger.error('invalid code', err);
// expNode = t.identifier('undefined');
logger.error('code2expression failed, invalid code:', code);
}
return expNode;
}
@@ -194,7 +193,12 @@ export function code2jsxAttributeValueNode(code: string) {
return t.jsxExpressionContainer(code2expression(code));
}
// FIXME: 统一处理为 code2jsxAttributeValueNode
/**
* FIXME: 统一处理为 code2jsxAttributeValueNode
* 将 js value 转为 JSXAttributeValueNode
* @param value js value, or wrapped code
* @returns 返回 JSXAttributeValueNode,转换失败返回Node为 {undefined}
*/
export function value2jsxAttributeValueNode(value: any) {
let ret;
switch (typeof value) {
@@ -204,7 +208,8 @@ export function value2jsxAttributeValueNode(value: any) {
}
if (isWrappedCode(value)) {
const innerCode = getCodeOfWrappedCode(value);
ret = t.jsxExpressionContainer(code2expression(innerCode));
const node = code2expression(innerCode);
ret = t.jsxExpressionContainer(node || t.identifier('undefined'));
} else {
ret = t.stringLiteral(value);
}
@@ -217,6 +222,7 @@ 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) {
+10 -6
View File
@@ -20,6 +20,7 @@ export function prototype2importDeclarationData(
relativeFilepath?: string,
): { source: string; specifiers: IImportSpecifierData[] } {
let source = prototype.package;
const isSnippet = prototype.type === 'snippet';
if (relativeFilepath && isFilepath(source)) {
source = getRelativePath(relativeFilepath, source);
}
@@ -35,12 +36,15 @@ export function prototype2importDeclarationData(
type: 'ImportDefaultSpecifier',
});
} else {
[prototype.name, ...(prototype.relatedImports || [])].forEach((item) => {
specifiers.push({
localName: item,
type: 'ImportSpecifier',
});
});
// 忽略代码片段 name
[...(isSnippet ? [] : [prototype.name]), ...(prototype.relatedImports || [])].forEach(
(item) => {
specifiers.push({
localName: item,
type: 'ImportSpecifier',
});
},
);
}
return {
+76 -3
View File
@@ -1,5 +1,6 @@
import { action, computed, makeObservable, observable, toJS } from 'mobx';
import { IWorkspace } from './interfaces';
import { MenuDataType } from '@music163/tango-helpers';
export type SimulatorNameType = 'desktop' | 'phone';
@@ -19,7 +20,15 @@ interface IViewportBounding {
interface IDesignerOptions {
workspace: IWorkspace;
simulator?: SimulatorNameType | ISimulatorType;
/**
* 菜单配置
*/
menuData: MenuDataType;
activeSidebarPanel?: string;
/**
* 默认激活的视图模式
*/
activeView?: DesignerViewType;
}
const ISimulatorTypes: Record<string, ISimulatorType> = {
@@ -64,6 +73,16 @@ export class Designer {
*/
_showSmartWizard = false;
/**
* 是否显示添加组件面板
*/
_showAddComponentPopover = false;
/**
* 添加组件面板的位置
*/
_addComponentPopoverPosition = { clientX: 0, clientY: 0 };
/**
* 是否显示右侧面板
*/
@@ -75,9 +94,9 @@ export class Designer {
_isPreview = false;
/**
* 默认展开的侧边栏
* 菜单列表
*/
defaultActiveSidebarPanel?: string;
_menuData?: MenuDataType = null;
private readonly workspace: IWorkspace;
@@ -109,10 +128,31 @@ export class Designer {
return this._showRightPanel;
}
get showAddComponentPopover() {
return this._showAddComponentPopover;
}
get addComponentPopoverPosition() {
return this._addComponentPopoverPosition;
}
get menuData() {
return this._menuData ?? ([] as MenuDataType);
}
constructor(options: IDesignerOptions) {
this.workspace = options.workspace;
const { simulator, activeSidebarPanel: defaultActiveSidebarPanel } = options;
const {
simulator,
menuData,
activeSidebarPanel: defaultActiveSidebarPanel,
activeView: defaultActiveView,
} = options;
if (menuData) {
this.setMenuData(menuData);
}
// 默认设计器模式
if (simulator) {
@@ -124,6 +164,11 @@ export class Designer {
this.setActiveSidebarPanel(defaultActiveSidebarPanel);
}
// 默认激活的视图
if (defaultActiveView) {
this.setActiveView(defaultActiveView);
}
makeObservable(this, {
_simulator: observable,
_viewport: observable,
@@ -131,6 +176,9 @@ export class Designer {
_activeSidebarPanel: observable,
_showSmartWizard: observable,
_showRightPanel: observable,
_showAddComponentPopover: observable,
_addComponentPopoverPosition: observable,
_menuData: observable,
_isPreview: observable,
simulator: computed,
viewport: computed,
@@ -139,6 +187,9 @@ export class Designer {
isPreview: computed,
showRightPanel: computed,
showSmartWizard: computed,
showAddComponentPopover: computed,
addComponentPopoverPosition: computed,
menuData: computed,
setSimulator: action,
setViewport: action,
setActiveView: action,
@@ -147,6 +198,7 @@ export class Designer {
toggleRightPanel: action,
toggleSmartWizard: action,
toggleIsPreview: action,
toggleAddComponentPopover: action,
});
}
@@ -173,6 +225,11 @@ export class Designer {
this._activeSidebarPanel = '';
}
}
setMenuData(menuData: MenuDataType) {
this._menuData = menuData;
}
closeSidebarPanel() {
this._activeSidebarPanel = '';
}
@@ -185,6 +242,22 @@ export class Designer {
this._showRightPanel = value ?? !this._showRightPanel;
}
/**
* 显示添加组件面板
* @param value 是否显示
* @param position 坐标
*/
toggleAddComponentPopover(
value: boolean,
position: {
clientX: number;
clientY: number;
} = this.addComponentPopoverPosition,
) {
this._showAddComponentPopover = value;
this._addComponentPopoverPosition = position;
}
toggleIsPreview(value: boolean) {
this._isPreview = value ?? !this._isPreview;
if (value) {
+1 -1
View File
@@ -58,7 +58,7 @@ export class TangoHistory {
}
get couldBack() {
return this._records.length > 0 && this._index > -1;
return this._records.length > 0 && this._index > 0;
}
get couldForward() {
+14
View File
@@ -109,6 +109,20 @@ export class SelectSource {
this._start = null;
}
/**
* 选中当前选中节点的父节点
*/
selectParent() {
const parents = this.first?.parents || [];
if (parents.length) {
const [parent, ...rest] = parents;
this.select({
...parent,
parents: rest,
});
}
}
setStart(data: StartDataType) {
this._start = data;
}
+27 -3
View File
@@ -1,19 +1,28 @@
import { Identifier } from '@babel/types';
import {
object2node,
serviceConfig2Node,
isValidCode,
isValidExpressionCode,
code2expression,
value2jsxAttributeValueNode,
} from '../src/helpers';
describe('ast helpers', () => {
it('isValidCode', () => {
expect(isValidCode('')).toBeTruthy();
expect(isValidCode('1')).toBeTruthy();
expect(isValidCode('"hello"')).toBeTruthy();
expect(isValidCode('<div>hello</div>')).toBeTruthy();
expect(isValidCode('function fn() {}')).toBeTruthy();
// invalid function body
expect(isValidCode('() => { hello world }')).toBeFalsy();
// invalid function
expect(isValidCode('function() {}')).toBeFalsy();
});
it('isValidExpression', () => {
expect(isValidExpressionCode('() => { }')).toBeTruthy();
it('isValidExpressionCode', () => {
expect(isValidExpressionCode('1')).toBeTruthy();
expect(isValidExpressionCode('a = 1')).toBeTruthy();
expect(isValidExpressionCode('1 + 1')).toBeTruthy();
@@ -22,8 +31,11 @@ describe('ast helpers', () => {
expect(isValidExpressionCode('{ bizId: "vip", type: "category" }')).toBeTruthy();
expect(isValidExpressionCode('[1,2,3]')).toBeTruthy();
expect(isValidExpressionCode('<div>hello</div>')).toBeTruthy();
expect(isValidExpressionCode('<div>hello</div>')).toBeTruthy();
expect(isValidExpressionCode('tango.stores.app.title')).toBeTruthy();
expect(isValidExpressionCode('"hello" + window.location.path')).toBeTruthy();
expect(isValidExpressionCode('() => { }')).toBeTruthy();
expect(isValidExpressionCode('')).toBeFalsy();
expect(isValidExpressionCode('{1}')).toBeFalsy();
expect(isValidExpressionCode('{"1"}')).toBeFalsy();
expect(isValidExpressionCode('{ 1+1 }')).toBeFalsy();
@@ -68,4 +80,16 @@ describe('ast helpers', () => {
`;
expect(code2expression(arrayCode).type).toEqual('ArrayExpression');
});
it('value2jsxAttributeValueNode', () => {
expect(value2jsxAttributeValueNode('hello').type).toBe('StringLiteral');
expect(value2jsxAttributeValueNode(true).type).toBe('JSXExpressionContainer');
expect(value2jsxAttributeValueNode(1).type).toBe('JSXExpressionContainer');
expect(value2jsxAttributeValueNode('{{{ foo: "foo"}}}').type).toBe('JSXExpressionContainer');
// invalid code will return undefined
const node: any = value2jsxAttributeValueNode('{{tango.xx+}}');
expect(node.type).toBe('JSXExpressionContainer');
expect((node.expression as Identifier).name).toBe('undefined');
});
});
+32
View File
@@ -3,6 +3,38 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [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)
### Bug Fixes
- check local component prototypes ([#160](https://github.com/netease/tango/issues/160)) ([a0484e8](https://github.com/netease/tango/commit/a0484e8f64f20f67c30c25c3d5ce65de549b3e04))
# [1.2.0](https://github.com/netease/tango/compare/@music163/tango-designer@1.1.0...@music163/tango-designer@1.2.0) (2024-05-21)
### Bug Fixes
- enhance code value validate in SettingForm ([#152](https://github.com/netease/tango/issues/152)) ([791fbb1](https://github.com/netease/tango/commit/791fbb162a7147243924e01f54e9c0b586f14438))
- prototype2code & go back history error ([#156](https://github.com/netease/tango/issues/156)) ([8bf53a7](https://github.com/netease/tango/commit/8bf53a76f8a71eaf261ea68b9ee44e5bf19893aa))
- support add components with popover ([#155](https://github.com/netease/tango/issues/155)) ([f17ccbb](https://github.com/netease/tango/commit/f17ccbb7f645f8047ecd96d9f3f2185048a3b726))
### Features
- add history forward & back shortcut key ([#154](https://github.com/netease/tango/issues/154)) ([df3021f](https://github.com/netease/tango/commit/df3021f75e057e229756b429321c14d181223698))
- add select parent node of selected node ([#158](https://github.com/netease/tango/issues/158)) ([fe31246](https://github.com/netease/tango/commit/fe3124648325e72abfc58da8b2f8ff83301d40b8))
- event-setter & expression-setter use popover ([#159](https://github.com/netease/tango/issues/159)) ([9daf387](https://github.com/netease/tango/commit/9daf3872e743fcf706184877020bcbf1f75ffd25))
# [1.1.0](https://github.com/netease/tango/compare/@music163/tango-designer@1.0.3...@music163/tango-designer@1.1.0) (2024-05-17)
### Bug Fixes
+7 -7
View File
@@ -1,6 +1,6 @@
{
"name": "@music163/tango-designer",
"version": "1.1.0",
"version": "1.2.3",
"description": "lowcode designer",
"keywords": [
"react"
@@ -33,12 +33,12 @@
"dependencies": {
"@ant-design/icons": "^4.8.0",
"@music163/request": "^0.2.0",
"@music163/tango-context": "^1.1.0",
"@music163/tango-core": "^1.1.0",
"@music163/tango-helpers": "^1.1.0",
"@music163/tango-sandbox": "^1.0.3",
"@music163/tango-setting-form": "^1.2.0",
"@music163/tango-ui": "^1.1.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.4",
"@music163/tango-ui": "^1.2.3",
"antd": "^4.24.2",
"cash-dom": "^8.1.2",
"classnames": "^2.5.1",
@@ -0,0 +1,148 @@
import React, { useCallback, useMemo, useState } from 'react';
import { Box } from 'coral-system';
import { observer, useDesigner, useWorkspace } from '@music163/tango-context';
import { IconFont, DragPanel } from '@music163/tango-ui';
import { ComponentsPanel, ComponentsPanelProps } from '../sidebar';
import { IComponentPrototype } from '@music163/tango-helpers';
interface ComponentsPopoverProps {
// 添加组件位置
type?: 'inner' | 'before' | 'after';
// 弹出方式 手动触发/DOM 触发
isControlled?: boolean;
title?: string;
prototype?: IComponentPrototype;
children?: React.ReactNode;
}
export const ComponentsPopover = observer(
({
type = 'inner',
title = '添加组件',
isControlled = false,
children,
...popoverProps
}: ComponentsPopoverProps) => {
const [layout, setLayout] = useState<ComponentsPanelProps['layout']>('grid');
const workspace = useWorkspace();
const designer = useDesigner();
const { addComponentPopoverPosition, showAddComponentPopover } = designer;
const selectedNode = workspace.selectSource.selected?.[0];
const selectedNodeId = selectedNode?.codeId ?? '未选中';
const prototype =
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
workspace.componentPrototypes.get(selectedNode?.name) ?? ({} as IComponentPrototype);
// 推荐使用的子组件
const insertedList = useMemo(
() =>
Array.isArray(prototype?.childrenName)
? prototype?.childrenName
: [prototype?.childrenName].filter(Boolean),
[prototype?.childrenName],
);
// 推荐使用的代码片段
const siblingList = useMemo(() => prototype?.siblingNames ?? [], [prototype.siblingNames]);
const tipsTextMap = useMemo(
() => ({
before: `点击,在 ${selectedNodeId} 的前方添加节点`,
after: `点击,在 ${selectedNodeId} 的后方添加节点`,
inner: `点击,在 ${selectedNodeId} 内部添加节点`,
}),
[selectedNodeId],
);
const handleSelect = useCallback(
(name: string) => {
switch (type) {
case 'before':
workspace.insertBeforeSelectedNode(name);
break;
case 'after':
workspace.insertAfterSelectedNode(name);
break;
case 'inner':
workspace.insertToSelectedNode(name);
break;
default:
break;
}
},
[type, workspace],
);
const changeLayout = useCallback(() => {
setLayout(layout === 'grid' ? 'line' : 'grid');
}, [layout]);
const menuData = useMemo(() => {
const menuList = JSON.parse(JSON.stringify(designer.menuData));
const commonList = menuList['common'] ?? [];
if (commonList?.length && siblingList?.length) {
commonList.unshift({
title: '代码片段',
items: siblingList,
});
}
if (commonList?.length && insertedList?.length) {
commonList.unshift({
title: '推荐使用',
items: insertedList,
});
}
return menuList;
}, [insertedList, siblingList, designer.menuData]);
const innerTypeProps =
// 手动触发 适用于 点击添加组件
type === 'inner' && isControlled
? {
open: showAddComponentPopover,
onOpenChange: (open: boolean) => designer.toggleAddComponentPopover(open),
left: addComponentPopoverPosition.clientX,
top: addComponentPopoverPosition.clientY,
}
: {};
return (
<DragPanel
{...innerTypeProps}
title={title}
extra={
<Box fontSize="12px">
{layout === 'grid' ? (
<IconFont type="icon-liebiaoitem" onClick={changeLayout} />
) : (
<IconFont type="icon-grid1" onClick={changeLayout} />
)}
</Box>
}
footer={tipsTextMap[type]}
width="330px"
maskClosable
body={
<ComponentsPanel
isScope
showBizComps={false}
menuData={menuData}
layout={layout}
onItemSelect={handleSelect}
style={{
maxHeight: '400px',
overflow: 'auto',
}}
/>
}
{...popoverProps}
>
{children}
</DragPanel>
);
},
);
@@ -2,3 +2,4 @@ export * from './drag-box';
export * from './input-kv';
export * from './variable-tree';
export * from './variable-tree-modal';
export * from './components-popover';
@@ -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);
}}
>
+11 -3
View File
@@ -6,10 +6,14 @@ export interface IDesignerContext<S = any> {
* 沙箱查询实例
*/
sandboxQuery: DndQuery;
/**
* 预览沙箱查询实例
*/
previewSandboxQuery: DndQuery;
/**
* 远程服务
*/
remoteServices?: Record<string, S>;
remoteServices: Record<string, S>;
}
const [DesignerProvider, useDesigner] = createContext<IDesignerContext>({
@@ -19,9 +23,13 @@ const [DesignerProvider, useDesigner] = createContext<IDesignerContext>({
export { DesignerProvider };
export const useSandboxQuery = () => {
return useDesigner()?.sandboxQuery;
return useDesigner().sandboxQuery;
};
export const usePreviewSandboxQuery = () => {
return useDesigner().previewSandboxQuery;
};
export const useRemoteServices = () => {
return useDesigner()?.remoteServices;
return useDesigner().remoteServices;
};
+23 -3
View File
@@ -5,8 +5,18 @@ import { TangoEngineProvider, ITangoEngineContext } from '@music163/tango-contex
import zhCN from 'antd/lib/locale/zh_CN';
import { DesignerProvider, IDesignerContext } from './context';
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
*/
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]);
return (
<SystemProvider theme={theme} prefix="--tango">
<ConfigProvider locale={zhCN}>
<TangoEngineProvider value={{ engine, config }}>
<DesignerProvider value={{ sandboxQuery, remoteServices }}>{children}</DesignerProvider>
<DesignerProvider value={{ sandboxQuery, previewSandboxQuery, remoteServices }}>
{children}
</DesignerProvider>
</TangoEngineProvider>
</ConfigProvider>
</SystemProvider>
+6 -1
View File
@@ -12,7 +12,6 @@ export const DRAGGABLE_SELECTOR = `[${SLOT.dnd}]`;
interface DndQueryOptions {
/**
* DOM 查询上下文选择器
* TODO: 是不是可以合并成一个 API
*/
context?: string;
/**
@@ -63,6 +62,9 @@ export class DndQuery {
return false;
}
/**
* 沙箱内的 window 对象
*/
get window() {
if (this.context && 'defaultView' in this.context) {
return (this.context as unknown as Document).defaultView;
@@ -71,6 +73,9 @@ export class DndQuery {
return window;
}
/**
* 沙箱内的全局滚动偏移
*/
get scrollTop() {
if (this.context && 'documentElement' in this.context) {
return (this.context as unknown as Document).documentElement.scrollTop;
+20 -4
View File
@@ -58,6 +58,15 @@ export function useDnd({
'command+v,ctrl+v': () => {
workspace.pasteSelectedNode();
},
'command+arrowup,ctrl+arrowup': () => {
workspace.selectSource.selectParent();
},
'command+z,ctrl+z': () => {
workspace.history.back();
},
'command+shift+z,ctrl+shift+z': () => {
workspace.history.forward();
},
});
}, [workspace]);
@@ -84,10 +93,10 @@ export function useDnd({
const onMouseMove = (e: React.MouseEvent) => {
const point = sandboxQuery.getRelativePoint({ x: e.clientX, y: e.clientY });
setElementStyle('.SelectionMask', {
width: Math.abs(selectSource.start?.point.x - point.x) + 'px',
height: Math.abs(selectSource.start?.point.y - point.y) + 'px',
left: Math.min(selectSource.start?.point.x, point.x) + 'px',
top: Math.min(selectSource.start?.point.y, point.y) + 'px',
width: `${Math.abs(selectSource.start?.point.x - point.x)}px`,
height: `${Math.abs(selectSource.start?.point.y - point.y)}px`,
left: `${Math.min(selectSource.start?.point.x, point.x)}px`,
top: `${Math.min(selectSource.start?.point.y, point.y)}px`,
});
};
@@ -406,6 +415,13 @@ export function useDnd({
// 打开智能向导弹窗
designer.toggleSmartWizard(true);
break;
case 'addComponent':
// 打开添加组件面板
designer.toggleAddComponentPopover(true, {
clientX: (e.detail.meta as any).clientX + 40,
clientY: (e.detail.meta as any).clientY + 110,
});
break;
default:
break;
}
+10 -2
View File
@@ -1,7 +1,7 @@
import React, { useRef, useEffect, useCallback } from 'react';
import { Box } from 'coral-system';
import { MultiEditor, MultiEditorProps } from '@music163/tango-ui';
import { observer, useWorkspace } from '@music163/tango-context';
import { observer, useDesigner, useWorkspace } from '@music163/tango-context';
import { isValidCode } from '@music163/tango-core';
import { Modal } from 'antd';
@@ -32,6 +32,7 @@ export const CodeEditor = observer(
({ autoRemoveUnusedImports = true, ...rest }: CodeEditorProps) => {
const editorRef = useRef(null);
const workspace = useWorkspace();
const designer = useDesigner();
const files = workspace.listFiles();
const activeFile = workspace.activeFile;
@@ -101,8 +102,15 @@ export const CodeEditor = observer(
[workspace],
);
const borderStyle =
designer.activeView === 'dual'
? {
borderLeft: 'solid 1px var(--tango-colors-line2)',
}
: {};
return (
<Box display="flex" flexDirection="row" height="100%" bg="white">
<Box display="flex" flexDirection="row" height="100%" bg="white" {...borderStyle}>
<MultiEditor
ref={editorRef}
options={{
+14 -2
View File
@@ -7,6 +7,20 @@ import {
parseDndId,
} 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) {
return `[${SLOT.dnd}="${id}"]`;
}
@@ -15,8 +29,6 @@ export function getElement(selector: Selector) {
return $(selector).get(0);
}
export const DRAG_GHOST_ID = 'dragGhost';
export const getDragGhostElement = () => getElement(`#${DRAG_GHOST_ID}`);
export function setElementStyle(selector: Selector, style: any) {
+4 -2
View File
@@ -8,6 +8,7 @@ import { useSandboxQuery } from '../context';
import { DndQuery, useDnd } from '../dnd';
import { Navigator } from './navigator';
import { SelectionToolsProps } from '../simulator/selection';
import { DESIGN_SANDBOX_ID, PREVIEW_SANDBOX_ID } from '../helpers';
interface ISandboxEventHandlerConfig {
sandboxQuery?: DndQuery;
@@ -62,6 +63,7 @@ function useSandbox({
const workspace = useWorkspace();
const designer = useDesigner();
const sandboxQuery = useSandboxQuery();
const isPreview = isPreviewProp ?? designer.isPreview;
// 组件不一定会立即刷新,因此 isActive 需要实时获取
@@ -140,7 +142,7 @@ const PreviewSandbox = observer(
return (
<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>
);
},
@@ -188,7 +190,7 @@ const DesignSandbox = observer(
return (
<Box display={display} width="100%" height="100%">
<CodeSandbox ref={ref} {...sandboxProps} {...rest} />
<CodeSandbox ref={ref} iframeId={DESIGN_SANDBOX_ID} {...sandboxProps} {...rest} />
</Box>
);
},
@@ -1,3 +1,4 @@
export * from './copy-node';
export * from './delete-node';
export * from './select-parent-node';
export * from './view-source';
@@ -0,0 +1,18 @@
import React from 'react';
import { useWorkspace, observer } from '@music163/tango-context';
import { IconFont, SelectAction } from '@music163/tango-ui';
export const SelectParentNodeAction = observer(() => {
const workspace = useWorkspace();
return (
<SelectAction
tooltip="选中父节点"
onClick={() => {
workspace.selectSource.selectParent();
}}
>
<IconFont type="icon-huiche1" rotate={90} />
</SelectAction>
);
});
+28 -27
View File
@@ -1,11 +1,11 @@
import React, { useCallback, useState } from 'react';
import { css, Box } from 'coral-system';
import React, { useCallback, useMemo, useState } from 'react';
import { css, Box, Text } from 'coral-system';
import { AutoComplete } from 'antd';
import { ActionSelect } from '@music163/tango-ui';
import { FormItemComponentProps } from '@music163/tango-setting-form';
import { useWorkspace, useWorkspaceData } from '@music163/tango-context';
import { wrapCode } from '@music163/tango-helpers';
import { ExpressionModal } from './expression-setter';
import { ExpressionPopover } from './expression-setter';
import { value2code } from '@music163/tango-core';
enum EventAction {
@@ -25,15 +25,6 @@ const wrapperStyle = css`
}
`;
const options = [
{ label: '无动作', value: EventAction.NoAction },
{ label: '打印事件', value: EventAction.ConsoleLog },
{ label: '绑定 JS 表达式', value: EventAction.BindExpression },
{ label: '打开页面', value: EventAction.NavigateTo },
{ label: '打开弹窗', value: EventAction.OpenModal },
{ label: '关闭弹窗', value: EventAction.CloseModal },
];
export type EventSetterProps = FormItemComponentProps<string>;
/**
@@ -43,7 +34,6 @@ export function EventSetter(props: EventSetterProps) {
const { value, onChange, modalTitle } = props;
const [type, setType] = useState<EventAction>(); // 事件类型
const [temp, setTemp] = useState(''); // 二级暂存值
const [expModalVisible, setExpModalVisible] = useState(false); // 弹窗是否显示
const { actionVariables, routeOptions } = useWorkspaceData();
const workspace = useWorkspace();
const modalOptions = workspace.activeViewModule.listModals() || [];
@@ -61,15 +51,37 @@ export function EventSetter(props: EventSetterProps) {
},
[onChange, code],
);
const options = useMemo(
() => [
{ label: '无动作', value: EventAction.NoAction },
{ label: '打印事件', value: EventAction.ConsoleLog },
{
label: (
<ExpressionPopover
title={modalTitle}
value={value}
onOk={(nextValue) => {
handleChange(nextValue);
}}
dataSource={actionVariables}
>
<Text> JS </Text>
</ExpressionPopover>
),
value: EventAction.BindExpression,
},
{ label: '打开页面', value: EventAction.NavigateTo },
{ label: '打开弹窗', value: EventAction.OpenModal },
{ label: '关闭弹窗', value: EventAction.CloseModal },
],
[modalTitle, value, actionVariables, handleChange],
);
const onAction = (key: string) => {
setType(key as EventAction); // 记录事件类型
setTemp(''); // 重置二级选项值
switch (key) {
case EventAction.BindExpression:
setExpModalVisible(true);
break;
case EventAction.ConsoleLog:
handleChange('(...args) => console.log(...args)');
break;
@@ -86,17 +98,6 @@ export function EventSetter(props: EventSetterProps) {
return (
<Box css={wrapperStyle}>
<ActionSelect options={options} onSelect={onAction} text={actionText} />
<ExpressionModal
title={modalTitle}
value={code}
visible={expModalVisible}
onCancel={() => setExpModalVisible(false)}
onOk={(nextValue) => {
setExpModalVisible(false);
handleChange(nextValue);
}}
dataSource={actionVariables}
/>
{type === EventAction.NavigateTo && (
<AutoComplete
placeholder="选择或输入页面路由"
@@ -1,10 +1,10 @@
import React, { useState, useEffect, useCallback } from 'react';
import { Box, Text, css } from 'coral-system';
import { Dropdown, Modal } from 'antd';
import { Dropdown, Button } from 'antd';
import { isValidExpressionCode } from '@music163/tango-core';
import { noop, useBoolean, getValue, IVariableTreeNode } from '@music163/tango-helpers';
import { getValue, IVariableTreeNode, noop } from '@music163/tango-helpers';
import { CloseCircleFilled, ExpandAltOutlined, MenuOutlined } from '@ant-design/icons';
import { Panel, InputCode, Action } from '@music163/tango-ui';
import { Panel, InputCode, Action, DragPanel } from '@music163/tango-ui';
import { FormItemComponentProps } from '@music163/tango-setting-form';
import { useWorkspace, useWorkspaceData } from '@music163/tango-context';
import { VariableTree } from '../components';
@@ -60,7 +60,6 @@ export function ExpressionSetter(props: ExpressionSetterProps) {
} = props;
// const codeValue = getCodeOfWrappedCode(valueProp);
const [inputValue, setInputValue] = useState(valueProp);
const [visible, { on, off }] = useBoolean();
// when receive new value, sync state
useEffect(() => {
@@ -115,11 +114,16 @@ export function ExpressionSetter(props: ExpressionSetterProps) {
<Action tooltip="使用预设代码片段" icon={<MenuOutlined />} size="small" />
</Dropdown>
)}
<Action
tooltip="打开表达式变量选择面板"
icon={<ExpandAltOutlined />}
onClick={on}
size="small"
<ExpressionPopover
title={modalTitle}
subTitle={modalTip}
placeholder={placeholder}
autoCompleteOptions={autoCompleteOptions}
newStoreTemplate={newStoreTemplate}
value={inputValue}
onOk={(value) => {
change(value);
}}
/>
</Box>
}
@@ -133,32 +137,16 @@ export function ExpressionSetter(props: ExpressionSetterProps) {
status={status}
maxHeight="200px"
/>
<ExpressionModal
title={modalTitle}
subTitle={modalTip}
placeholder={placeholder}
autoCompleteOptions={autoCompleteOptions}
newStoreTemplate={newStoreTemplate}
visible={visible}
value={inputValue}
onCancel={() => off()}
onOk={(value) => {
change(value);
off();
}}
/>
</Box>
);
}
export interface ExpressionModalProps {
export interface ExpressionPopoverProps {
title?: string;
subTitle?: string;
placeholder?: string;
visible?: boolean;
defaultValue?: string;
value?: string;
onCancel?: () => void;
onOk?: (value: string) => void;
dataSource?: IVariableTreeNode[];
autoCompleteOptions?: string[];
@@ -166,25 +154,27 @@ export interface ExpressionModalProps {
* 新建 store 的模板代码
*/
newStoreTemplate?: string;
children?: React.ReactNode;
}
export function ExpressionModal({
export function ExpressionPopover({
title,
subTitle,
placeholder,
visible,
onCancel = noop,
onOk = noop,
defaultValue,
value,
dataSource,
autoCompleteOptions,
newStoreTemplate = CODE_TEMPLATES.newStoreTemplate,
}: ExpressionModalProps) {
children,
}: ExpressionPopoverProps) {
const [exp, setExp] = useState(value ?? defaultValue);
const [error, setError] = useState('');
const workspace = useWorkspace();
const selectNodePath = workspace.selectSource.selected[0]?.codeId;
const { expressionVariables } = useWorkspaceData();
const serviceModules = Object.keys(workspace.serviceModules).map((key) => ({
label: key === 'index' ? '默认模块' : key,
@@ -203,118 +193,145 @@ export function ExpressionModal({
}, [value]);
return (
<Modal
closable={false}
destroyOnClose
width="60%"
open={visible}
onCancel={onCancel}
onOk={() => {
onOk(exp);
<DragPanel
width={700}
title={`${selectNodePath}/${title} 设置为引用变量或自定义表达式`}
extra={subTitle}
onOpenChange={(open) => {
if (!open) {
setExp(undefined);
setError('');
}
}}
bodyStyle={{
padding: 0,
popoverStyle={{
height: '615px',
}}
body={
<>
<Panel shape="solid">
<InputCode
shape="inset"
minHeight="56px"
maxHeight="200px"
value={exp}
placeholder={placeholder}
onChange={handleExpInputChange}
onBlur={() => {
setError(expressionValueValidate(exp));
}}
autoCompleteContext={evaluateContext}
autoCompleteOptions={autoCompleteOptions}
/>
{error ? (
<Text color="red" fontSize="12px">
</Text>
) : (
<Text fontSize="12px" color="text3">
javascript 使 jsx
</Text>
)}
</Panel>
<Panel
title="从变量列表中选中"
shape="solid"
borderTop="0"
overflow="hidden"
bodyProps={{ overflow: 'hidden' }}
>
<VariableTree
height={380}
showViewButton
dataSource={dataSource || expressionVariables}
appContext={sandbox?.window['tango']}
getStoreNames={() => Object.keys(workspace.storeModules)}
serviceModules={serviceModules}
getServiceData={(serviceKey) => {
const data = workspace.getServiceFunction(serviceKey);
return {
name: data.name,
moduleName: data.moduleName,
method: 'get',
...data.config,
};
}}
onSelect={(node) => {
if (!node.key) {
return;
}
if (node.key.split('.').length < 2) {
return;
}
let str;
if (/^(stores|services)\./.test(node.key)) {
str = `tango.${node.key.replaceAll('.', '?.')}`;
} else {
str = `${node.key}`;
}
setExp(str);
}}
onAddStoreVariable={(storeName, data) => {
workspace.addStoreState(storeName, data.name, data.initialValue);
}}
onUpdateStoreVariable={(variableKey, code) => {
workspace.updateStoreVariable(variableKey, code);
}}
onAddStore={(storeName) => {
workspace.addStoreFile(storeName, newStoreTemplate);
}}
onRemoveStoreVariable={(variableKey) => {
workspace.removeStoreVariable(variableKey);
}}
onRemoveService={(serviceKey) => {
workspace.removeServiceFunction(serviceKey);
}}
onAddService={(data) => {
const { name, moduleName, ...payload } = shapeServiceValues(data);
workspace.addServiceFunction(name, payload, moduleName);
}}
onUpdateService={(data) => {
const { name, moduleName, ...payload } = shapeServiceValues(data);
workspace.updateServiceFunction(name, payload, moduleName);
}}
getPreviewValue={(node) => {
if (!node || !node.key) {
return;
}
if (node.type === 'function') {
return;
}
return getValue(evaluateContext['tango'], node.key);
}}
/>
</Panel>
</>
}
footer={(close) => (
<Box display="flex" justifyContent="flex-end" gap="5px">
<Button
type="primary"
size="small"
onClick={() => {
if (!error) {
onOk(exp);
close();
}
}}
>
</Button>
<Button size="small" onClick={close}>
</Button>
</Box>
)}
>
<Panel title={`${title} 设置为引用变量或自定义表达式`} subTitle={subTitle} shape="solid">
<InputCode
shape="inset"
minHeight="56px"
maxHeight="200px"
value={exp}
placeholder={placeholder}
onChange={handleExpInputChange}
onBlur={() => {
setError(expressionValueValidate(exp));
}}
autoCompleteContext={evaluateContext}
autoCompleteOptions={autoCompleteOptions}
/>
{error ? (
<Text color="red" fontSize="12px">
</Text>
) : (
<Text fontSize="12px" color="text3">
javascript 使 jsx
</Text>
)}
</Panel>
<Panel
title="从变量列表中选中"
shape="solid"
borderTop="0"
overflow="hidden"
bodyProps={{ overflow: 'hidden' }}
>
<VariableTree
height={380}
showViewButton
dataSource={dataSource || expressionVariables}
appContext={sandbox?.window['tango']}
getStoreNames={() => Object.keys(workspace.storeModules)}
serviceModules={serviceModules}
getServiceData={(serviceKey) => {
const data = workspace.getServiceFunction(serviceKey);
return {
name: data.name,
moduleName: data.moduleName,
method: 'get',
...data.config,
};
}}
onSelect={(node) => {
if (!node.key) {
return;
}
if (node.key.split('.').length < 2) {
return;
}
let str;
if (/^(stores|services)\./.test(node.key)) {
str = `tango.${node.key.replaceAll('.', '?.')}`;
} else {
str = `${node.key}`;
}
setExp(str);
}}
onAddStoreVariable={(storeName, data) => {
workspace.addStoreState(storeName, data.name, data.initialValue);
}}
onUpdateStoreVariable={(variableKey, code) => {
workspace.updateStoreVariable(variableKey, code);
}}
onAddStore={(storeName) => {
workspace.addStoreFile(storeName, newStoreTemplate);
}}
onRemoveStoreVariable={(variableKey) => {
workspace.removeStoreVariable(variableKey);
}}
onRemoveService={(serviceKey) => {
workspace.removeServiceFunction(serviceKey);
}}
onAddService={(data) => {
const { name, moduleName, ...payload } = shapeServiceValues(data);
workspace.addServiceFunction(name, payload, moduleName);
}}
onUpdateService={(data) => {
const { name, moduleName, ...payload } = shapeServiceValues(data);
workspace.updateServiceFunction(name, payload, moduleName);
}}
getPreviewValue={(node) => {
if (!node || !node.key) {
return;
}
if (node.type === 'function') {
return;
}
return getValue(evaluateContext['tango'], node.key);
}}
/>
</Panel>
</Modal>
{children || (
<Action tooltip="打开表达式变量选择面板" icon={<ExpandAltOutlined />} size="small" />
)}
</DragPanel>
);
}
+23
View File
@@ -26,6 +26,24 @@ import {
FlexDirectionSetter,
} from './style-setter';
import { ChoiceSetter } from './choice-setter';
import { isValidExpressionCode } from '@music163/tango-core';
const codeValidate: IFormItemCreateOptions['validate'] = (value, field) => {
if (!value) return;
const rawCode = field.detail.rawCode;
if (!rawCode) return;
return isValidExpressionCode(rawCode) ? '' : '请输入合法的 Javascript 代码片段';
};
const jsonValidate: IFormItemCreateOptions['validate'] = (value, field) => {
if (!value) return;
try {
JSON.parse(field.detail.rawCode);
return;
} catch (e) {
return '请输入合法的 JSON 字符串';
}
};
export const BUILT_IN_SETTERS: IFormItemCreateOptions[] = [
{
@@ -33,6 +51,7 @@ export const BUILT_IN_SETTERS: IFormItemCreateOptions[] = [
alias: ['expSetter', 'expressionSetter'],
component: ExpressionSetter,
type: 'code',
validate: codeValidate,
},
{
name: 'radioGroupSetter',
@@ -81,11 +100,13 @@ export const BUILT_IN_SETTERS: IFormItemCreateOptions[] = [
name: 'jsonSetter',
component: JSONSetter,
type: 'code',
validate: jsonValidate,
},
{
name: 'jsxSetter',
component: JsxSetter,
type: 'code',
validate: codeValidate,
},
{
name: 'listSetter',
@@ -108,6 +129,7 @@ export const BUILT_IN_SETTERS: IFormItemCreateOptions[] = [
name: 'renderPropsSetter',
component: RenderSetter,
type: 'code',
validate: codeValidate,
},
{
name: 'tableCellSetter',
@@ -118,6 +140,7 @@ export const BUILT_IN_SETTERS: IFormItemCreateOptions[] = [
name: 'tableExpandableSetter',
component: TableExpandableSetter,
type: 'code',
validate: codeValidate,
},
{
name: 'routerSetter',
+5 -1
View File
@@ -6,7 +6,11 @@ import { clone, parseDndId } from '@music163/tango-helpers';
import { observer, useDesigner, useWorkspace } from '@music163/tango-context';
import { registerBuiltinSetters } from './setters';
registerBuiltinSetters();
let registered = false;
if (!registered) {
registerBuiltinSetters();
registered = true;
}
export interface SettingPanelProps extends SettingFormProps {
title?: React.ReactNode;
@@ -1,21 +1,29 @@
import React, { useMemo, useState } from 'react';
import { Box, Grid, Text } from 'coral-system';
import styled from 'styled-components';
import styled, { css } from 'styled-components';
import {
IComponentPrototype,
MenuDataType,
MenuValueType,
createContext,
logger,
PartialRecord,
upperCamelCase,
} from '@music163/tango-helpers';
import { CollapsePanel, IconFont, Search, Tabs } from '@music163/tango-ui';
import { observer, useWorkspace } from '@music163/tango-context';
import { QuestionCircleOutlined } from '@ant-design/icons';
import { Button, Empty, Spin, Popover } from 'antd';
import { Button, Empty, Spin, Popover, TabsProps } from 'antd';
import { getDragGhostElement } from '../helpers';
type MenuKeyType = 'common' | 'atom' | 'snippet' | 'bizComp' | 'localComp';
type MenuValueType = Array<{ title: string; items: string[] }>;
export type MenuDataType = PartialRecord<MenuKeyType, MenuValueType>;
type IComponentsPanelContext = {
isScope: boolean;
onItemSelect: (name: string) => void;
layout: 'grid' | 'line';
};
const [ComponentsPanelProvider, usePanelContext] = createContext<IComponentsPanelContext>({
name: 'ComponentsPanelContext',
});
export interface ComponentsPanelProps {
/**
@@ -36,6 +44,26 @@ export interface ComponentsPanelProps {
* @returns
*/
getBizCompName?: (name: string) => string;
/**
* 是否局部模式 (快捷添加组件面板中使用)
*/
isScope?: boolean;
/**
* 组件选中回调
*/
onItemSelect?: (name: string) => void;
/**
* 自定义样式
*/
style?: React.CSSProperties;
/**
* tabProps
*/
tabProps?: TabsProps;
/**
* 布局模式,默认网格布局
*/
layout?: 'grid' | 'line';
}
const localeMap = {
@@ -72,10 +100,15 @@ export function useFlatMenuData<T>(menuData: T) {
export const ComponentsPanel = observer(
({
isScope = false,
menuData = emptyMenuData,
showBizComps = true,
getBizCompName = upperCamelCase,
loading = false,
style,
tabProps,
onItemSelect,
layout = 'grid',
}: ComponentsPanelProps) => {
const [keyword, setKeyword] = useState<string>('');
const allList = useFlatMenuData<MenuDataType>(menuData);
@@ -109,22 +142,44 @@ export const ComponentsPanel = observer(
children: <MaterialList data={localCompData} />,
});
}
const contentNode =
tabs.length === 1 ? (
tabs[0].children
) : (
<Tabs centered isTabBarSticky tabBarStickyOffset={48} items={tabs} />
<Tabs
size={isScope ? 'small' : 'middle'}
centered
isTabBarSticky
tabBarStickyOffset={48}
items={tabs}
{...tabProps}
/>
);
return (
<Box className="ComponentsView" overflowY="auto">
<Box px="l" py="m" position="sticky" top="0" zIndex={1} bg="white">
<Search placeholder="搜索物料" onChange={setKeyword} />
<ComponentsPanelProvider
value={{
isScope,
onItemSelect,
layout,
}}
>
<Box className="ComponentsView" overflowY="auto" style={style}>
<Box px="l" py="m" position="sticky" top="0" zIndex={3} bg="white">
<Search
style={{
borderRadius: '4px',
}}
placeholder="搜索物料"
onChange={setKeyword}
/>
</Box>
<Spin spinning={loading} tip="正在加载物料列表...">
{!keyword ? contentNode : <MaterialList data={allList} filterKeyword={keyword} />}
</Spin>
</Box>
<Spin spinning={loading} tip="正在加载物料列表...">
{!keyword ? contentNode : <MaterialList data={allList} filterKeyword={keyword} />}
</Spin>
</Box>
</ComponentsPanelProvider>
);
},
);
@@ -146,6 +201,9 @@ interface MaterialListProps {
function MaterialList({ data, filterKeyword, type = 'common' }: MaterialListProps) {
const workspace = useWorkspace();
const { layout } = usePanelContext();
const isGrid = layout === 'grid';
return (
<Box className="ComponentsViewList">
{data.map((cate) => {
@@ -166,15 +224,21 @@ function MaterialList({ data, filterKeyword, type = 'common' }: MaterialListProp
title={cate.title}
borderBottom="solid"
borderColor="line.normal"
showBottomBorder={false}
bodyProps={{
padding: isGrid ? '4px 12px 12px' : '0',
}}
>
{!items.length && (
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="没有匹配到任何组件" />
)}
<Grid
columns={type === 'localComp' ? 1 : 2}
templateColumns={isGrid ? 'repeat(auto-fit,minmax(55px,1fr))' : '1fr'}
spacing="1px"
bg="background.normal"
padding="0"
gap={isGrid ? '12px 8px' : '0'}
backgroundColor="white"
>
{items.map((item) => {
const prototype = workspace.componentPrototypes.get(item);
@@ -202,9 +266,8 @@ const StyledCommonGridItem = styled.div`
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 8px;
cursor: move;
justify-content: start;
cursor: ${(props) => (props.draggable ? 'grab' : 'pointer')};
text-align: center;
color: var(--tango-colors-text-body);
background-color: #fff;
@@ -213,7 +276,23 @@ const StyledCommonGridItem = styled.div`
white-space: nowrap;
.material-icon {
font-size: 40px;
display: flex;
align-items: center;
justify-content: center;
font-size: 36px;
background: #f9f9f9;
border-radius: 4px;
border: 1px solid #ebebeb;
width: 100%;
height: 52px;
position: relative;
transition: 0.15s ease-in-out;
transition-property: transform;
will-change: transform;
img {
width: unset;
height: 85%;
}
}
.info {
@@ -225,19 +304,13 @@ const StyledCommonGridItem = styled.div`
.anticon-question-circle {
display: none;
font-size: 13px;
position: absolute;
top: 8px;
right: 8px;
}
img {
height: 40px;
width: 40px;
top: 4px;
right: 4px;
}
&:hover {
box-shadow: 0 0 10px rgb(0 0 0 / 10%);
> span {
color: var(--tango-colors-brand);
}
@@ -245,11 +318,26 @@ const StyledCommonGridItem = styled.div`
.anticon-question-circle {
display: inline-block;
}
.material-icon {
border-color: #c7c7c7;
}
}
`;
const GridLineItemStyle = css`
cursor: pointer;
user-select: none;
&:hover {
background-color: var(--tango-colors-fill2);
}
`;
function MaterialGrid({ data }: MaterialProps) {
const workspace = useWorkspace();
const { isScope, onItemSelect, layout } = usePanelContext();
const isLine = layout === 'line';
const handleDragStart = (e: React.DragEvent) => {
e.dataTransfer.effectAllowed = 'move';
@@ -266,28 +354,92 @@ function MaterialGrid({ data }: MaterialProps) {
workspace.dragSource.clear();
};
const handleSelect = () => {
onItemSelect?.(data.name);
};
const icon = data.icon || 'icon-placeholder';
const iconNode = icon.startsWith('icon-') ? (
<IconFont className="material-icon" type={data.icon || 'icon-placeholder'} />
) : (
<Box className="material-icon">
<img src={icon} alt={data.name} />
</Box>
);
if (isLine) {
return (
<Box
key={data.name}
display="flex"
columnGap="m"
px="l"
py="m"
fontSize="12px"
css={GridLineItemStyle}
onClick={handleSelect}
>
<Box
p="m"
fontSize="32px"
width="46px"
height="46px"
background="fill1"
border="1px solid"
borderRadius="4px"
borderColor="line2"
display="flex"
alignItems="center"
justifyContent="center"
>
{iconNode}
</Box>
<Box flex={1}>
<Box fontWeight="500" display="flex" justifyContent="space-between">
<Text flex={1}>{data.title}</Text>
{data.docs ? (
<Popover
zIndex={9999}
placement="right"
title={data.title}
content={<CommonMaterialInfoBox docs={data.docs} />}
>
<QuestionCircleOutlined />
</Popover>
) : null}
</Box>
<Box color="text2" fontStyle="italic">
{data.help ?? data.title}
</Box>
</Box>
</Box>
);
}
return (
<StyledCommonGridItem
draggable
key={data.name}
draggable={!isScope}
data-name={data.name}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
onClick={handleSelect}
>
{icon.startsWith('icon-') ? (
<IconFont className="material-icon" type={data.icon || 'icon-placeholder'} />
) : (
<img src={icon} alt={data.name} />
)}
<Text fontSize="12px" lineHeight="1.5">
{data.title}
{iconNode}
<Text fontSize="12px" marginTop="4px">
{data.title ?? data.name}
</Text>
<Text fontSize="12px" color="gray.50">
<Text fontSize="10px" color="gray.50">
{data.name}
</Text>
{data.docs || data.help ? (
<Popover placement="right" title={data.title} content={<CommonMaterialInfoBox {...data} />}>
<Popover
zIndex={9999}
placement="right"
title={data.title}
content={<CommonMaterialInfoBox {...data} />}
>
<QuestionCircleOutlined />
</Popover>
) : null}
@@ -295,7 +447,7 @@ function MaterialGrid({ data }: MaterialProps) {
);
}
function CommonMaterialInfoBox({ help, docs }: IComponentPrototype) {
function CommonMaterialInfoBox({ help, docs }: Pick<IComponentPrototype, 'help' | 'docs'>) {
return (
<Box maxWidth={300}>
{!!help && <Box mb="m">{help}</Box>}
+1 -1
View File
@@ -74,7 +74,7 @@ export interface SidebarPanelItemProps
widgetProps?: object;
}
function BaseSidebarPanel({ panelWidth: defaultPanelWidth = 280, footer, children }: SidebarProps) {
function BaseSidebarPanel({ panelWidth: defaultPanelWidth = 266, footer, children }: SidebarProps) {
const designer = useDesigner();
const items = useMemo(() => {
@@ -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) => {
+21 -176
View File
@@ -1,13 +1,13 @@
import React from 'react';
import styled, { css, keyframes } from 'styled-components';
import { Box, Button, Group, HTMLCoralProps } from 'coral-system';
import { Dropdown, DropdownProps, Tooltip } from 'antd';
import { Tooltip } from 'antd';
import { HolderOutlined, InfoCircleOutlined, PlusOutlined } from '@ant-design/icons';
import { ISelectedItemData, isString, noop } from '@music163/tango-helpers';
import { observer, useDesigner, useWorkspace } from '@music163/tango-context';
import { IconFont } from '@music163/tango-ui';
import { getDragGhostElement } from '../helpers';
import { getWidget } from '../widgets';
import { ComponentsPopover } from '../components';
/**
* 选择辅助工具的对齐方式
@@ -24,7 +24,9 @@ export interface SelectionToolsProps {
}
export const SelectionTools = observer(
({ actions: actionsProp = ['viewSource', 'copyNode', 'deleteNode'] }: SelectionToolsProps) => {
({
actions: actionsProp = ['selectParentNode', 'viewSource', 'copyNode', 'deleteNode'],
}: SelectionToolsProps) => {
const workspace = useWorkspace();
const selectSource = workspace.selectSource;
const actions = actionsProp.map((item) => {
@@ -79,13 +81,6 @@ const bottomAddSiblingBtnStyle = css`
pointer-events: auto;
`;
interface IInsertedData {
name: string;
label: string;
icon: string;
description: string;
}
export interface SelectionBoxProps {
/**
* 是否显示操作按钮
@@ -117,36 +112,6 @@ function SelectionBox({ showActions, actions, data }: SelectionBoxProps) {
const prototype = workspace.componentPrototypes.get(data.name);
const isPage = prototype?.type === 'page';
// 如果声明了 childrenName,提供快捷子元素创建入口
let insertedList: IInsertedData[] = [];
if (prototype?.childrenName) {
const names = Array.isArray(prototype?.childrenName)
? prototype.childrenName
: [prototype.childrenName];
insertedList = names.map((child) => {
const proto = workspace.componentPrototypes.get(child);
return {
name: child,
label: proto?.title || child,
icon: proto?.icon,
description: proto?.help,
};
});
}
let siblingList: IInsertedData[] = [];
if (prototype?.siblingNames) {
siblingList = prototype.siblingNames?.map((item) => {
const proto = workspace.componentPrototypes.get(item);
return {
name: item,
label: proto?.title || item,
icon: proto?.icon,
description: proto?.help,
};
});
}
let selectionHelpersAlign: SelectionHelperAlignType = 'top-right';
if (data.bounding) {
if (data.bounding.left + data.bounding.width + boundingOffset < designer.viewport.width) {
@@ -159,6 +124,7 @@ function SelectionBox({ showActions, actions, data }: SelectionBoxProps) {
}
const isFromCurrentFile = data.filename === workspace.activeViewFile;
const selectedNodeName = workspace.selectSource?.selected?.[0]?.codeId ?? '未选中';
let style: React.CSSProperties;
if (data.bounding) {
@@ -183,32 +149,18 @@ function SelectionBox({ showActions, actions, data }: SelectionBoxProps) {
css={selectionBoxStyle}
style={style}
>
{siblingList.length > 0 ? (
<>
<InsertedDropdown
title="在当前节点的前方添加兄弟节点"
options={siblingList}
onSelect={(name) => {
workspace.insertBeforeSelectedNode(name);
}}
>
<Tooltip title="在当前节点的前方添加兄弟节点">
<SelectionHelper icon={<PlusOutlined />} css={topAddSiblingBtnStyle} />
</Tooltip>
</InsertedDropdown>
<InsertedDropdown
title="在当前节点的后方添加兄弟节点"
options={siblingList}
onSelect={(name) => {
workspace.insertAfterSelectedNode(name);
}}
>
<Tooltip title="在当前节点的后方添加兄弟节点">
<SelectionHelper icon={<PlusOutlined />} css={bottomAddSiblingBtnStyle} />
</Tooltip>
</InsertedDropdown>
</>
) : null}
<>
<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>
</>
{showActions && (
<SelectionHelpers align={selectionHelpersAlign}>
<SelectionHelper
@@ -246,17 +198,12 @@ function SelectionBox({ showActions, actions, data }: SelectionBoxProps) {
}
/>
<SelectionToolSet>{!isPage && actions}</SelectionToolSet>
{insertedList.length > 0 && (
<InsertedDropdown
options={insertedList}
onSelect={(name) => {
workspace.insertToSelectedNode(name);
}}
>
{isFromCurrentFile && prototype?.hasChildren !== false && (
<ComponentsPopover>
<Tooltip title="快捷添加子元素">
<SelectionHelper icon={<PlusOutlined />} />
</Tooltip>
</InsertedDropdown>
</ComponentsPopover>
)}
</SelectionHelpers>
)}
@@ -429,105 +376,3 @@ const NameSelector = ({ label, parents = [], onSelect = noop }: NameSelectorProp
</NameSelectorWrapper>
);
};
interface InsertedDropdownProps extends DropdownProps {
title?: string;
options?: IInsertedData[];
onSelect?: (name: string) => void;
}
function InsertedDropdown({
title = '为当前节点添加子元素',
options = [],
onSelect,
...props
}: InsertedDropdownProps) {
return (
<Dropdown
trigger={['click']}
dropdownRender={() => {
return (
<Box
bg="#FFF"
borderRadius="m"
boxShadow="lowDown"
border="solid"
borderColor="line2"
overflow="hidden"
width="320px"
>
<Box px="l" py="m" color="text2">
{title}
</Box>
<Box maxHeight={360} overflowY="auto">
{options.map((item) => (
<InsertedItem
key={item.name}
label={item.label}
icon={item.icon}
description={item.description || '暂无组件描述'}
onClick={() => onSelect?.(item.name)}
/>
))}
</Box>
</Box>
);
}}
{...props}
/>
);
}
const insertedItemStyle = css`
cursor: pointer;
user-select: none;
&:hover {
background-color: var(--tango-colors-fill2);
}
`;
function InsertedItem({
label,
icon,
description,
...rest
}: HTMLCoralProps<'div'> & Omit<IInsertedData, 'name'>) {
let iconNode;
if (!icon) {
iconNode = <IconFont className="material-icon" type="icon-placeholder" />;
} else if (icon.startsWith('icon-')) {
iconNode = <IconFont className="material-icon" type={icon} />;
} else {
iconNode = <img src={icon} alt={label} />;
}
return (
<Box
display="flex"
columnGap="m"
px="l"
py="m"
fontSize="12px"
css={insertedItemStyle}
{...rest}
>
<Box
size="35px"
fontSize="32px"
background="fill1"
border="1px solid"
borderColor="line2"
display="flex"
alignItems="center"
justifyContent="center"
>
{iconNode}
</Box>
<Box>
<Box fontWeight="500">{label}</Box>
<Box color="text2">{description}</Box>
</Box>
</Box>
);
}
+1 -1
View File
@@ -17,7 +17,7 @@ export const PreviewTool = observer(() => {
designer.setActiveView('design');
}
}}
tooltip="预览"
tooltip={designer.isPreview ? '切换到设计模式' : '切换到预览模式'}
>
<EyeOutlined />
</ToggleButton>
+1 -1
View File
@@ -60,7 +60,7 @@ export function Toolbar({ children }: ToolbarProps) {
<Group display="flex" alignItems="center" gap="m">
{centerTools}
</Group>
<Group display="flex" alignItems="center" gap="m">
<Group display="flex" alignItems="center" justifyContent="flex-end" gap="m">
{rightTools}
</Group>
</Box>
+7 -1
View File
@@ -13,7 +13,12 @@ import {
OutlinePanel,
VariablePanel,
} from './sidebar';
import { CopyNodeAction, DeleteNodeAction, ViewSourceAction } from './selection-menu';
import {
CopyNodeAction,
DeleteNodeAction,
ViewSourceAction,
SelectParentNodeAction,
} from './selection-menu';
const widgets = {};
@@ -48,4 +53,5 @@ registerWidget('sidebar.dataSource', DataSourcePanel);
registerWidget('selectionMenu.copyNode', CopyNodeAction);
registerWidget('selectionMenu.deleteNode', DeleteNodeAction);
registerWidget('selectionMenu.selectParentNode', SelectParentNodeAction);
registerWidget('selectionMenu.viewSource', ViewSourceAction);
+3
View File
@@ -2,6 +2,7 @@ import React from 'react';
import { Box } from 'coral-system';
import { observer, useDesigner } from '@music163/tango-context';
import { DesignerViewType } from '@music163/tango-core';
import { ComponentsPopover } from './components';
export interface WorkspaceViewProps {
/**
@@ -27,6 +28,8 @@ export const WorkspaceView = observer((props: WorkspaceViewProps) => {
position="relative"
>
{children}
{/* 添加组件弹层 */}
{display === 'block' && <ComponentsPopover type="inner" isControlled />}
</Box>
);
});
+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.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
- support add components with popover ([#155](https://github.com/netease/tango/issues/155)) ([f17ccbb](https://github.com/netease/tango/commit/f17ccbb7f645f8047ecd96d9f3f2185048a3b726))
# [1.1.0](https://github.com/netease/tango/compare/@music163/tango-helpers@1.0.0...@music163/tango-helpers@1.1.0) (2024-05-17)
### Bug Fixes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@music163/tango-helpers",
"version": "1.1.0",
"version": "1.1.1",
"description": "Shared types, helpers, and hooks of tango-apps",
"keywords": [
"shared",
+17
View File
@@ -3,6 +3,7 @@
*/
import { OptionType } from './advanced';
import { PartialRecord } from './base';
/**
* @deprecated 请使用 IComponentProp 代替
@@ -327,3 +328,19 @@ export interface ITangoConfigJson {
autoGenerateComponentId: boolean;
};
}
/**
* 物料类型
common: '基础组件',
atom: '原子组件',
snippet: '组合',
bizComp: '业务组件',
localComp: '本地组件',
*/
export type MenuKeyType = 'common' | 'atom' | 'snippet' | 'bizComp' | 'localComp';
export type MenuValueType = Array<{ title: string; items: string[] }>;
/**
* 菜单项类型
*/
export type MenuDataType = PartialRecord<MenuKeyType, MenuValueType>;
+4
View File
@@ -3,6 +3,10 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.0.4](https://github.com/netease/tango/compare/@music163/tango-sandbox@1.0.3...@music163/tango-sandbox@1.0.4) (2024-05-21)
**Note:** Version bump only for package @music163/tango-sandbox
## [1.0.3](https://github.com/netease/tango/compare/@music163/tango-sandbox@1.0.2...@music163/tango-sandbox@1.0.3) (2024-05-17)
**Note:** Version bump only for package @music163/tango-sandbox
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@music163/tango-sandbox",
"version": "1.0.3",
"version": "1.0.4",
"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.1.0",
"@music163/tango-helpers": "^1.1.0",
"@music163/tango-core": "^1.2.0",
"@music163/tango-helpers": "^1.1.1",
"crypto-js": "^4.1.1",
"lodash.isequal": "4.5.0",
"react-frame-component": "^5.2.4"
+19
View File
@@ -3,6 +3,25 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.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)
**Note:** Version bump only for package @music163/tango-setting-form
## [1.2.1](https://github.com/netease/tango/compare/@music163/tango-setting-form@1.2.0...@music163/tango-setting-form@1.2.1) (2024-05-21)
### Bug Fixes
- enhance code value validate in SettingForm ([#152](https://github.com/netease/tango/issues/152)) ([791fbb1](https://github.com/netease/tango/commit/791fbb162a7147243924e01f54e9c0b586f14438))
- prototype2code & go back history error ([#156](https://github.com/netease/tango/issues/156)) ([8bf53a7](https://github.com/netease/tango/commit/8bf53a76f8a71eaf261ea68b9ee44e5bf19893aa))
# [1.2.0](https://github.com/netease/tango/compare/@music163/tango-setting-form@1.1.0...@music163/tango-setting-form@1.2.0) (2024-05-17)
### Bug Fixes
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@music163/tango-setting-form",
"version": "1.2.0",
"version": "1.2.4",
"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.1.0",
"@music163/tango-helpers": "^1.1.0",
"@music163/tango-ui": "^1.1.0",
"@music163/tango-core": "^1.2.0",
"@music163/tango-helpers": "^1.1.1",
"@music163/tango-ui": "^1.2.3",
"antd": "^4.24.2",
"coral-system": "^1.0.5",
"mobx": "6.12.3",
+21 -5
View File
@@ -88,7 +88,12 @@ function parseFieldValue(fieldValue: any) {
const isCodeString = isString(fieldValue) && isWrappedCode(fieldValue);
if (isCodeString) {
code = getCodeOfWrappedCode(fieldValue);
value = code2value(code);
try {
// 避免 code 报错的情况
value = code2value(code);
} catch (err) {
// do nothing
}
} else {
code = value2code(fieldValue);
value = fieldValue;
@@ -188,7 +193,7 @@ export function createFormItem(options: IFormItemCreateOptions) {
extra,
footer,
noStyle,
validate,
validate = options.validate,
}: FormItemProps) {
const { disableSwitchExpressionSetter, showItemSubtitle } = useFormVariable();
const model = useFormModel();
@@ -202,14 +207,15 @@ export function createFormItem(options: IFormItemCreateOptions) {
});
field.setConfig({
validate: validate || options.validate,
validate: setter === 'codeSetter' ? getSetter('codeSetter').config.validate : validate,
});
let baseComponentProps: FormItemComponentProps = {
value: setterValue,
defaultValue,
onChange(value, detail) {
onChange(value, detail = {}) {
if ((setterType === 'code' || isCodeSetter) && isString(value) && value) {
detail.rawCode = value; // 在 detail 中记录原始的 code
value = wrapCode(value);
}
field.setValue(value, detail);
@@ -296,14 +302,24 @@ export function createFormItem(options: IFormItemCreateOptions) {
// 已注册的 setter 查找表
const REGISTERED_FORM_ITEM_MAP: Record<string, ReturnType<typeof createFormItem>> = {};
/**
* 获取已注册的 setter
* @param name
* @returns
*/
export function getSetter(name: string) {
return REGISTERED_FORM_ITEM_MAP[name];
}
/**
* Setter 注册
* @param config 注册选项
*/
export function register(config: IFormItemCreateOptions) {
// 允许直接覆盖同名 setter
REGISTERED_FORM_ITEM_MAP[config.name] = createFormItem(config);
(Array.isArray(config.alias) ? config.alias : []).forEach((alias) => {
REGISTERED_FORM_ITEM_MAP[alias] = REGISTERED_FORM_ITEM_MAP[config.name];
REGISTERED_FORM_ITEM_MAP[alias] = getSetter(config.name);
});
}
+13 -5
View File
@@ -249,14 +249,22 @@ interface FormHeaderProps {
export function FormHeader({ title, extra, subTitle }: FormHeaderProps) {
return (
<Box className="FormHeader">
<Box display="flex" alignItems="center">
<Box flex="1" display="flex" alignItems="center">
<Box fontSize="16px" fontWeight="500" mr="s">
<Box display="flex" alignItems="center" className="FormHeaderMain">
<Box flex="1" display="flex" alignItems="center" className="FormHeaderMainBody">
<Box
fontSize="16px"
fontWeight="500"
mr="s"
whiteSpace="nowrap"
className="FormHeaderTitle"
textOverflow="ellipsis"
overflow="hidden"
>
{title}
</Box>
{subTitle && <Box>{subTitle}</Box>}
{subTitle && <Box className="FormHeaderSubTitle">{subTitle}</Box>}
</Box>
{extra && <Box>{extra}</Box>}
{extra && <Box className="FormHeaderExtra">{extra}</Box>}
</Box>
</Box>
);
+1
View File
@@ -235,6 +235,7 @@ export function SettingForm({
icon={<QuestionCircleOutlined />}
tooltip="查看组件文档"
href={prototype.docs}
size="small"
/>
) : null
}
@@ -38,7 +38,18 @@ const BASIC_SETTERS: IFormItemCreateOptions[] = [
},
];
let registered = false;
/**
* 注册内置的基础类型 Setter
*/
export function registerBuiltinSetters() {
if (registered) {
// 防止重复注册
return;
}
// 预注册基础 Setter
BASIC_SETTERS.forEach(register);
registered = true;
}
+1 -1
View File
@@ -1,4 +1,4 @@
export interface ISetterOnChangeCallbackDetail {
relatedImports?: string[];
isRawCode?: boolean;
rawCode?: string;
}
+26
View File
@@ -3,6 +3,32 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [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)
**Note:** Version bump only for package @music163/tango-ui
# [1.2.0](https://github.com/netease/tango/compare/@music163/tango-ui@1.1.0...@music163/tango-ui@1.2.0) (2024-05-21)
### Bug Fixes
- support add components with popover ([#155](https://github.com/netease/tango/issues/155)) ([f17ccbb](https://github.com/netease/tango/commit/f17ccbb7f645f8047ecd96d9f3f2185048a3b726))
### Features
- event-setter & expression-setter use popover ([#159](https://github.com/netease/tango/issues/159)) ([9daf387](https://github.com/netease/tango/commit/9daf3872e743fcf706184877020bcbf1f75ffd25))
# [1.1.0](https://github.com/netease/tango/compare/@music163/tango-ui@1.0.3...@music163/tango-ui@1.1.0) (2024-05-17)
### Features
+4 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@music163/tango-ui",
"version": "1.1.0",
"version": "1.2.3",
"description": "ui widgets of tango",
"keywords": [
"react",
@@ -38,14 +38,15 @@
"@codemirror/lang-javascript": "^6.2.2",
"@codemirror/lint": "^6.7.1",
"@codemirror/search": "^6.5.6",
"@music163/tango-helpers": "^1.1.0",
"@music163/tango-helpers": "^1.1.1",
"@uiw/react-codemirror": "^4.22.0",
"antd": "^4.24.2",
"classnames": "^2.5.1",
"coral-system": "^1.0.5",
"eslint-linter-browserify": "^8.51.0",
"react-draggable": "^4.4.5",
"react-json-view": "^1.21.3",
"react-monaco-editor-lite": "^1.3.9"
"react-monaco-editor-lite": "^1.3.11"
},
"publishConfig": {
"access": "public",
+1 -1
View File
@@ -4,7 +4,7 @@ import { Button, Dropdown, Input, Menu } from 'antd';
import { DownOutlined, PlusSquareOutlined } from '@ant-design/icons';
type ActionOptionType = {
label?: string;
label?: string | React.ReactNode;
value?: string;
[key: string]: any;
};
+1
View File
@@ -14,6 +14,7 @@ const actionStyle = css`
border-radius: var(--tango-radii-s);
color: var(--tango-colors-text2);
background-color: transparent;
white-space: nowrap;
&:hover {
background-color: var(--tango-colors-fill2);
+6 -2
View File
@@ -84,7 +84,9 @@ export function CollapsePanel(props: CollapsePanelProps) {
borderBottom: 'solid',
borderBottomColor: 'line2',
}
: {};
: {
border: 'none!important',
};
return (
<Box className="CollapsePanel" {...CollapsePanelBorderStyle} {...rest}>
@@ -94,7 +96,9 @@ export function CollapsePanel(props: CollapsePanelProps) {
justifyContent="space-between"
className="CollapsePanelHeader"
onClick={() => setCollapsed(!collapsed)}
p="m"
p="l"
paddingTop={'m'}
paddingBottom={'m'}
{...stickHeaderProps}
{...headerProps}
css={headerStyle}
+145
View File
@@ -0,0 +1,145 @@
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 { 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)`
cursor: pointer;
margin-left: 10px;
padding: 2px;
font-size: 13px;
&:hover {
color: var(--tango-colors-text1);
background-color: var(--tango-colors-line1);
border-radius: 4px;
}
`;
interface DragPanelProps extends Omit<PopoverProps, 'overlay' | 'open'> {
// 标题
title?: React.ReactNode | string;
// 内容
body?: React.ReactNode | string;
// 底部
footer?: ((close: () => void) => React.ReactNode) | React.ReactNode | string;
// 宽度
width?: number | string;
// 右上角区域
extra?: React.ReactNode | string;
children?: React.ReactNode;
}
export function DragPanel({
title,
footer,
body,
children,
width = 330,
extra,
onOpenChange = noop,
...props
}: DragPanelProps) {
const [open, setOpen] = useState(false);
const footerNode =
typeof footer === 'function'
? footer(() => {
setOpen(false);
onOpenChange(false);
})
: footer;
return (
<Popover
open={open}
onOpenChange={(innerOpen) => {
setOpen(innerOpen);
onOpenChange(innerOpen);
}}
overlay={
<Draggable
handle=".selection-drag-bar"
onStart={() => {
injectStyleToBody();
}}
>
<Box
bg="#FFF"
borderRadius="m"
boxShadow="lowDown"
border="solid"
borderColor="line2"
overflow="hidden"
width={width}
>
{/* 头部区域 */}
<Box
px="l"
py="m"
className="selection-drag-bar"
borderBottom="1px solid var(--tango-colors-line2)"
cursor="move"
display="flex"
justifyContent="space-between"
>
<Box fontSize="12px" color="text2">
<IconFont type="icon-applications" />
<Text marginLeft={'5px'}>{title}</Text>
</Box>
<Box color="text2" fontSize="12px" display="flex" alignItems="center">
{extra}
<CloseIcon
onClick={() => {
setOpen(false);
onOpenChange(false);
}}
/>
</Box>
</Box>
{/* 主体区域 */}
{body}
{/* 底部 */}
{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>
)}
</Box>
</Draggable>
}
{...props}
>
{children}
</Popover>
);
}
+2
View File
@@ -22,3 +22,5 @@ export * from './tabs';
export * from './select-action';
export * from './copy-clipboard';
export * from './tag-select';
export * from './popover';
export * from './drag-panel';
+1 -1
View File
@@ -1,5 +1,5 @@
import React, { useMemo } from 'react';
import { Box, HTMLCoralProps, css } from 'coral-system';
import { Box, HTMLCoralProps } from 'coral-system';
import CodeMirror, { ReactCodeMirrorProps } from '@uiw/react-codemirror';
import { javascript, javascriptLanguage, esLint } from '@codemirror/lang-javascript';
import { CompletionContext } from '@codemirror/autocomplete';
+171
View File
@@ -0,0 +1,171 @@
import React, { useState, useRef, useEffect, useMemo, useCallback, useLayoutEffect } from 'react';
import ReactDOM from 'react-dom';
import { noop } from '@music163/tango-helpers';
import { Box } from 'coral-system';
export interface PopoverProps {
open?: boolean;
/**
* 浮层内容
*/
overlay: React.ReactNode;
/**
* 浮层打开或关闭时的回调
*/
onOpenChange?: (open: boolean) => void;
/**
* 浮层被遮挡时自动调整位置
*/
autoAdjustOverflow?: boolean;
/**
* 点击蒙层是否允许关闭
*/
maskClosable?: boolean;
/**
* 手动唤起时的位置
*/
left?: number;
/**
* 手动唤起时的位置
*/
top?: number;
/**
* z-index
*/
zIndex?: number;
/**
* popoverStyle
*/
popoverStyle?: React.CSSProperties;
children?: React.ReactNode;
}
export const Popover: React.FC<PopoverProps> = ({
open,
overlay,
maskClosable = false,
autoAdjustOverflow = true,
left: controlledLeft,
top: controlledTop,
children,
popoverStyle,
onOpenChange = noop,
zIndex = 9999,
}) => {
const [visible, setVisible] = useState(false);
const [left, setLeft] = useState(0);
const [top, setTop] = useState(0);
const popoverRef = useRef<HTMLDivElement>(null);
// 唤起位置受控
const isControlledPostion = useMemo(
() => controlledLeft !== undefined || controlledTop !== undefined,
[controlledLeft, controlledTop],
);
useEffect(() => {
if (typeof controlledTop === 'number') {
setTop(controlledTop);
}
if (typeof controlledLeft === 'number') {
setLeft(controlledLeft);
}
}, [controlledTop, controlledLeft]);
useLayoutEffect(() => {
const handleDocumentClick = (e: MouseEvent) => {
if (
maskClosable &&
visible &&
popoverRef.current &&
!popoverRef.current.contains(e.target as Node)
) {
setVisible(false);
onOpenChange(false);
}
};
if (maskClosable && visible) {
document.addEventListener('click', handleDocumentClick, true);
}
return () => {
document.removeEventListener('click', handleDocumentClick);
};
}, [maskClosable, onOpenChange, visible]);
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);
setTop(y + 10);
setVisible(true);
onOpenChange(true);
},
[visible, onOpenChange],
);
useEffect(() => {
setVisible(open);
}, [open]);
const getAdjustedPosition = () => {
const popoverElement = popoverRef.current;
if (popoverElement) {
const popoverRect = popoverElement.getBoundingClientRect();
if (popoverRect.right > window.innerWidth) {
setLeft(window.innerWidth - popoverRect.width);
}
if (popoverRect.bottom > window.innerHeight) {
setTop(window.innerHeight - popoverRect.height);
}
}
};
useEffect(() => {
if (visible && autoAdjustOverflow) {
getAdjustedPosition();
}
}, [visible, autoAdjustOverflow]);
const overlayStyle: React.CSSProperties = useMemo(
() => ({
display: visible ? 'block' : 'none',
position: 'fixed',
left,
top,
zIndex,
...popoverStyle,
}),
[left, popoverStyle, top, visible, zIndex],
);
const overlayDom = (
<Box className="popover">
<Box ref={popoverRef} className="overlay" style={overlayStyle}>
{overlay}
</Box>
</Box>
);
return (
<>
{!isControlledPostion &&
React.cloneElement(children as any, {
onClick: (e: React.MouseEvent) => {
e.stopPropagation();
handleClick(e);
(children as any).props?.onClick?.(e);
},
})}
{visible ? ReactDOM.createPortal(overlayDom, document.body) : null}
</>
);
};
+1 -1
View File
@@ -91,7 +91,7 @@ const buttonStyle = css`
background-color: var(--tango-colors-custom-toolbarButtonBg, rgba(223, 223, 223, 0.08));
color: var(--tango-colors-custom-toolbarButtonTextColor, #999);
border: 0;
border-radius: var(--tango-radii-m);
border-radius: var(--tango-radii-s);
&:hover {
color: var(--tango-colors-custom-toolbarButtonTextColorHover, #fff);
+8 -33
View File
@@ -16101,7 +16101,7 @@ react-dom@^17.0.0:
object-assign "^4.1.1"
scheduler "^0.20.2"
react-draggable@^4.0.3:
react-draggable@^4.0.3, react-draggable@^4.4.5:
version "4.4.6"
resolved "https://registry.npmmirror.com/react-draggable/-/react-draggable-4.4.6.tgz"
integrity sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw==
@@ -16195,10 +16195,10 @@ react-merge-refs@^1.1.0:
resolved "https://registry.npmmirror.com/react-merge-refs/-/react-merge-refs-1.1.0.tgz"
integrity sha512-alTKsjEL0dKH/ru1Iyn7vliS2QRcBp9zZPGoWxUOvRGWPUYgjo+V01is7p04It6KhgrzhJGnIj9GgX8W4bZoCQ==
react-monaco-editor-lite@^1.3.9:
version "1.3.9"
resolved "https://registry.npmmirror.com/react-monaco-editor-lite/-/react-monaco-editor-lite-1.3.9.tgz#377a2126f2a26de7e478323c70b13a50f0c5c760"
integrity sha512-pE6CydX/kkisHArbV8GE+TvhukIiT9YYUYBq/cnDL7tfwhX/h1tKQKMJwItnkJvjozaB5uS+oK77GWV874figQ==
react-monaco-editor-lite@^1.3.11:
version "1.3.11"
resolved "https://registry.npmmirror.com/react-monaco-editor-lite/-/react-monaco-editor-lite-1.3.11.tgz#b81b681d23f3ca46f54d4e01f13781bcff6a4a03"
integrity sha512-+9xlIn98Yp2hD8OFjRNpQiBx+wLFzsvvT5EgNTxReFrDhFAPPjivdLUCiex1ktzpkGTdo3fxDFUokckvO7hVvQ==
dependencies:
monaco-editor "^0.38.0"
monaco-editor-textmate "^4.0.0"
@@ -17697,16 +17697,7 @@ string-length@^4.0.1:
char-regex "^1.0.2"
strip-ansi "^6.0.0"
"string-width-cjs@npm:string-width@^4.2.0":
version "4.2.3"
resolved "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3:
"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -17819,7 +17810,7 @@ string_decoder@~1.1.1:
dependencies:
safe-buffer "~5.1.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -17833,13 +17824,6 @@ strip-ansi@^3.0.1:
dependencies:
ansi-regex "^2.0.0"
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^7.0.1, strip-ansi@^7.1.0:
version "7.1.0"
resolved "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-7.1.0.tgz"
@@ -19376,7 +19360,7 @@ workerpool@^9.1.1:
resolved "https://registry.npmmirror.com/workerpool/-/workerpool-9.1.1.tgz#9ba4d534a79a5517c1e1b9d1014151516829be8d"
integrity sha512-EFoFTSEo9m4V4wNrwzVRjxnf/E/oBpOzcI/R5CIugJhl9RsCiq525rszo4AtqcjQQoqFdu2E3H82AnbtpaQHvg==
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@@ -19394,15 +19378,6 @@ wrap-ansi@^6.0.1:
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^8.1.0:
version "8.1.0"
resolved "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz"