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 = () => ( + + + The sales intelligence platform that helps you uncover qualified leads. + + + 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 ( -
- your input: - copy input: -
-
- - - +
+ your input: + copy input: +
+
+ + +
+
+ + + + +
); } } - 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 (