mirror of
https://github.com/NetEase/tango.git
synced 2026-08-31 20:06:59 +08:00
Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 22a2e9bdd1 | |||
| 696f82bd26 | |||
| a0484e8f64 | |||
| cf31ef19f4 | |||
| fe31246483 | |||
| 9daf3872e7 | |||
| f17ccbb7f6 | |||
| f854f75b8a | |||
| df3021f75e | |||
| 8bf53a76f8 | |||
| 791fbb162a | |||
| 704277b8f0 | |||
| 1fc22a4535 | |||
| ffaa276b5c | |||
| 56d0877507 | |||
| 897fe0a5a1 | |||
| b52bbad547 | |||
| f7c837a8a9 | |||
| f64d178ab8 | |||
| f2dcc942ed | |||
| ac48a8880e | |||
| 0f1711868d | |||
| 06cd3b3a18 | |||
| 2664613ab2 | |||
| e39a913a87 | |||
| 52864b5400 | |||
| 27166ec949 | |||
| 1a8d15c9da |
@@ -4,7 +4,7 @@
|
||||
"private": true,
|
||||
"author": "Wells <ww.sun@outlook.com>",
|
||||
"scripts": {
|
||||
"dev": "HOST=local.netease.com PORT=8001 umi dev",
|
||||
"dev": "cross-env HOST=local.netease.com PORT=8001 umi dev",
|
||||
"build": "umi build",
|
||||
"postinstall": "umi setup",
|
||||
"setup": "umi setup",
|
||||
@@ -15,6 +15,6 @@
|
||||
"@music163/antd": "^0.2.4",
|
||||
"antd": "^4.24.2",
|
||||
"coral-system": "^1.0.5",
|
||||
"umi": "^4.0.89"
|
||||
"umi": "^4.2.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ const packageJson = {
|
||||
name: 'demo',
|
||||
private: true,
|
||||
dependencies: {
|
||||
'@music163/antd': '0.2.2',
|
||||
'@music163/antd': '0.2.5',
|
||||
'@music163/tango-boot': '0.2.5',
|
||||
react: '17.0.2',
|
||||
'react-dom': '17.0.2',
|
||||
@@ -56,11 +56,12 @@ const tangoConfigJson = {
|
||||
},
|
||||
'@music163/antd': {
|
||||
description: '云音乐低代码中后台应用基础物料',
|
||||
version: '0.2.2',
|
||||
version: '0.2.5',
|
||||
library: 'TangoAntd',
|
||||
type: 'baseDependency',
|
||||
resources: [
|
||||
'https://unpkg.com/@music163/antd@{{version}}/dist/index.js',
|
||||
// 'http://localhost:9002/designer.js',
|
||||
'https://unpkg.com/antd@4.24.13/dist/antd.css',
|
||||
],
|
||||
designerResources: [
|
||||
@@ -85,6 +86,7 @@ export function registerComponentPrototype(proto) {
|
||||
|
||||
const routesCode = `
|
||||
import Index from "./pages/list";
|
||||
import Detail from "./pages/detail";
|
||||
|
||||
const routes = [
|
||||
{
|
||||
@@ -92,6 +94,11 @@ const routes = [
|
||||
exact: true,
|
||||
component: Index
|
||||
},
|
||||
{
|
||||
path: '/detail',
|
||||
exact: true,
|
||||
component: Detail
|
||||
},
|
||||
];
|
||||
|
||||
export default routes;
|
||||
@@ -150,20 +157,33 @@ import {
|
||||
Input,
|
||||
FormilyForm,
|
||||
FormilyFormItem,
|
||||
Table,
|
||||
} 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}>
|
||||
<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} />
|
||||
<Table
|
||||
columns={[
|
||||
{ title: "姓名", dataIndex: "name", key: "name" },
|
||||
{ title: "年龄", dataIndex: "age", key: "age" },
|
||||
{ title: "住址", dataIndex: "address", key: "address" },
|
||||
]}
|
||||
tid="table1"
|
||||
/>
|
||||
</Section>
|
||||
<Section tid="section2">
|
||||
<Space tid="space1">
|
||||
<LocalButton />
|
||||
<OutButton />
|
||||
<Button tid="button1">button</Button>
|
||||
</Space>
|
||||
</Section>
|
||||
@@ -173,7 +193,10 @@ class App extends React.Component {
|
||||
<FormilyFormItem name="select1" component="Select" label="表单项" />
|
||||
</FormilyForm>
|
||||
</Section>
|
||||
<Section title="原生 DOM" tid="section3">
|
||||
<Section title="原生 DOM" tid="section4">
|
||||
<h1 style={{ ...{ color: "red" }, fontSize: 64 }}>
|
||||
hello world
|
||||
</h1>
|
||||
<div
|
||||
style={{
|
||||
border: "1px solid #ccc",
|
||||
@@ -234,6 +257,23 @@ class App extends React.Component {
|
||||
export default definePage(App);
|
||||
`;
|
||||
|
||||
export const emptyPageCode = `
|
||||
import React from "react";
|
||||
import { definePage } from "@music163/tango-boot";
|
||||
import {
|
||||
Page,
|
||||
Section,
|
||||
} from "@music163/antd";
|
||||
|
||||
function App() {
|
||||
return (<Page title="Detail Page">
|
||||
<Section></Section>
|
||||
</Page>)
|
||||
}
|
||||
|
||||
export default definePage(App);
|
||||
`;
|
||||
|
||||
const componentsButtonCode = `
|
||||
import React from 'react';
|
||||
import { registerComponentPrototype } from '../utils';
|
||||
@@ -253,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';
|
||||
@@ -275,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 = `
|
||||
@@ -373,7 +422,9 @@ export const sampleFiles = [
|
||||
{ filename: '/src/style.css', code: cssCode },
|
||||
{ filename: '/src/index.js', code: entryCode },
|
||||
{ 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 },
|
||||
|
||||
@@ -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 = () => {
|
||||
@@ -174,6 +174,30 @@ const prototypes: Dict<IComponentPrototype> = {
|
||||
package: '@music163/antd',
|
||||
hasChildren: true,
|
||||
siblingNames: ['Box'],
|
||||
props: [
|
||||
{
|
||||
name: 'aaa',
|
||||
title: 'aaa',
|
||||
setter: 'textSetter',
|
||||
},
|
||||
{
|
||||
name: 'bbb',
|
||||
title: 'bbb',
|
||||
setter: 'textSetter',
|
||||
deprecated: true,
|
||||
},
|
||||
{
|
||||
name: 'ccc',
|
||||
title: 'ccc',
|
||||
setter: 'textSetter',
|
||||
deprecated: true,
|
||||
},
|
||||
{
|
||||
name: 'd',
|
||||
title: 'd',
|
||||
setter: 'textSetter',
|
||||
},
|
||||
],
|
||||
},
|
||||
Columns: {
|
||||
name: 'Columns',
|
||||
|
||||
@@ -3,9 +3,5 @@ import { Outlet } from 'umi';
|
||||
import './index.less';
|
||||
|
||||
export default function Layout() {
|
||||
return (
|
||||
<div>
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
return <Outlet />;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Box } from 'coral-system';
|
||||
import { Button, Space } from 'antd';
|
||||
import { Button, Form, Input, Modal, Space } from 'antd';
|
||||
import {
|
||||
Designer,
|
||||
DesignerPanel,
|
||||
@@ -15,43 +15,18 @@ import {
|
||||
} from '@music163/tango-designer';
|
||||
import { createEngine, Workspace } from '@music163/tango-core';
|
||||
import prototypes from '../helpers/prototypes';
|
||||
import { Logo, ProjectDetail, bootHelperVariables, sampleFiles } from '../helpers';
|
||||
import { Logo, ProjectDetail, bootHelperVariables, emptyPageCode, sampleFiles } from '../helpers';
|
||||
import {
|
||||
ApiOutlined,
|
||||
AppstoreAddOutlined,
|
||||
BuildOutlined,
|
||||
ClusterOutlined,
|
||||
FunctionOutlined,
|
||||
PlusOutlined,
|
||||
createFromIconfontCN,
|
||||
} from '@ant-design/icons';
|
||||
|
||||
// 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',
|
||||
});
|
||||
import { Action } from '@music163/tango-ui';
|
||||
import { useState } from 'react';
|
||||
|
||||
const menuData = {
|
||||
common: [
|
||||
@@ -63,6 +38,7 @@ const menuData = {
|
||||
'Columns',
|
||||
'Column',
|
||||
'Box',
|
||||
'Text',
|
||||
'Space',
|
||||
'Typography',
|
||||
'Title',
|
||||
@@ -79,13 +55,49 @@ const menuData = {
|
||||
title: 'Formily表单',
|
||||
items: ['FormilyForm', 'FormilyFormItem', 'FormilySubmit', 'FormilyReset'],
|
||||
},
|
||||
{
|
||||
title: '数据展示',
|
||||
items: ['Comment'],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// 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/
|
||||
*/
|
||||
export default function App() {
|
||||
const [showNewPageModal, setShowNewPageModal] = useState(false);
|
||||
const [form] = Form.useForm();
|
||||
return (
|
||||
<Designer
|
||||
theme={themeLight}
|
||||
@@ -103,6 +115,14 @@ export default function App() {
|
||||
<Box px="l">
|
||||
<Toolbar>
|
||||
<Toolbar.Item key="routeSwitch" placement="left" />
|
||||
<Toolbar.Item key="addPage" placement="left">
|
||||
<Action
|
||||
tooltip="添加页面"
|
||||
shape="outline"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => setShowNewPageModal(true)}
|
||||
/>
|
||||
</Toolbar.Item>
|
||||
<Toolbar.Item key="history" placement="left" />
|
||||
<Toolbar.Item key="preview" placement="left" />
|
||||
<Toolbar.Item key="modeSwitch" placement="right" />
|
||||
@@ -114,6 +134,30 @@ export default function App() {
|
||||
</Space>
|
||||
</Toolbar.Item>
|
||||
</Toolbar>
|
||||
<Modal
|
||||
title="添加新页面"
|
||||
open={showNewPageModal}
|
||||
onCancel={() => setShowNewPageModal(false)}
|
||||
footer={null}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
onFinish={(values) => {
|
||||
workspace.addViewFile(values.name, emptyPageCode);
|
||||
setShowNewPageModal(false);
|
||||
}}
|
||||
layout="vertical"
|
||||
>
|
||||
<Form.Item label="文件名" name="name" required rules={[{ required: true }]}>
|
||||
<Input placeholder="请输入文件名" />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit">
|
||||
提交
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
"dependencies": {
|
||||
"@music163/tango-setting-form": "*",
|
||||
"@music163/tango-ui": "*",
|
||||
"mobx": "6.12.0",
|
||||
"mobx-react-lite": "4.0.5"
|
||||
"mobx": "6.12.3",
|
||||
"mobx-react-lite": "4.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ant-design/icons": "^4.8.0",
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import React from 'react';
|
||||
import { FormModel, SettingForm, register } from '@music163/tango-setting-form';
|
||||
import { IComponentPrototype } from '@music163/tango-helpers';
|
||||
import { BorderSetter, DisplaySetter } from '@music163/tango-designer/src/setters/style-setter';
|
||||
import { JsxSetter } from '@music163/tango-designer/src/setters/jsx-setter';
|
||||
import { RenderSetter, TableCellSetter } from '@music163/tango-designer/src/setters/render-setter';
|
||||
import { BUILT_IN_SETTERS } from '@music163/tango-designer/src/setters';
|
||||
import { Box } from 'coral-system';
|
||||
import { JsonView } from '@music163/tango-ui';
|
||||
import { toJS } from 'mobx';
|
||||
@@ -11,272 +9,18 @@ import { observer } from 'mobx-react-lite';
|
||||
import { Card } from 'antd';
|
||||
import { createFromIconfontCN } from '@ant-design/icons';
|
||||
|
||||
// 这里按需注入,因为部分 setter 依赖 Designer 的上下文
|
||||
register({
|
||||
name: 'borderSetter',
|
||||
component: BorderSetter,
|
||||
});
|
||||
const BLACK_LIST = ['codeSetter', 'eventSetter', 'modelSetter', 'routerSetter'];
|
||||
|
||||
register({
|
||||
name: 'displaySetter',
|
||||
component: DisplaySetter,
|
||||
});
|
||||
|
||||
register({
|
||||
name: 'jsxSetter',
|
||||
component: JsxSetter,
|
||||
});
|
||||
|
||||
register({
|
||||
name: 'renderSetter',
|
||||
component: RenderSetter,
|
||||
});
|
||||
|
||||
register({
|
||||
name: 'tableCellSetter',
|
||||
component: TableCellSetter,
|
||||
});
|
||||
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 {
|
||||
title: 'SettingForm',
|
||||
};
|
||||
|
||||
const prototype: IComponentPrototype = {
|
||||
name: 'Test',
|
||||
exportType: 'namedExport',
|
||||
title: '测试',
|
||||
icon: 'icon-test',
|
||||
type: 'element',
|
||||
category: 'basic',
|
||||
package: '@music163/antd',
|
||||
hasChildren: false,
|
||||
props: [
|
||||
{
|
||||
name: 'text',
|
||||
title: 'textSetter',
|
||||
setter: 'textSetter',
|
||||
tip: '这是一个文本属性',
|
||||
docs: 'https://music-one.fn.netease.com/docs/button',
|
||||
deprecated: '使用 text2 替代',
|
||||
},
|
||||
{
|
||||
name: 'display',
|
||||
title: 'displaySetter',
|
||||
setter: 'displaySetter',
|
||||
},
|
||||
{
|
||||
name: 'border',
|
||||
title: 'borderSetter',
|
||||
setter: 'borderSetter',
|
||||
},
|
||||
{
|
||||
name: 'onClick',
|
||||
title: 'eventSetter',
|
||||
tip: '当点击按钮时',
|
||||
setter: 'eventSetter',
|
||||
group: 'event',
|
||||
},
|
||||
{
|
||||
name: 'children',
|
||||
title: 'jsxSetter',
|
||||
setter: 'jsxSetter',
|
||||
},
|
||||
{
|
||||
name: 'render',
|
||||
title: 'renderSetter',
|
||||
setter: 'renderSetter',
|
||||
},
|
||||
{
|
||||
name: 'cell',
|
||||
title: 'tableCellSetter',
|
||||
setter: 'tableCellSetter',
|
||||
},
|
||||
{
|
||||
name: 'router',
|
||||
title: 'routerSetter',
|
||||
setter: 'routerSetter',
|
||||
},
|
||||
{
|
||||
name: 'object',
|
||||
title: '对象属性',
|
||||
tip: '一个嵌套的对象',
|
||||
props: [
|
||||
{
|
||||
name: 'name',
|
||||
title: 'Name',
|
||||
setter: 'textSetter',
|
||||
},
|
||||
{
|
||||
name: 'age',
|
||||
title: 'Age',
|
||||
setter: 'numberSetter',
|
||||
},
|
||||
{
|
||||
name: 'address',
|
||||
title: 'Address',
|
||||
props: [
|
||||
{
|
||||
name: 'city',
|
||||
title: 'City',
|
||||
setter: 'textSetter',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
getVisible: (form) => {
|
||||
return form.getValue('text') !== 'test';
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'expression',
|
||||
title: 'expressionSetter',
|
||||
setter: 'expressionSetter',
|
||||
},
|
||||
{
|
||||
name: 'model',
|
||||
title: 'modelSetter',
|
||||
setter: 'modelSetter',
|
||||
},
|
||||
{
|
||||
name: 'image',
|
||||
title: 'imageSetter',
|
||||
setter: 'imageSetter',
|
||||
},
|
||||
{
|
||||
name: 'css',
|
||||
title: 'cssSetter',
|
||||
setter: 'cssSetter',
|
||||
},
|
||||
{
|
||||
name: 'extra',
|
||||
title: 'jsxSetter',
|
||||
setter: 'jsxSetter',
|
||||
},
|
||||
{
|
||||
name: 'icon',
|
||||
title: 'iconSetter',
|
||||
setter: 'iconSetter',
|
||||
},
|
||||
{
|
||||
name: 'iconType',
|
||||
title: 'iconTypeSetter',
|
||||
setter: 'iconTypeSetter',
|
||||
},
|
||||
|
||||
{
|
||||
name: 'onClick2',
|
||||
title: 'actionSetter',
|
||||
tip: '当点击按钮时',
|
||||
setter: 'actionSetter',
|
||||
group: 'event',
|
||||
},
|
||||
{
|
||||
name: 'disabled',
|
||||
title: 'boolSetter',
|
||||
tip: 'disabled 是否禁用',
|
||||
defaultValue: false,
|
||||
setter: 'boolSetter',
|
||||
},
|
||||
{
|
||||
name: 'size',
|
||||
title: 'choiceSetter',
|
||||
tip: 'size 按钮的尺寸',
|
||||
defaultValue: 'medium',
|
||||
setter: 'choiceSetter',
|
||||
setterProps: {
|
||||
options: [
|
||||
{ label: '小', value: 'small' },
|
||||
{ label: '中', value: 'medium' },
|
||||
{ label: '大', value: 'large' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'color',
|
||||
title: 'colorSetter',
|
||||
setter: 'colorSetter',
|
||||
},
|
||||
{
|
||||
name: 'columns',
|
||||
title: 'columnSetter',
|
||||
setter: 'columnSetter',
|
||||
},
|
||||
{
|
||||
name: 'date',
|
||||
title: 'dateSetter',
|
||||
setter: 'dateSetter',
|
||||
},
|
||||
{
|
||||
name: 'dateRange',
|
||||
title: 'dateRangeSetter',
|
||||
setter: 'dateRangeSetter',
|
||||
},
|
||||
{
|
||||
name: 'enum',
|
||||
title: 'enumSetter',
|
||||
setter: 'enumSetter',
|
||||
setterProps: {},
|
||||
},
|
||||
{
|
||||
name: 'time',
|
||||
title: 'timeSetter',
|
||||
setter: 'timeSetter',
|
||||
},
|
||||
{
|
||||
name: 'timeRange',
|
||||
title: 'timeRangeSetter',
|
||||
setter: 'timeRangeSetter',
|
||||
},
|
||||
{
|
||||
name: 'dataSource',
|
||||
title: 'jsonSetter 不推荐',
|
||||
setter: 'jsonSetter',
|
||||
},
|
||||
{
|
||||
name: 'listSetter',
|
||||
title: 'listSetter',
|
||||
setter: 'listSetter',
|
||||
},
|
||||
{
|
||||
name: 'count',
|
||||
title: 'numberSetter',
|
||||
setter: 'numberSetter',
|
||||
},
|
||||
{
|
||||
name: 'options',
|
||||
title: 'optionSetter',
|
||||
setter: 'optionSetter',
|
||||
},
|
||||
{
|
||||
name: 'type',
|
||||
title: 'pickerSetter',
|
||||
defaultValue: 'solid',
|
||||
setter: 'pickerSetter',
|
||||
setterProps: {
|
||||
options: [
|
||||
{ label: '文本型', value: 'text' },
|
||||
{ label: '实体型', value: 'solid' },
|
||||
{ label: '幽灵', value: 'ghost' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'title',
|
||||
title: 'textSetter',
|
||||
setter: 'textSetter',
|
||||
},
|
||||
{
|
||||
name: 'invalid',
|
||||
title: 'invalidSetter',
|
||||
setter: 'invalidSetter',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* 表单值预览
|
||||
*/
|
||||
@@ -285,28 +29,22 @@ const FormValuePreview = observer(({ model }: { model: FormModel }) => {
|
||||
return <JsonView src={data} />;
|
||||
});
|
||||
|
||||
export function Basic() {
|
||||
const model = new FormModel(
|
||||
{
|
||||
router: 'www.163.com',
|
||||
expression: `{ foo: 'foo' }`,
|
||||
object: {
|
||||
name: 'Alice',
|
||||
},
|
||||
image:
|
||||
'https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/13270238619/2cc5/0782/1d6e/009b96bf90c557b9bbde09b1687a2c80.png',
|
||||
},
|
||||
{ onChange: console.log },
|
||||
);
|
||||
interface SettingFormDemoProps {
|
||||
initValues?: object;
|
||||
prototype?: IComponentPrototype;
|
||||
}
|
||||
|
||||
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}
|
||||
showIdentifier={{
|
||||
identifierKey: 'tid',
|
||||
getIdentifier: () => `time${Date.now()}`,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
@@ -319,25 +57,488 @@ export function Basic() {
|
||||
);
|
||||
}
|
||||
|
||||
const prototypeHasBasicProps: IComponentPrototype = {
|
||||
name: 'Sample',
|
||||
title: '演示组件',
|
||||
package: 'sample-pkg',
|
||||
type: 'element',
|
||||
docs: 'https://4x-ant-design.antgroup.com/components/slider-cn',
|
||||
props: [
|
||||
{
|
||||
name: 'code',
|
||||
title: 'codeSetter',
|
||||
setter: 'codeSetter',
|
||||
},
|
||||
{
|
||||
name: 'text',
|
||||
title: 'textSetter',
|
||||
setter: 'textSetter',
|
||||
},
|
||||
{
|
||||
name: 'text2',
|
||||
title: 'textAreaSetter',
|
||||
setter: 'textAreaSetter',
|
||||
},
|
||||
{
|
||||
name: 'number',
|
||||
title: 'numberSetter',
|
||||
setter: 'numberSetter',
|
||||
},
|
||||
{
|
||||
name: 'number2',
|
||||
title: 'sliderSetter',
|
||||
setter: 'sliderSetter',
|
||||
},
|
||||
{
|
||||
name: 'bool',
|
||||
title: 'boolSetter',
|
||||
setter: 'boolSetter',
|
||||
},
|
||||
{
|
||||
name: 'enum',
|
||||
title: 'enumSetter',
|
||||
setter: 'enumSetter',
|
||||
},
|
||||
{
|
||||
name: 'list',
|
||||
title: 'listSetter',
|
||||
setter: 'listSetter',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export function Basic() {
|
||||
return (
|
||||
<SettingFormDemo
|
||||
initValues={{
|
||||
bool: true,
|
||||
enum: {
|
||||
aaa: 'aaa',
|
||||
bbb: 'bbb',
|
||||
ccc: 'ccc',
|
||||
},
|
||||
list: [{ key: 1 }, { key: 2 }],
|
||||
}}
|
||||
prototype={prototypeHasBasicProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function DeprecatedProp() {
|
||||
return (
|
||||
<SettingFormDemo
|
||||
prototype={{
|
||||
name: 'Deprecated',
|
||||
package: 'sample-pkg',
|
||||
type: 'element',
|
||||
props: [
|
||||
{
|
||||
name: 'number',
|
||||
title: 'numberSetter',
|
||||
setter: 'numberSetter',
|
||||
tip: '这是一个文本属性',
|
||||
docs: 'https://4x-ant-design.antgroup.com/components/slider-cn',
|
||||
deprecated: '使用 text2 替代',
|
||||
},
|
||||
{
|
||||
name: 'number1',
|
||||
title: 'sliderSetter',
|
||||
setter: 'sliderSetter',
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function Validate() {
|
||||
return (
|
||||
<SettingFormDemo
|
||||
prototype={{
|
||||
name: 'Validate',
|
||||
package: 'sample-pkg',
|
||||
type: 'element',
|
||||
props: [
|
||||
{
|
||||
name: 'number',
|
||||
title: 'numberSetter',
|
||||
setter: 'numberSetter',
|
||||
validate: (value) => {
|
||||
if (!value && value !== 0) {
|
||||
return '必填';
|
||||
}
|
||||
if (value < 0) {
|
||||
return '必须大于 0';
|
||||
}
|
||||
if (value > 10) {
|
||||
return '必须小于等于 10';
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function ObjectSetter() {
|
||||
return (
|
||||
<SettingFormDemo
|
||||
prototype={{
|
||||
name: 'Object',
|
||||
package: 'sample-pkg',
|
||||
type: 'element',
|
||||
props: [
|
||||
{
|
||||
name: 'text',
|
||||
title: 'textSetter',
|
||||
setter: 'textSetter',
|
||||
},
|
||||
{
|
||||
name: 'object',
|
||||
title: 'objectSetter',
|
||||
setter: 'objectSetter',
|
||||
props: [
|
||||
{
|
||||
name: 'text',
|
||||
title: 'textSetter',
|
||||
setter: 'textSetter',
|
||||
},
|
||||
{
|
||||
name: 'number',
|
||||
title: 'numberSetter',
|
||||
setter: 'numberSetter',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function InitValues() {
|
||||
return (
|
||||
<SettingFormDemo
|
||||
initValues={{
|
||||
bool: true,
|
||||
bool1: '{{true}}',
|
||||
style: {
|
||||
background: 'red',
|
||||
},
|
||||
object: {
|
||||
text: 'text',
|
||||
number: 10,
|
||||
},
|
||||
object1: {
|
||||
text: 'text',
|
||||
number: '{{tango.stores.user?.age}}',
|
||||
},
|
||||
// 只会有一种情况传下来的是字符串,就是用户代码里存在 rest operator,这时候不需要额外处理,提示用户就使用代码模式
|
||||
object2: '{{{ text: "text22", number: 22, ...{ extra: "some" } }}}',
|
||||
list: [{ key: 'aaa' }, { key: 'bbb' }], // list object
|
||||
list1: "{{[{ key: 'aaa' }, { key: 'bbb' }]}}", // raw code
|
||||
}}
|
||||
prototype={{
|
||||
name: 'InitValues',
|
||||
package: 'sample-pkg',
|
||||
type: 'element',
|
||||
props: [
|
||||
{
|
||||
name: 'bool',
|
||||
title: 'value初始化',
|
||||
setter: 'boolSetter',
|
||||
},
|
||||
{
|
||||
name: 'bool1',
|
||||
title: 'value初始化',
|
||||
setter: 'boolSetter',
|
||||
},
|
||||
{
|
||||
name: 'bool2',
|
||||
title: '无初值',
|
||||
setter: 'boolSetter',
|
||||
},
|
||||
{
|
||||
name: 'style',
|
||||
title: 'codeSetter',
|
||||
setter: 'codeSetter',
|
||||
},
|
||||
{
|
||||
name: 'object',
|
||||
props: [
|
||||
{
|
||||
name: 'text',
|
||||
title: 'text',
|
||||
setter: 'textSetter',
|
||||
},
|
||||
{
|
||||
name: 'number',
|
||||
title: 'number',
|
||||
setter: 'numberSetter',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'object1',
|
||||
props: [
|
||||
{
|
||||
name: 'text',
|
||||
title: 'text',
|
||||
setter: 'textSetter',
|
||||
},
|
||||
{
|
||||
name: 'number',
|
||||
title: 'number',
|
||||
setter: 'numberSetter',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'object2',
|
||||
props: [
|
||||
{
|
||||
name: 'text',
|
||||
title: 'text',
|
||||
setter: 'textSetter',
|
||||
},
|
||||
{
|
||||
name: 'number',
|
||||
title: 'number',
|
||||
setter: 'numberSetter',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'list',
|
||||
title: 'listSetter',
|
||||
setter: 'listSetter',
|
||||
},
|
||||
{
|
||||
name: 'list1',
|
||||
title: 'listSetter',
|
||||
setter: 'listSetter',
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function Lite() {
|
||||
return (
|
||||
<Box width={320} border="solid">
|
||||
<Box width={320} border="solid" borderColor="line2">
|
||||
<SettingForm
|
||||
showSearch={false}
|
||||
showGroups={false}
|
||||
showItemSubtitle={false}
|
||||
prototype={prototype}
|
||||
prototype={prototypeHasBasicProps}
|
||||
disableSwitchExpressionSetter
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export function NoExpressionSwitch() {
|
||||
export function HideToggleCode() {
|
||||
const model = new FormModel({});
|
||||
return (
|
||||
<Box>
|
||||
<SettingForm model={model} prototype={prototype} disableSwitchExpressionSetter />
|
||||
<Box width={320} border="solid" borderColor="line2">
|
||||
<SettingForm model={model} prototype={prototypeHasBasicProps} disableSwitchExpressionSetter />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const prototypeHasExtraProps: IComponentPrototype = {
|
||||
name: 'ExtraProps',
|
||||
type: 'element',
|
||||
package: '@music163/antd',
|
||||
props: [
|
||||
{
|
||||
name: 'choice',
|
||||
title: 'choiceSetter',
|
||||
setter: 'choiceSetter',
|
||||
options: [
|
||||
{ label: '选项1', value: '1' },
|
||||
{ label: '选项2', value: '2' },
|
||||
{ label: '选项3', value: '3' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'picker',
|
||||
title: 'pickerSetter',
|
||||
setter: 'pickerSetter',
|
||||
options: [
|
||||
{ label: '选项1', value: '1' },
|
||||
{ label: '选项2', value: '2' },
|
||||
{ label: '选项3', value: '3' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'actionList',
|
||||
title: 'actionListSetter',
|
||||
setter: 'actionListSetter',
|
||||
},
|
||||
{
|
||||
name: 'list',
|
||||
title: 'listSetter',
|
||||
setter: 'listSetter',
|
||||
},
|
||||
{
|
||||
name: 'options',
|
||||
title: 'optionSetter',
|
||||
setter: 'optionSetter',
|
||||
},
|
||||
{
|
||||
name: 'columns',
|
||||
title: 'tableColumnsSetter',
|
||||
setter: 'tableColumnsSetter',
|
||||
},
|
||||
{
|
||||
name: 'css',
|
||||
title: 'cssSetter',
|
||||
setter: 'cssSetter',
|
||||
},
|
||||
{
|
||||
name: 'date',
|
||||
title: 'dateSetter',
|
||||
setter: 'dateSetter',
|
||||
},
|
||||
{
|
||||
name: 'dateRange',
|
||||
title: 'dateRangeSetter',
|
||||
setter: 'dateRangeSetter',
|
||||
},
|
||||
{
|
||||
name: 'time',
|
||||
title: 'timeSetter',
|
||||
setter: 'timeSetter',
|
||||
},
|
||||
{
|
||||
name: 'time',
|
||||
title: 'timeRangeSetter',
|
||||
setter: 'timeRangeSetter',
|
||||
},
|
||||
{
|
||||
name: 'enum',
|
||||
title: 'enumSetter',
|
||||
setter: 'enumSetter',
|
||||
},
|
||||
{
|
||||
name: 'event',
|
||||
title: 'eventSetter',
|
||||
setter: 'eventSetter',
|
||||
},
|
||||
{
|
||||
name: 'json',
|
||||
title: 'jsonSetter',
|
||||
setter: 'jsonSetter',
|
||||
},
|
||||
{
|
||||
name: 'jsx',
|
||||
title: 'jsxSetter',
|
||||
setter: 'jsxSetter',
|
||||
},
|
||||
{
|
||||
name: 'render',
|
||||
title: 'renderPropsSetter',
|
||||
setter: 'renderPropsSetter',
|
||||
},
|
||||
{
|
||||
name: 'cell',
|
||||
title: 'tableCellSetter',
|
||||
setter: 'tableCellSetter',
|
||||
},
|
||||
{
|
||||
name: 'expandable',
|
||||
title: 'tableExpandableSetter',
|
||||
setter: 'tableExpandableSetter',
|
||||
},
|
||||
{
|
||||
name: 'router',
|
||||
title: 'routerSetter',
|
||||
setter: 'routerSetter',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export function ExtraSetters() {
|
||||
return (
|
||||
<SettingFormDemo
|
||||
initValues={{
|
||||
router: 'www.163.com',
|
||||
expression: `{ foo: 'foo' }`,
|
||||
object: {
|
||||
name: 'Alice',
|
||||
},
|
||||
image:
|
||||
'https://p6.music.126.net/obj/wonDlsKUwrLClGjCm8Kx/13270238619/2cc5/0782/1d6e/009b96bf90c557b9bbde09b1687a2c80.png',
|
||||
}}
|
||||
prototype={prototypeHasExtraProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function StyleProps() {
|
||||
return (
|
||||
<SettingFormDemo
|
||||
prototype={{
|
||||
name: 'Box',
|
||||
title: 'Box',
|
||||
type: 'element',
|
||||
package: '@music163/antd',
|
||||
props: [
|
||||
{
|
||||
name: 'style',
|
||||
title: 'styleSetter',
|
||||
setter: 'styleSetter',
|
||||
},
|
||||
{
|
||||
name: 'display',
|
||||
title: 'displaySetter',
|
||||
setter: 'displaySetter',
|
||||
},
|
||||
{
|
||||
name: 'flexDirection',
|
||||
title: 'flexDirectionSetter',
|
||||
setter: 'flexDirectionSetter',
|
||||
},
|
||||
{
|
||||
name: 'flexGap',
|
||||
title: 'flexGapSetter',
|
||||
setter: 'flexGapSetter',
|
||||
},
|
||||
{
|
||||
name: 'flexJustifyContent',
|
||||
title: 'flexJustifyContentSetter',
|
||||
setter: 'flexJustifyContentSetter',
|
||||
},
|
||||
{
|
||||
name: 'flexAlignItems',
|
||||
title: 'flexAlignItemsSetter',
|
||||
setter: 'flexAlignItemsSetter',
|
||||
},
|
||||
{
|
||||
name: 'spacing',
|
||||
title: 'spacingSetter',
|
||||
setter: 'spacingSetter',
|
||||
},
|
||||
{
|
||||
name: 'color',
|
||||
title: 'colorSetter',
|
||||
setter: 'colorSetter',
|
||||
},
|
||||
{
|
||||
name: 'bg',
|
||||
title: 'bgSetter',
|
||||
setter: 'bgSetter',
|
||||
},
|
||||
{
|
||||
name: 'border',
|
||||
title: 'borderSetter',
|
||||
setter: 'borderSetter',
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"@typescript-eslint/parser": "^6.13.2",
|
||||
"conventional-changelog-cli": "^4.1.0",
|
||||
"copyfiles": "^2.4.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.55.0",
|
||||
"eslint-config-ali": "^14.0.2",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
|
||||
@@ -3,6 +3,22 @@
|
||||
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
|
||||
|
||||
- refactor parse attribute value ([#149](https://github.com/netease/tango/issues/149)) ([ffaa276](https://github.com/netease/tango/commit/ffaa276b5c205ed962d37e2fdb358a703f8fad01))
|
||||
|
||||
## [1.0.1](https://github.com/netease/tango/compare/@music163/tango-context@1.0.0...@music163/tango-context@1.0.1) (2024-04-22)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- allow reload state tree & display pageStore in variable tree ([#135](https://github.com/netease/tango/issues/135)) ([2664613](https://github.com/netease/tango/commit/2664613ab263aead2a5239fa012454fc7fd5ff99))
|
||||
|
||||
# [1.0.0-alpha.10](https://github.com/netease/tango/compare/@music163/tango-context@1.0.0-alpha.9...@music163/tango-context@1.0.0-alpha.10) (2024-04-09)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-context
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-context",
|
||||
"version": "1.0.0",
|
||||
"version": "1.1.1",
|
||||
"description": "react context for tango-apps",
|
||||
"keywords": [
|
||||
"react",
|
||||
@@ -31,9 +31,9 @@
|
||||
"react": ">= 16.8"
|
||||
},
|
||||
"dependencies": {
|
||||
"@music163/tango-core": "^1.0.0",
|
||||
"@music163/tango-helpers": "^1.0.0-alpha.6",
|
||||
"mobx-react-lite": "4.0.5"
|
||||
"@music163/tango-core": "^1.2.0",
|
||||
"@music163/tango-helpers": "^1.1.1",
|
||||
"mobx-react-lite": "4.0.7"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
|
||||
@@ -35,8 +35,14 @@ export const useWorkspaceData = () => {
|
||||
const modelVariables: IVariableTreeNode[] = []; // 绑定变量列表
|
||||
const storeActionVariables: IVariableTreeNode[] = []; // 模型中的所有 actions
|
||||
const storeVariables: IVariableTreeNode[] = []; // 模型中的所有变量
|
||||
const pageStoreVariables: IVariableTreeNode[] = []; // 页面中的组件实例变量
|
||||
const serviceVariables: IVariableTreeNode[] = []; // 服务中的所有变量
|
||||
|
||||
pageStoreVariables.push({
|
||||
title: 'pageStore',
|
||||
key: 'pageStore',
|
||||
});
|
||||
|
||||
Object.values(workspace.storeModules).forEach((file) => {
|
||||
const prefix = `stores.${file.name}`;
|
||||
const states: IVariableTreeNode[] = file.states.map((item) => ({
|
||||
@@ -109,6 +115,7 @@ export const useWorkspaceData = () => {
|
||||
}
|
||||
|
||||
let expressionVariables: IVariableTreeNode[] = [
|
||||
buildVariableOptions('当前页面组件实例', '$pageStore', pageStoreVariables),
|
||||
buildVariableOptions('数据模型', '$stores', storeVariables),
|
||||
buildVariableOptions('服务函数', '$services', serviceVariables),
|
||||
];
|
||||
|
||||
@@ -3,6 +3,33 @@
|
||||
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
|
||||
|
||||
- parse module with module alias ([#147](https://github.com/netease/tango/issues/147)) ([56d0877](https://github.com/netease/tango/commit/56d0877507b0138877eac6f36db288147b43c7d9))
|
||||
|
||||
### Features
|
||||
|
||||
- refactor parse attribute value ([#149](https://github.com/netease/tango/issues/149)) ([ffaa276](https://github.com/netease/tango/commit/ffaa276b5c205ed962d37e2fdb358a703f8fad01))
|
||||
|
||||
## [1.0.1](https://github.com/netease/tango/compare/@music163/tango-core@1.0.0...@music163/tango-core@1.0.1) (2024-04-22)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-core
|
||||
|
||||
# [1.0.0-alpha.10](https://github.com/netease/tango/compare/@music163/tango-core@1.0.0-alpha.9...@music163/tango-core@1.0.0-alpha.10) (2024-04-09)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-core",
|
||||
"version": "1.0.0",
|
||||
"version": "1.2.0",
|
||||
"description": "tango core",
|
||||
"author": "wwsun <ww.sun@outlook.com>",
|
||||
"homepage": "",
|
||||
@@ -28,10 +28,10 @@
|
||||
"@babel/parser": "^7.23.5",
|
||||
"@babel/traverse": "^7.23.5",
|
||||
"@babel/types": "^7.23.5",
|
||||
"@music163/tango-helpers": "^1.0.0-alpha.6",
|
||||
"@music163/tango-helpers": "^1.1.1",
|
||||
"@types/babel__generator": "^7.6.7",
|
||||
"@types/babel__traverse": "^7.20.4",
|
||||
"mobx": "6.12.0",
|
||||
"mobx": "6.12.3",
|
||||
"path-browserify": "^1.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ const templatePattern = /^{(.+)}$/s;
|
||||
/**
|
||||
* 判断给定字符串是否被表达式容器`{expCode}`包裹
|
||||
* @param code
|
||||
* @deprecated 新版改为 {{code}} 作为容器,使用 isWrappedCode 代替
|
||||
*/
|
||||
export function isWrappedByExpressionContainer(code: string, isStrict = true) {
|
||||
if (isStrict && isValidExpressionCode(code)) {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
import generator, { GeneratorOptions } from '@babel/generator';
|
||||
import * as t from '@babel/types';
|
||||
import { logger } from '@music163/tango-helpers';
|
||||
import { logger, wrapCode } from '@music163/tango-helpers';
|
||||
import { formatCode } from '../string';
|
||||
|
||||
const defaultGeneratorOptions: GeneratorOptions = {
|
||||
@@ -156,10 +156,10 @@ export function node2code(node: t.Node) {
|
||||
/**
|
||||
* 将 t.Node 生成为 js 值
|
||||
* @param node ast node
|
||||
* @param hasExpressionWrapper 是否包裹表达式
|
||||
* @param isWrapCode 是否包裹代码,例如 code -> {{code}}
|
||||
* @returns a plain javascript value
|
||||
*/
|
||||
export function node2value(node: t.Node, hasExpressionWrapper = true): any {
|
||||
export function node2value(node: t.Node, isWrapCode = true): any {
|
||||
let ret;
|
||||
switch (node.type) {
|
||||
case 'StringLiteral':
|
||||
@@ -171,39 +171,50 @@ export function node2value(node: t.Node, hasExpressionWrapper = true): any {
|
||||
case 'NullLiteral':
|
||||
ret = null;
|
||||
break;
|
||||
case 'Identifier': // {data}
|
||||
case 'MemberExpression': // {this.props.data}
|
||||
case 'OptionalMemberExpression': // {a?.b}
|
||||
case 'UnaryExpression': // {!false}
|
||||
case 'ArrowFunctionExpression': // {() => {}}
|
||||
case 'TemplateLiteral': // {`hello ${text}`}
|
||||
case 'ConditionalExpression': // {a ? 'foo' : 'bar'}
|
||||
case 'LogicalExpression': // { a || b}
|
||||
case 'BinaryExpression': // { a + b}
|
||||
case 'TaggedTemplateExpression': // {css``}
|
||||
case 'CallExpression': // {[1,2,3].map(fn)}
|
||||
case 'JSXElement': // {<Box>hello</Box>}
|
||||
case 'JSXFragment': // <><Box /></>
|
||||
case 'Identifier': // {{data}}
|
||||
case 'MemberExpression': // {{this.props.data}}
|
||||
case 'OptionalMemberExpression': // {{a?.b}}
|
||||
case 'UnaryExpression': // {{!false}}
|
||||
case 'ArrowFunctionExpression': // {{() => {}}}
|
||||
case 'TemplateLiteral': // {{`hello ${text}`}}
|
||||
case 'ConditionalExpression': // {{a ? 'foo' : 'bar'}}
|
||||
case 'LogicalExpression': // {{ a || b}}
|
||||
case 'BinaryExpression': // {{ a + b}}
|
||||
case 'TaggedTemplateExpression': // {{css``}}
|
||||
case 'CallExpression': // {{[1,2,3].map(fn)}}
|
||||
case 'JSXElement': // {{<Box>hello</Box>}}
|
||||
case 'JSXFragment': // {{<><Box /></>}}
|
||||
ret = expression2code(node);
|
||||
if (hasExpressionWrapper) {
|
||||
ret = `{${ret}}`;
|
||||
if (isWrapCode) {
|
||||
ret = wrapCode(ret);
|
||||
}
|
||||
break;
|
||||
case 'ObjectExpression': {
|
||||
ret = node.properties.reduce((prev, propertyNode) => {
|
||||
if (propertyNode.type === 'ObjectProperty') {
|
||||
const key = keyNode2value(propertyNode.key);
|
||||
const value = node2value(propertyNode.value, hasExpressionWrapper);
|
||||
// key 可能是字符串,也可能是数字
|
||||
prev[key] = value;
|
||||
const isSimpleObject = node.properties.every(
|
||||
(propertyNode) => propertyNode.type === 'ObjectProperty',
|
||||
);
|
||||
if (isSimpleObject) {
|
||||
// simple object: { key1, key2, key3 }
|
||||
ret = node.properties.reduce((prev, propertyNode) => {
|
||||
if (propertyNode.type === 'ObjectProperty') {
|
||||
const key = keyNode2value(propertyNode.key);
|
||||
const value = node2value(propertyNode.value, isWrapCode);
|
||||
prev[key] = value; // key 可能是字符串,也可能是数字
|
||||
}
|
||||
return prev;
|
||||
}, {});
|
||||
} else {
|
||||
// mixed object, object property maybe SpreadElement or ObjectMethod, e.g. { key1, fn() {}, ...obj1 }
|
||||
ret = expression2code(node);
|
||||
if (wrapCode) {
|
||||
ret = wrapCode(ret);
|
||||
}
|
||||
// FIXME: property is a SpreadElement
|
||||
return prev;
|
||||
}, {});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'ArrayExpression': {
|
||||
ret = node.elements.map((elementNode) => node2value(elementNode, hasExpressionWrapper));
|
||||
// FIXME: 有可能会解析失败
|
||||
ret = node.elements.map((elementNode) => node2value(elementNode, isWrapCode));
|
||||
break;
|
||||
}
|
||||
default:
|
||||
@@ -214,7 +225,7 @@ export function node2value(node: t.Node, hasExpressionWrapper = true): any {
|
||||
}
|
||||
|
||||
/**
|
||||
* jsx 属性值节点转为 js value
|
||||
* jsx prop value 节点转为 js value
|
||||
*/
|
||||
export function jsxAttributeValueNode2value(node: t.Node): any {
|
||||
// e.g. <Checkbox checked /> 此时没有 value node
|
||||
@@ -232,9 +243,16 @@ export function jsxAttributeValueNode2value(node: t.Node): any {
|
||||
// <Foo bar={[]}>
|
||||
ret = jsxAttributeValueNode2value(node.expression);
|
||||
break;
|
||||
default:
|
||||
case 'ArrayExpression': {
|
||||
// 数组统一处理为 code
|
||||
ret = expression2code(node);
|
||||
ret = wrapCode(ret);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
ret = node2value(node);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
|
||||
@@ -5,12 +5,12 @@ import { parse, parseExpression, ParserOptions } from '@babel/parser';
|
||||
import * as t from '@babel/types';
|
||||
import {
|
||||
logger,
|
||||
isValidObjectString,
|
||||
getVariableContent,
|
||||
isPlainObject,
|
||||
Dict,
|
||||
isWrappedCode,
|
||||
getCodeOfWrappedCode,
|
||||
} from '@music163/tango-helpers';
|
||||
import { isWrappedByExpressionContainer } from '../assert';
|
||||
|
||||
// @see https://babeljs.io/docs/en/babel-parser#pluginss
|
||||
const babelParserConfig: ParserOptions = {
|
||||
@@ -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;
|
||||
}
|
||||
@@ -109,9 +108,6 @@ export function code2expression(code: string) {
|
||||
* @returns File
|
||||
*/
|
||||
export function expressionCode2ast(code: string) {
|
||||
if (isWrappedByExpressionContainer(code)) {
|
||||
code = getVariableContent(code);
|
||||
}
|
||||
const node = code2expression(code);
|
||||
return t.file(t.program([t.blockStatement([t.expressionStatement(node)])]));
|
||||
}
|
||||
@@ -132,18 +128,19 @@ export function value2node(
|
||||
| t.Expression {
|
||||
let ret;
|
||||
switch (typeof value) {
|
||||
case 'number':
|
||||
ret = t.numericLiteral(value);
|
||||
break;
|
||||
case 'string':
|
||||
if (isWrappedByExpressionContainer(value)) {
|
||||
// 再检查是否是表达式容器,例如 {this.foo}, {1}
|
||||
const innerString = getVariableContent(value);
|
||||
ret = code2expression(innerString);
|
||||
if (isWrappedCode(value)) {
|
||||
// 再检查是否是代码 {{code}},例如 {{this.foo}}, {{1}}
|
||||
const innerCode = getCodeOfWrappedCode(value);
|
||||
ret = code2expression(innerCode);
|
||||
} else {
|
||||
// 否则当成字符串处理
|
||||
ret = t.stringLiteral(value);
|
||||
}
|
||||
break;
|
||||
case 'number':
|
||||
ret = t.numericLiteral(value);
|
||||
break;
|
||||
case 'boolean':
|
||||
ret = t.booleanLiteral(value);
|
||||
break;
|
||||
@@ -167,7 +164,7 @@ export function value2node(
|
||||
ret = t.identifier('undefined');
|
||||
break;
|
||||
default: {
|
||||
logger.error(`value2node: unsupport value <${value}>`);
|
||||
logger.error(`value2node: value <${value}> transform failed!`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -196,21 +193,23 @@ export function code2jsxAttributeValueNode(code: string) {
|
||||
return t.jsxExpressionContainer(code2expression(code));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
// FIXME: 重构这个逻辑,是不是统一当成 code 处理
|
||||
case 'string': {
|
||||
if (value.length > 1) {
|
||||
value = value.trim();
|
||||
}
|
||||
if (isValidObjectString(value)) {
|
||||
// 先检查是否是对象字符串
|
||||
ret = t.jsxExpressionContainer(code2expression(value));
|
||||
} else if (isWrappedByExpressionContainer(value)) {
|
||||
// 再检查是否是表达式容器,例如 {this.foo}, {1}
|
||||
const innerString = getVariableContent(value);
|
||||
ret = t.jsxExpressionContainer(code2expression(innerString));
|
||||
if (isWrappedCode(value)) {
|
||||
const innerCode = getCodeOfWrappedCode(value);
|
||||
const node = code2expression(innerCode);
|
||||
ret = t.jsxExpressionContainer(node || t.identifier('undefined'));
|
||||
} else {
|
||||
ret = t.stringLiteral(value);
|
||||
}
|
||||
@@ -223,11 +222,12 @@ export function value2jsxAttributeValueNode(value: any) {
|
||||
return ret;
|
||||
}
|
||||
|
||||
// TODO: 待校验
|
||||
export function value2jsxChildrenValueNode(value: any) {
|
||||
let ret: t.JSXElement | t.JSXFragment | t.JSXExpressionContainer | t.JSXSpreadChild | t.JSXText;
|
||||
switch (typeof value) {
|
||||
case 'string':
|
||||
if (isWrappedByExpressionContainer(value)) {
|
||||
if (isValidExpressionCode(value)) {
|
||||
const innerString = getVariableContent(value);
|
||||
ret = t.jsxExpressionContainer(code2expression(innerString));
|
||||
} else {
|
||||
|
||||
@@ -1,27 +1,8 @@
|
||||
import { getVariableContent } from '@music163/tango-helpers';
|
||||
import { value2node, expression2code, isValidExpressionCode } from './ast';
|
||||
import { isWrappedByExpressionContainer } from './assert';
|
||||
import { getCodeOfWrappedCode, isWrappedCode } from '@music163/tango-helpers';
|
||||
import { value2node, expression2code, code2expression, node2value } from './ast';
|
||||
|
||||
/**
|
||||
* 将 js value 转换为代码字符串
|
||||
*/
|
||||
export function value2code(value: any) {
|
||||
const node = value2node(value);
|
||||
const code = expression2code(node);
|
||||
return code;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否是字符串代码
|
||||
* @param code
|
||||
* @returns
|
||||
*/
|
||||
function isStringCode(code: string) {
|
||||
return /^".*"$/.test(code?.trim());
|
||||
}
|
||||
|
||||
/**
|
||||
* js value 转为表达式代码
|
||||
* js value 转为代码字符串
|
||||
* @example 1 => 1
|
||||
* @example hello => "hello"
|
||||
* @example { foo: bar } => {{ foo: bar }}
|
||||
@@ -30,34 +11,56 @@ function isStringCode(code: string) {
|
||||
* @param val js value
|
||||
* @returns 表达式代码
|
||||
*/
|
||||
export function value2expressionCode(val: any) {
|
||||
if (!val) return '';
|
||||
export function value2code(val: any) {
|
||||
if (val === undefined) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (val === null) {
|
||||
return 'null';
|
||||
}
|
||||
|
||||
let ret;
|
||||
|
||||
switch (typeof val) {
|
||||
case 'string': {
|
||||
if (isValidExpressionCode(val)) {
|
||||
ret = val;
|
||||
} else if (isWrappedByExpressionContainer(val, false)) {
|
||||
ret = getVariableContent(val);
|
||||
} else if (isStringCode(val)) {
|
||||
ret = val;
|
||||
if (isWrappedCode(val)) {
|
||||
ret = getCodeOfWrappedCode(val);
|
||||
} else {
|
||||
ret = `"${val}"`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'boolean':
|
||||
case 'function':
|
||||
case 'number':
|
||||
ret = String(val);
|
||||
break;
|
||||
case 'object':
|
||||
ret = value2code(val);
|
||||
break;
|
||||
default:
|
||||
ret = '';
|
||||
default: {
|
||||
// other cases, including array, object, null, undefined
|
||||
const node = value2node(val);
|
||||
ret = expression2code(node);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
export const value2expressionCode = value2code;
|
||||
|
||||
/**
|
||||
* 代码字符串转为具体的 js value
|
||||
* @example `() => {}` 返回 undefined
|
||||
*
|
||||
* @param rawCode 代码字符串
|
||||
* @returns 返回解析后的 js value,包括:string, number, boolean, simpleObject, simpleArray
|
||||
*/
|
||||
export function code2value(rawCode: string) {
|
||||
const node = code2expression(rawCode);
|
||||
const value = node2value(node);
|
||||
if (isWrappedCode(value)) {
|
||||
// 能转的就转,转不能的就返回空
|
||||
return;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -3,20 +3,24 @@ import {
|
||||
IComponentProp,
|
||||
IComponentPrototype,
|
||||
Dict,
|
||||
isNil,
|
||||
logger,
|
||||
uuid,
|
||||
isWrappedCode,
|
||||
getCodeOfWrappedCode,
|
||||
wrapCodeWithJSXExpressionContainer,
|
||||
} from '@music163/tango-helpers';
|
||||
import { getRelativePath, isFilepath } from './string';
|
||||
import type { IImportDeclarationPayload, IImportSpecifierData } from '../types';
|
||||
import { code2expression } from './ast';
|
||||
import { isWrappedByExpressionContainer } from './assert';
|
||||
import { value2code } from './code-helpers';
|
||||
|
||||
export function prototype2importDeclarationData(
|
||||
prototype: IComponentPrototype,
|
||||
relativeFilepath?: string,
|
||||
): { source: string; specifiers: IImportSpecifierData[] } {
|
||||
let source = prototype.package;
|
||||
const isSnippet = prototype.type === 'snippet';
|
||||
if (relativeFilepath && isFilepath(source)) {
|
||||
source = getRelativePath(relativeFilepath, source);
|
||||
}
|
||||
@@ -32,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 {
|
||||
@@ -84,48 +91,56 @@ export function getImportDeclarationPayloadByPrototype(
|
||||
|
||||
/**
|
||||
* 基于 key-value 生成 prop={value} 字符串
|
||||
* @param key
|
||||
* @param value
|
||||
* @example { name: 'foo', initValue: false } >> name={false}
|
||||
* @example { name: 'foo', initValue: 1 } >> name={1}
|
||||
* @example { name: 'foo', initValue: () => {} } >> name={()=>{}}
|
||||
* @example { name: 'foo', initValue: { foo: 'bar' } } >> name={{ foo: 'bar' }}
|
||||
* @example { name: 'foo', initValue: [{ foo: 'bar' }] } >> name={[{ foo: 'bar' }]}
|
||||
* @example { name: 'foo', initValue: 'bar' } >> name="bar"
|
||||
* @example { name: 'foo', initValue: '{() => {}}' } >> name={()=>{}}
|
||||
* @example { name: 'foo', initValue: '{{() => {}}}' } >> name={() => {}}
|
||||
* @example { name: 'foo', initValue: '{bar}' } >> name={bar}
|
||||
* @returns
|
||||
*/
|
||||
function getPropKeyValuePair(item: IComponentProp, generateValue: (...args: any[]) => string) {
|
||||
export function propDataToKeyValueString(
|
||||
item: IComponentProp,
|
||||
generateValue?: (...args: any[]) => string,
|
||||
) {
|
||||
const key = item.name;
|
||||
|
||||
let value = item.initValue;
|
||||
|
||||
if (!value && item.autoInitValue) {
|
||||
value = generateValue(3);
|
||||
value = generateValue?.(3) || uuid(key, 3);
|
||||
}
|
||||
|
||||
if (isNil(value)) {
|
||||
if (value === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (typeof value) {
|
||||
case 'number':
|
||||
case 'boolean': {
|
||||
value = `{${value}}`;
|
||||
case 'boolean':
|
||||
case 'function': {
|
||||
value = wrapCodeWithJSXExpressionContainer(String(value));
|
||||
break;
|
||||
}
|
||||
case 'object': {
|
||||
// TIP: bugfix 如果 object 里有 jsx 或者 function 会失败
|
||||
try {
|
||||
value = `{${JSON.stringify(value)}}`;
|
||||
value = wrapCodeWithJSXExpressionContainer(value2code(value));
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'function': {
|
||||
value = `{${(value as object).toString()}}`;
|
||||
break;
|
||||
}
|
||||
case 'string': {
|
||||
if (!isWrappedByExpressionContainer(value)) {
|
||||
// 不是变量字符串
|
||||
value = `"${value}"`;
|
||||
if (isWrappedCode(value)) {
|
||||
const innerCode = getCodeOfWrappedCode(value);
|
||||
value = wrapCodeWithJSXExpressionContainer(innerCode);
|
||||
} else if (isWrappedByExpressionContainer(value)) {
|
||||
// TIP: 兼容旧版逻辑,如果是变量字符串,无需处理
|
||||
} else {
|
||||
// 如果是变量字符串,无需处理
|
||||
value = `"${value}"`;
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -155,11 +170,11 @@ export function prototype2code(prototype: IComponentPrototype, extraProps?: Dict
|
||||
}))
|
||||
: [];
|
||||
// merge extraProps to props
|
||||
const props = [...prototype.props, ...propList];
|
||||
const props = [...(prototype.props || []), ...propList];
|
||||
|
||||
const keys =
|
||||
props.reduce((acc, item) => {
|
||||
const pair = getPropKeyValuePair(item, (fractionDigits: number) =>
|
||||
const pair = propDataToKeyValueString(item, (fractionDigits: number) =>
|
||||
uuid(prototype.name, fractionDigits),
|
||||
);
|
||||
return pair ? ` ${acc} ${pair}` : acc;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -285,9 +285,16 @@ export interface IWorkspace {
|
||||
* 解析后的项目配置信息
|
||||
*/
|
||||
get projectConfig(): ITangoConfigJson;
|
||||
/**
|
||||
* 当前活动的视图文件
|
||||
*/
|
||||
get activeViewModule(): IViewFile;
|
||||
get pages(): any[];
|
||||
get bizComps(): string[];
|
||||
get baseComps(): string[];
|
||||
get localComps(): string[];
|
||||
/**
|
||||
* 是否是合法的项目
|
||||
*/
|
||||
get isValid(): boolean;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -144,6 +144,13 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
*/
|
||||
private copyTempNodes: TangoNode[];
|
||||
|
||||
/**
|
||||
* 是否是合法的项目
|
||||
*/
|
||||
get isValid() {
|
||||
return !!this.tangoConfigJson && !!this.activeViewModule;
|
||||
}
|
||||
|
||||
/**
|
||||
* 项目配置,返回解析后的 tango.config.json 文件
|
||||
*/
|
||||
@@ -1226,8 +1233,17 @@ export class Workspace extends EventTarget implements IWorkspace {
|
||||
let filename: string;
|
||||
this.routeModule?.routes.forEach((route) => {
|
||||
if (isPathnameMatchRoute(routePath, route.path) && route.importPath) {
|
||||
const absolutePath = route.importPath.replace('.', '/src');
|
||||
filename = this.getRealViewFilePath(absolutePath);
|
||||
if (route.importPath.startsWith('@/')) {
|
||||
filename = route.importPath;
|
||||
const alias = this.tangoConfigJson.getValue('sandbox.alias') || {};
|
||||
if (alias['@']) {
|
||||
filename = filename.replace('@', alias['@']);
|
||||
}
|
||||
filename = this.getRealViewFilePath(filename);
|
||||
} else {
|
||||
const absolutePath = route.importPath.replace('.', '/src');
|
||||
filename = this.getRealViewFilePath(absolutePath);
|
||||
}
|
||||
}
|
||||
});
|
||||
return filename;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isTangoVariable, isWrappedByExpressionContainer } from '../src/helpers';
|
||||
import { isTangoVariable } from '../src/helpers';
|
||||
|
||||
describe('assert', () => {
|
||||
it('isTangoVariable', () => {
|
||||
@@ -7,19 +7,4 @@ describe('assert', () => {
|
||||
expect(isTangoVariable('tango.stores.app?.name')).toBeTruthy();
|
||||
// expect(isTangoVariable('tango.copyToClipboard')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('isWrappedByExpressionContainer', () => {
|
||||
expect(isWrappedByExpressionContainer('{this.foo}')).toBeTruthy();
|
||||
expect(isWrappedByExpressionContainer('{!false}')).toBeTruthy();
|
||||
expect(isWrappedByExpressionContainer('{[]}')).toBeTruthy();
|
||||
expect(isWrappedByExpressionContainer('{{ foo: "bar" }}')).toBeTruthy();
|
||||
expect(isWrappedByExpressionContainer('{[{ foo: "bar" }]}')).toBeTruthy();
|
||||
expect(isWrappedByExpressionContainer('{123}')).toBeTruthy();
|
||||
expect(isWrappedByExpressionContainer('{"hello"}')).toBeTruthy();
|
||||
expect(isWrappedByExpressionContainer('{ foo: "bar" }')).toBeFalsy();
|
||||
expect(isWrappedByExpressionContainer('{ type: tango.stores?.homePage?.tabKey }')).toBeFalsy();
|
||||
expect(isWrappedByExpressionContainer('{ type: tango.stores.homePage.tabKey }')).toBeFalsy();
|
||||
expect(isWrappedByExpressionContainer('{ foo: "bar" }')).toBeFalsy();
|
||||
expect(isWrappedByExpressionContainer('{ color: tango.stores.app.color }')).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
@@ -54,8 +66,7 @@ describe('ast helpers', () => {
|
||||
|
||||
it('code2expression', () => {
|
||||
expect(code2expression('')).toBeUndefined();
|
||||
expect(code2expression('{tango.stores.app}')).toBeUndefined();
|
||||
|
||||
expect(code2expression('tango.stores.app').type).toBe('MemberExpression');
|
||||
expect(code2expression('{ type: window.bar }').type).toEqual('ObjectExpression');
|
||||
expect(code2expression('() => {};').type).toEqual('ArrowFunctionExpression');
|
||||
expect(code2expression('<Button>hello</Button>').type).toBe('JSXElement');
|
||||
@@ -69,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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
getJSXElementAttributes,
|
||||
inferFileType,
|
||||
deepCloneNode,
|
||||
code2value,
|
||||
} from '../src/helpers';
|
||||
import { FileType } from '../src/types';
|
||||
|
||||
@@ -27,15 +28,18 @@ describe('helpers', () => {
|
||||
|
||||
it('parse jsxElement attributes', () => {
|
||||
const node = code2expression(
|
||||
"<XColumn dataIndex='col' enumMap={{ 1: '已解决', 2: '未解决' }} />",
|
||||
"<Foo id={tango.user.id} num={1} str='col' enumMap={{ 1: '已解决', 2: '未解决' }} list={[{ key: 1 }, { key: 2 }]} />",
|
||||
);
|
||||
const attributes = getJSXElementAttributes(node as JSXElement);
|
||||
expect(attributes).toEqual({
|
||||
dataIndex: 'col',
|
||||
id: '{{tango.user.id}}',
|
||||
num: 1,
|
||||
str: 'col',
|
||||
enumMap: {
|
||||
1: '已解决',
|
||||
2: '未解决',
|
||||
},
|
||||
list: '{{[{ key: 1 }, { key: 2 }]}}',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -80,15 +84,22 @@ describe('helpers', () => {
|
||||
|
||||
it('expressionCode2ast', () => {
|
||||
expect(expressionCode2ast('<Button>hello</Button>').type).toEqual('File');
|
||||
expect(expressionCode2ast('{<Button>hello</Button>}').type).toEqual('File');
|
||||
expect(expressionCode2ast('() => <Button>hello</Button>').type).toEqual('File');
|
||||
expect(expressionCode2ast('{() => <Button>hello</Button>}').type).toEqual('File');
|
||||
});
|
||||
});
|
||||
|
||||
describe('string helpers', () => {
|
||||
it('value2code: empty array', () => {
|
||||
expect(value2code([])).toEqual('[]');
|
||||
expect(value2code({})).toEqual('{}');
|
||||
expect(value2code(() => {})).toEqual('() => {}');
|
||||
expect(value2code(true)).toEqual('true');
|
||||
expect(value2code(false)).toEqual('false');
|
||||
expect(value2code(1)).toEqual('1');
|
||||
expect(value2code('hello')).toEqual('"hello"');
|
||||
expect(value2code('{{window.tango}}')).toEqual('window.tango');
|
||||
expect(value2code('{{() => {}}}')).toEqual('() => {}');
|
||||
expect(value2code('{{1111}}')).toEqual('1111');
|
||||
});
|
||||
|
||||
it('value2code: array', () => {
|
||||
@@ -201,10 +212,26 @@ describe('schema helpers', () => {
|
||||
},
|
||||
],
|
||||
};
|
||||
const cloned = deepCloneNode(schema);
|
||||
expect(cloned.props.id).toBe(schema.props.id);
|
||||
expect(cloned.children[0].props.id).toBe(schema.children[0].props.id);
|
||||
|
||||
expect(cloned.id).not.toBe(schema.id);
|
||||
expect(cloned.children[0].id).not.toBe(schema.children[0].id);
|
||||
it('deepCloneNode', () => {
|
||||
const cloned = deepCloneNode(schema);
|
||||
expect(cloned.props.id).toBe(schema.props.id);
|
||||
expect(cloned.children[0].props.id).toBe(schema.children[0].props.id);
|
||||
expect(cloned.id).not.toBe(schema.id);
|
||||
expect(cloned.children[0].id).not.toBe(schema.children[0].id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('code helper', () => {
|
||||
it('code2value', () => {
|
||||
expect(code2value(`1`)).toEqual(1);
|
||||
expect(code2value(`false`)).toEqual(false);
|
||||
expect(code2value(`"foo"`)).toEqual('foo');
|
||||
expect(code2value(`{ foo: "foo" }`)).toEqual({ foo: 'foo' });
|
||||
expect(code2value(`[{ foo: "foo" }]`)).toEqual([{ foo: 'foo' }]);
|
||||
expect(code2value(`{ foo: "foo", ...{ bar: "bar"} }`)).toBe(undefined);
|
||||
expect(code2value(`() => {}`)).toBe(undefined);
|
||||
expect(code2value(`tango.stores.app.name`)).toBe(undefined);
|
||||
expect(code2value(`window`)).toBe(undefined);
|
||||
expect(code2value(`<div>hello</div>`)).toBe(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { propDataToKeyValueString } from '../src/helpers';
|
||||
|
||||
describe('prototype helpers', () => {
|
||||
it('propDataToKeyValueString', () => {
|
||||
// basic
|
||||
expect(propDataToKeyValueString({ name: 'foo', initValue: 'bar' })).toEqual('foo="bar"');
|
||||
expect(propDataToKeyValueString({ name: 'foo', initValue: 1 })).toEqual('foo={1}');
|
||||
expect(propDataToKeyValueString({ name: 'foo', initValue: false })).toEqual('foo={false}');
|
||||
expect(propDataToKeyValueString({ name: 'foo', initValue: null })).toEqual('foo={null}');
|
||||
expect(propDataToKeyValueString({ name: 'foo', initValue: [] })).toEqual('foo={[]}');
|
||||
expect(propDataToKeyValueString({ name: 'foo', initValue: {} })).toEqual('foo={{}}');
|
||||
expect(propDataToKeyValueString({ name: 'foo', initValue: () => {} })).toEqual(
|
||||
'foo={() => {}}',
|
||||
);
|
||||
expect(propDataToKeyValueString({ name: 'foo', initValue: { foo: 'bar' } })).toEqual(
|
||||
'foo={{ foo: "bar" }}',
|
||||
);
|
||||
expect(propDataToKeyValueString({ name: 'foo', initValue: [{ foo: 'bar' }] })).toEqual(
|
||||
'foo={[{ foo: "bar" }]}',
|
||||
);
|
||||
|
||||
// wrapped code
|
||||
expect(
|
||||
propDataToKeyValueString({ name: 'foo', initValue: '{{<Placeholder text="放置替换" />}}' }),
|
||||
).toEqual('foo={<Placeholder text="放置替换" />}');
|
||||
expect(propDataToKeyValueString({ name: 'foo', initValue: '{{tango}}' })).toEqual(
|
||||
'foo={tango}',
|
||||
);
|
||||
expect(propDataToKeyValueString({ name: 'foo', initValue: '{{"bar"}}' })).toEqual(
|
||||
'foo={"bar"}',
|
||||
);
|
||||
expect(propDataToKeyValueString({ name: 'foo', initValue: '{{() => {}}}' })).toEqual(
|
||||
'foo={() => {}}',
|
||||
);
|
||||
|
||||
// compatible with old version
|
||||
expect(propDataToKeyValueString({ name: 'foo', initValue: '{() => {}}' })).toEqual(
|
||||
'foo={() => {}}',
|
||||
);
|
||||
expect(
|
||||
propDataToKeyValueString({ name: 'foo', initValue: '{<Placeholder text="放置替换" />}' }),
|
||||
).toEqual('foo={<Placeholder text="放置替换" />}');
|
||||
// expect(propDataToKeyValueString({ name: 'foo', initValue: '{tango}' })).toEqual('foo={tango}');
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,49 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [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
|
||||
|
||||
- support quick set tid in SettingForm ([#151](https://github.com/netease/tango/issues/151)) ([1fc22a4](https://github.com/netease/tango/commit/1fc22a4535a8bdc12a018a41ebd5ab908fc46817))
|
||||
|
||||
### Features
|
||||
|
||||
- refactor parse attribute value ([#149](https://github.com/netease/tango/issues/149)) ([ffaa276](https://github.com/netease/tango/commit/ffaa276b5c205ed962d37e2fdb358a703f8fad01))
|
||||
|
||||
## [1.0.2](https://github.com/netease/tango/compare/@music163/tango-designer@1.0.1...@music163/tango-designer@1.0.2) (2024-04-22)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- allow reload state tree & display pageStore in variable tree ([#135](https://github.com/netease/tango/issues/135)) ([2664613](https://github.com/netease/tango/commit/2664613ab263aead2a5239fa012454fc7fd5ff99))
|
||||
|
||||
## [1.0.1](https://github.com/netease/tango/compare/@music163/tango-designer@1.0.0...@music163/tango-designer@1.0.1) (2024-04-16)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- render-setter expression value bug ([#133](https://github.com/netease/tango/issues/133)) ([27166ec](https://github.com/netease/tango/commit/27166ec94976979cb59130788399d90977cf0aec))
|
||||
- variable tree remove button event handling ([#134](https://github.com/netease/tango/issues/134)) ([52864b5](https://github.com/netease/tango/commit/52864b540095025232ae237134d6be5701d20549))
|
||||
|
||||
# [1.0.0-alpha.18](https://github.com/netease/tango/compare/@music163/tango-designer@1.0.0-alpha.17...@music163/tango-designer@1.0.0-alpha.18) (2024-04-09)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-designer",
|
||||
"version": "1.0.0",
|
||||
"version": "1.2.1",
|
||||
"description": "lowcode designer",
|
||||
"keywords": [
|
||||
"react"
|
||||
@@ -32,25 +32,25 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^4.8.0",
|
||||
"@music163/request": "^0.1.2",
|
||||
"@music163/tango-context": "^1.0.0",
|
||||
"@music163/tango-core": "^1.0.0",
|
||||
"@music163/tango-helpers": "^1.0.0-alpha.6",
|
||||
"@music163/tango-sandbox": "^1.0.0",
|
||||
"@music163/tango-setting-form": "^1.0.0",
|
||||
"@music163/tango-ui": "^1.0.0",
|
||||
"@music163/request": "^0.2.0",
|
||||
"@music163/tango-context": "^1.1.1",
|
||||
"@music163/tango-core": "^1.2.0",
|
||||
"@music163/tango-helpers": "^1.1.1",
|
||||
"@music163/tango-sandbox": "^1.0.4",
|
||||
"@music163/tango-setting-form": "^1.2.2",
|
||||
"@music163/tango-ui": "^1.2.1",
|
||||
"antd": "^4.24.2",
|
||||
"cash-dom": "^8.1.2",
|
||||
"classnames": "^2.3.2",
|
||||
"classnames": "^2.5.1",
|
||||
"color": "^4.2.3",
|
||||
"coral-system": "^1.0.5",
|
||||
"cssjson": "^2.1.3",
|
||||
"date-fns": "^2.29.2",
|
||||
"lodash-es": "^4.17.21",
|
||||
"moment": "^2.29.4",
|
||||
"moment": "^2.30.1",
|
||||
"react-color": "^2.19.3",
|
||||
"react-resizable": "^3.0.5",
|
||||
"semver": "^7.3.8"
|
||||
"semver": "^7.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/color": "^3.0.5",
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -204,7 +204,8 @@ export function VariableTree(props: VariableTreeProps) {
|
||||
{node.showRemoveButton && (
|
||||
<Popconfirm
|
||||
title={`确认删除吗 ${node.title}?该操作会导致引用此模型的代码报错,请谨慎操作!`}
|
||||
onConfirm={() => {
|
||||
onConfirm={(e) => {
|
||||
e.stopPropagation();
|
||||
if (isStoreVariablePath(node.key)) {
|
||||
onRemoveStoreVariable(node.key);
|
||||
} else {
|
||||
|
||||
@@ -3,7 +3,8 @@ import { Box } from 'coral-system';
|
||||
import { Button, Empty } from 'antd';
|
||||
import { PlayCircleOutlined } from '@ant-design/icons';
|
||||
import { InputCode, Panel, JsonView } from '@music163/tango-ui';
|
||||
import { isNil, logger, code2object, getValue } from '@music163/tango-helpers';
|
||||
import { isNil, logger, getValue } from '@music163/tango-helpers';
|
||||
import { code2value } from '@music163/tango-core';
|
||||
|
||||
export interface ServicePreviewProps {
|
||||
appContext?: any;
|
||||
@@ -22,7 +23,7 @@ export function ServicePreview({ appContext, functionKey }: ServicePreviewProps)
|
||||
editable
|
||||
showLineNumbers
|
||||
onChange={(value: string) => {
|
||||
const obj = code2object(value);
|
||||
const obj = code2value(value); // 转为 object 对象
|
||||
setPayload(obj);
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -75,7 +75,7 @@ export function getElementData(
|
||||
const display = getElementCSSDisplay(element);
|
||||
return {
|
||||
id: dnd.id,
|
||||
codeId: dnd.index,
|
||||
codeId: dnd.codeId,
|
||||
name: dnd.component || element.tagName.toLowerCase(),
|
||||
filename: dnd.filename,
|
||||
bounding,
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
@@ -2,9 +2,8 @@ import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { Box } from 'coral-system';
|
||||
// @ts-ignore
|
||||
import { toJSON } from 'cssjson';
|
||||
import { InputNumber, Space } from 'antd';
|
||||
import { InputNumber, Slider, Space } from 'antd';
|
||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||
import { SliderSetter } from './number-setter';
|
||||
import {
|
||||
BgSetter,
|
||||
BorderSetter,
|
||||
@@ -16,10 +15,11 @@ import {
|
||||
FlexJustifyContentSetter,
|
||||
SpacingSetter,
|
||||
} from './style-setter';
|
||||
import { wrapCode } from '@music163/tango-helpers';
|
||||
|
||||
const getRawCssValue = (value: string) => {
|
||||
if (value && value.startsWith('{css`')) {
|
||||
return value.split('').slice(5, -2).join('').trim();
|
||||
if (value && value.startsWith('{{css`')) {
|
||||
return value.split('').slice(6, -3).join('').trim();
|
||||
}
|
||||
return value;
|
||||
};
|
||||
@@ -51,7 +51,8 @@ const cssPattern = (string: string, name: string): any => {
|
||||
};
|
||||
|
||||
/**
|
||||
* 废弃
|
||||
* coral-system css prop
|
||||
* @example 提供 css-in-js 代码支持,例如 css`background: red;`
|
||||
* @deprecated 使用嵌套属性代替
|
||||
*/
|
||||
export function CssSetter(props: FormItemComponentProps<string>) {
|
||||
@@ -82,16 +83,14 @@ export function CssSetter(props: FormItemComponentProps<string>) {
|
||||
|
||||
useEffect(() => {
|
||||
if (contentValue || contentValue === '') {
|
||||
onChange(`{css\`${contentValue}\`}`, {
|
||||
// relatedImports: ['']
|
||||
});
|
||||
onChange(wrapCode(`css\`${contentValue}\``));
|
||||
}
|
||||
}, [contentValue]);
|
||||
|
||||
const isFlex = ['flex', 'inline-flex'].includes(display);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box bg="fill1">
|
||||
<ItemGroup title="布局方式">
|
||||
<DisplaySetter
|
||||
value={toJSON(contentValue).attributes?.display}
|
||||
@@ -204,12 +203,12 @@ export function CssSetter(props: FormItemComponentProps<string>) {
|
||||
/>
|
||||
</ItemGroup>
|
||||
<ItemGroup title="透明度">
|
||||
<SliderSetter
|
||||
<Slider
|
||||
max={1}
|
||||
min={0}
|
||||
step={0.1}
|
||||
value={toJSON(contentValue).attributes?.opacity || 1}
|
||||
onChange={(v) => {
|
||||
onChange={(v: number) => {
|
||||
changeStyle(v, 'opacity');
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -25,13 +25,18 @@ const style = {
|
||||
width: '100%',
|
||||
};
|
||||
|
||||
export function DateSetter({ value, onChange, format = 'YYYY-MM-DD', ...rest }: FormItemComponentProps<string>) {
|
||||
export function DateSetter({
|
||||
value,
|
||||
onChange,
|
||||
format = 'YYYY-MM-DD',
|
||||
...rest
|
||||
}: FormItemComponentProps<string>) {
|
||||
return (
|
||||
<DatePicker
|
||||
{...rest}
|
||||
format={format}
|
||||
style={style}
|
||||
value={toMoment(value, format)}
|
||||
value={value ? toMoment(value, format) : undefined}
|
||||
onChange={(val, str) => {
|
||||
onChange && onChange(str);
|
||||
}}
|
||||
@@ -46,7 +51,12 @@ function toMoments(value: string[], format: string): moment.Moment[] {
|
||||
return [];
|
||||
}
|
||||
|
||||
export function DateRangeSetter({ value, onChange, format = 'YYYY-MM-DD', ...rest }: FormItemComponentProps<string[]>) {
|
||||
export function DateRangeSetter({
|
||||
value,
|
||||
onChange,
|
||||
format = 'YYYY-MM-DD',
|
||||
...rest
|
||||
}: FormItemComponentProps<string[]>) {
|
||||
return (
|
||||
<DatePicker.RangePicker
|
||||
{...rest}
|
||||
@@ -60,7 +70,12 @@ export function DateRangeSetter({ value, onChange, format = 'YYYY-MM-DD', ...res
|
||||
);
|
||||
}
|
||||
|
||||
export function TimeSetter({ value, onChange, format = 'HH:mm:ss', ...rest }: FormItemComponentProps<string>) {
|
||||
export function TimeSetter({
|
||||
value,
|
||||
onChange,
|
||||
format = 'HH:mm:ss',
|
||||
...rest
|
||||
}: FormItemComponentProps<string>) {
|
||||
return (
|
||||
<TimePicker
|
||||
{...rest}
|
||||
@@ -74,7 +89,12 @@ export function TimeSetter({ value, onChange, format = 'HH:mm:ss', ...rest }: Fo
|
||||
);
|
||||
}
|
||||
|
||||
export function TimeRangeSetter({ value, onChange, format = 'HH:mm:ss', ...rest }: FormItemComponentProps<string[]>) {
|
||||
export function TimeRangeSetter({
|
||||
value,
|
||||
onChange,
|
||||
format = 'HH:mm:ss',
|
||||
...rest
|
||||
}: FormItemComponentProps<string[]>) {
|
||||
return (
|
||||
<TimePicker.RangePicker
|
||||
{...rest}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
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 { ExpressionModal, getWrappedExpressionCode } from './expression-setter';
|
||||
import { wrapCode } from '@music163/tango-helpers';
|
||||
import { ExpressionPopover } from './expression-setter';
|
||||
import { value2code } from '@music163/tango-core';
|
||||
|
||||
enum EventAction {
|
||||
NoAction = 'noAction',
|
||||
@@ -23,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>;
|
||||
|
||||
/**
|
||||
@@ -41,20 +34,47 @@ 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() || [];
|
||||
|
||||
const code = value2code(value);
|
||||
|
||||
const handleChange = useCallback<FormItemComponentProps['onChange']>(
|
||||
(nextValue: any, ...args) => {
|
||||
const ret = getWrappedExpressionCode(nextValue);
|
||||
if (ret !== value) {
|
||||
onChange(ret, ...args);
|
||||
if (!nextValue) {
|
||||
onChange(undefined);
|
||||
}
|
||||
if (nextValue !== code) {
|
||||
onChange(wrapCode(nextValue), ...args);
|
||||
}
|
||||
},
|
||||
[onChange, value],
|
||||
[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) => {
|
||||
@@ -62,9 +82,6 @@ export function EventSetter(props: EventSetterProps) {
|
||||
setTemp(''); // 重置二级选项值
|
||||
|
||||
switch (key) {
|
||||
case EventAction.BindExpression:
|
||||
setExpModalVisible(true);
|
||||
break;
|
||||
case EventAction.ConsoleLog:
|
||||
handleChange('(...args) => console.log(...args)');
|
||||
break;
|
||||
@@ -76,23 +93,11 @@ export function EventSetter(props: EventSetterProps) {
|
||||
}
|
||||
};
|
||||
|
||||
const actionText = getActionText(type, temp, value);
|
||||
const inputValue = value;
|
||||
const actionText = getActionText(type, temp, code);
|
||||
|
||||
return (
|
||||
<Box css={wrapperStyle}>
|
||||
<ActionSelect options={options} onSelect={onAction} text={actionText} />
|
||||
<ExpressionModal
|
||||
title={modalTitle}
|
||||
value={inputValue}
|
||||
visible={expModalVisible}
|
||||
onCancel={() => setExpModalVisible(false)}
|
||||
onOk={(nextValue) => {
|
||||
setExpModalVisible(false);
|
||||
handleChange(nextValue);
|
||||
}}
|
||||
dataSource={actionVariables}
|
||||
/>
|
||||
{type === EventAction.NavigateTo && (
|
||||
<AutoComplete
|
||||
placeholder="选择或输入页面路由"
|
||||
@@ -142,12 +147,12 @@ const handlerMap = {
|
||||
[EventAction.NavigateTo]: 'navigateTo',
|
||||
};
|
||||
|
||||
function getActionText(type: EventAction, temp: string, value: any) {
|
||||
function getActionText(type: EventAction, temp: string, fallbackCode: string) {
|
||||
let text;
|
||||
if (handlerMap[type]) {
|
||||
text = getExpressionValue(type, temp);
|
||||
} else if (value) {
|
||||
text = value;
|
||||
} else if (fallbackCode) {
|
||||
text = fallbackCode;
|
||||
}
|
||||
text = text || '请选择';
|
||||
return text;
|
||||
@@ -156,6 +161,6 @@ function getActionText(type: EventAction, temp: string, value: any) {
|
||||
function getExpressionValue(type: EventAction, value = '') {
|
||||
const handler = handlerMap[type];
|
||||
if (handler) {
|
||||
return `{() => tango.${handler}("${value}")}`;
|
||||
return `() => tango.${handler}("${value}")`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import React from 'react';
|
||||
import { SingleMonacoEditor } from '@music163/tango-ui';
|
||||
import { Box } from 'coral-system';
|
||||
import { FormItemComponentProps } from '@music163/tango-setting-form/src/form-item';
|
||||
|
||||
export function ExpressionSetter({ value, onChange }: FormItemComponentProps) {
|
||||
return (
|
||||
<Box height="120px" border="solid" borderColor="line.normal" borderRadius="s">
|
||||
<SingleMonacoEditor
|
||||
defaultValue={value}
|
||||
onBlur={(newValue) => {
|
||||
if (newValue !== value) {
|
||||
onChange(`{${newValue}}`);
|
||||
}
|
||||
}}
|
||||
language="javascript"
|
||||
options={{
|
||||
lineNumbers: 'off',
|
||||
minimap: {
|
||||
enabled: false,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -1,26 +1,10 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { Box, Text, css } from 'coral-system';
|
||||
import { Dropdown, Modal } from 'antd';
|
||||
import {
|
||||
isValidExpressionCode,
|
||||
isWrappedByExpressionContainer,
|
||||
value2expressionCode,
|
||||
} from '@music163/tango-core';
|
||||
import {
|
||||
getVariableContent,
|
||||
noop,
|
||||
useBoolean,
|
||||
getValue,
|
||||
IVariableTreeNode,
|
||||
} from '@music163/tango-helpers';
|
||||
import {
|
||||
CloseCircleFilled,
|
||||
ExpandAltOutlined,
|
||||
InfoOutlined,
|
||||
KeyOutlined,
|
||||
MenuOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Panel, InputCode, Action, CodeOutlined } from '@music163/tango-ui';
|
||||
import { Dropdown, Button } from 'antd';
|
||||
import { isValidExpressionCode } from '@music163/tango-core';
|
||||
import { getValue, IVariableTreeNode, noop } from '@music163/tango-helpers';
|
||||
import { CloseCircleFilled, ExpandAltOutlined, MenuOutlined } from '@ant-design/icons';
|
||||
import { Panel, InputCode, Action, DragPanel } from '@music163/tango-ui';
|
||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||
import { useWorkspace, useWorkspaceData } from '@music163/tango-context';
|
||||
import { VariableTree } from '../components';
|
||||
@@ -29,42 +13,19 @@ import { CODE_TEMPLATES } from '../helpers';
|
||||
import { shapeServiceValues } from '../sidebar/datasource-panel/interface-config';
|
||||
|
||||
export const expressionValueValidate = (value: string) => {
|
||||
if (isWrappedByExpressionContainer(value)) {
|
||||
const exp = getVariableContent(value);
|
||||
if (!isValidExpressionCode(exp)) {
|
||||
return '表达式存在语法错误!';
|
||||
}
|
||||
if (!isValidExpressionCode(value)) {
|
||||
return '表达式存在语法错误!';
|
||||
}
|
||||
};
|
||||
|
||||
export const jsonValueValidate = (value: string) => {
|
||||
if (isWrappedByExpressionContainer(value)) {
|
||||
const jsonStr = getVariableContent(value);
|
||||
try {
|
||||
JSON.parse(jsonStr);
|
||||
} catch (e) {
|
||||
return '不是合法的 JSON 语法!';
|
||||
}
|
||||
try {
|
||||
JSON.parse(value);
|
||||
} catch (e) {
|
||||
return '不是合法的 JSON 语法!';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 返回 `{}` 包裹后的表达式代码
|
||||
* @param code 原始代码
|
||||
* @returns
|
||||
*/
|
||||
export function getWrappedExpressionCode(code: string) {
|
||||
let ret;
|
||||
if (!code) {
|
||||
// do nothing
|
||||
} else if (isWrappedByExpressionContainer(code)) {
|
||||
ret = code;
|
||||
} else {
|
||||
ret = `{${code}}`;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
const suffixStyle = css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -90,23 +51,19 @@ export function ExpressionSetter(props: ExpressionSetterProps) {
|
||||
modalTitle,
|
||||
modalTip,
|
||||
autoCompleteOptions,
|
||||
placeholder = '输入JS代码',
|
||||
placeholder = '在这里输入JS代码',
|
||||
value: valueProp,
|
||||
status,
|
||||
allowClear = true,
|
||||
newStoreTemplate,
|
||||
showOptionsDropDown = true,
|
||||
} = props;
|
||||
const [visible, { on, off }] = useBoolean();
|
||||
const [inputValue, setInputValue] = useState(() => {
|
||||
return value2expressionCode(valueProp);
|
||||
});
|
||||
const sandbox = useSandboxQuery();
|
||||
const evaluateContext = sandbox.window;
|
||||
// const codeValue = getCodeOfWrappedCode(valueProp);
|
||||
const [inputValue, setInputValue] = useState(valueProp);
|
||||
|
||||
// when receive new value, sync state
|
||||
useEffect(() => {
|
||||
setInputValue(value2expressionCode(valueProp));
|
||||
setInputValue(valueProp);
|
||||
}, [valueProp]);
|
||||
|
||||
const change = useCallback(
|
||||
@@ -114,18 +71,14 @@ export function ExpressionSetter(props: ExpressionSetterProps) {
|
||||
if (code === valueProp) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ret = getWrappedExpressionCode(code);
|
||||
|
||||
if (ret === valueProp) {
|
||||
return;
|
||||
}
|
||||
|
||||
onChange(ret);
|
||||
onChange(code);
|
||||
},
|
||||
[valueProp, onChange],
|
||||
);
|
||||
|
||||
const sandbox = useSandboxQuery();
|
||||
const evaluateContext = sandbox.window;
|
||||
|
||||
return (
|
||||
<Box className="ExpressionSetter">
|
||||
{/* 同时支持下拉框展示 */}
|
||||
@@ -161,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>
|
||||
}
|
||||
@@ -179,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[];
|
||||
@@ -212,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,
|
||||
@@ -249,109 +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">输入的表达式存在语法错误,请修改后再提交!</Text> : null}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -25,44 +25,46 @@ import {
|
||||
FlexAlignItemsSetter,
|
||||
FlexDirectionSetter,
|
||||
} from './style-setter';
|
||||
import { BoolSetter } from './bool-setter';
|
||||
import { ChoiceSetter } from './choice-setter';
|
||||
import { NumberSetter, SliderSetter } from './number-setter';
|
||||
import { TextAreaSetter, TextSetter } from './text-setter';
|
||||
import { isValidExpressionCode } from '@music163/tango-core';
|
||||
|
||||
const setters: IFormItemCreateOptions[] = [
|
||||
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[] = [
|
||||
{
|
||||
name: 'boolSetter',
|
||||
component: BoolSetter,
|
||||
},
|
||||
{
|
||||
name: 'choiceSetter',
|
||||
component: ChoiceSetter,
|
||||
},
|
||||
{
|
||||
name: 'expSetter',
|
||||
alias: ['expressionSetter'],
|
||||
name: 'codeSetter',
|
||||
alias: ['expSetter', 'expressionSetter'],
|
||||
component: ExpressionSetter,
|
||||
type: 'code',
|
||||
validate: codeValidate,
|
||||
},
|
||||
{
|
||||
name: 'numberSetter',
|
||||
component: NumberSetter,
|
||||
},
|
||||
{
|
||||
name: 'textSetter',
|
||||
component: TextSetter,
|
||||
},
|
||||
{ name: 'textAreaSetter', component: TextAreaSetter },
|
||||
{
|
||||
name: 'sliderSetter',
|
||||
component: SliderSetter,
|
||||
name: 'radioGroupSetter',
|
||||
alias: ['choiceSetter'],
|
||||
component: ChoiceSetter,
|
||||
},
|
||||
{
|
||||
name: 'actionListSetter',
|
||||
component: ActionListSetter,
|
||||
},
|
||||
{
|
||||
name: 'columnSetter',
|
||||
name: 'tableColumnsSetter',
|
||||
alias: ['columnSetter'], // 兼容
|
||||
component: ColumnSetter,
|
||||
},
|
||||
{
|
||||
@@ -94,18 +96,17 @@ const setters: IFormItemCreateOptions[] = [
|
||||
alias: ['actionSetter', 'functionSetter', 'callbackSetter'],
|
||||
component: EventSetter,
|
||||
},
|
||||
{
|
||||
name: 'expressionSetter',
|
||||
alias: ['expSetter'],
|
||||
component: ExpressionSetter,
|
||||
},
|
||||
{
|
||||
name: 'jsonSetter',
|
||||
component: JSONSetter,
|
||||
type: 'code',
|
||||
validate: jsonValidate,
|
||||
},
|
||||
{
|
||||
name: 'jsxSetter',
|
||||
component: JsxSetter,
|
||||
type: 'code',
|
||||
validate: codeValidate,
|
||||
},
|
||||
{
|
||||
name: 'listSetter',
|
||||
@@ -120,20 +121,26 @@ const setters: IFormItemCreateOptions[] = [
|
||||
component: OptionSetter,
|
||||
},
|
||||
{
|
||||
name: 'pickerSetter',
|
||||
name: 'selectSetter',
|
||||
alias: ['pickerSetter'],
|
||||
component: PickerSetter,
|
||||
},
|
||||
{
|
||||
name: 'renderPropsSetter',
|
||||
component: RenderSetter,
|
||||
type: 'code',
|
||||
validate: codeValidate,
|
||||
},
|
||||
{
|
||||
name: 'tableCellSetter',
|
||||
component: TableCellSetter,
|
||||
type: 'code',
|
||||
},
|
||||
{
|
||||
name: 'tableExpandableSetter',
|
||||
component: TableExpandableSetter,
|
||||
type: 'code',
|
||||
validate: codeValidate,
|
||||
},
|
||||
{
|
||||
name: 'routerSetter',
|
||||
@@ -169,5 +176,5 @@ const setters: IFormItemCreateOptions[] = [
|
||||
];
|
||||
|
||||
export function registerBuiltinSetters() {
|
||||
setters.forEach(register);
|
||||
BUILT_IN_SETTERS.forEach(register);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { Box } from 'coral-system';
|
||||
import { SingleMonacoEditor } from '@music163/tango-ui';
|
||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||
@@ -6,14 +6,18 @@ import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||
/**
|
||||
* JSON Setter
|
||||
*/
|
||||
export function JSONSetter({ value, onChange }: FormItemComponentProps) {
|
||||
export function JSONSetter({ value: valueProp, onChange }: FormItemComponentProps) {
|
||||
const [value, setValue] = useState(valueProp || '');
|
||||
return (
|
||||
<Box height="120px" border="solid" borderColor="line.normal" borderRadius="s">
|
||||
<SingleMonacoEditor
|
||||
defaultValue={value}
|
||||
onChange={(newValue) => {
|
||||
setValue(newValue);
|
||||
}}
|
||||
onBlur={(newValue) => {
|
||||
if (newValue !== value) {
|
||||
onChange(`{${newValue}}`);
|
||||
onChange(newValue);
|
||||
}
|
||||
}}
|
||||
language="json"
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React from 'react';
|
||||
import { isPlainString } from '@music163/tango-helpers';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { ActionSelect, InputCode } from '@music163/tango-ui';
|
||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||
import { Box } from 'coral-system';
|
||||
@@ -54,24 +53,36 @@ const defaultGetTemplate = (key: string) => {
|
||||
*/
|
||||
export function JsxSetter(props: JsxSetterProps) {
|
||||
const { showInput, getTemplate = defaultGetTemplate, value, onChange } = props;
|
||||
const [inputValue, setInputValue] = useState(value);
|
||||
useEffect(() => {
|
||||
setInputValue(value);
|
||||
}, [value]);
|
||||
return (
|
||||
<Box>
|
||||
<ActionSelect
|
||||
showInput={showInput}
|
||||
defaultInputValue={isPlainString(value) ? value : undefined}
|
||||
defaultInputValue={value}
|
||||
options={options}
|
||||
text="设置此区域为"
|
||||
onInputChange={onChange}
|
||||
onSelect={(key) => {
|
||||
const [tpl, deps] = getTemplate(key);
|
||||
if (tpl) {
|
||||
onChange(`{${tpl}}`, { relatedImports: deps });
|
||||
onChange(tpl, { relatedImports: deps });
|
||||
} else {
|
||||
onChange(undefined);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{value && <InputCode value={value} readOnly editable={false} />}
|
||||
{value && (
|
||||
<InputCode
|
||||
value={value}
|
||||
onChange={(val) => setInputValue(val)}
|
||||
onBlur={() => {
|
||||
onChange(inputValue);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -234,6 +234,7 @@ function NewOptionForm({ fields = [], initialValues = {}, onSubmit }: NewOptionF
|
||||
label={item.label}
|
||||
name={item.name}
|
||||
required={item.required}
|
||||
rules={[{ required: item.required }]}
|
||||
valuePropName={item.valuePropName}
|
||||
extra={item.extra}
|
||||
style={{
|
||||
@@ -260,7 +261,7 @@ interface ListSetterProps extends FormItemComponentProps<any[]> {
|
||||
addBtnText?: string;
|
||||
getListItemKey?: (item: any) => React.Key;
|
||||
renderItem?: (item: any) => React.ReactNode;
|
||||
};
|
||||
}
|
||||
|
||||
const defaultListItemFormFields: ListSetterItemProps['formFields'] = [
|
||||
{ label: 'key', name: 'key', required: true },
|
||||
|
||||
@@ -16,6 +16,11 @@ export interface RenderSetterProps {
|
||||
fallbackOption?: IRenderOption;
|
||||
}
|
||||
|
||||
const defaultOptions: IRenderOption[] = [
|
||||
{ label: '取消自定义', value: '' },
|
||||
{ label: '自定义渲染', value: 'Box', render: '() => <Box></Box>' },
|
||||
];
|
||||
|
||||
/**
|
||||
* Render Props Setters
|
||||
*/
|
||||
@@ -23,11 +28,10 @@ export function RenderSetter({
|
||||
value,
|
||||
onChange,
|
||||
text = '自定义渲染为',
|
||||
options = [],
|
||||
options = defaultOptions,
|
||||
fallbackOption,
|
||||
}: FormItemComponentProps & RenderSetterProps) {
|
||||
const [inputValue, setInputValue] = useState(value);
|
||||
|
||||
const [inputValue, setInputValue] = useState(value || '');
|
||||
useEffect(() => {
|
||||
setInputValue(value);
|
||||
}, [value]);
|
||||
@@ -53,9 +57,9 @@ export function RenderSetter({
|
||||
return (
|
||||
<Box>
|
||||
<ActionSelect text={text} options={options} onSelect={onSelect} />
|
||||
{value && (
|
||||
{inputValue && (
|
||||
<InputCode
|
||||
value={value}
|
||||
value={inputValue}
|
||||
onChange={(val) => setInputValue(val)}
|
||||
onBlur={() => onChange(inputValue)}
|
||||
/>
|
||||
@@ -65,17 +69,22 @@ export function RenderSetter({
|
||||
}
|
||||
|
||||
const getRender = (content: string, type?: 'tableCell' | 'tableExpandable') => {
|
||||
let code;
|
||||
switch (type) {
|
||||
case 'tableCell':
|
||||
return `{(value, record, index) => ${content}}`;
|
||||
code = `(value, record, index) => ${content}`;
|
||||
break;
|
||||
case 'tableExpandable':
|
||||
return `{{
|
||||
expandedRowRender: (record) => ${content},
|
||||
rowExpandable: (record) => true
|
||||
}}`;
|
||||
code = `{
|
||||
expandedRowRender: (record) => ${content},
|
||||
rowExpandable: (record) => true
|
||||
}`;
|
||||
break;
|
||||
default:
|
||||
return `{() => ${content}}`;
|
||||
code = `() => ${content}`;
|
||||
break;
|
||||
}
|
||||
return code;
|
||||
};
|
||||
|
||||
const tableCellOptions: RenderSetterProps['options'] = [
|
||||
@@ -120,6 +129,7 @@ export function TableCellSetter(props: FormItemComponentProps) {
|
||||
return <RenderSetter options={tableCellOptions} {...props} />;
|
||||
}
|
||||
|
||||
// FIXME: 应该直接用 props 嵌套的模式
|
||||
export function TableExpandableSetter(props: FormItemComponentProps) {
|
||||
return <RenderSetter options={tableExpandableOptions} text="配置表格可展开行" {...props} />;
|
||||
}
|
||||
|
||||
@@ -7,8 +7,6 @@ import { BgColorsOutlined, EyeInvisibleOutlined, FileImageOutlined } from '@ant-
|
||||
import { SingleMonacoEditor, LineSolidOutlined, LineDashedOutlined } from '@music163/tango-ui';
|
||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||
import { ChoiceSetter } from './choice-setter';
|
||||
import { TextSetter } from './text-setter';
|
||||
// import { ImageSetter } from './image-setter';
|
||||
|
||||
function getRawCssValue(value: string) {
|
||||
if (value && value.startsWith('{css`')) {
|
||||
@@ -17,12 +15,13 @@ function getRawCssValue(value: string) {
|
||||
return value;
|
||||
}
|
||||
|
||||
// TODO: style object setter
|
||||
|
||||
/**
|
||||
* 不稳定,暂不推荐使用
|
||||
*/
|
||||
export function CssCodeSetter({ value, onChange }: FormItemComponentProps<string>) {
|
||||
const contentValue = getRawCssValue(value);
|
||||
// TODO: relatedImports
|
||||
return (
|
||||
<Popover
|
||||
placement="leftBottom"
|
||||
@@ -34,9 +33,7 @@ export function CssCodeSetter({ value, onChange }: FormItemComponentProps<string
|
||||
value={contentValue}
|
||||
onBlur={(newCode) => {
|
||||
if (newCode != contentValue) {
|
||||
onChange(`{css\`${newCode}\`}`, {
|
||||
// relatedImports: ['']
|
||||
});
|
||||
onChange(`{css\`${newCode}\`}`, {});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
@@ -239,9 +236,10 @@ export function BgSetter({ value, onChange }: FormItemComponentProps<string>) {
|
||||
<Box mt="m">
|
||||
{mode === 'color' && <ColorSetter value={value} onChange={onChange} />}
|
||||
{mode === 'image' && (
|
||||
<TextSetter
|
||||
<Input
|
||||
value={getImageUrl(value)}
|
||||
onChange={(imgUrl) => {
|
||||
onChange={(e) => {
|
||||
const imgUrl = e.target.value;
|
||||
onChange(imgUrl ? `url(${imgUrl})` : undefined);
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -2,11 +2,15 @@ import React from 'react';
|
||||
import { Box } from 'coral-system';
|
||||
import { SettingForm, FormModel, SettingFormProps } from '@music163/tango-setting-form';
|
||||
import { Panel } from '@music163/tango-ui';
|
||||
import { clone } from '@music163/tango-helpers';
|
||||
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;
|
||||
@@ -103,6 +107,11 @@ export const SettingPanel = observer((props: SettingPanelProps) => {
|
||||
prototype={prototype}
|
||||
showIdentifier={{
|
||||
identifierKey: 'tid',
|
||||
getIdentifier: () => {
|
||||
// 直接拿当前的虚拟 id 作为 tid
|
||||
const { codeId } = parseDndId(workspace.selectSource.first.id);
|
||||
return codeId;
|
||||
},
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@@ -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,8 +1,6 @@
|
||||
import React from 'react';
|
||||
import { observer, useWorkspace, useWorkspaceData } from '@music163/tango-context';
|
||||
import { Box } from 'coral-system';
|
||||
import { getVariableContent } from '@music163/tango-helpers';
|
||||
import { isWrappedByExpressionContainer } from '@music163/tango-core';
|
||||
import { VariableTree } from '../../components';
|
||||
import { useSandboxQuery } from '../../context';
|
||||
|
||||
@@ -10,10 +8,6 @@ import { useSandboxQuery } from '../../context';
|
||||
export function shapeServiceValues(val: any) {
|
||||
const shapeValues = { ...val };
|
||||
delete shapeValues.type;
|
||||
// 兼容旧版,如果 formatter 包裹了 {} 则删掉首尾
|
||||
if (shapeValues.formatter && isWrappedByExpressionContainer(shapeValues.formatter)) {
|
||||
shapeValues.formatter = getVariableContent(shapeValues.formatter);
|
||||
}
|
||||
return shapeValues;
|
||||
}
|
||||
|
||||
|
||||
@@ -81,8 +81,8 @@ const OutlineTreeNode: React.FC<{ node: ITangoViewNodeData } & ComponentsTreePro
|
||||
const sandboxQuery = useSandboxQuery();
|
||||
const [visible, setVisible] = useState(true);
|
||||
const nodeLabel = (() => {
|
||||
const { index } = parseDndId(node.id);
|
||||
return index;
|
||||
const { codeId } = parseDndId(node.id);
|
||||
return codeId;
|
||||
})();
|
||||
const componentPrototype = workspace.componentPrototypes.get(node.component);
|
||||
const icon = componentPrototype?.icon || 'icon-placeholder';
|
||||
|
||||
@@ -23,11 +23,7 @@ export function OutlinePanel({ showStateView = true, ...treeProps }: OutlineView
|
||||
>
|
||||
<ComponentsTree {...treeProps} />
|
||||
</CollapsePanel>
|
||||
{showStateView && (
|
||||
<CollapsePanel title="页面状态" stickyHeader maxHeight="90%" overflowY="auto">
|
||||
<StateTree />
|
||||
</CollapsePanel>
|
||||
)}
|
||||
{showStateView && <StateTree />}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React, { useEffect, useReducer } from 'react';
|
||||
import { observer } from '@music163/tango-context';
|
||||
import { isFunction, isObject, pick } from '@music163/tango-helpers';
|
||||
import { JsonView } from '@music163/tango-ui';
|
||||
import { CollapsePanel, IconButton, JsonView } from '@music163/tango-ui';
|
||||
import { ReloadOutlined } from '@ant-design/icons';
|
||||
import { useSandboxQuery } from '../../context';
|
||||
|
||||
function processTango(obj: any = {}, index = 0) {
|
||||
@@ -26,7 +27,7 @@ export const StateTree = observer(() => {
|
||||
const [, forceUpdate] = useReducer((x) => x + 1, 0);
|
||||
const tangoContext = pick(sandboxQuery.window['tango'] || {}, [
|
||||
'stores',
|
||||
'page',
|
||||
'pageStore',
|
||||
'services',
|
||||
'config',
|
||||
]);
|
||||
@@ -34,6 +35,7 @@ export const StateTree = observer(() => {
|
||||
forceUpdate();
|
||||
};
|
||||
useEffect(() => {
|
||||
// FIXME: 优化这段代码,应该监听 tangoContext 的变化,来刷新
|
||||
const settingForm = document.querySelector('.SettingPanel');
|
||||
const ob = new MutationObserver(callback);
|
||||
if (settingForm) {
|
||||
@@ -48,13 +50,31 @@ export const StateTree = observer(() => {
|
||||
};
|
||||
}, []);
|
||||
return (
|
||||
<JsonView
|
||||
src={processTango(tangoContext)}
|
||||
collapsed={1}
|
||||
name="tango"
|
||||
displayObjectSize={false}
|
||||
indentWidth={2}
|
||||
enableCopy
|
||||
/>
|
||||
<CollapsePanel
|
||||
title="页面状态"
|
||||
stickyHeader
|
||||
maxHeight="90%"
|
||||
overflowY="auto"
|
||||
extra={
|
||||
<IconButton
|
||||
tooltip="同步状态"
|
||||
size="small"
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
forceUpdate();
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<JsonView
|
||||
src={processTango(tangoContext)}
|
||||
collapsed={1}
|
||||
name="tango"
|
||||
displayObjectSize={false}
|
||||
indentWidth={2}
|
||||
enableCopy
|
||||
/>
|
||||
</CollapsePanel>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -3,6 +3,29 @@
|
||||
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
|
||||
|
||||
- parse module with module alias ([#147](https://github.com/netease/tango/issues/147)) ([56d0877](https://github.com/netease/tango/commit/56d0877507b0138877eac6f36db288147b43c7d9))
|
||||
- support quick set tid in SettingForm ([#151](https://github.com/netease/tango/issues/151)) ([1fc22a4](https://github.com/netease/tango/commit/1fc22a4535a8bdc12a018a41ebd5ab908fc46817))
|
||||
|
||||
### Features
|
||||
|
||||
- refactor parse attribute value ([#149](https://github.com/netease/tango/issues/149)) ([ffaa276](https://github.com/netease/tango/commit/ffaa276b5c205ed962d37e2fdb358a703f8fad01))
|
||||
|
||||
# [1.0.0-alpha.7](https://github.com/netease/tango/compare/@music163/tango-helpers@1.0.0-alpha.6...@music163/tango-helpers@1.0.0-alpha.7) (2024-04-22)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- allow reload state tree & display pageStore in variable tree ([#135](https://github.com/netease/tango/issues/135)) ([2664613](https://github.com/netease/tango/commit/2664613ab263aead2a5239fa012454fc7fd5ff99))
|
||||
|
||||
# [1.0.0-alpha.6](https://github.com/netease/tango/compare/@music163/tango-helpers@1.0.0-alpha.5...@music163/tango-helpers@1.0.0-alpha.6) (2024-04-09)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-helpers",
|
||||
"version": "1.0.0-alpha.6",
|
||||
"version": "1.1.1",
|
||||
"description": "Shared types, helpers, and hooks of tango-apps",
|
||||
"keywords": [
|
||||
"shared",
|
||||
|
||||
@@ -50,7 +50,7 @@ export function isNil(val: any) {
|
||||
* @returns
|
||||
*/
|
||||
export function isStoreVariablePath(key: string) {
|
||||
return /^stores\.[a-zA-Z0-9]+\.\w+$/.test(key);
|
||||
return key === 'pageStore' || /^stores\.[a-zA-Z0-9]+\.\w+$/.test(key);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
import { isString } from './assert';
|
||||
|
||||
/**
|
||||
* 给定字符串是否是合法的 JSON 字符串
|
||||
* @param str
|
||||
*/
|
||||
export function isJSONString(str: string) {
|
||||
if (!str) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let json;
|
||||
try {
|
||||
json = JSON.parse(str);
|
||||
return typeof json === 'object';
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 给定代码是否是有效的函数代码
|
||||
* @param str
|
||||
*/
|
||||
export function isValidFunctionCode(str: string) {
|
||||
try {
|
||||
// eslint-disable-next-line no-eval
|
||||
const ret = eval(`typeof (${str})`);
|
||||
return ret === 'function';
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const templatePattern = /^{{(.+)}}$/s;
|
||||
|
||||
/**
|
||||
* 判断给定代码是否被双花括号包裹
|
||||
* @example {{[]}}
|
||||
* @example {{{}}}
|
||||
* @example {{this.foo}}
|
||||
* @example {{123}}
|
||||
* @param str
|
||||
* @returns
|
||||
*/
|
||||
export function isWrappedCode(str: string) {
|
||||
// 排除简单对象后,再用正则匹配
|
||||
return templatePattern.test(str);
|
||||
}
|
||||
|
||||
export const isVariableString = isWrappedCode;
|
||||
|
||||
/**
|
||||
* 从包裹的代码中获取代码内容
|
||||
* @param str
|
||||
* @returns
|
||||
*/
|
||||
export function getCodeOfWrappedCode(str: string) {
|
||||
const match = templatePattern.exec(str);
|
||||
if (match && match.length) {
|
||||
return match[1];
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
export const getVariableContent = getCodeOfWrappedCode;
|
||||
|
||||
/**
|
||||
* 给输入代码加上双花括号 code -> {{code}}
|
||||
* @example foo -> {{foo}}
|
||||
* @example "hello" => {{"hello"}}
|
||||
* @example () => {} => {{() => {}}}
|
||||
*
|
||||
* @param code 输入代码
|
||||
* @returns 加上花括号后的代码
|
||||
*/
|
||||
export function wrapCode(code: string) {
|
||||
if (isWrappedCode(code)) {
|
||||
return code;
|
||||
}
|
||||
return `{{${code}}}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 JSX 表达式容器包裹代码
|
||||
* @example foo -> {foo}
|
||||
* @example "hello" => {"hello"}
|
||||
* @param code
|
||||
* @returns
|
||||
*/
|
||||
export function wrapCodeWithJSXExpressionContainer(code: string) {
|
||||
return `{${code}}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为简单字符串,非变量字符串
|
||||
* @param str
|
||||
*/
|
||||
export function isPlainString(str: string) {
|
||||
const isWrapped = isString(str) && isWrappedCode(str);
|
||||
return isString && !isWrapped;
|
||||
}
|
||||
|
||||
const codeBlockPattern = /```(\w*)([\s\S]*?)```/g;
|
||||
|
||||
/**
|
||||
* 从 markdown 中解析出代码片段,仅返回第一个匹配的代码片段
|
||||
* @param markdown
|
||||
* @returns
|
||||
*/
|
||||
export function getCodeBlockFormMarkdown(markdown: string) {
|
||||
const match = codeBlockPattern.exec(markdown.trim());
|
||||
if (match && match.length) {
|
||||
return match[2];
|
||||
}
|
||||
}
|
||||
|
||||
export function url2serviceName(url: string) {
|
||||
if (url.startsWith('http')) {
|
||||
// 去除域名前缀
|
||||
url = url
|
||||
.replace(/https?:\/\//, '')
|
||||
.split('/')
|
||||
.slice(1)
|
||||
.join('/');
|
||||
}
|
||||
|
||||
return (
|
||||
url
|
||||
// 去除 api + 模块名前缀
|
||||
// - 云音乐 api 规范为 /api/模块名/
|
||||
// - 后端公技基本使用 /模块名/api/
|
||||
// - 中台类服务似乎常用 /api/middle/模块名/
|
||||
// 目前的实现是去除了模块名,只干掉 /api/middle/ 和 /api/backend/ 这种常用前缀
|
||||
.replace(/^\/[^/]+?\/api\/|^\/api\/middle\/|^\/api\/backend\/|^\/api\//, '')
|
||||
// 去除路由参数
|
||||
.replace(/\/\{.*?\}/, '')
|
||||
// 忽略下划线与减号,将后面的字符转成大驼峰
|
||||
.replace(/[-/_]+\w/g, (str) => str.replace(/[-/_]+/, '').toUpperCase())
|
||||
// 首字母转小写
|
||||
.replace(/^./, (str) => str.toLowerCase())
|
||||
// 方法名以数字开头,添加 api 前缀
|
||||
.replace(/^\d/, (str) => `api${str}`)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析状态变量的 path
|
||||
* @example stores.foo.bar => { storeName: 'foo', variableName: 'bar' }
|
||||
* @example stores.user.count => { storeName: 'user', variableName: 'count' }
|
||||
*
|
||||
* @param variablePath
|
||||
* @returns
|
||||
*/
|
||||
export function parseStoreVariablePath(variablePath: string) {
|
||||
const [, storeName, variableName] = variablePath.split('.');
|
||||
return {
|
||||
storeName,
|
||||
variableName,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析服务变量的 path
|
||||
* @param variablePath
|
||||
* @returns
|
||||
*
|
||||
* @example services.list => { moduleName: 'index', name: 'list' }
|
||||
* @example services.sub.list => { moduleName: 'sub', name: 'list' }
|
||||
* @example foo => undefined
|
||||
*/
|
||||
export function parseServiceVariablePath(variablePath: string) {
|
||||
const parts = variablePath.split('.');
|
||||
if (parts[0] !== 'services') {
|
||||
return {};
|
||||
}
|
||||
|
||||
let moduleName = 'index';
|
||||
let name = '';
|
||||
switch (parts.length) {
|
||||
case 2: {
|
||||
name = parts[1];
|
||||
break;
|
||||
}
|
||||
case 3: {
|
||||
moduleName = parts[1];
|
||||
name = parts[2];
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return {
|
||||
moduleName,
|
||||
name,
|
||||
};
|
||||
}
|
||||
@@ -7,5 +7,6 @@ export * from './events';
|
||||
export * from './function';
|
||||
export * from './logger';
|
||||
export * from './string';
|
||||
export * from './react-helper';
|
||||
export * from './object';
|
||||
export * from './react-helper';
|
||||
export * from './code-helper';
|
||||
|
||||
@@ -75,9 +75,13 @@ export function parseDndTrackId(str: string) {
|
||||
|
||||
interface DndIdParsedType {
|
||||
/**
|
||||
* 完整的 ID
|
||||
* full id
|
||||
*/
|
||||
id?: string;
|
||||
/**
|
||||
* 组件代码中的 id,一般对应组件的 tid 属性
|
||||
*/
|
||||
codeId?: string;
|
||||
/**
|
||||
* 组件名
|
||||
*/
|
||||
@@ -86,19 +90,11 @@ interface DndIdParsedType {
|
||||
* 文件名
|
||||
*/
|
||||
filename?: string;
|
||||
/**
|
||||
* @deprecated 使用 filename 代替
|
||||
*/
|
||||
module?: string;
|
||||
/**
|
||||
* 序号
|
||||
*/
|
||||
index?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 dnd id
|
||||
* @example Button:123 -> { component: "Button", id: "Button:123" }
|
||||
* @example Button:button123 -> { component: "Button", id: "Button:123" }
|
||||
* @example LocalBlock:Button:123 -> { filename: LocalBlock, component: "Button", id: "Button:123" }
|
||||
* @param str
|
||||
*/
|
||||
@@ -109,277 +105,23 @@ export function parseDndId(str: string): DndIdParsedType {
|
||||
|
||||
const parts = str.split(':');
|
||||
if (parts.length === 2) {
|
||||
// e.g. Button:button123
|
||||
return {
|
||||
component: parts[0],
|
||||
index: parts[1],
|
||||
id: str,
|
||||
component: parts[0],
|
||||
codeId: parts[1],
|
||||
};
|
||||
} else if (parts.length >= 3) {
|
||||
// e.g. LocalBlock:Button:button123
|
||||
const filename = decodeURIComponent(parts[0]);
|
||||
return {
|
||||
module: filename,
|
||||
id: str,
|
||||
filename,
|
||||
component: parts[1],
|
||||
index: parts[2],
|
||||
id: str,
|
||||
codeId: parts[2],
|
||||
};
|
||||
}
|
||||
return {
|
||||
id: str,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 给定字符串是否是合法的 JSON 字符串
|
||||
* @param str
|
||||
*/
|
||||
export function isJSONString(str: string) {
|
||||
if (!str) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let json;
|
||||
try {
|
||||
json = JSON.parse(str);
|
||||
return typeof json === 'object';
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 给定代码是否是有效的函数代码
|
||||
* @param str
|
||||
*/
|
||||
export function isValidFunctionCode(str: string) {
|
||||
try {
|
||||
// eslint-disable-next-line no-eval
|
||||
const ret = eval(`typeof (${str})`);
|
||||
return ret === 'function';
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否是有效的对象字符串
|
||||
*
|
||||
* @example { foo: 'foo' }
|
||||
* @example [{ foo: 'foo' }]
|
||||
* TODO: 考虑箭头函数的情况 () => {}
|
||||
*
|
||||
* @param str
|
||||
* @returns
|
||||
*/
|
||||
export function isValidObjectString(str: string) {
|
||||
const obj = code2object(str);
|
||||
if (obj && typeof obj === 'object') {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const templatePattern = /^{(.+)}$/s;
|
||||
|
||||
/**
|
||||
* 判断给定字符串是否为变量字符串
|
||||
* @deprecated 使用 isWrappedByExpressionContainer 代替
|
||||
*
|
||||
* @example {[]}
|
||||
* @example {{}}
|
||||
* @example {this.foo}
|
||||
* @example {123}
|
||||
* @param str
|
||||
* @returns
|
||||
*/
|
||||
export function isVariableString(str: string) {
|
||||
// 先检查是否是简单的对象
|
||||
// FIXME: 这里有问题,如果代码中有引用,会被误判
|
||||
if (code2object(str)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 排除简单对象后,再用正则匹配
|
||||
return templatePattern.test(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* 给输入代码加上花括号
|
||||
* @example foo -> {foo}
|
||||
* @example "hello" => {"hello"}
|
||||
* @example () => {} => {() => {}}
|
||||
*
|
||||
* @deprecated 有问题,不要使用
|
||||
*
|
||||
* @param code 输入代码
|
||||
* @returns 加上花括号后的代码
|
||||
*/
|
||||
export function wrapCode(code: string) {
|
||||
if (isVariableString(code)) {
|
||||
return code;
|
||||
}
|
||||
return `{${code}}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为简单字符串,非变量字符串
|
||||
* @param str
|
||||
*/
|
||||
export function isPlainString(str: string) {
|
||||
const isString = typeof str === 'string';
|
||||
const isVarString = isString && isVariableString(str);
|
||||
return isString && !isVarString;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析并获取变量字符串的内容
|
||||
* @param str
|
||||
* @returns
|
||||
*/
|
||||
export function getVariableContent(str: string) {
|
||||
const match = templatePattern.exec(str);
|
||||
if (match && match.length) {
|
||||
return match[1];
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
// 提供给代码执行环境的全局变量
|
||||
const patchCode = `
|
||||
var tango = {
|
||||
stores: {},
|
||||
services: {},
|
||||
config: {},
|
||||
refs: {},
|
||||
};
|
||||
`;
|
||||
|
||||
/**
|
||||
* 将代码放到函数体中进行执行
|
||||
* @param code
|
||||
* @returns 函数执行的结果
|
||||
*/
|
||||
export function runCode(code: string) {
|
||||
let ret;
|
||||
try {
|
||||
// eslint-disable-next-line no-new-func
|
||||
ret = new Function(`${patchCode}\n return ${code}`)();
|
||||
} catch (err) {
|
||||
// ignore error
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
const objectWrapperPattern = /^[{\[].*[}\]]$/s;
|
||||
|
||||
/**
|
||||
* 将代码片段转成 js 对象
|
||||
* @param code 代码文本
|
||||
* @param isStrict 是否为严格模式(是否废弃)
|
||||
* @returns
|
||||
*/
|
||||
export function code2object(code: string, isStrict = true) {
|
||||
// 非严格模式直接执行
|
||||
// 严格模式下需检测 code 是一个对象
|
||||
if (!isStrict || (isStrict && objectWrapperPattern.test(code))) {
|
||||
const ret = runCode(code);
|
||||
return typeof ret === 'object' ? ret : undefined;
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
const codeBlockPattern = /```(\w*)([\s\S]*?)```/g;
|
||||
|
||||
/**
|
||||
* 从 markdown 中解析出代码片段,仅返回第一个匹配的代码片段
|
||||
* @param markdown
|
||||
* @returns
|
||||
*/
|
||||
export function getCodeBlockFormMarkdown(markdown: string) {
|
||||
const match = codeBlockPattern.exec(markdown.trim());
|
||||
if (match && match.length) {
|
||||
return match[2];
|
||||
}
|
||||
}
|
||||
|
||||
export function url2serviceName(url: string) {
|
||||
if (url.startsWith('http')) {
|
||||
// 去除域名前缀
|
||||
url = url
|
||||
.replace(/https?:\/\//, '')
|
||||
.split('/')
|
||||
.slice(1)
|
||||
.join('/');
|
||||
}
|
||||
|
||||
return (
|
||||
url
|
||||
// 去除 api + 模块名前缀
|
||||
// - 云音乐 api 规范为 /api/模块名/
|
||||
// - 后端公技基本使用 /模块名/api/
|
||||
// - 中台类服务似乎常用 /api/middle/模块名/
|
||||
// 目前的实现是去除了模块名,只干掉 /api/middle/ 和 /api/backend/ 这种常用前缀
|
||||
.replace(/^\/[^/]+?\/api\/|^\/api\/middle\/|^\/api\/backend\/|^\/api\//, '')
|
||||
// 去除路由参数
|
||||
.replace(/\/\{.*?\}/, '')
|
||||
// 忽略下划线与减号,将后面的字符转成大驼峰
|
||||
.replace(/[-/_]+\w/g, (str) => str.replace(/[-/_]+/, '').toUpperCase())
|
||||
// 首字母转小写
|
||||
.replace(/^./, (str) => str.toLowerCase())
|
||||
// 方法名以数字开头,添加 api 前缀
|
||||
.replace(/^\d/, (str) => `api${str}`)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析状态变量的 path
|
||||
* @example stores.foo.bar => { storeName: 'foo', variableName: 'bar' }
|
||||
* @example stores.user.count => { storeName: 'user', variableName: 'count' }
|
||||
*
|
||||
* @param variablePath
|
||||
* @returns
|
||||
*/
|
||||
export function parseStoreVariablePath(variablePath: string) {
|
||||
const [, storeName, variableName] = variablePath.split('.');
|
||||
return {
|
||||
storeName,
|
||||
variableName,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析服务变量的 path
|
||||
* @param variablePath
|
||||
* @returns
|
||||
*
|
||||
* @example services.list => { moduleName: 'index', name: 'list' }
|
||||
* @example services.sub.list => { moduleName: 'sub', name: 'list' }
|
||||
* @example foo => undefined
|
||||
*/
|
||||
export function parseServiceVariablePath(variablePath: string) {
|
||||
const parts = variablePath.split('.');
|
||||
if (parts[0] !== 'services') {
|
||||
return {};
|
||||
}
|
||||
|
||||
let moduleName = 'index';
|
||||
let name = '';
|
||||
switch (parts.length) {
|
||||
case 2: {
|
||||
name = parts[1];
|
||||
break;
|
||||
}
|
||||
case 3: {
|
||||
moduleName = parts[1];
|
||||
name = parts[2];
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return {
|
||||
moduleName,
|
||||
name,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
*/
|
||||
|
||||
import { OptionType } from './advanced';
|
||||
import { PartialRecord } from './base';
|
||||
|
||||
/**
|
||||
* @deprecated 请使用 IComponentProp 代替
|
||||
@@ -19,6 +20,15 @@ export type ComponentDndRulesType = IComponentDndRules;
|
||||
*/
|
||||
export type ComponentPrototypeType = IComponentPrototype;
|
||||
|
||||
/**
|
||||
* 组件校验规则
|
||||
*/
|
||||
export type ComponentPropValidate = (
|
||||
value: any,
|
||||
field: any,
|
||||
trigger: string,
|
||||
) => string | Promise<string>;
|
||||
|
||||
/**
|
||||
* 组件属性类型
|
||||
*/
|
||||
@@ -107,6 +117,10 @@ export interface IComponentProp<T = any> {
|
||||
* 动态设置表单项是否展示
|
||||
*/
|
||||
getVisible?: (form: any) => boolean;
|
||||
/**
|
||||
* 表单值校验逻辑
|
||||
*/
|
||||
validate?: ComponentPropValidate;
|
||||
/**
|
||||
* 标记属性是否已废弃
|
||||
*/
|
||||
@@ -287,6 +301,19 @@ export interface ITangoConfigJson {
|
||||
* 外部资源,例如 js, css 文件等
|
||||
*/
|
||||
externalResources?: Record<string, any>;
|
||||
/**
|
||||
* 沙箱配置
|
||||
*/
|
||||
sandbox?: {
|
||||
/**
|
||||
* 在沙箱中执行的脚本
|
||||
*/
|
||||
evaluateJavaScript?: string;
|
||||
/**
|
||||
* 模块的别名
|
||||
*/
|
||||
alias?: Record<string, string>;
|
||||
};
|
||||
/**
|
||||
* 设计器配置
|
||||
*/
|
||||
@@ -301,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>;
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import {
|
||||
isVariableString,
|
||||
isValidObjectString,
|
||||
code2object,
|
||||
parseDndTrackId,
|
||||
getVariableContent,
|
||||
camelCase,
|
||||
@@ -35,40 +33,22 @@ describe('string', () => {
|
||||
});
|
||||
|
||||
it('isVariableString', () => {
|
||||
expect(isVariableString('{this.foo}')).toBeTruthy();
|
||||
expect(isVariableString('{!false}')).toBeTruthy();
|
||||
expect(isVariableString('{[]}')).toBeTruthy();
|
||||
expect(isVariableString('{{ foo: "bar" }}')).toBeTruthy();
|
||||
expect(isVariableString('{[{ foo: "bar" }]}')).toBeTruthy();
|
||||
expect(isVariableString('{123}')).toBeTruthy();
|
||||
expect(isVariableString('{value}')).toBeTruthy();
|
||||
expect(isVariableString('{"hello"}')).toBeTruthy();
|
||||
expect(isVariableString('{{this.foo}}')).toBeTruthy();
|
||||
expect(isVariableString('{{!false}}')).toBeTruthy();
|
||||
expect(isVariableString('{{[]}}')).toBeTruthy();
|
||||
expect(isVariableString('{{{ foo: "bar" }}}')).toBeTruthy();
|
||||
expect(isVariableString('{{[{ foo: "bar" }]}}')).toBeTruthy();
|
||||
expect(isVariableString('{{123}}')).toBeTruthy();
|
||||
expect(isVariableString('{{value}}')).toBeTruthy();
|
||||
expect(isVariableString('{{"hello"}}')).toBeTruthy();
|
||||
expect(isVariableString('{ foo: "bar" }')).toBeFalsy();
|
||||
expect(isVariableString('{ type: tango.stores?.homePage?.tabKey }')).toBeFalsy();
|
||||
// expect(isVariableString('{ type: tango.stores.homePage.tabKey }')).toBeFalsy(); // TIP: failed
|
||||
expect(isVariableString('{ type: tango.stores.homePage.tabKey }')).toBeFalsy();
|
||||
expect(isVariableString('{ foo: "bar" }')).toBeFalsy();
|
||||
});
|
||||
|
||||
it('getVariableContent', () => {
|
||||
expect(getVariableContent('{!false}')).toBe('!false');
|
||||
});
|
||||
|
||||
it('isValidObjectString', () => {
|
||||
expect(isValidObjectString('{ foo: "bar" }')).toBeTruthy();
|
||||
expect(isValidObjectString('[{ foo: "bar" }]')).toBeTruthy();
|
||||
expect(isValidObjectString('[1,2,3]')).toBeTruthy();
|
||||
expect(isValidObjectString('["hello", "world"]')).toBeTruthy();
|
||||
// expect(isValidObjectString('() => {}')).toBeTruthy();
|
||||
expect(isValidObjectString('hello')).toBeFalsy();
|
||||
});
|
||||
|
||||
it('code2object', () => {
|
||||
expect(code2object(`{ foo: 12 }`)).toEqual({ foo: 12 });
|
||||
expect(code2object(`[]`)).toEqual([]);
|
||||
expect(code2object(`{this.foo}`)).toBeUndefined();
|
||||
expect(code2object(`{foo}`)).toBeUndefined();
|
||||
expect(code2object('() => {}')).toEqual('() => {}');
|
||||
expect(code2object('hello')).toEqual('hello');
|
||||
expect(getVariableContent('{{!false}}')).toBe('!false');
|
||||
});
|
||||
|
||||
it('parseDndTrackId', () => {
|
||||
@@ -78,22 +58,21 @@ describe('string', () => {
|
||||
|
||||
it('parseDndId', () => {
|
||||
expect(parseDndId('123')).toEqual({ id: '123' });
|
||||
expect(parseDndId('Button:123')).toEqual({
|
||||
expect(parseDndId('Button:button123')).toEqual({
|
||||
component: 'Button',
|
||||
id: 'Button:123',
|
||||
index: '123',
|
||||
id: 'Button:button123',
|
||||
codeId: 'button123',
|
||||
});
|
||||
expect(parseDndId('Button.Group:123')).toEqual({
|
||||
expect(parseDndId('Button.Group:buttonGroup123')).toEqual({
|
||||
component: 'Button.Group',
|
||||
id: 'Button.Group:123',
|
||||
index: '123',
|
||||
id: 'Button.Group:buttonGroup123',
|
||||
codeId: 'buttonGroup123',
|
||||
});
|
||||
expect(parseDndId('LocalBlock:Button.Group:123')).toEqual({
|
||||
expect(parseDndId('LocalBlock:Button.Group:buttonGroup123')).toEqual({
|
||||
id: 'LocalBlock:Button.Group:buttonGroup123',
|
||||
filename: 'LocalBlock',
|
||||
module: 'LocalBlock',
|
||||
component: 'Button.Group',
|
||||
id: 'LocalBlock:Button.Group:123',
|
||||
index: '123',
|
||||
codeId: 'buttonGroup123',
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -3,6 +3,18 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.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
|
||||
|
||||
## [1.0.1](https://github.com/netease/tango/compare/@music163/tango-sandbox@1.0.0...@music163/tango-sandbox@1.0.1) (2024-04-22)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-sandbox
|
||||
|
||||
# [1.0.0-alpha.10](https://github.com/netease/tango/compare/@music163/tango-sandbox@1.0.0-alpha.9...@music163/tango-sandbox@1.0.0-alpha.10) (2024-04-09)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-sandbox
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-sandbox",
|
||||
"version": "1.0.0",
|
||||
"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.0.0",
|
||||
"@music163/tango-helpers": "^1.0.0-alpha.6",
|
||||
"@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"
|
||||
|
||||
@@ -3,6 +3,35 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [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
|
||||
|
||||
- support quick set tid in SettingForm ([#151](https://github.com/netease/tango/issues/151)) ([1fc22a4](https://github.com/netease/tango/commit/1fc22a4535a8bdc12a018a41ebd5ab908fc46817))
|
||||
|
||||
### Features
|
||||
|
||||
- refactor parse attribute value ([#149](https://github.com/netease/tango/issues/149)) ([ffaa276](https://github.com/netease/tango/commit/ffaa276b5c205ed962d37e2fdb358a703f8fad01))
|
||||
|
||||
## [1.0.2](https://github.com/netease/tango/compare/@music163/tango-setting-form@1.0.1...@music163/tango-setting-form@1.0.2) (2024-04-22)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-setting-form
|
||||
|
||||
## [1.0.1](https://github.com/netease/tango/compare/@music163/tango-setting-form@1.0.0...@music163/tango-setting-form@1.0.1) (2024-04-16)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-setting-form
|
||||
|
||||
# [1.0.0-alpha.14](https://github.com/netease/tango/compare/@music163/tango-setting-form@1.0.0-alpha.13...@music163/tango-setting-form@1.0.0-alpha.14) (2024-04-09)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-setting-form",
|
||||
"version": "1.0.0",
|
||||
"version": "1.2.2",
|
||||
"description": "setting form of tango-apps",
|
||||
"author": "wwsun <ww.sun@outlook.com>",
|
||||
"homepage": "",
|
||||
@@ -28,13 +28,13 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^4.8.0",
|
||||
"@music163/tango-core": "^1.0.0",
|
||||
"@music163/tango-helpers": "^1.0.0-alpha.6",
|
||||
"@music163/tango-ui": "^1.0.0",
|
||||
"@music163/tango-core": "^1.2.0",
|
||||
"@music163/tango-helpers": "^1.1.1",
|
||||
"@music163/tango-ui": "^1.2.1",
|
||||
"antd": "^4.24.2",
|
||||
"coral-system": "^1.0.5",
|
||||
"mobx": "6.12.0",
|
||||
"mobx-react-lite": "4.0.5"
|
||||
"mobx": "6.12.3",
|
||||
"mobx-react-lite": "4.0.7"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
|
||||
@@ -1,12 +1,21 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { toJS } from 'mobx';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { clone, IComponentProp, useBoolean } from '@music163/tango-helpers';
|
||||
import { isWrappedByExpressionContainer } from '@music163/tango-core';
|
||||
import { ToggleButton, CodeOutlined } from '@music163/tango-ui';
|
||||
import {
|
||||
clone,
|
||||
ComponentPropValidate,
|
||||
getCodeOfWrappedCode,
|
||||
IComponentProp,
|
||||
isNil,
|
||||
isString,
|
||||
isWrappedCode,
|
||||
wrapCode,
|
||||
} from '@music163/tango-helpers';
|
||||
import { ErrorBoundary } from '@music163/tango-ui';
|
||||
import { code2value, value2code } from '@music163/tango-core';
|
||||
import { InputProps } from 'antd';
|
||||
import { useFormModel, useFormVariable } from './context';
|
||||
import { FormControl } from './form-ui';
|
||||
import { FormControl, ToggleCodeButton } from './form-ui';
|
||||
import { Box, Text } from 'coral-system';
|
||||
import { ISetterOnChangeCallbackDetail } from './types';
|
||||
|
||||
@@ -43,6 +52,10 @@ export interface IFormItemCreateOptions {
|
||||
* 设置器别名列表,支持多个名字
|
||||
*/
|
||||
alias?: string[];
|
||||
/**
|
||||
* 设置器类型,value类设置器支持切换到codeSetter,默认为 value setter
|
||||
*/
|
||||
type?: 'code' | 'value';
|
||||
/**
|
||||
* 渲染设置器使用的组件
|
||||
*/
|
||||
@@ -58,15 +71,109 @@ export interface IFormItemCreateOptions {
|
||||
/**
|
||||
* 默认的表单值校验逻辑
|
||||
*/
|
||||
validate?: (value: string) => string | Promise<any>;
|
||||
validate?: ComponentPropValidate;
|
||||
}
|
||||
|
||||
const defaultGetSetterProps = () => ({});
|
||||
const defaultGetVisible = () => true;
|
||||
|
||||
function parseFieldValue(fieldValue: any) {
|
||||
let value: any;
|
||||
let code: string;
|
||||
|
||||
if (!fieldValue) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const isCodeString = isString(fieldValue) && isWrappedCode(fieldValue);
|
||||
if (isCodeString) {
|
||||
code = getCodeOfWrappedCode(fieldValue);
|
||||
try {
|
||||
// 避免 code 报错的情况
|
||||
value = code2value(code);
|
||||
} catch (err) {
|
||||
// do nothing
|
||||
}
|
||||
} else {
|
||||
code = value2code(fieldValue);
|
||||
value = fieldValue;
|
||||
}
|
||||
return [value, code];
|
||||
}
|
||||
|
||||
interface UseSetterValueProps {
|
||||
fieldValue: any;
|
||||
setter?: string;
|
||||
setterType?: IFormItemCreateOptions['type'];
|
||||
/**
|
||||
* 强制初始化为 codeSetter,适用于外部需要特别干预的情况
|
||||
*/
|
||||
forceCodeSetter?: boolean;
|
||||
}
|
||||
|
||||
export function useSetterValue({
|
||||
fieldValue,
|
||||
setter,
|
||||
setterType,
|
||||
forceCodeSetter,
|
||||
}: UseSetterValueProps) {
|
||||
const [value, code] = parseFieldValue(fieldValue);
|
||||
const [isCodeSetter, setIsCodeSetter] = useState(() => {
|
||||
if (forceCodeSetter) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 同时不存在,表示是空置,使用默认模式
|
||||
if (!code && !value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// value 解析出错的情况,使用 codeSetter
|
||||
if (isNil(value)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 其他情况,均使用默认模式
|
||||
return false;
|
||||
});
|
||||
|
||||
const toggleSetter = () => {
|
||||
setIsCodeSetter(!isCodeSetter);
|
||||
};
|
||||
|
||||
let fixedSetter: string;
|
||||
let setterValue: any;
|
||||
if (setterType === 'code') {
|
||||
fixedSetter = setter;
|
||||
setterValue = code;
|
||||
} else {
|
||||
fixedSetter = isCodeSetter ? 'codeSetter' : setter;
|
||||
setterValue = isCodeSetter ? code : value;
|
||||
}
|
||||
|
||||
return {
|
||||
value,
|
||||
code,
|
||||
setter: fixedSetter,
|
||||
setterValue, // setter value
|
||||
isCodeSetter, // 是否为 codeSetter
|
||||
toggleSetter, // 切换 setter
|
||||
};
|
||||
}
|
||||
|
||||
export function createFormItem(options: IFormItemCreateOptions) {
|
||||
const renderSetter =
|
||||
options.render ?? ((props: any) => React.createElement(options.component, props));
|
||||
const setterType = options.type ?? 'value'; // 设置器的模式
|
||||
|
||||
function getShowToggleCodeButton(disableVariableSetter = options.disableVariableSetter) {
|
||||
if (setterType === 'code') {
|
||||
// codeSetter 无需切换按钮
|
||||
return false;
|
||||
}
|
||||
// 如果用户设置了 disableVariableSetter,则不显示切换按钮
|
||||
return !disableVariableSetter;
|
||||
}
|
||||
|
||||
function FormItem({
|
||||
name,
|
||||
@@ -75,51 +182,57 @@ export function createFormItem(options: IFormItemCreateOptions) {
|
||||
placeholder,
|
||||
docs,
|
||||
autoCompleteOptions,
|
||||
setter,
|
||||
setter: setterProp,
|
||||
setterProps,
|
||||
defaultValue,
|
||||
options: setterOptions,
|
||||
disableVariableSetter: disableVariableSetterProp = options.disableVariableSetter,
|
||||
disableVariableSetter,
|
||||
getVisible: getVisibleProp,
|
||||
getSetterProps: getSetterPropsProp,
|
||||
deprecated,
|
||||
extra,
|
||||
footer,
|
||||
noStyle,
|
||||
validate = options.validate,
|
||||
}: FormItemProps) {
|
||||
const { disableSwitchExpressionSetter, showItemSubtitle } = useFormVariable();
|
||||
const model = useFormModel();
|
||||
const field = model.getField(name);
|
||||
const value = toJS(field.value ?? defaultValue);
|
||||
const disableVariableSetter = disableSwitchExpressionSetter ?? disableVariableSetterProp; // Form 的设置优先
|
||||
const [isVariable, { toggle: toggleIsVariable }] = useBoolean(
|
||||
() => !disableVariableSetter && isWrappedByExpressionContainer(value),
|
||||
);
|
||||
|
||||
const setterName = isVariable ? 'expressionSetter' : setter;
|
||||
|
||||
field.setConfig({
|
||||
validate: options.validate,
|
||||
const fieldValue = toJS(field.value ?? defaultValue);
|
||||
const { setterValue, setter, isCodeSetter, toggleSetter } = useSetterValue({
|
||||
fieldValue,
|
||||
setter: setterProp,
|
||||
setterType,
|
||||
});
|
||||
|
||||
const baseComponentProps = clone(
|
||||
{
|
||||
value,
|
||||
defaultValue,
|
||||
onChange: field.handleChange,
|
||||
status: field.error ? 'error' : undefined,
|
||||
placeholder,
|
||||
options: setterOptions,
|
||||
field.setConfig({
|
||||
validate: setter === 'codeSetter' ? getSetter('codeSetter').config.validate : validate,
|
||||
});
|
||||
|
||||
let baseComponentProps: FormItemComponentProps = {
|
||||
value: setterValue,
|
||||
defaultValue,
|
||||
onChange(value, detail = {}) {
|
||||
if ((setterType === 'code' || isCodeSetter) && isString(value) && value) {
|
||||
detail.rawCode = value; // 在 detail 中记录原始的 code
|
||||
value = wrapCode(value);
|
||||
}
|
||||
field.setValue(value, detail);
|
||||
},
|
||||
false,
|
||||
) as FormItemComponentProps;
|
||||
status: field.error ? 'error' : undefined,
|
||||
placeholder,
|
||||
options: setterOptions,
|
||||
};
|
||||
baseComponentProps = clone(baseComponentProps, false);
|
||||
|
||||
let expProps = {};
|
||||
|
||||
// FIXME: 重新考虑这段代码的位置,外置这个逻辑
|
||||
if (
|
||||
['expressionSetter', 'expSetter', 'actionSetter', 'eventSetter'].includes(setter) ||
|
||||
isVariable
|
||||
['codeSetter', 'expressionSetter', 'expSetter', 'actionSetter', 'eventSetter'].includes(
|
||||
setter,
|
||||
)
|
||||
) {
|
||||
expProps = {
|
||||
modalTitle: title,
|
||||
@@ -130,10 +243,10 @@ export function createFormItem(options: IFormItemCreateOptions) {
|
||||
|
||||
const getSetterProps = getSetterPropsProp || defaultGetSetterProps;
|
||||
// 从注册表中获取 expSetter
|
||||
const ExpressionSetter = REGISTERED_FORM_ITEM_MAP['expressionSetter']?.config?.component;
|
||||
const CodeSetter = REGISTERED_FORM_ITEM_MAP['codeSetter']?.config?.component;
|
||||
|
||||
const setterNode = isVariable ? (
|
||||
<ExpressionSetter {...expProps} {...baseComponentProps} />
|
||||
const setterNode = isCodeSetter ? (
|
||||
<CodeSetter {...expProps} {...baseComponentProps} />
|
||||
) : (
|
||||
renderSetter({
|
||||
...expProps,
|
||||
@@ -147,9 +260,13 @@ export function createFormItem(options: IFormItemCreateOptions) {
|
||||
|
||||
if (noStyle) {
|
||||
// 无样式模式
|
||||
return getVisible(model) ? setterNode : <div data-setter={setterName} data-field={name} />;
|
||||
return getVisible(model) ? setterNode : <div data-setter={setter} data-field={name} />;
|
||||
}
|
||||
|
||||
const showToggleCodeButton = getShowToggleCodeButton(
|
||||
disableSwitchExpressionSetter || disableVariableSetter,
|
||||
);
|
||||
|
||||
return (
|
||||
<FormControl
|
||||
visible={getVisible(model)}
|
||||
@@ -162,27 +279,16 @@ export function createFormItem(options: IFormItemCreateOptions) {
|
||||
extra={
|
||||
<Box>
|
||||
{extra}
|
||||
{!disableVariableSetter ? (
|
||||
<ToggleButton
|
||||
borderRadius="s"
|
||||
size="s"
|
||||
shape="text"
|
||||
type="primary"
|
||||
tooltip={isVariable ? '关闭 JS 表达式' : '使用 JS 表达式'}
|
||||
tooltipPlacement="left"
|
||||
selected={isVariable}
|
||||
onClick={() => toggleIsVariable()}
|
||||
>
|
||||
<CodeOutlined />
|
||||
</ToggleButton>
|
||||
{showToggleCodeButton ? (
|
||||
<ToggleCodeButton selected={isCodeSetter} onToggle={toggleSetter} />
|
||||
) : null}
|
||||
</Box>
|
||||
}
|
||||
footer={footer}
|
||||
data-setter={setterName}
|
||||
data-setter={setter}
|
||||
data-field={name}
|
||||
>
|
||||
{setterNode}
|
||||
<ErrorBoundary>{setterNode}</ErrorBoundary>
|
||||
</FormControl>
|
||||
);
|
||||
}
|
||||
@@ -196,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) {
|
||||
const names = [config.name, ...(config.alias ?? [])];
|
||||
names.forEach((name) => {
|
||||
REGISTERED_FORM_ITEM_MAP[name] = createFormItem(config);
|
||||
// 允许直接覆盖同名 setter
|
||||
REGISTERED_FORM_ITEM_MAP[config.name] = createFormItem(config);
|
||||
(Array.isArray(config.alias) ? config.alias : []).forEach((alias) => {
|
||||
REGISTERED_FORM_ITEM_MAP[alias] = getSetter(config.name);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -211,13 +327,13 @@ export function SettingFormItem(props: FormItemProps) {
|
||||
const { setter } = props;
|
||||
const Comp = REGISTERED_FORM_ITEM_MAP[setter];
|
||||
if (Comp == null) {
|
||||
const Fallback = REGISTERED_FORM_ITEM_MAP.expressionSetter;
|
||||
const Fallback = REGISTERED_FORM_ITEM_MAP.codeSetter;
|
||||
return (
|
||||
<Fallback
|
||||
{...props}
|
||||
footer={
|
||||
<Text color="red" mt="m">
|
||||
{props.setter} is invalid
|
||||
<Text color="#faad14" mt="m">
|
||||
invalid {props.setter}, fallback to codeSetter
|
||||
</Text>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -195,7 +195,7 @@ export class Field {
|
||||
error: computed,
|
||||
validate: action,
|
||||
handleBlur: action,
|
||||
handleChange: action,
|
||||
setValue: action,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -234,11 +234,11 @@ export class Field {
|
||||
this.state.validating = false;
|
||||
this.state.error = error;
|
||||
|
||||
// 校验错误,不触发 onChange
|
||||
if (error) {
|
||||
return error;
|
||||
}
|
||||
|
||||
// 校验错误,不触发 onChange
|
||||
this.parent?.onChange?.(this.name, this.value, this);
|
||||
}),
|
||||
);
|
||||
@@ -248,7 +248,7 @@ export class Field {
|
||||
return this.validate('blur');
|
||||
};
|
||||
|
||||
handleChange = (nextValue: any, valueDetail: any) => {
|
||||
setValue = (nextValue: any, valueDetail: any) => {
|
||||
this.detail = valueDetail;
|
||||
this.value = nextValue;
|
||||
return this.validate('change');
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import React from 'react';
|
||||
import { Box } from 'coral-system';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { IComponentProp } from '@music163/tango-helpers';
|
||||
import { SettingFormItem } from './form-item';
|
||||
import { IComponentProp, isString, wrapCode } from '@music163/tango-helpers';
|
||||
import { SettingFormItem, useSetterValue } from './form-item';
|
||||
import { FormModelProvider, useFormModel } from './context';
|
||||
import { FormControlGroup } from './form-ui';
|
||||
import { FormControlGroup, ToggleCodeButton } from './form-ui';
|
||||
import { isValidNestProps } from './helpers';
|
||||
import { CodeSetter } from './setters';
|
||||
|
||||
export type SettingFormObjectProps = IComponentProp;
|
||||
|
||||
@@ -27,6 +28,12 @@ export const SettingFormObject = observer(
|
||||
const parent = useFormModel();
|
||||
const visible = getVisible(parent);
|
||||
const subModel = parent.getSubModel(name);
|
||||
const subModelValue = subModel.values || defaultValue;
|
||||
const forceCodeSetter = isString(subModelValue);
|
||||
const { setterValue, isCodeSetter, toggleSetter } = useSetterValue({
|
||||
fieldValue: subModelValue,
|
||||
forceCodeSetter, // TODO: 最好是在内部 code2value 失败,而不是在这里强制设置
|
||||
});
|
||||
return (
|
||||
<FormModelProvider value={subModel}>
|
||||
<Box className="FormObject" display={visible ? 'block' : 'none'}>
|
||||
@@ -41,13 +48,31 @@ export const SettingFormObject = observer(
|
||||
parent.setValue(name, nextValue);
|
||||
parent.onChange(name, nextValue); // 非 Field 发起, 主动调一次
|
||||
}}
|
||||
extra={
|
||||
<ToggleCodeButton
|
||||
confirm={forceCodeSetter}
|
||||
selected={isCodeSetter}
|
||||
onToggle={toggleSetter}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{props.map((prop) => {
|
||||
if (isValidNestProps(prop.props)) {
|
||||
return <SettingFormObject key={prop.name} {...prop} />;
|
||||
}
|
||||
return <SettingFormItem key={prop.name} {...prop} />;
|
||||
})}
|
||||
{isCodeSetter ? (
|
||||
<CodeSetter
|
||||
value={setterValue}
|
||||
onChange={(val) => {
|
||||
const nextVal = val ? wrapCode(val) : undefined;
|
||||
parent.setValue(name, nextVal);
|
||||
parent.onChange(name, nextVal); // 非 Field 发起, 主动调一次
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
props.map((prop) => {
|
||||
if (isValidNestProps(prop.props)) {
|
||||
return <SettingFormObject key={prop.name} {...prop} />;
|
||||
}
|
||||
return <SettingFormItem key={prop.name} {...prop} />;
|
||||
})
|
||||
)}
|
||||
</FormControlGroup>
|
||||
</Box>
|
||||
</FormModelProvider>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, { useState } from 'react';
|
||||
import { css, Box, HTMLCoralProps, Link } from 'coral-system';
|
||||
import { Checkbox, Tooltip } from 'antd';
|
||||
import { CollapsePanel } from '@music163/tango-ui';
|
||||
import { isNil, isString } from '@music163/tango-helpers';
|
||||
import { Checkbox, Popconfirm, Tooltip } from 'antd';
|
||||
import { CodeOutlined, CollapsePanel, ToggleButton } from '@music163/tango-ui';
|
||||
import { isString } from '@music163/tango-helpers';
|
||||
import { WarningOutlined } from '@ant-design/icons';
|
||||
|
||||
export interface FormControlProps extends Omit<FormLabelProps, 'type'> {
|
||||
@@ -35,7 +35,7 @@ export function FormControl({
|
||||
<Box>{children}</Box>
|
||||
{footer}
|
||||
{!!error && (
|
||||
<Box mt="m" color="red">
|
||||
<Box mt="m" color="red" fontSize="12px">
|
||||
{error}
|
||||
</Box>
|
||||
)}
|
||||
@@ -94,7 +94,7 @@ export function FormControlGroup({
|
||||
onCheck?.(nextChecked);
|
||||
}}
|
||||
/>
|
||||
<FormLabel type="secondary" label={label} note={note} tip={tip} docs={docs} />
|
||||
<FormLabel label={label} note={note} tip={tip} docs={docs} />
|
||||
</Box>
|
||||
}
|
||||
extra={
|
||||
@@ -148,11 +148,6 @@ const tipStyle = css`
|
||||
`;
|
||||
|
||||
interface FormLabelProps extends HTMLCoralProps<'div'> {
|
||||
/**
|
||||
* 类型
|
||||
*/
|
||||
// eslint-disable-next-line react/no-unused-prop-types
|
||||
type?: 'normal' | 'secondary';
|
||||
/**
|
||||
* 标签
|
||||
*/
|
||||
@@ -176,15 +171,7 @@ interface FormLabelProps extends HTMLCoralProps<'div'> {
|
||||
deprecated?: boolean | string;
|
||||
}
|
||||
|
||||
function FormLabel({
|
||||
type = 'normal',
|
||||
label,
|
||||
note,
|
||||
tip,
|
||||
docs,
|
||||
deprecated,
|
||||
...rest
|
||||
}: FormLabelProps) {
|
||||
function FormLabel({ label, note, tip, docs, deprecated, ...rest }: FormLabelProps) {
|
||||
let help: React.ReactNode;
|
||||
if (deprecated || docs) {
|
||||
help = (
|
||||
@@ -209,16 +196,11 @@ function FormLabel({
|
||||
help = tip;
|
||||
}
|
||||
|
||||
const labelColor = {
|
||||
normal: 'text.body',
|
||||
secondary: 'text.note',
|
||||
}[type];
|
||||
|
||||
let labelNode = (
|
||||
<Box
|
||||
as="span"
|
||||
display="inline-block"
|
||||
color={labelColor}
|
||||
color="text2"
|
||||
className={help ? 'hasHelp' : ''}
|
||||
css={labelStyle}
|
||||
title={isString(label) ? label : undefined}
|
||||
@@ -236,7 +218,7 @@ function FormLabel({
|
||||
<Box fontSize="14px" {...rest}>
|
||||
{labelNode}
|
||||
{note && (
|
||||
<Box as="i" display="inline-block" fontSize="12px" color="text.note" ml="m">
|
||||
<Box as="i" display="inline-block" fontSize="12px" color="text3" ml="m">
|
||||
{note}
|
||||
</Box>
|
||||
)}
|
||||
@@ -267,15 +249,86 @@ 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>
|
||||
);
|
||||
}
|
||||
|
||||
export interface ToggleCodeButtonProps {
|
||||
/**
|
||||
* 在反选时是否需要用户确认操作
|
||||
*/
|
||||
confirm?: boolean;
|
||||
selected: boolean;
|
||||
onToggle: () => void;
|
||||
}
|
||||
|
||||
export function ToggleCodeButton({ confirm, selected, onToggle }: ToggleCodeButtonProps) {
|
||||
if (confirm && selected) {
|
||||
return (
|
||||
<Popconfirm
|
||||
title="当前代码在切换模式后可能会解析失败,是否确认切换?"
|
||||
color="#fff2f0"
|
||||
onConfirm={(e) => {
|
||||
e.stopPropagation();
|
||||
onToggle?.();
|
||||
}}
|
||||
onCancel={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<div
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<ToggleButton
|
||||
borderRadius="s"
|
||||
size="s"
|
||||
shape="text"
|
||||
type="primary"
|
||||
tooltip={selected ? undefined : '使用 JS 表达式'}
|
||||
tooltipPlacement="left"
|
||||
selected={selected}
|
||||
>
|
||||
<CodeOutlined />
|
||||
</ToggleButton>
|
||||
</div>
|
||||
</Popconfirm>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ToggleButton
|
||||
borderRadius="s"
|
||||
size="s"
|
||||
shape="text"
|
||||
type="primary"
|
||||
tooltip={selected ? '关闭 JS 表达式' : '使用 JS 表达式'}
|
||||
tooltipPlacement="left"
|
||||
selected={selected}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onToggle?.();
|
||||
}}
|
||||
>
|
||||
<CodeOutlined />
|
||||
</ToggleButton>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import { FormModelProvider, FormVariableProvider } from './context';
|
||||
import { FormModel, FormModelOptionsType } from './form-model';
|
||||
import { SettingFormObject } from './form-object';
|
||||
import { isValidNestProps } from './helpers';
|
||||
import { registerBuiltinSetters } from './setter';
|
||||
import { registerBuiltinSetters } from './setters/register';
|
||||
import { FormHeader } from './form-ui';
|
||||
import { QuestionCircleOutlined } from '@ant-design/icons';
|
||||
|
||||
@@ -76,7 +76,7 @@ interface IFormTabsGroupOption {
|
||||
|
||||
const internalGroups: IFormTabsGroupOption[] = [
|
||||
{ label: '基本', value: 'basic' },
|
||||
// { label: '事件', value: 'event' },
|
||||
{ label: '事件', value: 'event' },
|
||||
{ label: '样式', value: 'style' },
|
||||
{ label: '高级', value: 'advanced' },
|
||||
];
|
||||
@@ -104,7 +104,8 @@ export interface SettingFormProps {
|
||||
showIdentifier?:
|
||||
| false
|
||||
| {
|
||||
identifierKey: string;
|
||||
identifierKey: string; // 唯一标识符属性key
|
||||
getIdentifier?: () => string; // 获取唯一标识符的方法
|
||||
};
|
||||
/**
|
||||
* 是否显示搜索框
|
||||
@@ -127,7 +128,7 @@ export interface SettingFormProps {
|
||||
*/
|
||||
renderItemExtra?: (props: IComponentProp) => React.ReactNode;
|
||||
/**
|
||||
* 是否允许表单项切换到表达式设置器
|
||||
* 是否禁用 codeSetter 切换,默认所有的 setter 都支持切换到 codeSetter
|
||||
*/
|
||||
disableSwitchExpressionSetter?: boolean;
|
||||
}
|
||||
@@ -165,7 +166,15 @@ export function SettingForm({
|
||||
// 没有进行搜索的情况
|
||||
if (!keyword) {
|
||||
if (showGroups) {
|
||||
return componentPropGroups[tabKey] || [];
|
||||
return (
|
||||
componentPropGroups[tabKey].sort((a, b) => {
|
||||
// TIP: 将废弃属性往后移
|
||||
if (!a.deprecated && b.deprecated) {
|
||||
return -1;
|
||||
}
|
||||
return 1;
|
||||
}) || []
|
||||
);
|
||||
} else {
|
||||
return computedProps;
|
||||
}
|
||||
@@ -209,11 +218,14 @@ export function SettingForm({
|
||||
<Box px="l" py="m">
|
||||
{showIdentifier && (
|
||||
<FormHeader
|
||||
title={prototype.title}
|
||||
title={prototype.title || prototype.name}
|
||||
subTitle={
|
||||
<SettingFormItem
|
||||
noStyle
|
||||
setter="idSetter"
|
||||
setterProps={{
|
||||
getId: showIdentifier.getIdentifier,
|
||||
}}
|
||||
name={showIdentifier.identifierKey}
|
||||
/>
|
||||
}
|
||||
@@ -223,6 +235,7 @@ export function SettingForm({
|
||||
icon={<QuestionCircleOutlined />}
|
||||
tooltip="查看组件文档"
|
||||
href={prototype.docs}
|
||||
size="small"
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
|
||||
@@ -3,3 +3,4 @@ export * from './form-item';
|
||||
export * from './form-object';
|
||||
export * from './form-model';
|
||||
export * from './context';
|
||||
export * from './setters';
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Input, InputProps } from 'antd';
|
||||
import { InputCode } from '@music163/tango-ui';
|
||||
import { FormItemComponentProps, register } from './form-item';
|
||||
import { Box, css } from 'coral-system';
|
||||
|
||||
export function ExpressionSetter({ value, onChange, ...rest }: FormItemComponentProps<string>) {
|
||||
return <InputCode onChange={(val) => onChange?.(val)} value={value || ''} {...rest} />;
|
||||
}
|
||||
|
||||
export function TextSetter({ onChange, ...rest }: FormItemComponentProps<string>) {
|
||||
return <Input onChange={(e) => onChange?.(e.target.value)} {...rest} />;
|
||||
}
|
||||
|
||||
const idInputStyle = css`
|
||||
> .ant-input-borderless {
|
||||
border: 1px solid transparent;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
> .ant-input-borderless:hover {
|
||||
background-color: var(--tango-colors-fill1);
|
||||
}
|
||||
`;
|
||||
|
||||
const idPattern = /^[a-z]+[\w]*$/;
|
||||
|
||||
export function IdSetter({
|
||||
value: valueProp,
|
||||
defaultValue,
|
||||
onChange,
|
||||
placeholder = '输入唯一组件ID',
|
||||
...rest
|
||||
}: FormItemComponentProps<string>) {
|
||||
const [value, setValue] = useState(valueProp || defaultValue);
|
||||
const [error, setError] = useState('');
|
||||
const [editable, setEditable] = useState(false);
|
||||
|
||||
// value controlled
|
||||
useEffect(() => {
|
||||
setValue(valueProp);
|
||||
}, [valueProp]);
|
||||
|
||||
const __props: InputProps = editable
|
||||
? {
|
||||
status: error ? 'error' : undefined,
|
||||
onBlur() {
|
||||
setEditable(false);
|
||||
if (!error) {
|
||||
onChange?.(value || undefined);
|
||||
}
|
||||
},
|
||||
onChange(e) {
|
||||
const newValue = e.target.value;
|
||||
setValue(e.target.value);
|
||||
setError(newValue && !idPattern.test(newValue) ? 'error' : '');
|
||||
},
|
||||
}
|
||||
: {
|
||||
readOnly: true,
|
||||
bordered: false,
|
||||
onClick() {
|
||||
setEditable(true);
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<Box css={idInputStyle}>
|
||||
<Input placeholder={placeholder} value={value} {...__props} {...rest} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export function registerBuiltinSetters() {
|
||||
// 预注册基础 Setter
|
||||
register({
|
||||
name: 'expressionSetter',
|
||||
component: ExpressionSetter,
|
||||
});
|
||||
|
||||
register({
|
||||
name: 'textSetter',
|
||||
component: TextSetter,
|
||||
});
|
||||
|
||||
register({
|
||||
name: 'idSetter',
|
||||
component: IdSetter,
|
||||
});
|
||||
}
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Switch } from 'antd';
|
||||
import { FormItemComponentProps } from '@music163/tango-setting-form/src/form-item';
|
||||
import { FormItemComponentProps } from '../form-item';
|
||||
|
||||
export function BoolSetter({ value, onChange, ...props }: FormItemComponentProps<boolean>) {
|
||||
return <Switch checked={value} onChange={(val) => onChange?.(val)} {...props} />;
|
||||
@@ -0,0 +1,23 @@
|
||||
import React, { useState } from 'react';
|
||||
import { FormItemComponentProps } from '../form-item';
|
||||
import { InputCode } from '@music163/tango-ui';
|
||||
|
||||
export function CodeSetter({
|
||||
value: valueProp,
|
||||
onChange,
|
||||
...rest
|
||||
}: FormItemComponentProps<string>) {
|
||||
const [value, setValue] = useState(valueProp || '');
|
||||
return (
|
||||
<InputCode
|
||||
onChange={(val) => {
|
||||
setValue(val);
|
||||
}}
|
||||
onBlur={() => {
|
||||
onChange?.(value);
|
||||
}}
|
||||
value={value}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Input, InputProps, Tooltip } from 'antd';
|
||||
import { css, Box } from 'coral-system';
|
||||
import { FormItemComponentProps } from '../form-item';
|
||||
import { ExclamationCircleOutlined, ThunderboltOutlined } from '@ant-design/icons';
|
||||
import { Action } from '@music163/tango-ui';
|
||||
|
||||
const idInputStyle = css`
|
||||
> .ant-input-borderless {
|
||||
border: 1px solid transparent;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
> .ant-input-borderless:hover {
|
||||
background-color: var(--tango-colors-fill1);
|
||||
}
|
||||
`;
|
||||
|
||||
const idPattern = /^[a-z]+[\w]*$/;
|
||||
|
||||
export function IdSetter({
|
||||
value: valueProp,
|
||||
defaultValue,
|
||||
onChange,
|
||||
placeholder = '输入唯一组件ID',
|
||||
getId,
|
||||
...rest
|
||||
}: FormItemComponentProps<string>) {
|
||||
const [value, setValue] = useState(valueProp || defaultValue);
|
||||
const [error, setError] = useState('');
|
||||
const [editable, setEditable] = useState(false);
|
||||
|
||||
// value controlled
|
||||
useEffect(() => {
|
||||
setValue(valueProp);
|
||||
}, [valueProp]);
|
||||
|
||||
const __props: InputProps = editable
|
||||
? {
|
||||
status: error ? 'error' : undefined,
|
||||
onBlur() {
|
||||
setEditable(false);
|
||||
if (!error) {
|
||||
onChange?.(value || undefined);
|
||||
}
|
||||
},
|
||||
onChange(e) {
|
||||
const newValue = e.target.value;
|
||||
setValue(e.target.value);
|
||||
setError(
|
||||
newValue && !idPattern.test(newValue)
|
||||
? '非法的组件ID,必须使用字母开头的字母数字组合,例如 button1'
|
||||
: '',
|
||||
);
|
||||
},
|
||||
}
|
||||
: {
|
||||
readOnly: true,
|
||||
bordered: false,
|
||||
onClick() {
|
||||
setEditable(true);
|
||||
},
|
||||
};
|
||||
|
||||
const showQuickIdButton = !value || error; // 没有设置 id 或 id 不合法的时候显示快捷生成按钮
|
||||
|
||||
return (
|
||||
<Box css={idInputStyle}>
|
||||
<Input
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
{...__props}
|
||||
{...rest}
|
||||
prefix={
|
||||
error ? (
|
||||
<Tooltip title={error} color="#ff4d4f">
|
||||
<ExclamationCircleOutlined style={{ color: 'red' }} />
|
||||
</Tooltip>
|
||||
) : null
|
||||
}
|
||||
suffix={
|
||||
showQuickIdButton ? (
|
||||
<Action
|
||||
size="small"
|
||||
icon={<ThunderboltOutlined />}
|
||||
tooltip="快捷设置组件ID"
|
||||
onClick={() => {
|
||||
const id = getId?.();
|
||||
if (id) {
|
||||
setValue(id);
|
||||
onChange?.(id);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export * from './bool-setter';
|
||||
export * from './code-setter';
|
||||
export * from './id-setter';
|
||||
export * from './number-setter';
|
||||
export * from './text-setter';
|
||||
export * from './register';
|
||||
+3
-3
@@ -1,12 +1,12 @@
|
||||
import React from 'react';
|
||||
import { InputNumber, InputNumberProps, Slider } from 'antd';
|
||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||
import { InputNumber, Slider } from 'antd';
|
||||
import { FormItemComponentProps } from '../form-item';
|
||||
|
||||
const style = {
|
||||
width: '100%',
|
||||
};
|
||||
|
||||
export function NumberSetter({ onChange, ...props }: InputNumberProps) {
|
||||
export function NumberSetter({ onChange, ...props }: FormItemComponentProps<number>) {
|
||||
return (
|
||||
<InputNumber
|
||||
placeholder="请输入数字"
|
||||
@@ -0,0 +1,55 @@
|
||||
import { CodeSetter } from './code-setter';
|
||||
import { BoolSetter } from './bool-setter';
|
||||
import { IdSetter } from './id-setter';
|
||||
import { TextAreaSetter, TextSetter } from './text-setter';
|
||||
import { NumberSetter, SliderSetter } from './number-setter';
|
||||
import { IFormItemCreateOptions, register } from '../form-item';
|
||||
|
||||
const BASIC_SETTERS: IFormItemCreateOptions[] = [
|
||||
{
|
||||
name: 'codeSetter',
|
||||
alias: ['expSetter', 'expressionSetter'],
|
||||
component: CodeSetter,
|
||||
type: 'code',
|
||||
},
|
||||
{
|
||||
name: 'textSetter',
|
||||
component: TextSetter,
|
||||
},
|
||||
{
|
||||
name: 'textAreaSetter',
|
||||
component: TextAreaSetter,
|
||||
},
|
||||
{
|
||||
name: 'boolSetter',
|
||||
component: BoolSetter,
|
||||
},
|
||||
{
|
||||
name: 'numberSetter',
|
||||
component: NumberSetter,
|
||||
},
|
||||
{
|
||||
name: 'sliderSetter',
|
||||
component: SliderSetter,
|
||||
},
|
||||
{
|
||||
name: 'idSetter',
|
||||
component: IdSetter,
|
||||
},
|
||||
];
|
||||
|
||||
let registered = false;
|
||||
|
||||
/**
|
||||
* 注册内置的基础类型 Setter
|
||||
*/
|
||||
export function registerBuiltinSetters() {
|
||||
if (registered) {
|
||||
// 防止重复注册
|
||||
return;
|
||||
}
|
||||
|
||||
// 预注册基础 Setter
|
||||
BASIC_SETTERS.forEach(register);
|
||||
registered = true;
|
||||
}
|
||||
+11
-12
@@ -1,20 +1,15 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Input, InputProps } from 'antd';
|
||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||
import { Input } from 'antd';
|
||||
import { FormItemComponentProps } from '../form-item';
|
||||
|
||||
const noop = () => {};
|
||||
|
||||
interface TextSetterProps extends Omit<InputProps, 'onChange' | 'value'> {
|
||||
value?: string;
|
||||
onChange?: (value: string) => void;
|
||||
}
|
||||
|
||||
export function TextSetter({
|
||||
value: valueProp,
|
||||
onChange = noop,
|
||||
placeholder = '请输入',
|
||||
placeholder = '请输入文本',
|
||||
...props
|
||||
}: TextSetterProps) {
|
||||
}: FormItemComponentProps<string>) {
|
||||
const [valueState, setValue] = useState(valueProp);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -28,7 +23,9 @@ export function TextSetter({
|
||||
value={valueState}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
onBlur={() => {
|
||||
onChange(valueState);
|
||||
if (valueState !== valueProp) {
|
||||
onChange(valueState);
|
||||
}
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
@@ -43,7 +40,7 @@ const autoSize = {
|
||||
export function TextAreaSetter({
|
||||
value: valueProp,
|
||||
onChange = noop,
|
||||
placeholder = '请输入',
|
||||
placeholder = '请输入文本',
|
||||
...props
|
||||
}: FormItemComponentProps<string>) {
|
||||
const [valueState, setValue] = useState(valueProp);
|
||||
@@ -59,7 +56,9 @@ export function TextAreaSetter({
|
||||
value={valueState}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
onBlur={() => {
|
||||
onChange(valueState);
|
||||
if (valueState !== valueProp) {
|
||||
onChange(valueState);
|
||||
}
|
||||
}}
|
||||
autoSize={autoSize}
|
||||
{...props}
|
||||
@@ -1,4 +1,4 @@
|
||||
export interface ISetterOnChangeCallbackDetail {
|
||||
relatedImports?: string[];
|
||||
isRawCode?: boolean;
|
||||
rawCode?: string;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,36 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
||||
|
||||
## [1.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
|
||||
|
||||
- refactor parse attribute value ([#149](https://github.com/netease/tango/issues/149)) ([ffaa276](https://github.com/netease/tango/commit/ffaa276b5c205ed962d37e2fdb358a703f8fad01))
|
||||
|
||||
## [1.0.2](https://github.com/netease/tango/compare/@music163/tango-ui@1.0.1...@music163/tango-ui@1.0.2) (2024-04-22)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- allow reload state tree & display pageStore in variable tree ([#135](https://github.com/netease/tango/issues/135)) ([2664613](https://github.com/netease/tango/commit/2664613ab263aead2a5239fa012454fc7fd5ff99))
|
||||
|
||||
## [1.0.1](https://github.com/netease/tango/compare/@music163/tango-ui@1.0.0...@music163/tango-ui@1.0.1) (2024-04-16)
|
||||
|
||||
**Note:** Version bump only for package @music163/tango-ui
|
||||
|
||||
# [1.0.0-alpha.12](https://github.com/netease/tango/compare/@music163/tango-ui@1.0.0-alpha.11...@music163/tango-ui@1.0.0-alpha.12) (2024-04-09)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@music163/tango-ui",
|
||||
"version": "1.0.0",
|
||||
"version": "1.2.1",
|
||||
"description": "ui widgets of tango",
|
||||
"keywords": [
|
||||
"react",
|
||||
@@ -34,18 +34,19 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^4.8.0",
|
||||
"@codemirror/autocomplete": "^6.11.1",
|
||||
"@codemirror/lang-javascript": "^6.2.1",
|
||||
"@codemirror/lint": "^6.4.2",
|
||||
"@codemirror/search": "^6.5.5",
|
||||
"@music163/tango-helpers": "^1.0.0-alpha.6",
|
||||
"@uiw/react-codemirror": "^4.21.21",
|
||||
"@codemirror/autocomplete": "^6.16.0",
|
||||
"@codemirror/lang-javascript": "^6.2.2",
|
||||
"@codemirror/lint": "^6.7.1",
|
||||
"@codemirror/search": "^6.5.6",
|
||||
"@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.2.4"
|
||||
"react-monaco-editor-lite": "^1.3.11"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -68,6 +68,7 @@ export interface ActionProps extends HTMLCoralProps<any> {
|
||||
*/
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
onClick?: React.MouseEventHandler;
|
||||
}
|
||||
|
||||
export function Action({
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
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';
|
||||
|
||||
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'> {
|
||||
// 标题
|
||||
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">
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -200,6 +200,7 @@ function useInputCode({
|
||||
lineNumbers: showLineNumbers ?? lineNumbers,
|
||||
foldGutter: showFoldGutter ?? foldGutter,
|
||||
searchKeymap: false, // 默认关闭搜索快捷键,原因:https://github.com/uiwjs/react-codemirror/issues/280
|
||||
scrollbarStyle: 'null',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
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();
|
||||
const x = e.clientX;
|
||||
const y = e.clientY;
|
||||
setLeft(x);
|
||||
setTop(y + 10);
|
||||
setVisible(true);
|
||||
onOpenChange(true);
|
||||
},
|
||||
[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
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://typedoc.org/schema.json",
|
||||
"name": "Tango LowCode Builder",
|
||||
"entryPoints": ["packages/core", "packages/designer", "packages/helpers"],
|
||||
"entryPoints": ["packages/core", "packages/designer","packages/setting-form", "packages/helpers"],
|
||||
"entryPointStrategy": "packages",
|
||||
"exclude": ["packages/**/tests/**/*"],
|
||||
"out": "docs",
|
||||
|
||||
Reference in New Issue
Block a user