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:
Wells
2024-08-30 18:09:01 +08:00
committed by GitHub
parent 179de0cc52
commit c4e1ed67f5
12 changed files with 160 additions and 79 deletions

View File

@@ -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} />

View File

@@ -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: {

View File

@@ -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,

View 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,
},
};

View File

@@ -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 =
// 手动触发 适用于 点击添加组件

View File

@@ -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}");`);
}
}

View File

@@ -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}`;
}

View File

@@ -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>
);

View File

@@ -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];
});
}

View File

@@ -88,6 +88,11 @@ export interface IComponentProp<T = any> {
* 自动补全的提示值,仅对 ExpressionSetter 有效
*/
autoCompleteOptions?: string[];
/**
* value 的模板,一般用于函数类型的属性,便于 setter 用来拼装返回值
* @example "(arg1, arg2, arg3) => { {{content}}}"
*/
template?: string;
/**
* 设置器
*/

View File

@@ -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,
};
}

View File

@@ -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>