From 791fbb162a7147243924e01f54e9c0b586f14438 Mon Sep 17 00:00:00 2001 From: Wells Date: Fri, 17 May 2024 18:20:08 +0800 Subject: [PATCH] fix: enhance code value validate in SettingForm (#152) * 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 --- apps/storybook/src/setting-form.stories.tsx | 4 ++- packages/core/src/helpers/ast/parse.ts | 14 ++++++--- packages/core/tests/ast.test.ts | 30 +++++++++++++++++-- packages/designer/src/setters/index.ts | 23 ++++++++++++++ packages/designer/src/setting-panel.tsx | 6 +++- packages/setting-form/src/form-item.tsx | 26 ++++++++++++---- packages/setting-form/src/form-ui.tsx | 16 ++++++---- packages/setting-form/src/form.tsx | 1 + packages/setting-form/src/setters/register.ts | 11 +++++++ packages/setting-form/src/types.ts | 2 +- 10 files changed, 113 insertions(+), 20 deletions(-) diff --git a/apps/storybook/src/setting-form.stories.tsx b/apps/storybook/src/setting-form.stories.tsx index 48a7b901..b539d6cd 100644 --- a/apps/storybook/src/setting-form.stories.tsx +++ b/apps/storybook/src/setting-form.stories.tsx @@ -38,7 +38,7 @@ function SettingFormDemo({ initValues, prototype }: SettingFormDemoProps) { const model = new FormModel(initValues, { onChange: console.log }); return ( - + { it('isValidCode', () => { + expect(isValidCode('')).toBeTruthy(); + expect(isValidCode('1')).toBeTruthy(); + expect(isValidCode('"hello"')).toBeTruthy(); + expect(isValidCode('
hello
')).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(); @@ -22,8 +31,11 @@ describe('ast helpers', () => { expect(isValidExpressionCode('{ bizId: "vip", type: "category" }')).toBeTruthy(); expect(isValidExpressionCode('[1,2,3]')).toBeTruthy(); expect(isValidExpressionCode('
hello
')).toBeTruthy(); - expect(isValidExpressionCode('
hello
')).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(); @@ -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'); + }); }); diff --git a/packages/designer/src/setters/index.ts b/packages/designer/src/setters/index.ts index bd1f373a..53a64001 100644 --- a/packages/designer/src/setters/index.ts +++ b/packages/designer/src/setters/index.ts @@ -26,6 +26,24 @@ 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[] = [ { @@ -33,6 +51,7 @@ export const BUILT_IN_SETTERS: IFormItemCreateOptions[] = [ alias: ['expSetter', 'expressionSetter'], component: ExpressionSetter, type: 'code', + validate: codeValidate, }, { name: 'radioGroupSetter', @@ -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', @@ -108,6 +129,7 @@ export const BUILT_IN_SETTERS: IFormItemCreateOptions[] = [ name: 'renderPropsSetter', component: RenderSetter, type: 'code', + validate: codeValidate, }, { name: 'tableCellSetter', @@ -118,6 +140,7 @@ export const BUILT_IN_SETTERS: IFormItemCreateOptions[] = [ name: 'tableExpandableSetter', component: TableExpandableSetter, type: 'code', + validate: codeValidate, }, { name: 'routerSetter', diff --git a/packages/designer/src/setting-panel.tsx b/packages/designer/src/setting-panel.tsx index 88f9e706..1be927ed 100644 --- a/packages/designer/src/setting-panel.tsx +++ b/packages/designer/src/setting-panel.tsx @@ -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; diff --git a/packages/setting-form/src/form-item.tsx b/packages/setting-form/src/form-item.tsx index 5e0f17f5..ffe7e3cd 100644 --- a/packages/setting-form/src/form-item.tsx +++ b/packages/setting-form/src/form-item.tsx @@ -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; @@ -188,7 +193,7 @@ export function createFormItem(options: IFormItemCreateOptions) { extra, footer, noStyle, - validate, + validate = options.validate, }: FormItemProps) { const { disableSwitchExpressionSetter, showItemSubtitle } = useFormVariable(); const model = useFormModel(); @@ -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); @@ -296,14 +302,24 @@ export function createFormItem(options: IFormItemCreateOptions) { // 已注册的 setter 查找表 const REGISTERED_FORM_ITEM_MAP: Record> = {}; +/** + * 获取已注册的 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); }); } diff --git a/packages/setting-form/src/form-ui.tsx b/packages/setting-form/src/form-ui.tsx index 56b1f878..71ad05d3 100644 --- a/packages/setting-form/src/form-ui.tsx +++ b/packages/setting-form/src/form-ui.tsx @@ -249,14 +249,20 @@ interface FormHeaderProps { export function FormHeader({ title, extra, subTitle }: FormHeaderProps) { return ( - - - + + + {title} - {subTitle && {subTitle}} + {subTitle && {subTitle}} - {extra && {extra}} + {extra && {extra}} ); diff --git a/packages/setting-form/src/form.tsx b/packages/setting-form/src/form.tsx index 806d72f4..8d192296 100644 --- a/packages/setting-form/src/form.tsx +++ b/packages/setting-form/src/form.tsx @@ -235,6 +235,7 @@ export function SettingForm({ icon={} tooltip="查看组件文档" href={prototype.docs} + size="small" /> ) : null } diff --git a/packages/setting-form/src/setters/register.ts b/packages/setting-form/src/setters/register.ts index 6f432790..ae78c65a 100644 --- a/packages/setting-form/src/setters/register.ts +++ b/packages/setting-form/src/setters/register.ts @@ -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; } diff --git a/packages/setting-form/src/types.ts b/packages/setting-form/src/types.ts index 38bf1da2..1a1c2e8d 100644 --- a/packages/setting-form/src/types.ts +++ b/packages/setting-form/src/types.ts @@ -1,4 +1,4 @@ export interface ISetterOnChangeCallbackDetail { relatedImports?: string[]; - isRawCode?: boolean; + rawCode?: string; }