diff --git a/apps/playground/.umirc.ts b/apps/playground/.umirc.ts
index 32fdc3cf..33d840fe 100644
--- a/apps/playground/.umirc.ts
+++ b/apps/playground/.umirc.ts
@@ -7,6 +7,7 @@ const resolvePackageIndex = (relativeEntry: string) =>
export default defineConfig({
routes: [
{ path: '/', component: 'index' },
+ { path: '/mail', component: 'mail' },
{ path: '/docs', component: 'docs' },
],
npmClient: 'yarn',
diff --git a/apps/playground/src/helpers/index.tsx b/apps/playground/src/helpers/index.tsx
index 2994fa76..431b0d18 100644
--- a/apps/playground/src/helpers/index.tsx
+++ b/apps/playground/src/helpers/index.tsx
@@ -136,7 +136,7 @@ const SnippetButtonGroup: ComponentPrototypeType = {
relatedImports: ['Space', 'Button'],
};
-export const prototypes = {
+export const extendPrototypes = {
CtPcToggleButton: bizToggleButtonPrototype,
SnippetSuccessResult,
Snippet2ColumnLayout,
diff --git a/apps/playground/src/helpers/mail-files.ts b/apps/playground/src/helpers/mail-files.ts
new file mode 100644
index 00000000..0b9271d7
--- /dev/null
+++ b/apps/playground/src/helpers/mail-files.ts
@@ -0,0 +1,180 @@
+const packageJson = {
+ name: 'demo',
+ private: true,
+ dependencies: {
+ '@music163/tango-mail': '0.1.1',
+ '@music163/tango-boot': '0.2.5',
+ react: '17.0.2',
+ 'react-dom': '17.0.2',
+ 'prop-types': '15.7.2',
+ tslib: '2.5.0',
+ },
+};
+
+const tangoConfigJson = {
+ designerConfig: {
+ autoGenerateComponentId: true,
+ },
+ packages: {
+ react: {
+ version: '17.0.2',
+ library: 'React',
+ type: 'dependency',
+ resources: ['https://unpkg.com/react@{{version}}/umd/react.development.js'],
+ },
+ 'react-dom': {
+ version: '17.0.2',
+ library: 'ReactDOM',
+ type: 'dependency',
+ resources: ['https://unpkg.com/react-dom@{{version}}/umd/react-dom.development.js'],
+ },
+ 'react-is': {
+ version: '16.13.1',
+ library: 'ReactIs',
+ type: 'dependency',
+ resources: ['https://unpkg.com/react-is@{{version}}/umd/react-is.production.min.js'],
+ },
+ '@music163/tango-boot': {
+ description: '云音乐低代码运行时框架',
+ version: '0.2.5',
+ library: 'TangoBoot',
+ type: 'baseDependency',
+ resources: ['https://unpkg.com/@music163/tango-boot@{{version}}/dist/boot.js'],
+ // resources: ['http://localhost:9001/boot.js'],
+ },
+ '@music163/tango-mail': {
+ description: 'TangoMail 基础物料',
+ version: '0.1.1',
+ library: 'TangoMail',
+ type: 'baseDependency',
+ resources: ['https://unpkg.com/@music163/tango-mail@{{version}}/dist/index.js'],
+ designerResources: ['https://unpkg.com/@music163/tango-mail@{{version}}/dist/designer.js'],
+ },
+ },
+};
+
+const routesCode = `
+import Mail from "./pages/mail";
+
+const routes = [
+ {
+ path: '/',
+ exact: true,
+ component: Mail,
+ },
+];
+
+export default routes;
+`;
+
+const entryCode = `
+import { runApp } from '@music163/tango-boot';
+import routes from './routes';
+
+runApp({
+ boot: {
+ mountElement: document.querySelector('#root'),
+ qiankun: false,
+ },
+
+ router: {
+ type: 'browser',
+ config: routes,
+ },
+});
+`;
+
+const viewHomeCode = `
+import React from 'react';
+import {
+ Body,
+ Button,
+ Container,
+ Head,
+ Hr,
+ Html,
+ Preview,
+ Section,
+ Text,
+} from '@music163/tango-mail';
+
+const WelcomeEmail = () => (
+
+
+
+ Hi wells,
+
+ Welcome to Koala, the sales intelligence platform that helps you uncover qualified leads
+ and close deals faster.
+
+
+
+ Best,
+
+ The Koala team
+
+
+ 470 Noor Ave STE B #1148, South San Francisco, CA 94080
+
+
+
+);
+
+const main = {
+ backgroundColor: '#ffffff',
+ fontFamily:
+ '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif',
+};
+
+const container = {
+ margin: '0 auto',
+ padding: '20px 0 48px',
+};
+
+const paragraph = {
+ fontSize: '16px',
+ lineHeight: '26px',
+};
+
+const btnContainer = {
+ textAlign: 'center',
+};
+
+const button = {
+ backgroundColor: '#5F51E8',
+ borderRadius: '3px',
+ color: '#fff',
+ fontSize: '16px',
+ textDecoration: 'none',
+ textAlign: 'center',
+ display: 'block',
+ padding: '12px',
+};
+
+const hr = {
+ borderColor: '#cccccc',
+ margin: '20px 0',
+};
+
+const footer = {
+ color: '#8898aa',
+ fontSize: '12px',
+};
+
+export default WelcomeEmail;
+`;
+
+export const mailFiles = [
+ { filename: '/package.json', code: JSON.stringify(packageJson) },
+ { filename: '/tango.config.json', code: JSON.stringify(tangoConfigJson) },
+ { filename: '/README.md', code: '# readme' },
+ { filename: '/src/index.js', code: entryCode },
+ { filename: '/src/pages/mail.js', code: viewHomeCode },
+ { filename: '/src/routes.js', code: routesCode },
+];
diff --git a/apps/playground/src/helpers/mock-files.ts b/apps/playground/src/helpers/mock-files.ts
index d223dc11..5ed38c05 100644
--- a/apps/playground/src/helpers/mock-files.ts
+++ b/apps/playground/src/helpers/mock-files.ts
@@ -149,29 +149,34 @@ import {
Button,
Input,
FormilyForm,
+ FormilyFormItem,
} from "@music163/antd";
-import { Space } from '@music163/antd';
-import { LocalButton } from '../components';
-
+import { Space } from "@music163/antd";
+import { LocalButton } from "../components";
class App extends React.Component {
render() {
return (
-
-
);
}
}
-
export default definePage(App);
`;
diff --git a/apps/playground/src/pages/index.tsx b/apps/playground/src/pages/index.tsx
index a8c65eda..f86c7e45 100644
--- a/apps/playground/src/pages/index.tsx
+++ b/apps/playground/src/pages/index.tsx
@@ -15,7 +15,13 @@ import {
themeLight,
} from '@music163/tango-designer';
import { createEngine, Workspace } from '@music163/tango-core';
-import { Logo, ProjectDetail, bootHelperVariables, sampleFiles } from '../helpers';
+import {
+ Logo,
+ ProjectDetail,
+ bootHelperVariables,
+ extendPrototypes,
+ sampleFiles,
+} from '../helpers';
import {
ApiOutlined,
AppstoreAddOutlined,
@@ -29,6 +35,7 @@ import {
const workspace = new Workspace({
entry: '/src/index.js',
files: sampleFiles,
+ prototypes: extendPrototypes,
});
// 2. 引擎初始化
@@ -49,12 +56,39 @@ createFromIconfontCN({
scriptUrl: '//at.alicdn.com/t/c/font_2891794_cou9i7556tl.js',
});
+const menuData = {
+ common: [
+ {
+ title: '基本',
+ items: [
+ 'Button',
+ 'Section',
+ 'Columns',
+ 'Column',
+ 'Box',
+ 'Space',
+ 'Typography',
+ 'Title',
+ 'Paragraph',
+ ],
+ },
+ {
+ title: '输入',
+ items: ['Input', 'InputNumber', 'Select'],
+ },
+ {
+ title: 'Formily表单',
+ items: ['FormilyForm', 'FormilyFormItem', 'FormilySubmit', 'FormilyReset'],
+ },
+ ],
+};
+
/**
* 5. 平台初始化,访问 https://local.netease.com:6006/
*/
export default function App() {
const [menuLoading, setMenuLoading] = useState(true);
- const [menuData, setMenuData] = useState(false);
+ // const [menuData, setMenuData] = useState(false);
return (
}
widgetProps={{
- menuData: menuData as any,
+ menuData,
loading: menuLoading,
}}
/>
@@ -120,10 +154,23 @@ export default function App() {
if (e.type === 'done') {
const sandboxWindow: any = sandboxQuery.window;
if (sandboxWindow.TangoAntd) {
- if (sandboxWindow.TangoAntd.menuData) {
- setMenuData(sandboxWindow.TangoAntd.menuData);
- }
+ // if (sandboxWindow.TangoAntd.menuData) {
+ // setMenuData(sandboxWindow.TangoAntd.menuData);
+ // }
if (sandboxWindow.TangoAntd.prototypes) {
+ sandboxWindow.TangoAntd.prototypes['Section'].siblingNames = [
+ 'SnippetButtonGroup',
+ 'Section',
+ 'Section',
+ 'Section',
+ 'Section',
+ 'Section',
+ 'Section',
+ 'Section',
+ ];
+ sandboxWindow.TangoAntd.prototypes['FormilyFormItem'].siblingNames = [
+ 'FormilyFormItem',
+ ];
workspace.setComponentPrototypes(sandboxWindow.TangoAntd.prototypes);
}
}
diff --git a/apps/playground/src/pages/mail.tsx b/apps/playground/src/pages/mail.tsx
new file mode 100644
index 00000000..9cbd9b4e
--- /dev/null
+++ b/apps/playground/src/pages/mail.tsx
@@ -0,0 +1,134 @@
+import React, { useState } from 'react';
+import { Box } from 'coral-system';
+import { Button, Space } from 'antd';
+import {
+ Designer,
+ DesignerPanel,
+ SettingPanel,
+ Sidebar,
+ Toolbar,
+ WorkspacePanel,
+ WorkspaceView,
+ CodeEditor,
+ Sandbox,
+ DndQuery,
+ themeLight,
+} from '@music163/tango-designer';
+import { createEngine, Workspace } from '@music163/tango-core';
+import { Logo, ProjectDetail, bootHelperVariables } from '@/helpers';
+import {
+ AppstoreAddOutlined,
+ BuildOutlined,
+ ClusterOutlined,
+ createFromIconfontCN,
+} from '@ant-design/icons';
+import { mailFiles } from '@/helpers/mail-files';
+
+// 1. 实例化工作区
+const workspace = new Workspace({
+ entry: '/src/index.js',
+ files: mailFiles,
+});
+
+// 2. 引擎初始化
+const engine = createEngine({
+ workspace,
+});
+
+// @ts-ignore
+window.__mailWorkspace__ = workspace;
+
+// 3. 沙箱初始化
+const sandboxQuery = new DndQuery({
+ context: 'iframe',
+});
+
+// 4. 图标库初始化(物料面板和组件树使用了 iconfont 里的图标)
+createFromIconfontCN({
+ scriptUrl: '//at.alicdn.com/t/c/font_2891794_cou9i7556tl.js',
+});
+
+/**
+ * 5. 平台初始化,访问 https://local.netease.com:6006/
+ */
+export default function App() {
+ const [menuLoading, setMenuLoading] = useState(true);
+ const [menuData, setMenuData] = useState(false);
+ return (
+
+ }
+ description={}
+ actions={
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+ >
+
+ }
+ widgetProps={{
+ menuData: menuData as any,
+ loading: menuLoading,
+ }}
+ />
+ } />
+ }
+ isFloat
+ width={800}
+ />
+
+
+
+ {
+ if (e.type === 'done') {
+ const sandboxWindow: any = sandboxQuery.window;
+ if (sandboxWindow.TangoMail) {
+ if (sandboxWindow.TangoMail.menuData) {
+ setMenuData(sandboxWindow.TangoMail.menuData);
+ }
+ if (sandboxWindow.TangoMail.prototypes) {
+ workspace.setComponentPrototypes(sandboxWindow.TangoMail.prototypes);
+ }
+ }
+ setMenuLoading(false);
+ }
+ }}
+ navigatorExtra={}
+ />
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/packages/core/src/models/history.ts b/packages/core/src/models/history.ts
index 2bf4de6f..e0b16897 100644
--- a/packages/core/src/models/history.ts
+++ b/packages/core/src/models/history.ts
@@ -11,6 +11,8 @@ export enum HistoryMessage {
ReplaceNode = 'replaceNode',
CloneNode = 'cloneNode',
InsertNode = 'insertNode',
+ InsertBeforeNode = 'insertBeforeNode',
+ InsertAfterNode = 'insertAfterNode',
DropNode = 'dropNode',
UpdateAttribute = 'updateAttribute',
UpdateCode = 'updateCode',
diff --git a/packages/core/src/models/interfaces.ts b/packages/core/src/models/interfaces.ts
index 2d5d4c82..4262ec3c 100644
--- a/packages/core/src/models/interfaces.ts
+++ b/packages/core/src/models/interfaces.ts
@@ -205,6 +205,8 @@ export interface IWorkspace {
copySelectedNode: () => void;
pasteSelectedNode: () => void;
insertToSelectedNode: (childNameOrPrototype: string | ComponentPrototypeType) => void;
+ insertBeforeSelectedNode: (sourceNameOrPrototype: string | ComponentPrototypeType) => void;
+ insertAfterSelectedNode: (sourceNameOrPrototype: string | ComponentPrototypeType) => void;
dropNode: () => void;
insertToNode: (
targetNodeId: string,
diff --git a/packages/core/src/models/workspace.ts b/packages/core/src/models/workspace.ts
index bb597c27..f043a8eb 100644
--- a/packages/core/src/models/workspace.ts
+++ b/packages/core/src/models/workspace.ts
@@ -1022,6 +1022,46 @@ export class Workspace extends EventTarget implements IWorkspace {
}
}
+ insertBeforeSelectedNode(sourceName: string | ComponentPrototypeType) {
+ if (!sourceName) {
+ return;
+ }
+ const targetNodeId = this.selectSource.first.id;
+ const sourcePrototype = this.getPrototype(sourceName);
+ const newNode = prototype2jsxElement(sourcePrototype);
+ const file = this.getNode(targetNodeId).file;
+ const { source, specifiers } = prototype2importDeclarationData(sourcePrototype, file.filename);
+
+ file.insertBefore(targetNodeId, newNode).addImportSpecifiers(source, specifiers).update();
+
+ this.history.push({
+ message: HistoryMessage.InsertBeforeNode,
+ data: {
+ [file.filename]: file.code,
+ },
+ });
+ }
+
+ insertAfterSelectedNode(sourceName: string | ComponentPrototypeType) {
+ if (!sourceName) {
+ return;
+ }
+ const targetNodeId = this.selectSource.first.id;
+ const sourcePrototype = this.getPrototype(sourceName);
+ const newNode = prototype2jsxElement(sourcePrototype);
+ const file = this.getNode(targetNodeId).file;
+ const { source, specifiers } = prototype2importDeclarationData(sourcePrototype, file.filename);
+
+ file.insertAfter(targetNodeId, newNode).addImportSpecifiers(source, specifiers).update();
+
+ this.history.push({
+ message: HistoryMessage.InsertAfterNode,
+ data: {
+ [file.filename]: file.code,
+ },
+ });
+ }
+
updateSelectedNodeAttributes(
attributes: Record = {},
relatedImports: string[] = [],
diff --git a/packages/designer/src/components/variable-tree/index.tsx b/packages/designer/src/components/variable-tree/index.tsx
index 9ca533e0..7241788f 100644
--- a/packages/designer/src/components/variable-tree/index.tsx
+++ b/packages/designer/src/components/variable-tree/index.tsx
@@ -164,8 +164,6 @@ export function VariableTree(props: VariableTreeProps) {
const state = { activeNode, mode, setMode, clear };
- console.log(treeData);
-
return (
diff --git a/packages/designer/src/setters/expression-setter.tsx b/packages/designer/src/setters/expression-setter.tsx
index 3548278f..55d8df5b 100644
--- a/packages/designer/src/setters/expression-setter.tsx
+++ b/packages/designer/src/setters/expression-setter.tsx
@@ -81,7 +81,7 @@ export function ExpressionSetter(props: ExpressionSetterProps) {
modalTitle,
modalTip,
autoCompleteOptions,
- placeholder = '输入JS表达式代码',
+ placeholder = '输入JS代码',
value: valueProp,
status,
allowClear = true,
diff --git a/packages/designer/src/simulator/selection.tsx b/packages/designer/src/simulator/selection.tsx
index fc0930a0..33b73d61 100644
--- a/packages/designer/src/simulator/selection.tsx
+++ b/packages/designer/src/simulator/selection.tsx
@@ -2,7 +2,7 @@ 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 { PlusSquareOutlined, HolderOutlined, InfoCircleOutlined } from '@ant-design/icons';
+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';
@@ -59,6 +59,26 @@ const selectionBoxStyle = css`
top: 0;
`;
+const topAddSiblingBtnStyle = css`
+ position: absolute;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ top: 0;
+ border-radius: 32px;
+ z-index: 999;
+ pointer-events: auto;
+`;
+
+const bottomAddSiblingBtnStyle = css`
+ position: absolute;
+ left: 50%;
+ transform: translate(-50%, 50%);
+ bottom: 0;
+ border-radius: 32px;
+ z-index: 999;
+ pointer-events: auto;
+`;
+
interface IInsertedData {
name: string;
label: string;
@@ -114,6 +134,19 @@ function SelectionBox({ showActions, actions, data }: SelectionBoxProps) {
});
}
+ 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) {
@@ -150,6 +183,32 @@ function SelectionBox({ showActions, actions, data }: SelectionBoxProps) {
css={selectionBoxStyle}
style={style}
>
+ {siblingList.length > 0 ? (
+ <>
+ {
+ workspace.insertBeforeSelectedNode(name);
+ }}
+ >
+
+ } css={topAddSiblingBtnStyle} />
+
+
+ {
+ workspace.insertAfterSelectedNode(name);
+ }}
+ >
+
+ } css={bottomAddSiblingBtnStyle} />
+
+
+ >
+ ) : null}
{showActions && (
- } />
+
+ } />
+
)}
@@ -254,7 +315,13 @@ interface SelectionHelperProps extends HTMLCoralProps<'button'> {
label?: React.ReactNode;
}
-const SelectionHelper = ({ icon, label, children, ...rest }: SelectionHelperProps) => {
+const SelectionHelper = ({
+ icon,
+ label,
+ children,
+ css: customCss,
+ ...rest
+}: SelectionHelperProps) => {
return (