fix: refactor action, formHeader, selectionDropdown (#114)

* fix: add new action component

* fix: refactor form header component and add documentation link

* fix: refactor dropdown menu for selected item

* fix: update

* fix: add store variable in modelSetter

* fix: code styles
This commit is contained in:
Wells
2024-03-18 11:43:45 +08:00
committed by GitHub
parent 23c2e2e9b6
commit 489118b88a
21 changed files with 389 additions and 132 deletions
+2
View File
@@ -20,10 +20,12 @@
"mobx-react-lite": "4.0.5"
},
"devDependencies": {
"@ant-design/icons": "^4.8.0",
"@storybook/addon-actions": "^6.5.14",
"@storybook/addon-essentials": "^6.5.14",
"@storybook/addon-links": "^6.5.14",
"@storybook/react": "^6.5.14",
"antd": "^4.24.2",
"babel-plugin-styled-components": "^2.0.7",
"tsconfig-paths-webpack-plugin": "^3.5.2"
}
+68
View File
@@ -0,0 +1,68 @@
import React from 'react';
import { Action } from '@music163/tango-ui';
import { PlusOutlined, QuestionCircleOutlined } from '@ant-design/icons';
import { Space } from 'antd';
export default {
title: 'UI/Action',
component: Action,
};
export const Basic = {
args: {
icon: <PlusOutlined />,
tooltip: '添加服务函数',
},
};
export const Link = {
args: {
icon: <QuestionCircleOutlined />,
tooltip: '查看帮助文档',
href: 'https://music.163.com',
},
};
export const Outline = {
args: {
icon: <PlusOutlined />,
tooltip: '添加服务函数',
shape: 'outline',
},
};
export const Small = {
args: {
size: 'small',
icon: <PlusOutlined />,
tooltip: '添加服务函数',
},
};
export const Disabled = () => {
return (
<Space>
<Action icon={<PlusOutlined />} tooltip="添加服务函数" disabled />
<Action icon={<PlusOutlined />} tooltip="添加服务函数" disabled shape="outline" />
<Action
icon={<QuestionCircleOutlined />}
tooltip="查看帮助文档"
href="https://music.163.com"
disabled
/>
</Space>
);
};
export const List = () => {
return (
<Space>
<Action icon={<PlusOutlined />} tooltip="添加服务函数" />
<Action
icon={<QuestionCircleOutlined />}
tooltip="查看帮助文档"
href="https://music.163.com"
/>
</Space>
);
};
+2 -2
View File
@@ -1,5 +1,5 @@
import React from 'react';
import { IconButton, InputCode } from '@music163/tango-ui';
import { Action, InputCode } from '@music163/tango-ui';
import { BlockOutlined } from '@ant-design/icons';
export default {
@@ -22,7 +22,7 @@ const context = {
export function Basic() {
return (
<InputCode
suffix={<IconButton icon={<BlockOutlined />} />}
suffix={<Action icon={<BlockOutlined />} size="small" />}
autoCompleteContext={{ tango: context }}
/>
);
+4 -4
View File
@@ -1,6 +1,6 @@
import React from 'react';
import { Box, css, Group } from 'coral-system';
import { IconButton, ToggleButton } from '@music163/tango-ui';
import { Action, ToggleButton } from '@music163/tango-ui';
import { observer, useDesigner } from '@music163/tango-context';
import { Input } from 'antd';
import {
@@ -84,9 +84,9 @@ export class Navigator extends React.Component<NavigatorProps, NavigatorState> {
css={navigatorStyle}
>
<Box>
<IconButton icon={<ArrowLeftOutlined />} onClick={onBack} title="返回" />
<IconButton icon={<ArrowRightOutlined />} onClick={onForward} title="前进" />
<IconButton icon={<ReloadOutlined />} onClick={onRefresh} title="刷新" />
<Action size="small" icon={<ArrowLeftOutlined />} onClick={onBack} title="返回" />
<Action size="small" icon={<ArrowRightOutlined />} onClick={onForward} title="前进" />
<Action size="small" icon={<ReloadOutlined />} onClick={onRefresh} title="刷新" />
</Box>
<Input
className="navigatorInput"
@@ -14,7 +14,7 @@ import {
IVariableTreeNode,
} from '@music163/tango-helpers';
import { CloseCircleFilled, ExpandAltOutlined } from '@ant-design/icons';
import { IconButton, Panel, InputCode } from '@music163/tango-ui';
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';
@@ -130,10 +130,11 @@ export function ExpressionSetter(props: ExpressionSetterProps) {
}}
/>
)}
<IconButton
<Action
tooltip="打开表达式变量选择面板"
icon={<ExpandAltOutlined />}
onClick={on}
size="small"
/>
</Box>
}
@@ -84,6 +84,8 @@ export function ModelSetter({
if (!definedVariables.includes(keyPath)) {
ret.showAddButton = false;
ret.showRemoveButton = false;
} else if (/^stores\.[a-zA-Z0-9]+$/.test(keyPath)) {
ret.showAddButton = true;
}
return ret;
+14 -16
View File
@@ -20,7 +20,7 @@ export const SettingPanel = observer((props: SettingPanelProps) => {
const workspace = useWorkspace();
const designer = useDesigner();
if (!designer.showRightPanel) {
if (!designer.showRightPanel || !workspace.selectSource.isSelected) {
return <Box className="SettingPanel" />;
}
@@ -93,21 +93,19 @@ export const SettingPanel = observer((props: SettingPanelProps) => {
headerProps={headerProps}
className="SettingPanel"
>
{workspace.selectSource.isSelected ? (
prototype ? (
<SettingForm
key={workspace.selectSource.first.id}
model={formModel}
prototype={prototype}
showIdentifier={{
identifierKey: 'tid',
}}
{...props}
/>
) : (
<Box p="m"></Box>
)
) : null}
{prototype ? (
<SettingForm
key={workspace.selectSource.first.id}
model={formModel}
prototype={prototype}
showIdentifier={{
identifierKey: 'tid',
}}
{...props}
/>
) : (
<Box p="m"></Box>
)}
</Panel>
);
});
+111 -31
View File
@@ -1,10 +1,11 @@
import React from 'react';
import styled, { css, keyframes } from 'styled-components';
import { Box, Button, Group, HTMLCoralProps } from 'coral-system';
import { Dropdown, DropdownProps } from 'antd';
import { PlusSquareOutlined, HolderOutlined, InfoCircleOutlined } from '@ant-design/icons';
import { ISelectedItemData, isString, noop } from '@music163/tango-helpers';
import { observer, useDesigner, useWorkspace } from '@music163/tango-context';
import { IconButton } from '@music163/tango-ui';
import { Action, IconFont } from '@music163/tango-ui';
import { getDragGhostElement } from '../helpers';
import { getWidget } from '../widgets';
@@ -58,6 +59,13 @@ const selectionBoxStyle = css`
top: 0;
`;
interface IInsertedData {
name: string;
label: string;
icon: string;
description: string;
}
export interface SelectionBoxProps {
/**
* 是否显示操作按钮
@@ -90,15 +98,20 @@ function SelectionBox({ showActions, actions, data }: SelectionBoxProps) {
const isPage = prototype?.type === 'page';
// 如果声明了 childrenName,提供快捷子元素创建入口
let insertedList: ISelectedItemData[] = [];
let insertedList: IInsertedData[] = [];
if (prototype?.childrenName) {
const names = Array.isArray(prototype?.childrenName)
? prototype.childrenName
: [prototype.childrenName];
insertedList = names.map((child) => ({
id: child,
name: child,
}));
insertedList = names.map((child) => {
const proto = workspace.componentPrototypes.get(child);
return {
name: child,
label: proto?.title || child,
icon: proto?.icon,
description: proto?.help,
};
});
}
let selectionHelpersAlign: SelectionHelperAlignType = 'top-right';
@@ -166,41 +179,28 @@ function SelectionBox({ showActions, actions, data }: SelectionBoxProps) {
}}
/>
{!isFromCurrentFile && (
<IconButton
<Action
as="span"
color="white"
tooltip={`该节点来自其他文件(${data.filename}`}
icon={<InfoCircleOutlined />}
size="small"
/>
)}
</>
}
/>
{insertedList.length > 0 && (
<SelectionHelper
icon={<PlusSquareOutlined />}
label={
insertedList.length === 1 ? (
<span
onClick={() => {
workspace.insertToSelectedNode(insertedList[0].name);
}}
>
{insertedList[0].name}
</span>
) : (
<NameSelector
label="添加"
parents={insertedList}
onSelect={(item) => {
workspace.insertToSelectedNode(item.name);
}}
/>
)
}
/>
)}
<SelectionToolSet>{!isPage && actions}</SelectionToolSet>
{insertedList.length > 0 && (
<InsertedDropdown
options={insertedList}
onSelect={(name) => {
workspace.insertToSelectedNode(name);
}}
>
<SelectionHelper icon={<PlusSquareOutlined />} />
</InsertedDropdown>
)}
</SelectionHelpers>
)}
</Box>
@@ -370,3 +370,83 @@ const NameSelector = ({ label, parents = [], onSelect = noop }: NameSelectorProp
</NameSelectorWrapper>
);
};
interface InsertedDropdownProps extends DropdownProps {
options?: IInsertedData[];
onSelect?: (name: string) => void;
}
function InsertedDropdown({ options = [], onSelect, ...props }: InsertedDropdownProps) {
return (
<Dropdown
trigger={['click']}
dropdownRender={() => {
return (
<Box
bg="#FFF"
borderRadius="m"
boxShadow="lowDown"
border="solid"
borderColor="line2"
overflow="hidden"
>
<Box px="l" py="m" color="text2">
</Box>
<Box>
{options.map((item) => (
<InsertedItem
key={item.name}
label={item.label}
icon={item.icon}
description={item.description}
onClick={() => onSelect?.(item.name)}
/>
))}
</Box>
</Box>
);
}}
{...props}
/>
);
}
const insertedItemStyle = css`
cursor: pointer;
&:hover {
background-color: var(--tango-colors-fill2);
}
`;
function InsertedItem({
label,
icon = 'icon-placeholder',
description,
}: HTMLCoralProps<'div'> & Omit<IInsertedData, 'name'>) {
return (
<Box display="flex" columnGap="m" px="l" py="m" fontSize="12px" css={insertedItemStyle}>
<Box
size="35px"
fontSize="32px"
background="fill1"
border="1px solid"
borderColor="line2"
display="flex"
alignItems="center"
justifyContent="center"
>
{icon.startsWith('icon-') ? (
<IconFont className="material-icon" type={icon} />
) : (
<img src={icon} alt={label} />
)}
</Box>
<Box>
<Box fontWeight="500">{label}</Box>
<Box color="text2">{description}</Box>
</Box>
</Box>
);
}
+2
View File
@@ -1,3 +1,5 @@
/* eslint-disable no-console */
export const logger = {
group(title: string, body: string) {
console.groupCollapsed(title);
+13 -7
View File
@@ -231,18 +231,24 @@ export function FieldSet({ title, extra, children }: FieldSetProps) {
);
}
interface FormIdentifierProps {
interface FormHeaderProps {
title?: React.ReactNode;
children?: React.ReactNode;
subTitle?: React.ReactNode;
extra?: React.ReactNode;
}
export function FormIdentifier({ title, children }: FormIdentifierProps) {
export function FormHeader({ title, extra, subTitle }: FormHeaderProps) {
return (
<Box className="FormIdentifier" display="flex" alignItems="center">
<Box fontSize="16px" fontWeight="500" mr="s">
{title}
<Box className="FormHeader">
<Box display="flex" alignItems="center">
<Box flex="1" display="flex" alignItems="center">
<Box fontSize="16px" fontWeight="500" mr="s">
{title}
</Box>
{subTitle && <Box>{subTitle}</Box>}
</Box>
{extra && <Box>{extra}</Box>}
</Box>
<Box>{children}</Box>
</Box>
);
}
+24 -7
View File
@@ -8,14 +8,15 @@ import {
filterTreeData,
mapTreeData,
} from '@music163/tango-helpers';
import { Search, Tabs } from '@music163/tango-ui';
import { Action, Search, Tabs } from '@music163/tango-ui';
import { SettingFormItem, register } from './form-item';
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 { FormIdentifier } from './form-ui';
import { FormHeader } from './form-ui';
import { QuestionCircleOutlined } from '@ant-design/icons';
registerBuiltinSetters();
@@ -169,8 +170,8 @@ export function SettingForm({
}, [keyword, componentProps, tabKey, model, showGroups, componentPropGroups]);
const renderProps = useCallback(
(props: ComponentPropType[]) =>
props.map((item) => {
(list: ComponentPropType[]) =>
list.map((item) => {
const { getProp, ...rest } = item;
const childProp = {
...rest,
@@ -201,9 +202,25 @@ export function SettingForm({
<Box className="SettingFormHeader" position="sticky" top="0" bg="white" zIndex="2">
<Box px="l" py="m">
{showIdentifier && (
<FormIdentifier title={prototype.title}>
<SettingFormItem noStyle setter="idSetter" name={showIdentifier.identifierKey} />
</FormIdentifier>
<FormHeader
title={prototype.title}
subTitle={
<SettingFormItem
noStyle
setter="idSetter"
name={showIdentifier.identifierKey}
/>
}
extra={
prototype?.docs ? (
<Action
icon={<QuestionCircleOutlined />}
tooltip="查看组件文档"
href={prototype.docs}
/>
) : null
}
/>
)}
{showSearch && (
<Box className="SettingFormSearch" mt="m">
+1 -1
View File
@@ -10,7 +10,7 @@ export function splitToPath(name: string) {
}
function isNumericKey(key: string) {
return String(Number.parseInt(key)) === key;
return String(Number.parseInt(key, 10)) === key;
}
/** lodash.get(...) for mobx observables */
+1
View File
@@ -41,6 +41,7 @@
"@music163/tango-helpers": "^1.0.0-alpha.2",
"@uiw/react-codemirror": "^4.21.21",
"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",
+114
View File
@@ -0,0 +1,114 @@
import { Tooltip } from 'antd';
import React from 'react';
import { css, Button, Link, HTMLCoralProps } from 'coral-system';
import cx from 'classnames';
const actionStyle = css`
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
outline: none;
border: 0;
border-radius: var(--tango-radii-s);
color: var(--tango-colors-text2);
background-color: transparent;
&:hover {
background-color: var(--tango-colors-fill2);
}
&.small {
width: 24px;
height: 24px;
}
&.outline {
background-color: var(--tango-colors-fill1);
}
&.outline:hover {
background-color: var(--tango-colors-fill3);
}
&.disabled {
pointer-events: none;
color: var(--tango-colors-text4);
}
`;
export interface ActionProps extends HTMLCoralProps<any> {
/**
* 图标
*/
icon?: React.ReactNode;
/**
* 提示文本
*/
tooltip?: string;
/**
* 超链接
*/
href?: string;
/**
* 尺寸
*/
size?: 'small';
/**
* 外观:outline-有边框
*/
shape?: 'outline';
/**
* 是否禁用
*/
disabled?: boolean;
/**
* 自定义样式
*/
className?: string;
children?: React.ReactNode;
}
export function Action({
icon,
tooltip,
href,
size,
shape,
disabled,
className,
children,
...rest
}: ActionProps) {
const classNames = cx(
{
'tango-action': true,
'link-action': !!href,
small: size === 'small',
outline: shape === 'outline',
disabled,
},
className,
);
let ret;
if (href) {
ret = (
<Link className={classNames} css={actionStyle} href={href} isExternal {...rest}>
{icon ?? children}
</Link>
);
} else {
ret = (
<Button type="button" className={classNames} css={actionStyle} {...rest}>
{icon ?? children}
</Button>
);
}
if (tooltip) {
return <Tooltip title={tooltip}>{ret}</Tooltip>;
}
return ret;
}
+8 -7
View File
@@ -1,5 +1,6 @@
import React from 'react';
import { Box } from 'coral-system';
import { logger } from '@music163/tango-helpers';
interface ErrorBoundaryProps {
children: React.ReactNode;
@@ -10,20 +11,20 @@ interface ErrorBoundaryState {
}
export class ErrorBoundary extends React.Component<ErrorBoundaryProps, ErrorBoundaryState> {
static getDerivedStateFromError(error: any) {
// Update state so the next render will show the fallback UI.
logger.error(error);
return { hasError: true };
}
constructor(props: ErrorBoundaryProps) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error: any) {
// Update state so the next render will show the fallback UI.
console.log(error);
return { hasError: true };
}
componentDidCatch(error: any, errorInfo: any) {
// You can also log the error to an error reporting service
console.error(error, errorInfo);
logger.error(error, errorInfo);
}
render() {
-44
View File
@@ -1,44 +0,0 @@
import React from 'react';
import { Button, Link, css, HTMLCoralProps } from 'coral-system';
import { Tooltip } from 'antd';
const btnStyle = css`
&:hover {
color: var(--tango-colors-brand);
}
`;
export interface IconButtonProps extends HTMLCoralProps<'button'> {
/**
* 图标
*/
icon?: React.ReactNode;
/**
* 气泡提示
*/
tooltip?: string;
/**
* 点击跳转的链接地址
*/
href?: string;
}
export function IconButton({ tooltip, icon, href, children, ...rest }: IconButtonProps) {
const btn = href ? (
<Link href={href} target="_blank" color="text2" css={btnStyle} {...(rest as any)}>
{icon ?? children}
</Link>
) : (
<Button border="0" bg="transparent" color="text2" px="s" css={btnStyle} {...rest}>
{icon ?? children}
</Button>
);
if (tooltip) {
return (
<Tooltip title={tooltip} placement="topRight">
{btn}
</Tooltip>
);
}
return btn;
}
+2 -1
View File
@@ -1,4 +1,6 @@
export * from './icons';
export * from './action';
export { Action as IconButton } from './action'; // 兼容旧版本
export * from './action-select';
export * from './code-editor';
export * from './chat-input';
@@ -9,7 +11,6 @@ export * from './iconfont';
export * from './menu';
export * from './panel';
export * from './toggle-button';
export * from './icon-button';
export * from './input-code';
export * from './input-list';
export * from './search';
+9 -10
View File
@@ -29,13 +29,13 @@ const eslintConfig = {
};
function completeProperties(from: number, object: Object) {
const options = [];
for (const name in object) {
const options: object[] = [];
Object.entries(object).forEach(([key, val]) => {
options.push({
label: name,
type: typeof object[name] === 'function' ? 'function' : 'variable',
label: key,
type: typeof val === 'function' ? 'function' : 'variable',
});
}
});
return {
from,
options,
@@ -58,10 +58,10 @@ function buildAutoComplete(scope: any = window, customOptions: Array<{ label: st
if (
completePropertyAfter.includes(nodeBefore.name) &&
nodeBefore.parent?.name == 'MemberExpression'
nodeBefore.parent?.name === 'MemberExpression'
) {
const object = nodeBefore.parent.getChild('Expression');
if (object?.name == 'VariableName') {
if (object?.name === 'VariableName') {
const from = /\./.test(nodeBefore.name) ? nodeBefore.to : nodeBefore.from;
const variableName = context.state.sliceDoc(object.from, object.to);
if (typeof scope[variableName] === 'object') {
@@ -83,11 +83,11 @@ function buildAutoComplete(scope: any = window, customOptions: Array<{ label: st
return completeProperties(from, result);
}
}
} else if (nodeBefore.name == 'VariableName') {
} else if (nodeBefore.name === 'VariableName') {
return completeProperties(nodeBefore.from, scope);
} else if (context.explicit && !dontCompleteIn.includes(nodeBefore.name)) {
return completeProperties(context.pos, scope);
} else if (!context.explicit && nodeBefore.name == '{') {
} else if (!context.explicit && nodeBefore.name === '{') {
return {
from: context.pos,
options: customOptions,
@@ -155,7 +155,6 @@ export function InputCode({
}, [autoCompleteContext, autoCompleteOptions]);
const extensions = [javascript({ jsx: true }), globalJavaScriptCompletions];
if (enableESLint) {
// @ts-ignore
extensions.push(lintGutter(), linter(esLint(new eslint.Linter(), eslintConfig)));
}
const { rootStyle, codeSetup } = useInputCode({ shape, status, showLineNumbers, showFoldGutter });
+1
View File
@@ -6,6 +6,7 @@ import { DeleteOutlined, PlusOutlined } from '@ant-design/icons';
export interface InputListProps {
value?: string[];
onChange?: (value: string[]) => void;
// TODO: 支持 unique value
}
export function InputList({ value = [], onChange }: InputListProps) {
+3
View File
@@ -154,6 +154,9 @@ export interface ToggleButtonProps extends Omit<HTMLCoralProps<'button'>, 'type'
dropdownProps?: DropDownProps;
}
/**
* TODO: 使用 Action 重构
*/
export function ToggleButton(props: ToggleButtonProps) {
const {
size = 'm',
+5
View File
@@ -6760,6 +6760,11 @@ classnames@2.x, classnames@^2.2.1, classnames@^2.2.3, classnames@^2.2.5, classna
resolved "https://registry.npmmirror.com/classnames/-/classnames-2.3.2.tgz#351d813bf0137fcc6a76a16b88208d2560a0d924"
integrity sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==
classnames@^2.5.1:
version "2.5.1"
resolved "https://registry.npmmirror.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b"
integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==
clean-css@^4.2.3:
version "4.2.4"
resolved "https://registry.npmmirror.com/clean-css/-/clean-css-4.2.4.tgz#733bf46eba4e607c6891ea57c24a989356831178"