Skip to content

Commit

Permalink
fix: enhance code value validate in SettingForm (#152)
Browse files Browse the repository at this point in the history
* fix: update register setter strategy & add getSetter

* fix: handle invalid code to prop value

* fix: add validate logic to code setters

* fix: update code validate

* fix: update ui of settingForm header
  • Loading branch information
wwsun committed May 17, 2024
1 parent 704277b commit 791fbb1
Show file tree
Hide file tree
Showing 10 changed files with 113 additions and 20 deletions.
4 changes: 3 additions & 1 deletion apps/storybook/src/setting-form.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ function SettingFormDemo({ initValues, prototype }: SettingFormDemoProps) {
const model = new FormModel(initValues, { onChange: console.log });
return (
<Box display="flex">
<Box flex="0 0 400px" overflow="hidden">
<Box flex="0 0 320px" overflow="hidden">
<SettingForm
model={model}
prototype={prototype}
Expand All @@ -59,8 +59,10 @@ function SettingFormDemo({ initValues, prototype }: SettingFormDemoProps) {

const prototypeHasBasicProps: IComponentPrototype = {
name: 'Sample',
title: '演示组件',
package: 'sample-pkg',
type: 'element',
docs: 'https://4x-ant-design.antgroup.com/components/slider-cn',
props: [
{
name: 'code',
Expand Down
14 changes: 10 additions & 4 deletions packages/core/src/helpers/ast/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,7 @@ export function code2expression(code: string) {
try {
expNode = t.cloneNode(parseExpression(code, babelParserConfig), false, true);
} catch (err) {
logger.error('invalid code', err);
// expNode = t.identifier('undefined');
logger.error('code2expression failed, invalid code:', code);
}
return expNode;
}
Expand Down Expand Up @@ -194,7 +193,12 @@ export function code2jsxAttributeValueNode(code: string) {
return t.jsxExpressionContainer(code2expression(code));
}

// FIXME: 统一处理为 code2jsxAttributeValueNode
/**
* FIXME: 统一处理为 code2jsxAttributeValueNode
* 将 js value 转为 JSXAttributeValueNode
* @param value js value, or wrapped code
* @returns 返回 JSXAttributeValueNode,转换失败返回Node为 {undefined}
*/
export function value2jsxAttributeValueNode(value: any) {
let ret;
switch (typeof value) {
Expand All @@ -204,7 +208,8 @@ export function value2jsxAttributeValueNode(value: any) {
}
if (isWrappedCode(value)) {
const innerCode = getCodeOfWrappedCode(value);
ret = t.jsxExpressionContainer(code2expression(innerCode));
const node = code2expression(innerCode);
ret = t.jsxExpressionContainer(node || t.identifier('undefined'));
} else {
ret = t.stringLiteral(value);
}
Expand All @@ -217,6 +222,7 @@ export function value2jsxAttributeValueNode(value: any) {
return ret;
}

// TODO: 待校验
export function value2jsxChildrenValueNode(value: any) {
let ret: t.JSXElement | t.JSXFragment | t.JSXExpressionContainer | t.JSXSpreadChild | t.JSXText;
switch (typeof value) {
Expand Down
30 changes: 27 additions & 3 deletions packages/core/tests/ast.test.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,28 @@
import { Identifier } from '@babel/types';
import {
object2node,
serviceConfig2Node,
isValidCode,
isValidExpressionCode,
code2expression,
value2jsxAttributeValueNode,
} from '../src/helpers';

describe('ast helpers', () => {
it('isValidCode', () => {
expect(isValidCode('')).toBeTruthy();
expect(isValidCode('1')).toBeTruthy();
expect(isValidCode('"hello"')).toBeTruthy();
expect(isValidCode('<div>hello</div>')).toBeTruthy();
expect(isValidCode('function fn() {}')).toBeTruthy();

// invalid function body
expect(isValidCode('() => { hello world }')).toBeFalsy();
// invalid function
expect(isValidCode('function() {}')).toBeFalsy();
});

it('isValidExpression', () => {
expect(isValidExpressionCode('() => { }')).toBeTruthy();
it('isValidExpressionCode', () => {
expect(isValidExpressionCode('1')).toBeTruthy();
expect(isValidExpressionCode('a = 1')).toBeTruthy();
expect(isValidExpressionCode('1 + 1')).toBeTruthy();
Expand All @@ -22,8 +31,11 @@ describe('ast helpers', () => {
expect(isValidExpressionCode('{ bizId: "vip", type: "category" }')).toBeTruthy();
expect(isValidExpressionCode('[1,2,3]')).toBeTruthy();
expect(isValidExpressionCode('<div>hello</div>')).toBeTruthy();
expect(isValidExpressionCode('<div>hello</div>')).toBeTruthy();
expect(isValidExpressionCode('tango.stores.app.title')).toBeTruthy();
expect(isValidExpressionCode('"hello" + window.location.path')).toBeTruthy();
expect(isValidExpressionCode('() => { }')).toBeTruthy();

expect(isValidExpressionCode('')).toBeFalsy();
expect(isValidExpressionCode('{1}')).toBeFalsy();
expect(isValidExpressionCode('{"1"}')).toBeFalsy();
expect(isValidExpressionCode('{ 1+1 }')).toBeFalsy();
Expand Down Expand Up @@ -68,4 +80,16 @@ describe('ast helpers', () => {
`;
expect(code2expression(arrayCode).type).toEqual('ArrayExpression');
});

it('value2jsxAttributeValueNode', () => {
expect(value2jsxAttributeValueNode('hello').type).toBe('StringLiteral');
expect(value2jsxAttributeValueNode(true).type).toBe('JSXExpressionContainer');
expect(value2jsxAttributeValueNode(1).type).toBe('JSXExpressionContainer');
expect(value2jsxAttributeValueNode('{{{ foo: "foo"}}}').type).toBe('JSXExpressionContainer');

// invalid code will return undefined
const node: any = value2jsxAttributeValueNode('{{tango.xx+}}');
expect(node.type).toBe('JSXExpressionContainer');
expect((node.expression as Identifier).name).toBe('undefined');
});
});
23 changes: 23 additions & 0 deletions packages/designer/src/setters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,32 @@ import {
FlexDirectionSetter,
} from './style-setter';
import { ChoiceSetter } from './choice-setter';
import { isValidExpressionCode } from '@music163/tango-core';

const codeValidate: IFormItemCreateOptions['validate'] = (value, field) => {
if (!value) return;
const rawCode = field.detail.rawCode;
if (!rawCode) return;
return isValidExpressionCode(rawCode) ? '' : '请输入合法的 Javascript 代码片段';
};

const jsonValidate: IFormItemCreateOptions['validate'] = (value, field) => {
if (!value) return;
try {
JSON.parse(field.detail.rawCode);
return;
} catch (e) {
return '请输入合法的 JSON 字符串';
}
};

export const BUILT_IN_SETTERS: IFormItemCreateOptions[] = [
{
name: 'codeSetter',
alias: ['expSetter', 'expressionSetter'],
component: ExpressionSetter,
type: 'code',
validate: codeValidate,
},
{
name: 'radioGroupSetter',
Expand Down Expand Up @@ -81,11 +100,13 @@ export const BUILT_IN_SETTERS: IFormItemCreateOptions[] = [
name: 'jsonSetter',
component: JSONSetter,
type: 'code',
validate: jsonValidate,
},
{
name: 'jsxSetter',
component: JsxSetter,
type: 'code',
validate: codeValidate,
},
{
name: 'listSetter',
Expand All @@ -108,6 +129,7 @@ export const BUILT_IN_SETTERS: IFormItemCreateOptions[] = [
name: 'renderPropsSetter',
component: RenderSetter,
type: 'code',
validate: codeValidate,
},
{
name: 'tableCellSetter',
Expand All @@ -118,6 +140,7 @@ export const BUILT_IN_SETTERS: IFormItemCreateOptions[] = [
name: 'tableExpandableSetter',
component: TableExpandableSetter,
type: 'code',
validate: codeValidate,
},
{
name: 'routerSetter',
Expand Down
6 changes: 5 additions & 1 deletion packages/designer/src/setting-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@ import { clone, parseDndId } from '@music163/tango-helpers';
import { observer, useDesigner, useWorkspace } from '@music163/tango-context';
import { registerBuiltinSetters } from './setters';

registerBuiltinSetters();
let registered = false;
if (!registered) {
registerBuiltinSetters();
registered = true;
}

export interface SettingPanelProps extends SettingFormProps {
title?: React.ReactNode;
Expand Down
26 changes: 21 additions & 5 deletions packages/setting-form/src/form-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,12 @@ function parseFieldValue(fieldValue: any) {
const isCodeString = isString(fieldValue) && isWrappedCode(fieldValue);
if (isCodeString) {
code = getCodeOfWrappedCode(fieldValue);
value = code2value(code);
try {
// 避免 code 报错的情况
value = code2value(code);
} catch (err) {
// do nothing
}
} else {
code = value2code(fieldValue);
value = fieldValue;
Expand Down Expand Up @@ -188,7 +193,7 @@ export function createFormItem(options: IFormItemCreateOptions) {
extra,
footer,
noStyle,
validate,
validate = options.validate,
}: FormItemProps) {
const { disableSwitchExpressionSetter, showItemSubtitle } = useFormVariable();
const model = useFormModel();
Expand All @@ -202,14 +207,15 @@ export function createFormItem(options: IFormItemCreateOptions) {
});

field.setConfig({
validate: validate || options.validate,
validate: setter === 'codeSetter' ? getSetter('codeSetter').config.validate : validate,
});

let baseComponentProps: FormItemComponentProps = {
value: setterValue,
defaultValue,
onChange(value, detail) {
onChange(value, detail = {}) {
if ((setterType === 'code' || isCodeSetter) && isString(value) && value) {
detail.rawCode = value; // 在 detail 中记录原始的 code
value = wrapCode(value);
}
field.setValue(value, detail);
Expand Down Expand Up @@ -296,14 +302,24 @@ export function createFormItem(options: IFormItemCreateOptions) {
// 已注册的 setter 查找表
const REGISTERED_FORM_ITEM_MAP: Record<string, ReturnType<typeof createFormItem>> = {};

/**
* 获取已注册的 setter
* @param name
* @returns
*/
export function getSetter(name: string) {
return REGISTERED_FORM_ITEM_MAP[name];
}

/**
* Setter 注册
* @param config 注册选项
*/
export function register(config: IFormItemCreateOptions) {
// 允许直接覆盖同名 setter
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];
REGISTERED_FORM_ITEM_MAP[alias] = getSetter(config.name);
});
}

Expand Down
16 changes: 11 additions & 5 deletions packages/setting-form/src/form-ui.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -249,14 +249,20 @@ interface FormHeaderProps {
export function FormHeader({ title, extra, subTitle }: FormHeaderProps) {
return (
<Box className="FormHeader">
<Box display="flex" alignItems="center">
<Box flex="1" display="flex" alignItems="center">
<Box fontSize="16px" fontWeight="500" mr="s">
<Box display="flex" alignItems="center" className="FormHeaderMain">
<Box flex="1" display="flex" alignItems="center" className="FormHeaderMainBody">
<Box
fontSize="16px"
fontWeight="500"
mr="s"
whiteSpace="nowrap"
className="FormHeaderTitle"
>
{title}
</Box>
{subTitle && <Box>{subTitle}</Box>}
{subTitle && <Box className="FormHeaderSubTitle">{subTitle}</Box>}
</Box>
{extra && <Box>{extra}</Box>}
{extra && <Box className="FormHeaderExtra">{extra}</Box>}
</Box>
</Box>
);
Expand Down
1 change: 1 addition & 0 deletions packages/setting-form/src/form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ export function SettingForm({
icon={<QuestionCircleOutlined />}
tooltip="查看组件文档"
href={prototype.docs}
size="small"
/>
) : null
}
Expand Down
11 changes: 11 additions & 0 deletions packages/setting-form/src/setters/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,18 @@ const BASIC_SETTERS: IFormItemCreateOptions[] = [
},
];

let registered = false;

/**
* 注册内置的基础类型 Setter
*/
export function registerBuiltinSetters() {
if (registered) {
// 防止重复注册
return;
}

// 预注册基础 Setter
BASIC_SETTERS.forEach(register);
registered = true;
}
2 changes: 1 addition & 1 deletion packages/setting-form/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export interface ISetterOnChangeCallbackDetail {
relatedImports?: string[];
isRawCode?: boolean;
rawCode?: string;
}

0 comments on commit 791fbb1

Please sign in to comment.