mirror of
https://github.com/NetEase/tango.git
synced 2026-07-01 03:54:20 +08:00
fix: add prop value template & fix components popover (#201)
* fix: clear value in CodeSetter * fix: update variable path in codesetter * fix: add prop value template * fix: update recommendedList for ComponentsPopover * fix: update icon size in line mode for ComponentsPanel
This commit is contained in:
@@ -153,6 +153,7 @@ import { definePage } from "@music163/tango-boot";
|
||||
import {
|
||||
Page,
|
||||
Section,
|
||||
Box,
|
||||
Button,
|
||||
Input,
|
||||
FormilyForm,
|
||||
@@ -167,7 +168,9 @@ class App extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<Page title={tango.stores.app.title} subTitle={<><Button>hello</Button></>}>
|
||||
<Section tid="section0" />
|
||||
<Section tid="section0">
|
||||
<Box></Box>
|
||||
</Section>
|
||||
<Section tid="section1" title="Section Title">
|
||||
your input: <Input tid="input1" defaultValue="hello" />
|
||||
copy input: <Input value={tango.page.input1?.value} />
|
||||
|
||||
@@ -200,6 +200,13 @@ const prototypes: Dict<IComponentPrototype> = {
|
||||
title: 'd',
|
||||
setter: 'textSetter',
|
||||
},
|
||||
{
|
||||
name: 'onClick',
|
||||
title: '点击事件',
|
||||
setter: 'eventSetter',
|
||||
template: '(e) => {\n {{content}}\n}',
|
||||
tip: '回调参数说明:e 为事件对象',
|
||||
},
|
||||
],
|
||||
},
|
||||
Columns: {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { SystemProvider } from 'coral-system';
|
||||
import 'antd/dist/antd.css';
|
||||
|
||||
export const parameters = {
|
||||
actions: { argTypesRegex: '^on[A-Z].*' },
|
||||
actions: { argTypesRegex: '^on.*' },
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
|
||||
29
apps/storybook/src/ui/action-select.stories.tsx
Normal file
29
apps/storybook/src/ui/action-select.stories.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import { ActionSelect } from '@music163/tango-ui';
|
||||
|
||||
export default {
|
||||
title: 'UI/ActionSelect',
|
||||
component: ActionSelect,
|
||||
};
|
||||
|
||||
const options = [
|
||||
{ label: 'action1', value: 'action1' },
|
||||
{ label: 'action2', value: 'action2' },
|
||||
{ label: 'action3', value: 'action3' },
|
||||
];
|
||||
|
||||
export const Basic = {
|
||||
args: {
|
||||
defaultText: '选择动作',
|
||||
options,
|
||||
onSelect: console.log,
|
||||
},
|
||||
};
|
||||
|
||||
export const showInput = {
|
||||
args: {
|
||||
text: '选择动作',
|
||||
options,
|
||||
showInput: true,
|
||||
},
|
||||
};
|
||||
@@ -34,17 +34,17 @@ export const ComponentsPopover = observer(
|
||||
// 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 recommendedList = useMemo(() => {
|
||||
if (type === 'inner') {
|
||||
return prototype?.childrenName
|
||||
? Array.isArray(prototype?.childrenName)
|
||||
? prototype.childrenName
|
||||
: [prototype.childrenName]
|
||||
: [];
|
||||
}
|
||||
// 默认推荐使用相同类型的组件作为兄弟节点
|
||||
return prototype.siblingNames || [prototype.name];
|
||||
}, [prototype.childrenName, prototype.siblingNames, prototype.name, type]);
|
||||
|
||||
const tipsTextMap = useMemo(
|
||||
() => ({
|
||||
@@ -82,22 +82,15 @@ export const ComponentsPopover = observer(
|
||||
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) {
|
||||
if (commonList?.length && recommendedList.length) {
|
||||
commonList.unshift({
|
||||
title: '推荐使用',
|
||||
items: insertedList,
|
||||
items: recommendedList,
|
||||
});
|
||||
}
|
||||
|
||||
return menuList;
|
||||
}, [insertedList, siblingList, designer.menuData]);
|
||||
}, [recommendedList, designer.menuData]);
|
||||
|
||||
const innerTypeProps =
|
||||
// 手动触发 适用于 点击添加组件
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { css, Box, Text } from 'coral-system';
|
||||
import { AutoComplete } from 'antd';
|
||||
import { AutoComplete, Input } from 'antd';
|
||||
import { ActionSelect } from '@music163/tango-ui';
|
||||
import { FormItemComponentProps } from '@music163/tango-setting-form';
|
||||
import { useWorkspace, useWorkspaceData } from '@music163/tango-context';
|
||||
import { Dict, wrapCode } from '@music163/tango-helpers';
|
||||
import { ExpressionPopover } from './expression-setter';
|
||||
import { ExpressionPopover, getCallbackValue } from './expression-setter';
|
||||
import { value2code } from '@music163/tango-core';
|
||||
|
||||
enum EventAction {
|
||||
@@ -31,7 +31,7 @@ export type EventSetterProps = FormItemComponentProps<string>;
|
||||
* 事件监听函数绑定器
|
||||
*/
|
||||
export function EventSetter(props: EventSetterProps) {
|
||||
const { value, onChange, modalTitle } = props;
|
||||
const { value, onChange, modalTitle, modalTip, template } = props;
|
||||
const [type, setType] = useState<EventAction>(); // 事件类型
|
||||
const [temp, setTemp] = useState(''); // 二级暂存值
|
||||
const { actionVariables, routeOptions } = useWorkspaceData();
|
||||
@@ -59,7 +59,9 @@ export function EventSetter(props: EventSetterProps) {
|
||||
label: (
|
||||
<ExpressionPopover
|
||||
title={modalTitle}
|
||||
subTitle={modalTip}
|
||||
value={value}
|
||||
template={template}
|
||||
onOk={(nextValue) => {
|
||||
handleChange(nextValue);
|
||||
}}
|
||||
@@ -74,30 +76,34 @@ export function EventSetter(props: EventSetterProps) {
|
||||
{ label: '打开弹窗', value: EventAction.OpenModal },
|
||||
{ label: '关闭弹窗', value: EventAction.CloseModal },
|
||||
],
|
||||
[modalTitle, value, actionVariables, handleChange],
|
||||
[modalTitle, value, actionVariables, template, handleChange],
|
||||
);
|
||||
|
||||
const onAction = (key: string) => {
|
||||
setType(key as EventAction); // 记录事件类型
|
||||
setTemp(''); // 重置二级选项值
|
||||
|
||||
switch (key) {
|
||||
case EventAction.ConsoleLog:
|
||||
handleChange('(...args) => console.log(...args)');
|
||||
break;
|
||||
case EventAction.NoAction:
|
||||
handleChange(undefined);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
if (key === EventAction.NoAction) {
|
||||
handleChange(undefined);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const actionText = getActionText(type, temp, code);
|
||||
|
||||
return (
|
||||
<Box css={wrapperStyle}>
|
||||
<ActionSelect options={options} onSelect={onAction} text={actionText} />
|
||||
<ActionSelect options={options} onSelect={onAction} defaultText="请选择动作类型" />
|
||||
{type === EventAction.ConsoleLog && (
|
||||
<Input
|
||||
placeholder="输入 Console.log 日志内容"
|
||||
value={temp}
|
||||
onChange={(e) => setTemp(e.target.value)}
|
||||
onBlur={() => {
|
||||
if (temp) {
|
||||
handleChange(getExpressionValue(type, temp));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{type === EventAction.NavigateTo && (
|
||||
<AutoComplete
|
||||
placeholder="选择或输入页面路由"
|
||||
@@ -142,25 +148,15 @@ export function EventSetter(props: EventSetterProps) {
|
||||
}
|
||||
|
||||
const handlerMap: Dict = {
|
||||
[EventAction.OpenModal]: 'openModal',
|
||||
[EventAction.CloseModal]: 'closeModal',
|
||||
[EventAction.NavigateTo]: 'navigateTo',
|
||||
[EventAction.ConsoleLog]: 'console.log',
|
||||
[EventAction.OpenModal]: 'tango.openModal',
|
||||
[EventAction.CloseModal]: 'tango.closeModal',
|
||||
[EventAction.NavigateTo]: 'tango.navigateTo',
|
||||
};
|
||||
|
||||
function getActionText(type: EventAction, temp: string, fallbackCode: string) {
|
||||
let text;
|
||||
if (handlerMap[type]) {
|
||||
text = getExpressionValue(type, temp);
|
||||
} else if (fallbackCode) {
|
||||
text = fallbackCode;
|
||||
}
|
||||
text = text || '请选择';
|
||||
return text;
|
||||
}
|
||||
|
||||
function getExpressionValue(type: EventAction, value = '') {
|
||||
const handler = handlerMap[type];
|
||||
if (handler) {
|
||||
return `() => tango.${handler}("${value}")`;
|
||||
return getCallbackValue(`${handler}("${value}");`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { Box, Text, css } from 'coral-system';
|
||||
import { Dropdown, Button } from 'antd';
|
||||
import { isValidExpressionCode } from '@music163/tango-core';
|
||||
import { getValue, IVariableTreeNode, noop } from '@music163/tango-helpers';
|
||||
import { getValue, interpolate, IVariableTreeNode, noop } from '@music163/tango-helpers';
|
||||
import { CloseCircleFilled, MenuOutlined } from '@ant-design/icons';
|
||||
import {
|
||||
Panel,
|
||||
@@ -34,6 +34,19 @@ export const jsonValueValidate = (value: string) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 拼装回调函数
|
||||
* @param value 回调函数体
|
||||
* @param template 回调函数模板
|
||||
* @returns
|
||||
*/
|
||||
export function getCallbackValue(value: string, template?: string) {
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
return template ? interpolate(template, { content: value }) : `() => {\n ${value}\n}`;
|
||||
}
|
||||
|
||||
const suffixStyle = css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -63,6 +76,7 @@ export function ExpressionSetter(props: ExpressionSetterProps) {
|
||||
modalTitle,
|
||||
modalTip,
|
||||
autoCompleteOptions,
|
||||
template,
|
||||
placeholder = '在这里输入代码',
|
||||
value: valueProp,
|
||||
status,
|
||||
@@ -105,7 +119,7 @@ export function ExpressionSetter(props: ExpressionSetterProps) {
|
||||
<CloseCircleFilled
|
||||
title="清空"
|
||||
onClick={() => {
|
||||
change('');
|
||||
change(undefined);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@@ -134,6 +148,7 @@ export function ExpressionSetter(props: ExpressionSetterProps) {
|
||||
subTitle={modalTip}
|
||||
placeholder={placeholder}
|
||||
autoCompleteOptions={autoCompleteOptions}
|
||||
template={template}
|
||||
newStoreTemplate={newStoreTemplate}
|
||||
value={inputValue}
|
||||
expressionType={expressionType}
|
||||
@@ -166,6 +181,10 @@ export interface ExpressionPopoverProps extends InputCodeProps {
|
||||
onOk?: (value: string) => void;
|
||||
dataSource?: IVariableTreeNode[];
|
||||
autoCompleteOptions?: string[];
|
||||
/**
|
||||
* 值的模板,一般用于定义函数模板
|
||||
*/
|
||||
template?: string;
|
||||
/**
|
||||
* 新建 store 的模板代码
|
||||
*/
|
||||
@@ -186,6 +205,7 @@ export function ExpressionPopover({
|
||||
value,
|
||||
dataSource,
|
||||
autoCompleteOptions,
|
||||
template,
|
||||
newStoreTemplate = CODE_TEMPLATES.newStoreTemplate,
|
||||
children,
|
||||
expressionType,
|
||||
@@ -245,19 +265,18 @@ export function ExpressionPopover({
|
||||
autoCompleteOptions={autoCompleteOptions}
|
||||
/>
|
||||
{error ? (
|
||||
<Text color="red" fontSize="12px" as="p">
|
||||
<Text color="red" fontSize="12px" as="div">
|
||||
出错了!输入的表达式存在语法错误,请修改后再提交!
|
||||
</Text>
|
||||
) : null}
|
||||
{subTitle && (
|
||||
<Text fontSize="12px" color="text3" as="p">
|
||||
{subTitle}
|
||||
<Box fontSize="12px" color="text2">
|
||||
<Text display="block">说明:</Text>
|
||||
{subTitle && <Text display="block">{subTitle}</Text>}
|
||||
<Text display="block">
|
||||
你可以在上面的代码输入框里输入常规的 javascript 代码,还可以直接使用 jsx
|
||||
代码,但需要符合该属性的接受值定义。
|
||||
</Text>
|
||||
)}
|
||||
<Text fontSize="12px" color="text3" as="p">
|
||||
说明:你可以在上面的代码输入框里输入常规的 javascript 代码,还可以直接使用 jsx
|
||||
代码,但需要符合该属性的接受值定义。
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
<Panel
|
||||
title="从变量列表中选中"
|
||||
@@ -296,7 +315,11 @@ export function ExpressionPopover({
|
||||
}
|
||||
let str;
|
||||
if (/^(stores|services)\./.test(node.key)) {
|
||||
str = `tango.${node.key.replaceAll('.', '?.')}`;
|
||||
// 从匹配到的第二个点开始替换为 ?.,因为第一个点是 stores 或 services
|
||||
str = `tango.${node.key.replace(/(?<=\..*?)\./g, '?.')}`;
|
||||
if (node.type === 'function') {
|
||||
str = getCallbackValue(`${str}();`, template);
|
||||
}
|
||||
} else {
|
||||
str = `${node.key}`;
|
||||
}
|
||||
|
||||
@@ -365,7 +365,7 @@ function MaterialGrid({ data }: MaterialProps) {
|
||||
<IconFont className="material-icon" type={data.icon || 'icon-placeholder'} />
|
||||
) : (
|
||||
<Box className="material-icon">
|
||||
<img src={icon} alt={data.name} />
|
||||
<img src={icon} alt={data.name} height="32" />
|
||||
</Box>
|
||||
);
|
||||
|
||||
|
||||
@@ -125,3 +125,17 @@ export function parseDndId(str: string): DndIdParsedType {
|
||||
id: str,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 替换模板中的变量
|
||||
* @example interpolate('hello {{name}}', { name: 'world' }) -> 'hello world'
|
||||
*
|
||||
* @param template 带模板变量的字符串
|
||||
* @param props 变量字典
|
||||
* @returns 返回替换后的字符串
|
||||
*/
|
||||
export function interpolate(template: string, props: Record<string, any>) {
|
||||
return template.replace(/\{\{(\w+)\}\}/g, (_match, key) => {
|
||||
return props[key];
|
||||
});
|
||||
}
|
||||
|
||||
@@ -88,6 +88,11 @@ export interface IComponentProp<T = any> {
|
||||
* 自动补全的提示值,仅对 ExpressionSetter 有效
|
||||
*/
|
||||
autoCompleteOptions?: string[];
|
||||
/**
|
||||
* value 的模板,一般用于函数类型的属性,便于 setter 用来拼装返回值
|
||||
* @example "(arg1, arg2, arg3) => { {{content}}}"
|
||||
*/
|
||||
template?: string;
|
||||
/**
|
||||
* 设置器
|
||||
*/
|
||||
|
||||
@@ -182,6 +182,7 @@ export function createFormItem(options: IFormItemCreateOptions) {
|
||||
placeholder,
|
||||
docs,
|
||||
autoCompleteOptions,
|
||||
template,
|
||||
setter: setterProp,
|
||||
setterProps,
|
||||
defaultValue,
|
||||
@@ -238,6 +239,7 @@ export function createFormItem(options: IFormItemCreateOptions) {
|
||||
modalTitle: title,
|
||||
modalTip: tip,
|
||||
autoCompleteOptions,
|
||||
template,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { Box, css, Text } from 'coral-system';
|
||||
import { Button, Dropdown, Input, Menu } from 'antd';
|
||||
import { DownOutlined, PlusSquareOutlined } from '@ant-design/icons';
|
||||
@@ -23,9 +23,13 @@ interface ActionSelectProps {
|
||||
*/
|
||||
onInputChange?: (value: string) => void;
|
||||
/**
|
||||
* 提示文本
|
||||
* 受控的提示文本
|
||||
*/
|
||||
text?: string;
|
||||
/**
|
||||
* 默认的提示文本
|
||||
*/
|
||||
defaultText?: string;
|
||||
/**
|
||||
* 选择菜单时的回调
|
||||
*/
|
||||
@@ -50,11 +54,6 @@ const actionInputStyle = css`
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
color: var(--tango-colors-text3);
|
||||
|
||||
&:hover {
|
||||
color: var(--tango-colors-text2);
|
||||
}
|
||||
}
|
||||
|
||||
.anticon-down {
|
||||
@@ -71,15 +70,25 @@ const inputStyle = { width: 'calc(100% - 82px)' };
|
||||
export function ActionSelect({
|
||||
showInput = false,
|
||||
defaultInputValue,
|
||||
text,
|
||||
text: textProp,
|
||||
defaultText,
|
||||
options = [],
|
||||
onSelect,
|
||||
onInputChange,
|
||||
}: ActionSelectProps) {
|
||||
const [text, setText] = useState<string>(defaultText);
|
||||
const menu = (
|
||||
<Menu onClick={({ key }) => onSelect(key)}>
|
||||
<Menu>
|
||||
{options.map((item) => (
|
||||
<Menu.Item key={item.value}>
|
||||
<Menu.Item
|
||||
key={item.value}
|
||||
onClick={() => {
|
||||
onSelect?.(item.value);
|
||||
if (!textProp) {
|
||||
setText(item.label as string);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Box display="flex" alignItems="center" minWidth={200} columnGap="m">
|
||||
<Box flex="1">{item.label}</Box>
|
||||
<Text fontSize="12px" color="text.note">
|
||||
@@ -122,8 +131,8 @@ export function ActionSelect({
|
||||
mb="m"
|
||||
css={actionInputStyle}
|
||||
>
|
||||
<Box className="action-input-text" color="text.body">
|
||||
{text}
|
||||
<Box className="action-input-text" color="text2">
|
||||
{textProp ?? text}
|
||||
</Box>
|
||||
<DownOutlined />
|
||||
</Box>
|
||||
|
||||
Reference in New Issue
Block a user