feat: refactor parse attribute value (#149)

* refactor: update attribute value parsing logic

* fix: refactor toggle setter logic

* fix: enhance basic setters

* fix: move basic value setter to setting-form package

* fix: add setter type

* fix: update

* feat: support toggle formObject to codeSetter

* fix: update invalid setter notice

* fix: update node2value

* fix: trigger onChange for formObject

* docs: update

* chore: up

* feat: improve code2value function and fix related issues

* Revert "chore: up"

This reverts commit a87d8fcce4.

* chore: update npm dependencies to latest versions
This commit is contained in:
Wells
2024-05-16 13:03:41 +08:00
committed by GitHub
parent 56d0877507
commit ffaa276b5c
52 changed files with 2224 additions and 1324 deletions
+1 -1
View File
@@ -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"
}
}
+40 -3
View File
@@ -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,16 +157,25 @@ import {
Input,
FormilyForm,
FormilyFormItem,
Table,
} from "@music163/antd";
import { Space } from "@music163/antd";
import { LocalButton } 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="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">
@@ -174,6 +190,9 @@ class App extends React.Component {
</FormilyForm>
</Section>
<Section title="原生 DOM" tid="section4">
<h1 style={{ ...{ color: "red" }, fontSize: 64 }}>
hello world
</h1>
<div
style={{
border: "1px solid #ccc",
@@ -234,6 +253,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';
@@ -373,6 +409,7 @@ 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/input.js', code: componentsInputCode },
{ filename: '/src/components/index.js', code: componentsEntryCode },
+2 -6
View File
@@ -1,11 +1,7 @@
import React, { StrictMode } from 'react';
import React from 'react';
import { Outlet } from 'umi';
import './index.less';
export default function Layout() {
return (
<StrictMode>
<Outlet />
</StrictMode>
);
return <Outlet />;
}
+43 -2
View File
@@ -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,15 +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';
import { Action } from '@music163/tango-ui';
import { useState } from 'react';
// 1. 实例化工作区
const workspace = new Workspace({
@@ -80,6 +83,10 @@ const menuData = {
title: 'Formily表单',
items: ['FormilyForm', 'FormilyFormItem', 'FormilySubmit', 'FormilyReset'],
},
{
title: '数据展示',
items: ['Comment'],
},
],
};
@@ -87,6 +94,8 @@ const menuData = {
* 5. 平台初始化,访问 https://local.netease.com:6006/
*/
export default function App() {
const [showNewPageModal, setShowNewPageModal] = useState(false);
const [form] = Form.useForm();
return (
<Designer
theme={themeLight}
@@ -104,6 +113,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" />
@@ -115,6 +132,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>
}
>
+2 -2
View File
@@ -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",
+475 -299
View File
@@ -1,10 +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 { NumberSetter } from '@music163/tango-designer/src/setters/number-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';
@@ -12,36 +9,9 @@ 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,
});
register({
name: 'numberSetter',
component: NumberSetter,
});
BUILT_IN_SETTERS.filter((setter) => !BLACK_LIST.includes(setter.name)).forEach(register);
createFromIconfontCN({
scriptUrl: '//at.alicdn.com/t/c/font_2891794_cou9i7556tl.js',
@@ -51,254 +21,6 @@ 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',
},
{
name: 'validate',
title: 'validate',
setter: 'numberSetter',
validate: (value) => {
if (!value && value !== 0) {
return '必填';
}
if (value < 0) {
return '必须大于 0';
}
if (value > 10) {
return '必须小于等于 10';
}
},
},
],
};
/**
* 表单值预览
*/
@@ -307,20 +29,13 @@ 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">
@@ -341,25 +56,486 @@ export function Basic() {
);
}
const prototypeHasBasicProps: IComponentPrototype = {
name: 'Sample',
package: 'sample-pkg',
type: 'element',
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',
},
],
}}
/>
);
}
+1 -1
View File
@@ -33,7 +33,7 @@
"dependencies": {
"@music163/tango-core": "^1.0.2",
"@music163/tango-helpers": "^1.0.0",
"mobx-react-lite": "4.0.5"
"mobx-react-lite": "4.0.7"
},
"publishConfig": {
"access": "public",
+1 -1
View File
@@ -31,7 +31,7 @@
"@music163/tango-helpers": "^1.0.0",
"@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
View File
@@ -40,6 +40,7 @@ const templatePattern = /^{(.+)}$/s;
/**
* 判断给定字符串是否被表达式容器`{expCode}`包裹
* @param code
* @deprecated 新版改为 {{code}} 作为容器,使用 isWrappedCode 代替
*/
export function isWrappedByExpressionContainer(code: string, isStrict = true) {
if (isStrict && isValidExpressionCode(code)) {
+48 -30
View File
@@ -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;
+16 -22
View File
@@ -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 = {
@@ -109,9 +109,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 +129,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 +165,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 +194,17 @@ export function code2jsxAttributeValueNode(code: string) {
return t.jsxExpressionContainer(code2expression(code));
}
// FIXME: 统一处理为 code2jsxAttributeValueNode
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);
ret = t.jsxExpressionContainer(code2expression(innerCode));
} else {
ret = t.stringLiteral(value);
}
@@ -227,7 +221,7 @@ 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 {
+39 -36
View File
@@ -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;
}
+30 -19
View File
@@ -3,14 +3,17 @@ 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,
@@ -84,48 +87,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;
}
@@ -159,7 +170,7 @@ export function prototype2code(prototype: IComponentPrototype, extraProps?: Dict
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 -16
View File
@@ -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 -2
View File
@@ -54,8 +54,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');
+37 -10
View File
@@ -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);
});
});
+45
View File
@@ -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}');
});
});
+4 -4
View File
@@ -32,7 +32,7 @@
},
"dependencies": {
"@ant-design/icons": "^4.8.0",
"@music163/request": "^0.1.2",
"@music163/request": "^0.2.0",
"@music163/tango-context": "^1.0.2",
"@music163/tango-core": "^1.0.2",
"@music163/tango-helpers": "^1.0.0",
@@ -41,16 +41,16 @@
"@music163/tango-ui": "^1.0.3",
"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",
@@ -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);
}}
/>
+10 -11
View File
@@ -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 -5
View File
@@ -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}
+17 -13
View File
@@ -4,7 +4,9 @@ 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 { ExpressionModal } from './expression-setter';
import { value2code } from '@music163/tango-core';
enum EventAction {
NoAction = 'noAction',
@@ -42,19 +44,22 @@ export function EventSetter(props: EventSetterProps) {
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 onAction = (key: string) => {
@@ -76,15 +81,14 @@ 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}
value={code}
visible={expModalVisible}
onCancel={() => setExpModalVisible(false)}
onOk={(nextValue) => {
@@ -142,12 +146,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 +160,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 { isValidExpressionCode } from '@music163/tango-core';
import { noop, useBoolean, getValue, IVariableTreeNode } from '@music163/tango-helpers';
import { CloseCircleFilled, ExpandAltOutlined, MenuOutlined } from '@ant-design/icons';
import { Panel, InputCode, Action } 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,20 @@ export function ExpressionSetter(props: ExpressionSetterProps) {
modalTitle,
modalTip,
autoCompleteOptions,
placeholder = '输入JS代码',
placeholder = '在这里输入JS代码',
value: valueProp,
status,
allowClear = true,
newStoreTemplate,
showOptionsDropDown = true,
} = props;
// const codeValue = getCodeOfWrappedCode(valueProp);
const [inputValue, setInputValue] = useState(valueProp);
const [visible, { on, off }] = useBoolean();
const [inputValue, setInputValue] = useState(() => {
return value2expressionCode(valueProp);
});
const sandbox = useSandboxQuery();
const evaluateContext = sandbox.window;
// when receive new value, sync state
useEffect(() => {
setInputValue(value2expressionCode(valueProp));
setInputValue(valueProp);
}, [valueProp]);
const change = useCallback(
@@ -114,18 +72,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">
{/* 同时支持下拉框展示 */}
@@ -276,7 +230,16 @@ export function ExpressionModal({
autoCompleteContext={evaluateContext}
autoCompleteOptions={autoCompleteOptions}
/>
{error ? <Text color="red"></Text> : null}
{error ? (
<Text color="red" fontSize="12px">
</Text>
) : (
<Text fontSize="12px" color="text3">
javascript 使 jsx
</Text>
)}
</Panel>
<Panel
title="从变量列表中选中"
@@ -310,9 +273,9 @@ export function ExpressionModal({
}
let str;
if (/^(stores|services)\./.test(node.key)) {
str = `{tango.${node.key.replaceAll('.', '?.')}}`;
str = `tango.${node.key.replaceAll('.', '?.')}`;
} else {
str = `{${node.key}}`;
str = `${node.key}`;
}
setExp(str);
}}
+17 -34
View File
@@ -25,45 +25,27 @@ 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';
const setters: IFormItemCreateOptions[] = [
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,
disableVariableSetter: true,
type: 'code',
},
{
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,
},
{
@@ -95,18 +77,15 @@ const setters: IFormItemCreateOptions[] = [
alias: ['actionSetter', 'functionSetter', 'callbackSetter'],
component: EventSetter,
},
{
name: 'expressionSetter',
alias: ['expSetter'],
component: ExpressionSetter,
},
{
name: 'jsonSetter',
component: JSONSetter,
type: 'code',
},
{
name: 'jsxSetter',
component: JsxSetter,
type: 'code',
},
{
name: 'listSetter',
@@ -121,20 +100,24 @@ const setters: IFormItemCreateOptions[] = [
component: OptionSetter,
},
{
name: 'pickerSetter',
name: 'selectSetter',
alias: ['pickerSetter'],
component: PickerSetter,
},
{
name: 'renderPropsSetter',
component: RenderSetter,
type: 'code',
},
{
name: 'tableCellSetter',
component: TableCellSetter,
type: 'code',
},
{
name: 'tableExpandableSetter',
component: TableExpandableSetter,
type: 'code',
},
{
name: 'routerSetter',
@@ -170,5 +153,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"
+16 -5
View File
@@ -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 },
+20 -12
View File
@@ -2,7 +2,6 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { ActionSelect, InputCode } from '@music163/tango-ui';
import { FormItemComponentProps } from '@music163/tango-setting-form';
import { Box } from 'coral-system';
import { value2expressionCode } from '@music163/tango-core';
interface IRenderOption {
label: string;
@@ -17,6 +16,11 @@ export interface RenderSetterProps {
fallbackOption?: IRenderOption;
}
const defaultOptions: IRenderOption[] = [
{ label: '取消自定义', value: '' },
{ label: '自定义渲染', value: 'Box', render: '() => <Box></Box>' },
];
/**
* Render Props Setters
*/
@@ -24,14 +28,12 @@ export function RenderSetter({
value,
onChange,
text = '自定义渲染为',
options = [],
options = defaultOptions,
fallbackOption,
}: FormItemComponentProps & RenderSetterProps) {
const [inputValue, setInputValue] = useState(() => {
return value2expressionCode(value);
});
const [inputValue, setInputValue] = useState(value || '');
useEffect(() => {
setInputValue(value2expressionCode(value));
setInputValue(value);
}, [value]);
const optionsMap = useMemo(() => {
@@ -67,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'] = [
@@ -122,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);
}}
/>
@@ -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;
}
+197
View File
@@ -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,
};
}
+2 -1
View File
@@ -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';
-255
View File
@@ -128,258 +128,3 @@ export function parseDndId(str: string): DndIdParsedType {
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,
};
}
+10 -30
View File
@@ -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', () => {
+2 -2
View File
@@ -33,8 +33,8 @@
"@music163/tango-ui": "^1.0.3",
"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",
+149 -50
View File
@@ -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, ComponentPropValidate, 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';
/**
* 渲染设置器使用的组件
*/
@@ -64,9 +77,98 @@ export interface IFormItemCreateOptions {
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);
value = code2value(code);
} 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,11 +177,11 @@ 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,
@@ -91,36 +193,40 @@ export function createFormItem(options: IFormItemCreateOptions) {
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;
const fieldValue = toJS(field.value ?? defaultValue);
const { setterValue, setter, isCodeSetter, toggleSetter } = useSetterValue({
fieldValue,
setter: setterProp,
setterType,
});
field.setConfig({
validate: validate || options.validate,
});
const baseComponentProps = clone(
{
value,
defaultValue,
onChange: field.handleChange,
status: field.error ? 'error' : undefined,
placeholder,
options: setterOptions,
let baseComponentProps: FormItemComponentProps = {
value: setterValue,
defaultValue,
onChange(value, detail) {
if ((setterType === 'code' || isCodeSetter) && isString(value) && value) {
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,
@@ -131,10 +237,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,
@@ -148,9 +254,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)}
@@ -163,27 +273,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>
);
}
@@ -202,9 +301,9 @@ const REGISTERED_FORM_ITEM_MAP: Record<string, ReturnType<typeof createFormItem>
* @param config 注册选项
*/
export function register(config: IFormItemCreateOptions) {
const names = [config.name, ...(config.alias ?? [])];
names.forEach((name) => {
REGISTERED_FORM_ITEM_MAP[name] = createFormItem(config);
REGISTERED_FORM_ITEM_MAP[config.name] = createFormItem(config);
(Array.isArray(config.alias) ? config.alias : []).forEach((alias) => {
REGISTERED_FORM_ITEM_MAP[alias] = REGISTERED_FORM_ITEM_MAP[config.name];
});
}
@@ -212,13 +311,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>
}
/>
+2 -2
View File
@@ -195,7 +195,7 @@ export class Field {
error: computed,
validate: action,
handleBlur: action,
handleChange: action,
setValue: action,
});
}
@@ -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');
+34 -9
View File
@@ -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>
+66 -3
View File
@@ -1,7 +1,7 @@
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 { 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';
@@ -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>
)}
@@ -261,3 +261,66 @@ export function FormHeader({ title, extra, subTitle }: FormHeaderProps) {
</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>
);
}
+4 -4
View File
@@ -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' },
];
@@ -127,7 +127,7 @@ export interface SettingFormProps {
*/
renderItemExtra?: (props: IComponentProp) => React.ReactNode;
/**
* 是否允许表单项切换到表达式设置器
* 是否禁用 codeSetter 切换,默认所有的 setter 都支持切换到 codeSetter
*/
disableSwitchExpressionSetter?: boolean;
}
@@ -217,7 +217,7 @@ export function SettingForm({
<Box px="l" py="m">
{showIdentifier && (
<FormHeader
title={prototype.title}
title={prototype.title || prototype.name}
subTitle={
<SettingFormItem
noStyle
+1
View File
@@ -3,3 +3,4 @@ export * from './form-item';
export * from './form-object';
export * from './form-model';
export * from './context';
export * from './setters';
@@ -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}
/>
);
}
@@ -1,16 +1,8 @@
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} />;
}
import { Input, InputProps, Tooltip } from 'antd';
import { css, Box } from 'coral-system';
import { FormItemComponentProps } from '../form-item';
import { ExclamationCircleOutlined } from '@ant-design/icons';
const idInputStyle = css`
> .ant-input-borderless {
@@ -53,7 +45,11 @@ export function IdSetter({
onChange(e) {
const newValue = e.target.value;
setValue(e.target.value);
setError(newValue && !idPattern.test(newValue) ? 'error' : '');
setError(
newValue && !idPattern.test(newValue)
? '非法的组件ID,必须使用字母开头的字母数字组合,例如 button1'
: '',
);
},
}
: {
@@ -66,26 +62,19 @@ export function IdSetter({
return (
<Box css={idInputStyle}>
<Input placeholder={placeholder} value={value} {...__props} {...rest} />
<Input
placeholder={placeholder}
value={value}
{...__props}
{...rest}
suffix={
error ? (
<Tooltip title={error} color="#ff4d4f">
<ExclamationCircleOutlined style={{ color: 'red' }} />
</Tooltip>
) : null
}
/>
</Box>
);
}
export function registerBuiltinSetters() {
// 预注册基础 Setter
register({
name: 'expressionSetter',
component: ExpressionSetter,
disableVariableSetter: true,
});
register({
name: 'textSetter',
component: TextSetter,
});
register({
name: 'idSetter',
component: IdSetter,
});
}
@@ -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';
@@ -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,44 @@
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,
},
];
export function registerBuiltinSetters() {
// 预注册基础 Setter
BASIC_SETTERS.forEach(register);
}
@@ -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}
+6 -6
View File
@@ -34,18 +34,18 @@
},
"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",
"@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.0.0",
"@uiw/react-codemirror": "^4.21.21",
"@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-json-view": "^1.21.3",
"react-monaco-editor-lite": "^1.3.2"
"react-monaco-editor-lite": "^1.3.9"
},
"publishConfig": {
"access": "public",
+2 -1
View File
@@ -1,5 +1,5 @@
import React, { useMemo } from 'react';
import { Box, HTMLCoralProps } from 'coral-system';
import { Box, HTMLCoralProps, css } from 'coral-system';
import CodeMirror, { ReactCodeMirrorProps } from '@uiw/react-codemirror';
import { javascript, javascriptLanguage, esLint } from '@codemirror/lang-javascript';
import { CompletionContext } from '@codemirror/autocomplete';
@@ -200,6 +200,7 @@ function useInputCode({
lineNumbers: showLineNumbers ?? lineNumbers,
foldGutter: showFoldGutter ?? foldGutter,
searchKeymap: false, // 默认关闭搜索快捷键,原因:https://github.com/uiwjs/react-codemirror/issues/280
scrollbarStyle: 'null',
},
};
}
+699 -265
View File
File diff suppressed because it is too large Load Diff