fix: remove some useless setters

This commit is contained in:
wwsun
2023-09-12 17:21:49 +08:00
parent 443ae589f6
commit 7e9ab12503
10 changed files with 36 additions and 645 deletions

View File

@@ -6,6 +6,8 @@
"@typescript-eslint/no-unused-vars": "warn",
"import/no-cycle": "off",
"no-nested-ternary": "off",
"no-useless-return":"off"
"no-useless-return":"off",
"no-param-reassign": "off",
"prefer-destructuring": "off"
}
}

View File

@@ -12,4 +12,4 @@ export * from './sidebar';
export * from './toolbar';
export * from './selection-menu';
export { register as registerSetter } from '@music163/tango-setting-form';
export { register as registerSetter, FormItemComponentProps, FormItemCreateOptionsType } from '@music163/tango-setting-form';

View File

@@ -9,23 +9,27 @@ import { DndQuery, useDnd } from '../dnd';
import { Navigator } from './navigator';
import { SelectionToolsProps } from '../simulator/selection';
type SandboxEventHandlerConfig = {
interface ISandboxEventHandlerConfig {
sandboxQuery?: DndQuery;
sandboxType?: 'design' | 'preview';
isActive: boolean;
[x: string]: any;
};
}
export type SandboxProps = Omit<CodeSandboxProps, 'files' | 'eventHandlers' | 'onMessage'> & {
isPreview?: boolean;
selectionTools?: SelectionToolsProps['actions'];
builtinSelectionMenuMap?: SelectionToolsProps['builtinActionMap'];
/**
* tangoConfigJson 处理器
*/
configFormatter?: IMergeTangoConfigJsonConfig['formatter'];
sandboxType?: 'design' | 'preview';
mode?: 'single' | 'combined';
injectScript?: string;
onViewChange?: (data: any, config?: SandboxEventHandlerConfig) => void;
onMessage?: (data: any, config?: SandboxEventHandlerConfig) => void;
onLoad?: (config?: SandboxEventHandlerConfig) => void;
onViewChange?: (data: any, config?: ISandboxEventHandlerConfig) => void;
onMessage?: (data: any, config?: ISandboxEventHandlerConfig) => void;
onLoad?: (config?: ISandboxEventHandlerConfig) => void;
};
export type CombinedSandboxRef = {
@@ -37,6 +41,7 @@ const LANDING_PAGE_PATH = '/__background_landing_page__';
function useSandbox({
isPreview: isPreviewProp,
configFormatter,
onViewChange,
onMessage: onMessageProp,
onLoad: onLoadProp,
@@ -70,12 +75,12 @@ function useSandbox({
let files = Array.from(workspace.files.keys()).reduce((prev, filename) => {
let code = workspace.getFile(filename).code;
if (filename === '/tango.config.json') {
code = mergeTangoConfigJson(code, isPreview, { injectScript });
code = mergeTangoConfigJson(code, { isPreview, injectScript, formatter: configFormatter });
}
prev[filename] = { code };
return prev;
}, {});
files = normalizeFiles(files, workspace.entry);
files = fixSandboxFiles(files, workspace.entry);
const onMessage = (data: any) => onMessageProp && onMessageProp(data, getSandboxConfig());
const onLoad = () => onLoadProp && onLoadProp(getSandboxConfig());
@@ -188,7 +193,7 @@ export const CombinedSandbox = observer(
const activeSandbox = useRef<string>();
const [startRoute, setStartRoute] = useState(workspace.activeRoute);
const onViewChange = (data: any, config: SandboxEventHandlerConfig) => {
const onViewChange = (data: any, config: ISandboxEventHandlerConfig) => {
if (config.isActive) {
const curPath = data?.pathname + data?.search;
const isSandboxChanged = config.sandboxType !== activeSandbox.current;
@@ -296,7 +301,7 @@ export const Sandbox = observer(
: combinedSandboxRef.current?.designSandbox;
}
const onViewChange = (data: any, { isActive }: SandboxEventHandlerConfig) => {
const onViewChange = (data: any, { isActive }: ISandboxEventHandlerConfig) => {
if (isActive) {
navigatorRef.current.changeRelativeUrl(data?.pathname + data?.search);
}
@@ -380,7 +385,7 @@ export const Sandbox = observer(
);
// 兼容 tango.config.json转成 sandbox.config.json
function normalizeFiles(files: object, entry = '/src/index.js') {
function fixSandboxFiles(files: Record<string, { code: string }>, entry = '/src/index.js') {
if (files['/tango.config.json']) {
const tangConfigJsonStr = files['/tango.config.json'].code;
const tangConfigJson = JSON.parse(tangConfigJsonStr);
@@ -388,6 +393,7 @@ function normalizeFiles(files: object, entry = '/src/index.js') {
code: JSON.stringify(tangConfigJson.sandbox, null, 2),
};
}
if (!files['/index.html']) {
files['/index.html'] = {
code: `
@@ -407,28 +413,30 @@ function normalizeFiles(files: object, entry = '/src/index.js') {
`,
};
}
return files;
}
function mergeTangoConfigJson(code: string, isPreview: boolean, config?: { [x: string]: any }) {
interface IMergeTangoConfigJsonConfig {
isPreview?: boolean;
injectScript?: string;
formatter?: (json: object) => object;
}
function mergeTangoConfigJson(
code: string,
{ isPreview, injectScript, formatter }: IMergeTangoConfigJsonConfig = {},
) {
let json;
try {
json = JSON.parse(code);
} catch (err) {
logger.error(err);
logger.error('Json parse failed!', err);
return code;
}
const ox = getValue(json, 'dataSource.ox');
const userJs = getValue(json, 'sandbox.evaluateJavaScript') || '';
let mergedUserJs = userJs;
const { injectScript } = config || {};
if (ox) {
// TIP: 自动拼装 __tango_ox__ 注入到沙箱中
mergedUserJs = `window.__tango_ox__=${JSON.stringify(ox)};${mergedUserJs}`;
}
if (injectScript) {
mergedUserJs = `${mergedUserJs};${injectScript}`;
}
@@ -437,15 +445,6 @@ function mergeTangoConfigJson(code: string, isPreview: boolean, config?: { [x: s
setValue(json, 'sandbox.evaluateJavaScript', mergedUserJs);
}
const i18n = getValue(json, 'i18n');
if (i18n) {
// TIP: 合并 i18n 配置到沙箱配置中
setValue(json, 'sandbox.i18n', {
id: i18n.appId,
preModule: i18n.preModule,
});
}
// 合并 packages 内的信息至 sandbox
const packages = getValue(json, 'packages');
const externals = getValue(json, 'sandbox.externals') || {};
@@ -492,6 +491,8 @@ function mergeTangoConfigJson(code: string, isPreview: boolean, config?: { [x: s
setValue(json, 'sandbox.externals', externals);
setValue(json, 'sandbox.externalResources', [...new Set(externalResources)]);
json = formatter?.(json);
return JSON.stringify(json);
}

View File

@@ -5,6 +5,7 @@ import {
clone,
ComponentPropType,
isVariableString,
logger,
SetterOnChangeDetailType,
useBoolean,
} from '@music163/tango-helpers';
@@ -241,7 +242,7 @@ export function SettingFormItem(props: FormItemProps) {
*/
export function register(options: FormItemCreateOptionsType) {
if (SETTERS_DICT[options.name]) {
throw Error(`Duplicate setter name: <${options.name}>`);
logger.log(`Internal setter override: <${options.name}>`);
}
SETTERS_DICT[options.name] = createFormItem(options);
}

View File

@@ -1,44 +0,0 @@
import React from 'react';
import { css } from 'styled-components';
import { Box } from 'coral-system';
import { Select } from 'antd';
// import { IconPro } from '@ant-design/icons';
import { FormItemComponentProps } from '../form-item';
const acStyle = css`
.ant-select {
width: 100%;
}
`;
const ALL_ICON_NAMES: string[] = [];
/**
* Icon 组件设置器
* onChange 返回 `{<Icon type="iconName" />}`
*/
export function IconSetter({ onChange, ...rest }: FormItemComponentProps) {
const handleChange = (val: string) => {
onChange &&
onChange(val, {
relatedImports: ['Icon'],
});
};
return (
<Box css={acStyle}>
<Select showSearch allowClear placeholder="请选择图标" onChange={handleChange} {...rest}>
{ALL_ICON_NAMES.map((icon) => {
const key = `{<Icon type="${icon}" />}`;
return (
<Select.Option key={key}>
<Box display="inline-block" mr="m">
{/* <IconPro type={icon as any} size="14px" /> */}
</Box>
{icon}
</Select.Option>
);
})}
</Select>
</Box>
);
}

View File

@@ -1,148 +0,0 @@
import React, { useEffect, useState } from 'react';
import { Button, Empty, Modal, Pagination, Radio } from 'antd';
import { Box, css, Grid, GridItem, Text } from 'coral-system';
import { isFunction, useBoolean } from '@music163/tango-helpers';
import { FormItemComponentProps } from '../form-item';
import { useFormVariable } from '../context';
const wrapperStyle = css`
img {
max-width: 100%;
max-height: 150px;
}
`;
export function ImageSetter({ value, onChange }: FormItemComponentProps) {
const [visible, { on, off }] = useBoolean();
const label = value ? '更新图片' : '上传图片';
const { remoteServices } = useFormVariable();
return (
<Box css={wrapperStyle}>
<Box border="solid" borderColor="line.normal" textAlign="center" mb="m">
{value ? <img src={value} alt="preview image" /> : null}
</Box>
<Button block onClick={on}>
{label}
</Button>
<Modal title={label} visible={visible} onCancel={off} width="60%">
<ImageCenter
imageService={remoteServices?.ImageService}
onSelect={(url) => {
onChange(url);
off();
}}
/>
</Modal>
</Box>
);
}
const categories = [
{ label: '我的素材', value: 'listMy' },
{ label: '我的收藏', value: 'listFav' },
{ label: '公共素材', value: 'listPub' },
];
interface ImageCenterProps {
onSelect?: ImageListProps['onSelect'];
imageService?: Record<string, (...args: any[]) => Promise<any>>;
}
const pageSize = 20;
function ImageCenter({ imageService, onSelect }: ImageCenterProps) {
const [cate, setCate] = useState('listMy');
const [page, setPage] = useState(1);
const [data, setData] = useState<any>({});
useEffect(() => {
imageService?.[cate]?.({ limit: pageSize, offset: (page - 1) * pageSize }).then((res: any) => {
setData(res || {});
});
}, [imageService, cate, page]);
return (
<Box>
<Box display="flex" justifyContent="space-between">
<Radio.Group
value={cate}
onChange={(e) => {
setCate(e.target.value);
}}
options={categories}
optionType="button"
buttonStyle="solid"
/>
<Button
type="primary"
href="https://music-fn.hz.netease.com/s/music-deer-web/user/image/myupload"
target="_blank"
>
</Button>
</Box>
{data.list?.length ? (
<ImageList dataSource={data.list} onSelect={onSelect} />
) : (
<Empty description="没有数据或数据服务请求出错" />
)}
{data.count > pageSize ? (
<Pagination current={page} total={data.count} pageSize={pageSize} onChange={setPage} />
) : null}
</Box>
);
}
const imageListWrapper = css`
img {
max-width: 100%;
max-height: 120px;
}
button {
border-color: transparent;
outline: none;
padding: 0;
margin: 0;
&:hover {
border-color: var(--tango-colors-brand);
}
}
`;
interface ImageListProps {
dataSource?: any[];
onSelect?: (url: string) => void;
}
function ImageList({ dataSource = [], onSelect }: ImageListProps) {
return (
<Grid my="m" columns={4} gap="12px" height="400px" overflow="auto" css={imageListWrapper}>
{dataSource.map((item) => (
<GridItem
key={item.id}
display="flex"
alignItems="center"
justifyContent="center"
bg="gray.20"
>
<Box
as="button"
display="inline-flex"
flexDirection="column"
alignItems="center"
gap="m"
bg="transparent"
onClick={() => {
isFunction(onSelect) && onSelect(item.url);
}}
>
<img src={item.url} />
<Text fontSize="12px" color="text.note">
{item.name}
</Text>
</Box>
</GridItem>
))}
</Grid>
);
}

View File

@@ -5,7 +5,6 @@ import { PickerSetter } from './picker-setter';
import { ChoiceSetter } from './choice-setter';
import { FormItemCreateOptionsType } from '../form-item';
import { ListSetter } from './list-setter';
import { IconSetter } from './icon-setter';
import { ColumnSetter } from './column-setter';
import { NumberSetter, SliderSetter } from './number-setter';
import { ExpressionSetter, expressionValueValidate, jsonValueValidate } from './expression-setter';
@@ -25,16 +24,8 @@ import {
FlexDirectionSetter,
} from './style-setter';
import { CssSetter } from './css-setter';
import { ListenerSetter } from './listener-setter';
import { RuleSetter } from './rule-setter';
import { ModelSetter } from './model-setter';
import {
RenderSetter,
TableCellSetter,
TableExpandableSetter,
XToolbarSetter,
} from './render-props-setter';
import { ImageSetter } from './image-setter';
import { RenderSetter, TableCellSetter, TableExpandableSetter } from './render-props-setter';
import { RouterSetter } from './router-setter';
import { BoolSetter } from './bool-setter';
@@ -122,10 +113,6 @@ export const INTERNAL_SETTERS: FormItemCreateOptionsType[] = [
name: 'choiceSetter',
component: ChoiceSetter,
},
{
name: 'iconSetter',
component: IconSetter,
},
{
name: 'optionSetter',
component: OptionSetter,
@@ -181,14 +168,6 @@ export const INTERNAL_SETTERS: FormItemCreateOptionsType[] = [
name: 'columnSetter',
component: ColumnSetter,
},
{
name: 'listenerSetter',
component: ListenerSetter,
},
{
name: 'ruleSetter',
component: RuleSetter,
},
{
name: 'renderPropsSetter',
component: RenderSetter,
@@ -201,14 +180,6 @@ export const INTERNAL_SETTERS: FormItemCreateOptionsType[] = [
name: 'tableExpandableSetter',
component: TableExpandableSetter,
},
{
name: 'xtoolbarSetter',
component: XToolbarSetter,
},
{
name: 'imageSetter',
component: ImageSetter,
},
{
name: 'routerSetter',
component: RouterSetter,

View File

@@ -1,203 +0,0 @@
import React, { FunctionComponent, useState } from 'react';
import { Box } from 'coral-system';
import { Select, Input, Tabs, Button } from 'antd';
import { FieldStringOutlined } from '@ant-design/icons';
import { SingleMonacoEditor } from '@music163/tango-ui';
import { FormItemComponentProps } from '../form-item';
import { useFormVariable } from '../context';
import { ListSetter } from './list-setter';
const { TabPane } = Tabs;
// TODO: 需考虑 option选项联动 & 组件属性联动等面板
const CustomComponent: FunctionComponent<any> = ({ value = {}, onChange }) => {
// FIXME: 这里之前判断逻辑有问题,导致每次 value 变更都执行 parseExpression
const [tabIndex, setTabIndex] = useState(typeof value === 'string' ? '2' : '1');
const triggerChange = (changedValue: any) => {
onChange({
...value,
...changedValue,
});
};
const customListenerDefaultValue = `/* {(valid, field, form) => {
form.fieldMap.input1.setValue("联动改变值");
}}*/`;
return (
<Tabs type="card" activeKey={tabIndex} onChange={setTabIndex}>
<TabPane tab="常用配置" key="1">
<Box display="flex" flexDirection="column" gap="20px">
<Select
value={value?.status}
onChange={(v) => {
triggerChange({
status: v,
});
}}
placeholder="UI状态"
options={[
{
label: '编辑',
value: 'edit',
},
{
label: '禁用',
value: 'disabled',
},
{
label: '隐藏',
value: 'hidden',
},
{
label: '预览',
value: 'preview',
},
]}
/>
<Select
value={value?.ui?.required}
onChange={(v) => {
triggerChange({
ui: {
...value?.ui,
required: v,
},
});
}}
placeholder="必填"
options={[
{
label: '必填',
value: true,
},
{
label: '不必填',
value: false,
},
]}
/>
<Input
placeholder="设置label标题"
value={value?.ui?.label}
onChange={(e) => {
triggerChange({
ui: {
...value?.ui,
label: e.target.value,
},
});
}}
/>
<Input
placeholder="设置字段值"
value={value?.value}
onChange={(e) => {
triggerChange({
value: e.target.value,
});
}}
/>
</Box>
</TabPane>
<TabPane tab="自定义配置" key="2">
<SingleMonacoEditor
defaultValue={customListenerDefaultValue}
value={
Object.prototype.toString.call(value) === '[object Object]'
? customListenerDefaultValue
: value
}
onChange={(v) => onChange(v.trim())}
hasBorder
height="150px"
options={{
lineNumbers: 'off',
fontSize: 12,
wordWrap: 'on',
minimap: {
enabled: false,
},
}}
/>
</TabPane>
</Tabs>
);
};
const renderSetterItem = (item: any) => {
return (
<>
<FieldStringOutlined />
<Box display="inline-block" ml="m">
{item.title}
</Box>
</>
);
};
/**
* XFormItem响应器
*/
export const ListenerSetter = (props: FormItemComponentProps<any[]>) => {
const { formFieldsOptions } = useFormVariable();
return (
<ListSetter
getListItemKey={(item) => item.key || item.dataIndex}
addBtnText="配置响应器"
listItemFormFields={[
{
label: '来源字段',
name: 'watch',
required: true,
component: <Select mode="tags" options={formFieldsOptions} />,
extra: <span></span>,
},
{
label: '触发条件',
name: 'condition',
required: true,
component: (
<SingleMonacoEditor
hasBorder
height="50px"
defaultValue="// input1.value === '123'"
options={{
lineNumbers: 'off',
fontSize: 12,
wordWrap: 'on',
minimap: {
enabled: false,
},
}}
/>
),
extra: <span></span>,
},
{
label: '响应行为',
name: 'set',
required: true,
component: <CustomComponent />,
width: '400px',
extra: (
<Button
type="link"
onClick={() =>
window.open(
'https://music-cms.hz.netease.com/xform-docs/docs/tutorial-basics/listeners#set-%E4%B8%BA-function',
)
}
>
</Button>
),
},
]}
newItemDefaultValues={[]}
renderItem={renderSetterItem}
{...props}
/>
);
};

View File

@@ -97,16 +97,6 @@ const tableExpandableOptions: RenderSetterProps['options'] = [
{ label: '取消可展开行', value: '' },
];
const xtoolbarOptions: RenderSetterProps['options'] = [
{
label: '配置左侧区域',
value: 'Box',
render: getRender('<Box display="flex"></Box>'),
relatedImports: ['Box'],
},
{ label: '取消左侧区域', value: undefined },
];
export function TableCellSetter(props: FormItemComponentProps) {
return <RenderSetter options={tableCellOptions} {...props} />;
}
@@ -114,7 +104,3 @@ export function TableCellSetter(props: FormItemComponentProps) {
export function TableExpandableSetter(props: FormItemComponentProps) {
return <RenderSetter options={tableExpandableOptions} text="配置表格可展开行" {...props} />;
}
export function XToolbarSetter(props: FormItemComponentProps) {
return <RenderSetter options={xtoolbarOptions} text="配置工具栏左侧区域" {...props} />;
}

View File

@@ -1,175 +0,0 @@
import React from 'react';
import { Box } from 'coral-system';
import { AutoComplete, Button, Select, Switch } from 'antd';
import { FieldStringOutlined } from '@ant-design/icons';
import { SingleMonacoEditor } from '@music163/tango-ui';
import { FormItemComponentProps } from '../form-item';
import { ListSetter, NewOptionFormFieldType } from './list-setter';
// 常用正则
const enumPattern = [
{
label: '纯数字',
value: '^[0-9]*$',
},
{
label: '纯汉字',
value: '^[\u4e00-\u9fa5]{0,}$',
},
{
label: '手机号',
value: '^[1][3-8][0-9]{9}$',
},
{
label: 'IP地址',
value: 'd+.d+.d+.d+',
},
{
label: '身份证号',
value: '^d{15}|d{18}$',
},
{
label: '邮政编码',
value: '[1-9]d{5}(?!d)',
},
{
label: '图片URL',
value: '(https?:[^:<>"]*/)([^:<>"]*)(.((png!thumbnail)|(png)|(jpg)|(webp)|(gif)))',
},
];
// async-validator type
// https://github.com/yiminghe/async-validator#type
const type = [
'string', // // Must be of type string. This is the default type.
// 'number', Input TextArea出来的value都是string / InputNumber是number。不在async-validator做数字类型的校验
'boolean', // Must be of type boolean.
'regexp', // Must be an instance of RegExp or a string that does not generate an exception when creating a new RegExp.
'integer', // Must be of type number and an integer.
'float', // Must be of type number and a floating point number.
'array', // Must be an array as determined by Array.isArray.
'object', // Must be of type object and not Array.isArray.
'date', // Value // Must be valid as determined by Date
'url', // Must be of type url.
'email', // Must be of type email.
];
const trigger = ['change', 'submit'];
const status = ['error', 'warning'];
/**
* 正则输入框
*/
const PatternInput: React.FunctionComponent<any> = (props) => {
return (
<>
<AutoComplete options={enumPattern} onSelect={props.onChange} {...props} />
</>
);
};
/**
* 必填选择
*/
const RequiredInput: React.FunctionComponent<any> = (props) => {
return (
<>
<Switch onChange={props.onChange} checked={props.value} {...props} />
</>
);
};
const optionFormFields: NewOptionFormFieldType[] = [
{
label: '字段类型',
name: 'type',
required: true,
component: <Select options={type.map((t) => ({ label: t, value: t }))} />,
},
{
label: '是否必填',
name: 'required',
component: <RequiredInput />,
},
{
label: '触发方式',
name: 'trigger',
component: <Select options={trigger.map((t) => ({ label: t, value: t }))} />,
},
{
label: '触发状态',
name: 'status',
component: <Select options={status.map((t) => ({ label: t, value: t }))} />,
},
{
label: '错误提示语',
name: 'message',
extra: '自定义错误提示',
},
{
label: '正则表达式',
name: 'pattern',
extra: '复杂场景,可以自定义正则。',
component: <PatternInput placeholder="例如: ^[0-9]*$" />,
},
{
label: '自定义校验',
name: 'validator',
width: '400px',
extra: (
<Button
type="link"
onClick={() => window.open('https://github.com/yiminghe/async-validator#validator')}
>
</Button>
),
component: (
<SingleMonacoEditor
hasBorder
height="108px"
defaultValue="/** {(rule, value, callback) => { return value === 'test';}}*/"
options={{
lineNumbers: 'off',
fontSize: 12,
wordWrap: 'on',
minimap: {
enabled: false,
},
}}
/>
),
},
];
const renderSetterItem = (item: any) => {
return (
<>
<FieldStringOutlined />
<Box display="inline-block" ml="m">
{item.title}
</Box>
</>
);
};
/**
* 校验属性配置
*/
export function RuleSetter(props: FormItemComponentProps<any[]>) {
return (
<ListSetter
getListItemKey={(item) => item.key || item.dataIndex}
addBtnText="配置校验器"
listItemFormFields={optionFormFields}
newItemDefaultValues={{
type: 'string',
trigger: 'change',
status: 'error',
}}
renderItem={renderSetterItem}
{...props}
/>
);
}