From dc76d8b5503a1f3649bd525cff59e6758b145655 Mon Sep 17 00:00:00 2001 From: MrWindlike Date: Mon, 11 Jul 2022 13:58:45 +0800 Subject: [PATCH 01/36] feat: refactor the validation trait --- .../runtime/src/traits/core/Validation.tsx | 434 ++++++++++++++---- 1 file changed, 353 insertions(+), 81 deletions(-) diff --git a/packages/runtime/src/traits/core/Validation.tsx b/packages/runtime/src/traits/core/Validation.tsx index 2960c9f7..5e98854f 100644 --- a/packages/runtime/src/traits/core/Validation.tsx +++ b/packages/runtime/src/traits/core/Validation.tsx @@ -1,112 +1,384 @@ -import { Static, Type } from '@sinclair/typebox'; -import { isEqual } from 'lodash'; +import { Type, Static, TSchema } from '@sinclair/typebox'; import { implementRuntimeTrait } from '../../utils/buildKit'; -import { CORE_VERSION, CoreTraitName } from '@sunmao-ui/shared'; +import { CORE_VERSION, CoreTraitName, StringUnion } from '@sunmao-ui/shared'; -type ValidationResult = Static; -type ValidationRule = (text: string) => ValidationResult; +type ParseValidateOption< + T extends Record, + OptionalKeys extends keyof T = '' +> = { + [K in keyof T as K extends OptionalKeys ? never : K]: Static; +} & { + [K in OptionalKeys]?: Static; +}; +type UnionToIntersection = ( + TUnion extends unknown ? (params: TUnion) => unknown : never +) extends (params: infer Params) => unknown + ? Params + : never; -const ResultSpec = Type.Object({ - isInvalid: Type.Boolean(), - errorMsg: Type.String(), -}); +const validateOptionMap = { + length: { + minLength: Type.Number({ title: 'Min length' }), + maxLength: Type.Number({ title: 'Max length' }), + }, + include: { + includeList: Type.Array(Type.String(), { title: 'Include list' }), + }, + exclude: { + excludeList: Type.Array(Type.String(), { title: 'Exclude List' }), + }, + number: { + min: Type.Number({ title: 'Min' }), + max: Type.Number({ title: 'Max' }), + }, + regex: { + regex: Type.String({ title: 'Regex', description: 'The regular expression string.' }), + flags: Type.String({ + title: 'Flags', + description: 'The flags of the regular expression.', + }), + }, +}; +const validateFnMap = { + required(value: string) { + return value !== undefined && value !== null && value !== ''; + }, + length( + value: string, + { + minLength, + maxLength, + }: ParseValidateOption + ) { + if (minLength !== undefined && value.length < minLength) { + return false; + } -export const ValidationTraitStateSpec = Type.Object({ - validResult: ResultSpec, + if (maxLength !== undefined && value.length > maxLength) { + return false; + } + + return true; + }, + include( + value: string, + { includeList }: ParseValidateOption + ) { + return includeList.includes(value); + }, + exclude( + value: string, + { excludeList }: ParseValidateOption + ) { + return !excludeList.includes(value); + }, + regex( + value: string, + { regex, flags }: ParseValidateOption + ) { + return new RegExp(regex, flags).test(value); + }, + number( + value: string | number, + { min, max }: ParseValidateOption + ) { + const num = Number(value); + + if (min !== undefined && num < min) { + return false; + } + + if (max !== undefined && num > max) { + return false; + } + + return true; + }, + ipv4(value: string) { + return /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/.test( + value + ); + }, + email(value: string) { + return /^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/.test(value); + }, + url(value: string) { + return /^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_+.~#?&/=]*)$/.test( + value + ); + }, +}; + +type AllFields = UnionToIntersection< + typeof validateOptionMap[keyof typeof validateOptionMap] +>; +type AllFieldsKeys = keyof UnionToIntersection; + +const ValidatorSpec = Type.Object({ + name: Type.String({ + title: 'Name', + description: 'The name is used for getting the validated result.', + }), + value: Type.Any({ title: 'Value', description: 'The value need to be validated.' }), + rules: Type.Array( + Type.Object( + { + type: StringUnion( + Object.keys(validateFnMap).concat(['custom']) as [ + keyof typeof validateFnMap, + 'custom' + ], + { + type: 'Type', + description: + 'The type of the rule. Setting it as `custom` to use custom validate function.', + } + ), + validate: + Type.Any({ + title: 'Validate', + description: + 'The validate function for the rule. Return `false` means it is invalid.', + conditions: [ + { + key: 'type', + value: 'custom', + }, + ], + }) || + Type.Function( + [Type.String(), Type.Record(Type.String(), Type.Any())], + Type.Boolean(), + { + conditions: [ + { + key: 'type', + value: 'custom', + }, + ], + } + ), + error: Type.Object( + { + message: Type.String({ + title: 'Message', + description: 'The message to display when the value is invalid.', + }), + }, + { title: 'Error' } + ), + ...(Object.keys(validateOptionMap) as [keyof typeof validateOptionMap]).reduce( + (result, key) => { + const option = validateOptionMap[key] as AllFields; + + (Object.keys(option) as [AllFieldsKeys]).forEach(optionKey => { + if (result[optionKey]) { + // if the different validate functions have the same parameter + throw Error( + "[Validation Trait]: The different validate function has the same parameter, please change the parameter's name." + ); + } else { + result[optionKey] = { + ...option[optionKey], + conditions: [{ key: 'type', value: key }], + } as any; + } + }); + + return result; + }, + {} as AllFields + ), + customOptions: Type.Record(Type.String(), Type.Any(), { + title: 'Custom options', + description: + 'The custom options would pass to the custom validate function as the second parameter.', + conditions: [ + { + key: 'type', + value: 'custom', + }, + ], + }), + }, + { + title: 'Rules', + } + ), + { + title: 'Rules', + widget: 'core/v1/array', + widgetOptions: { displayedKeys: ['type'] }, + } + ), }); export const ValidationTraitPropertiesSpec = Type.Object({ - value: Type.String(), - rule: Type.Optional(Type.String()), - maxLength: Type.Optional(Type.Integer()), - minLength: Type.Optional(Type.Integer()), + validators: Type.Array(ValidatorSpec, { + title: 'Validators', + widget: 'core/v1/array', + widgetOptions: { displayedKeys: ['name'] }, + }), +}); + +const ErrorSpec = Type.Object({ + message: Type.String(), +}); + +const ValidatedResultSpec = Type.Record( + Type.String(), + Type.Object({ + isInvalid: Type.Boolean(), + errors: Type.Array(ErrorSpec), + }) +); + +export const ValidationTraitStateSpec = Type.Object({ + validatedResult: ValidatedResultSpec, }); export default implementRuntimeTrait({ version: CORE_VERSION, metadata: { name: CoreTraitName.Validation, - description: 'validation trait', + description: 'A trait for the form validation.', }, spec: { properties: ValidationTraitPropertiesSpec, state: ValidationTraitStateSpec, - methods: [], + methods: [ + { + name: 'setErrors', + parameters: Type.Object({ + errorsMap: Type.Record(Type.String(), Type.Array(ErrorSpec)), + }), + }, + { + name: 'validateFields', + parameters: Type.Object({ + names: Type.Array(Type.String()), + }), + }, + { + name: 'validateAllFields', + parameters: Type.Object({}), + }, + { + name: 'clearErrors', + parameters: Type.Object({ names: Type.Array(Type.String()) }), + }, + { + name: 'clearAllErrors', + parameters: Type.Object({}), + }, + ], }, })(() => { - const rules = new Map(); - - function addValidationRule(name: string, rule: ValidationRule) { - rules.set(name, rule); - } - - addValidationRule('email', text => { - if (/^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/.test(text)) { - return { - isInvalid: false, - errorMsg: '', - }; - } else { - return { - isInvalid: true, - errorMsg: 'Please enter valid email.', - }; - } - }); - - addValidationRule('phoneNumber', text => { - if (/^1[3456789]\d{9}$/.test(text)) { - return { - isInvalid: false, - errorMsg: '', - }; - } else { - return { - isInvalid: true, - errorMsg: 'Please enter valid phone number.', - }; - } - }); - const ValidationResultCache: Record = {}; + const initialMap = new Map(); return props => { - const { value, minLength, maxLength, mergeState, componentId, rule } = props; + const { validators, componentId, subscribeMethods, mergeState } = props; + const validatorMap = validators.reduce((result, validator) => { + result[validator.name] = validator; - const result: ValidationResult = { - isInvalid: false, - errorMsg: '', - }; + return result; + }, {} as Record>); - if (maxLength !== undefined && value.length > maxLength) { - result.isInvalid = true; - result.errorMsg = `Can not be longer than ${maxLength}.`; - } else if (minLength !== undefined && value.length < minLength) { - result.isInvalid = true; - result.errorMsg = `Can not be shorter than ${minLength}.`; - } else { - const rulesArr = rule ? rule.split(',') : []; - for (const ruleName of rulesArr) { - const validateFunc = rules.get(ruleName); - if (validateFunc) { - const { isInvalid, errorMsg } = validateFunc(value); - if (isInvalid) { - result.isInvalid = true; - result.errorMsg = errorMsg; - break; - } - } - } + function setErrors({ + errorsMap, + }: { + errorsMap: Record[]>; + }) { + const validatedResult = Object.keys(errorsMap).reduce( + (result: Static, name) => { + result[name] = { + isInvalid: errorsMap[name].length !== 0, + errors: errorsMap[name], + }; + + return result; + }, + {} + ); + + mergeState({ + validatedResult, + }); + } + function validateFields({ names }: { names: string[] }) { + const validatedResult = names + .map(name => { + const validator = validatorMap[name]; + const { value, rules } = validator; + const errors = rules + .map(rule => { + const { type, error, validate, customOptions, ...options } = rule; + let isValid = true; + + if (type === 'custom') { + isValid = validate(value, customOptions); + } else { + isValid = validateFnMap[type](value, options); + } + + return isValid ? null : { message: error.message }; + }) + .filter((error): error is Static => error !== null); + + return { + name, + isInvalid: errors.length !== 0, + errors, + }; + }) + .reduce((result: Static, validatedResultItem) => { + result[validatedResultItem.name] = { + isInvalid: validatedResultItem.isInvalid, + errors: validatedResultItem.errors, + }; + + return result; + }, {}); + + mergeState({ validatedResult }); + } + function validateAllFields() { + validateFields({ names: validators.map(({ name }) => name) }); + } + function clearErrors({ names }: { names: string[] }) { + setErrors({ + errorsMap: names.reduce((result: Record, name) => { + result[name] = []; + + return result; + }, {}), + }); + } + function clearAllErrors() { + clearErrors({ names: validators.map(({ name }) => name) }); } - if (!isEqual(result, ValidationResultCache[componentId])) { - ValidationResultCache[componentId] = result; - mergeState({ - validResult: result, - }); + subscribeMethods({ + setErrors, + validateFields, + validateAllFields, + clearErrors, + clearAllErrors, + }); + + if (initialMap.has(componentId) === false) { + clearAllErrors(); + initialMap.set(componentId, true); } return { - props: null, + props: { + componentDidUnmount: [ + () => { + initialMap.delete(componentId); + }, + ], + }, }; }; }); From 7e5242d37da4cb70a3738f5fb0e7e3a40e37ac67 Mon Sep 17 00:00:00 2001 From: MrWindlike Date: Mon, 11 Jul 2022 13:59:34 +0800 Subject: [PATCH 02/36] refactor: change and add the validation trait examples --- examples/form/basic.json | 27 +- examples/form/formInDialog.json | 19 +- examples/form/formValidation.json | 855 ++++++++++++++++++ .../input-components/inputValidation.json | 57 +- examples/table/tableWithForm.json | 19 +- packages/runtime/index.html | 6 +- 6 files changed, 962 insertions(+), 21 deletions(-) create mode 100644 examples/form/formValidation.json diff --git a/examples/form/basic.json b/examples/form/basic.json index cbdf3a2a..b8011c33 100644 --- a/examples/form/basic.json +++ b/examples/form/basic.json @@ -139,10 +139,29 @@ { "type": "core/v1/validation", "properties": { - "value": "{{ phoneInput.value || \"\" }}", - "maxLength": 100, - "minLength": 0, - "rule": "phoneNumber" + "validators": [ + { + "name": "phone", + "value": "{{ phoneInput.value || \"\" }}", + "rules": [ + { + "type": "regex", + "regex": "^1[3456789]\\d{9}$", + "error": { + "message": "Please input the correct phone number." + } + }, + { + "type": "length", + "minLength": 0, + "maxLength": 100, + "error": { + "message": "Please input the length between 0 and 100." + } + } + ] + } + ] } } ] diff --git a/examples/form/formInDialog.json b/examples/form/formInDialog.json index ec686653..d9eaa1ad 100644 --- a/examples/form/formInDialog.json +++ b/examples/form/formInDialog.json @@ -314,9 +314,22 @@ { "type": "core/v1/validation", "properties": { - "value": "{{ nameInput.value || \"\" }}", - "maxLength": 10, - "minLength": 2 + "validators": [ + { + "name": "name", + "value": "{{ nameInput.value || \"\" }}", + "rules": [ + { + "type": "length", + "maxLength": 10, + "minLength": 2, + "error": { + "message": "Please input the length between 2 and 10." + } + } + ] + } + ] } } ] diff --git a/examples/form/formValidation.json b/examples/form/formValidation.json new file mode 100644 index 00000000..da07f238 --- /dev/null +++ b/examples/form/formValidation.json @@ -0,0 +1,855 @@ +{ + "app": { + "version": "sunmao/v1", + "kind": "Application", + "metadata": { + "name": "some App" + }, + "spec": { + "components": [ + { + "id": "name_form", + "type": "arco/v1/formControl", + "properties": { + "label": { + "format": "plain", + "raw": "name" + }, + "layout": "horizontal", + "required": true, + "hidden": false, + "extra": "", + "errorMsg": "{{name_form.validatedResult.name?.errors[0]?.message || '';}}", + "labelAlign": "left", + "colon": false, + "help": "", + "labelCol": { + "span": 3, + "offset": 0 + }, + "wrapperCol": { + "span": 21, + "offset": 0 + } + }, + "traits": [ + { + "type": "core/v1/validation", + "properties": { + "validators": [ + { + "name": "name", + "value": "{{name_input.value;}}", + "rules": [ + { + "type": "required", + "validate": null, + "error": { + "message": "Please input the name." + }, + "minLength": 0, + "maxLength": 0, + "list": [], + "min": 0, + "max": 0, + "regex": "", + "flags": "", + "customOptions": {} + }, + { + "type": "length", + "validate": null, + "error": { + "message": "The name is limited in length to between 1 and 10." + }, + "minLength": 1, + "maxLength": 10, + "list": [], + "min": 0, + "max": 0, + "regex": "", + "flags": "", + "customOptions": {} + } + ] + } + ] + } + } + ] + }, + { + "id": "name_input", + "type": "arco/v1/input", + "properties": { + "allowClear": false, + "disabled": false, + "readOnly": false, + "defaultValue": "", + "updateWhenDefaultValueChanges": false, + "placeholder": "please input the name.", + "error": "{{name_form.validatedResult.name.isInvalid}}", + "size": "default" + }, + "traits": [ + { + "type": "core/v1/slot", + "properties": { + "container": { + "id": "name_form", + "slot": "content" + }, + "ifCondition": true + } + }, + { + "type": "core/v1/event", + "properties": { + "handlers": [ + { + "type": "onChange", + "componentId": "name_form", + "method": { + "name": "validateFields", + "parameters": { + "names": "{{['name']}}" + } + } + }, + { + "type": "onBlur", + "componentId": "name_form", + "method": { + "name": "validateAllFields", + "parameters": {} + } + }, + { + "type": "onChange", + "componentId": "check_name", + "method": { + "name": "triggerFetch", + "parameters": {} + } + }, + { + "type": "onBlur", + "componentId": "check_name", + "method": { + "name": "triggerFetch", + "parameters": {} + } + } + ] + } + } + ] + }, + { + "id": "email_form", + "type": "arco/v1/formControl", + "properties": { + "label": { + "format": "plain", + "raw": "email" + }, + "layout": "horizontal", + "required": true, + "hidden": false, + "extra": "", + "errorMsg": "{{email_form.validatedResult.email?.errors[0]?.message;}}", + "labelAlign": "left", + "colon": false, + "help": "", + "labelCol": { + "span": 3, + "offset": 0 + }, + "wrapperCol": { + "span": 21, + "offset": 0 + } + }, + "traits": [ + { + "type": "core/v1/validation", + "properties": { + "validators": [ + { + "name": "email", + "value": "{{email_input.value;}}", + "rules": [ + { + "type": "required", + "validate": null, + "error": { + "message": "Please input the email." + }, + "minLength": 0, + "maxLength": 0, + "list": [], + "min": 0, + "max": 0, + "regex": "", + "flags": "", + "customOptions": {} + }, + { + "type": "email", + "validate": null, + "error": { + "message": "Please input the correct email." + }, + "minLength": 0, + "maxLength": 0, + "list": [], + "min": 0, + "max": 0, + "regex": "", + "flags": "", + "customOptions": {} + } + ] + } + ] + } + } + ] + }, + { + "id": "email_input", + "type": "arco/v1/input", + "properties": { + "allowClear": false, + "disabled": false, + "readOnly": false, + "defaultValue": "", + "updateWhenDefaultValueChanges": false, + "placeholder": "please input the email.", + "error": "{{email_form.validatedResult.email.isInvalid}}", + "size": "default" + }, + "traits": [ + { + "type": "core/v1/slot", + "properties": { + "container": { + "id": "email_form", + "slot": "content" + }, + "ifCondition": true + } + }, + { + "type": "core/v1/event", + "properties": { + "handlers": [ + { + "type": "onChange", + "componentId": "email_form", + "method": { + "name": "validateAllFields", + "parameters": {} + } + }, + { + "type": "onBlur", + "componentId": "email_form", + "method": { + "name": "validateAllFields", + "parameters": {} + } + } + ] + } + } + ] + }, + { + "id": "url_form", + "type": "arco/v1/formControl", + "properties": { + "label": { + "format": "plain", + "raw": "URL" + }, + "layout": "horizontal", + "required": true, + "hidden": false, + "extra": "", + "errorMsg": "{{url_form.validatedResult.url?.errors[0]?.message}}", + "labelAlign": "left", + "colon": false, + "help": "", + "labelCol": { + "span": 3, + "offset": 0 + }, + "wrapperCol": { + "span": 21, + "offset": 0 + } + }, + "traits": [ + { + "type": "core/v1/validation", + "properties": { + "validators": [ + { + "name": "url", + "value": "{{url_input.value}}", + "rules": [ + { + "type": "required", + "validate": null, + "error": { + "message": "Please input the URL. " + }, + "minLength": 0, + "maxLength": 0, + "list": [], + "min": 0, + "max": 0, + "regex": "", + "flags": "", + "customOptions": {} + }, + { + "type": "url", + "validate": null, + "error": { + "message": "Please input the correct URL." + }, + "minLength": 0, + "maxLength": 0, + "list": [], + "min": 0, + "max": 0, + "regex": "", + "flags": "", + "customOptions": {} + } + ] + } + ] + } + } + ] + }, + { + "id": "url_input", + "type": "arco/v1/input", + "properties": { + "allowClear": false, + "disabled": false, + "readOnly": false, + "defaultValue": "", + "updateWhenDefaultValueChanges": false, + "placeholder": "please input the URL.", + "error": "{{url_form.validatedResult.url.isInvalid}}", + "size": "default" + }, + "traits": [ + { + "type": "core/v1/slot", + "properties": { + "container": { + "id": "url_form", + "slot": "content" + }, + "ifCondition": true + } + }, + { + "type": "core/v1/event", + "properties": { + "handlers": [ + { + "type": "onChange", + "componentId": "url_form", + "method": { + "name": "validateAllFields", + "parameters": {} + } + }, + { + "type": "onBlur", + "componentId": "url_form", + "method": { + "name": "validateAllFields", + "parameters": {} + } + } + ] + } + } + ] + }, + { + "id": "ip_form", + "type": "arco/v1/formControl", + "properties": { + "label": { + "format": "plain", + "raw": "IP" + }, + "layout": "horizontal", + "required": true, + "hidden": false, + "extra": "", + "errorMsg": "{{ip_form.validatedResult.ip?.errors[0]?.message}}", + "labelAlign": "left", + "colon": false, + "help": "", + "labelCol": { + "span": 3, + "offset": 0 + }, + "wrapperCol": { + "span": 21, + "offset": 0 + } + }, + "traits": [ + { + "type": "core/v1/validation", + "properties": { + "validators": [ + { + "name": "ip", + "value": "{{ip_input.value}}", + "rules": [ + { + "type": "required", + "validate": null, + "error": { + "message": "Please input the IP." + }, + "minLength": 0, + "maxLength": 0, + "list": [], + "min": 0, + "max": 0, + "regex": "", + "flags": "", + "customOptions": {} + }, + { + "type": "ipv4", + "validate": null, + "error": { + "message": "Please input the correct IP." + }, + "minLength": 0, + "maxLength": 0, + "list": [], + "min": 0, + "max": 0, + "regex": "", + "flags": "", + "customOptions": {} + } + ] + } + ] + } + } + ] + }, + { + "id": "ip_input", + "type": "arco/v1/input", + "properties": { + "allowClear": false, + "disabled": false, + "readOnly": false, + "defaultValue": "", + "updateWhenDefaultValueChanges": false, + "placeholder": "please input the IP.", + "error": "{{ip_form.validatedResult.ip.isInvalid}}", + "size": "default" + }, + "traits": [ + { + "type": "core/v1/slot", + "properties": { + "container": { + "id": "ip_form", + "slot": "content" + }, + "ifCondition": true + } + }, + { + "type": "core/v1/event", + "properties": { + "handlers": [ + { + "type": "onChange", + "componentId": "ip_form", + "method": { + "name": "validateAllFields", + "parameters": {} + } + }, + { + "type": "onBlur", + "componentId": "ip_form", + "method": { + "name": "validateAllFields", + "parameters": {} + } + } + ] + } + } + ] + }, + { + "id": "phone_form", + "type": "arco/v1/formControl", + "properties": { + "label": { + "format": "plain", + "raw": "phone" + }, + "layout": "horizontal", + "required": true, + "hidden": false, + "extra": "", + "errorMsg": "{{phone_form.validatedResult.phone.errors[0]?.message}}", + "labelAlign": "left", + "colon": false, + "help": "", + "labelCol": { + "span": 3, + "offset": 0 + }, + "wrapperCol": { + "span": 21, + "offset": 0 + } + }, + "traits": [ + { + "type": "core/v1/validation", + "properties": { + "validators": [ + { + "name": "phone", + "value": "{{phone_input.value}}", + "rules": [ + { + "type": "required", + "validate": null, + "error": { + "message": "Please input the phone number." + }, + "minLength": 0, + "maxLength": 0, + "list": [], + "min": 0, + "max": 0, + "regex": "", + "flags": "", + "customOptions": {} + }, + { + "type": "regex", + "validate": null, + "error": { + "message": "Please input the correct phone number." + }, + "minLength": 0, + "maxLength": 0, + "list": [], + "min": 0, + "max": 0, + "regex": "^1[3456789]\\d{9}$", + "flags": "", + "customOptions": {} + } + ] + } + ] + } + } + ] + }, + { + "id": "phone_input", + "type": "arco/v1/input", + "properties": { + "allowClear": false, + "disabled": false, + "readOnly": false, + "defaultValue": "", + "updateWhenDefaultValueChanges": false, + "placeholder": "please input the phone number.", + "error": "{{phone_form.validatedResult.phone.isInvalid}}", + "size": "default" + }, + "traits": [ + { + "type": "core/v1/slot", + "properties": { + "container": { + "id": "phone_form", + "slot": "content" + }, + "ifCondition": true + } + }, + { + "type": "core/v1/event", + "properties": { + "handlers": [ + { + "type": "onChange", + "componentId": "phone_form", + "method": { + "name": "validateAllFields", + "parameters": {} + } + }, + { + "type": "onBlur", + "componentId": "phone_form", + "method": { + "name": "validateAllFields", + "parameters": {} + } + } + ] + } + } + ] + }, + { + "id": "city_form", + "type": "arco/v1/formControl", + "properties": { + "label": { + "format": "plain", + "raw": "city" + }, + "layout": "horizontal", + "required": false, + "hidden": false, + "extra": "", + "errorMsg": "{{city_form.validatedResult.city.errors[0]?.message}}", + "labelAlign": "left", + "colon": false, + "help": "", + "labelCol": { + "span": 3, + "offset": 0 + }, + "wrapperCol": { + "span": 21, + "offset": 0 + } + }, + "traits": [ + { + "type": "core/v1/validation", + "properties": { + "validators": [ + { + "name": "city", + "value": "{{city_select.value}}", + "rules": [ + { + "type": "include", + "validate": null, + "error": { + "message": "Please select \"Beijing\"." + }, + "minLength": 0, + "maxLength": 0, + "includeList": ["Beijing"], + "excludeList": [], + "min": 0, + "max": 0, + "regex": "", + "flags": "", + "customOptions": {} + } + ] + } + ] + } + } + ] + }, + { + "id": "city_select", + "type": "arco/v1/select", + "properties": { + "allowClear": false, + "multiple": false, + "allowCreate": false, + "bordered": true, + "defaultValue": "Beijing", + "disabled": false, + "labelInValue": false, + "loading": false, + "showSearch": false, + "unmountOnExit": false, + "options": [ + { + "value": "Beijing", + "text": "Beijing" + }, + { + "value": "London", + "text": "London" + }, + { + "value": "NewYork", + "text": "NewYork" + } + ], + "placeholder": "Select city", + "size": "default", + "error": false, + "updateWhenDefaultValueChanges": false + }, + "traits": [ + { + "type": "core/v1/slot", + "properties": { + "container": { + "id": "city_form", + "slot": "content" + }, + "ifCondition": true + } + }, + { + "type": "core/v1/event", + "properties": { + "handlers": [ + { + "type": "onChange", + "componentId": "city_form", + "method": { + "name": "validateAllFields", + "parameters": {} + } + } + ] + } + } + ] + }, + { + "id": "clear_button", + "type": "arco/v1/button", + "properties": { + "type": "default", + "status": "default", + "long": false, + "size": "default", + "disabled": false, + "loading": false, + "shape": "square", + "text": "clear" + }, + "traits": [ + { + "type": "core/v1/event", + "properties": { + "handlers": [ + { + "type": "onClick", + "componentId": "name_form", + "method": { + "name": "clearAllErrors", + "parameters": {} + } + }, + { + "type": "onClick", + "componentId": "email_form", + "method": { + "name": "clearAllErrors", + "parameters": {} + } + }, + { + "type": "onClick", + "componentId": "url_form", + "method": { + "name": "clearAllErrors", + "parameters": {} + } + }, + { + "type": "onClick", + "componentId": "ip_form", + "method": { + "name": "clearErrors", + "parameters": { + "names": "{{['ip']}}" + } + } + }, + { + "type": "onClick", + "componentId": "phone_form", + "method": { + "name": "clearErrors", + "parameters": { + "names": "{{['phone']}}" + } + } + }, + { + "type": "onClick", + "componentId": "city_form", + "method": { + "name": "clearAllErrors", + "parameters": {} + } + } + ] + } + } + ] + }, + { + "id": "check_name", + "type": "core/v1/dummy", + "properties": {}, + "traits": [ + { + "type": "core/v1/fetch", + "properties": { + "url": "", + "method": "get", + "lazy": false, + "disabled": false, + "headers": {}, + "body": {}, + "bodyType": "json", + "onComplete": [ + { + "componentId": "name_form", + "method": { + "name": "setErrors", + "parameters": { + "errorsMap": "{{\n{\n name: [...name_form.validatedResult.name.errors, { message: 'The name is exist.' }]\n}\n}}" + } + } + } + ], + "onError": [] + } + } + ] + } + ] + } + } +} diff --git a/examples/input-components/inputValidation.json b/examples/input-components/inputValidation.json index 21e72bc4..14149f71 100644 --- a/examples/input-components/inputValidation.json +++ b/examples/input-components/inputValidation.json @@ -21,10 +21,28 @@ { "type": "core/v1/validation", "properties": { - "value": "{{ emailInput.value || \"\" }}", - "maxLength": 20, - "minLength": 10, - "rule": "email" + "validators": [ + { + "name": "email", + "value": "{{ emailInput.value || \"\" }}", + "rules": [ + { + "type": "email", + "error": { + "message": "Please input the email." + } + }, + { + "type": "length", + "maxLength": 20, + "minLength": 10, + "error": { + "message": "Please input the length between 10 and 20." + } + } + ] + } + ] } } ] @@ -34,7 +52,7 @@ "type": "core/v1/text", "properties": { "value": { - "raw": "{{ emailInput.validResult.errorMsg }}", + "raw": "{{ emailInput.validatedResult.email.errors[0]?.message }}", "format": "plain" } }, @@ -54,10 +72,29 @@ { "type": "core/v1/validation", "properties": { - "value": "{{ phoneInput.value || \"\" }}", - "maxLength": 100, - "minLength": 0, - "rule": "phoneNumber" + "validators": [ + { + "name": "phone", + "value": "{{ phoneInput.value || \"\" }}", + "rules": [ + { + "type": "regex", + "regex": "^1[3456789]\\d{9}$", + "error": { + "message": "Please input the correct phone number." + } + }, + { + "type": "length", + "maxLength": 100, + "minLength": 0, + "error": { + "message": "Please input the length between 0 and 100." + } + } + ] + } + ] } } ] @@ -95,7 +132,7 @@ "parameters": "{{ `email:${ emailInput.value } phone:${ phoneInput.value }` }}" }, "wait": {}, - "disabled": "{{ emailInput.validResult.isInvalid || phoneInput.validResult.isInvalid }}" + "disabled": "{{ emailInput.validatedResult.email.isInvalid || phoneInput.validatedResult.phone.isInvalid }}" } ] } diff --git a/examples/table/tableWithForm.json b/examples/table/tableWithForm.json index c93cd7a0..78756fba 100644 --- a/examples/table/tableWithForm.json +++ b/examples/table/tableWithForm.json @@ -264,9 +264,22 @@ { "type": "core/v1/validation", "properties": { - "value": "{{ nameInput.value || \"\" }}", - "maxLength": 10, - "minLength": 2 + "validators": [ + { + "name": "name", + "value": "{{ nameInput.value || \"\" }}", + "rules": [ + { + "type": "length", + "maxLength": 10, + "minLength": 2, + "error": { + "message": "Please input the length between 2 and 10." + } + } + ] + } + ] } } ] diff --git a/packages/runtime/index.html b/packages/runtime/index.html index 731a90fb..4341016c 100644 --- a/packages/runtime/index.html +++ b/packages/runtime/index.html @@ -14,7 +14,9 @@ import { initSunmaoUI } from './src'; import { ChakraProvider } from '@chakra-ui/react'; import { sunmaoChakraUILib } from '@sunmao-ui/chakra-ui-lib'; + import { ArcoDesignLib } from '@sunmao-ui/arco-lib'; import examples from '@example.json'; + import '@arco-design/web-react/dist/css/arco.css'; const selectEl = document.querySelector('select'); for (const example of examples) { @@ -29,7 +31,9 @@ const rootEl = document.querySelector('#root'); const render = example => { ReactDOM.unmountComponentAtNode(rootEl); - const { App, registry } = initSunmaoUI({ libs: [sunmaoChakraUILib] }); + const { App, registry } = initSunmaoUI({ + libs: [sunmaoChakraUILib, ArcoDesignLib], + }); const { app, modules = [] } = example.value; window.registry = registry; modules.forEach(m => { From 9d0e7321637aba2633739cd4e1bfc53fc92e8a31 Mon Sep 17 00:00:00 2001 From: xzdry Date: Sun, 17 Jul 2022 17:03:24 +0800 Subject: [PATCH 03/36] feat(arco): add arco `TablePrimaryKeyWidget` --- packages/arco-lib/package.json | 2 +- packages/arco-lib/src/constants/widgets.ts | 1 + .../src/widgets/TablePrimaryKeyWidget.tsx | 49 +++++++++++++++++++ packages/arco-lib/src/widgets/index.ts | 3 ++ packages/editor/src/main.tsx | 3 +- packages/editor/types/widgets.d.ts | 3 ++ 6 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 packages/arco-lib/src/constants/widgets.ts create mode 100644 packages/arco-lib/src/widgets/TablePrimaryKeyWidget.tsx create mode 100644 packages/arco-lib/src/widgets/index.ts diff --git a/packages/arco-lib/package.json b/packages/arco-lib/package.json index 9bdda919..f4ad9253 100644 --- a/packages/arco-lib/package.json +++ b/packages/arco-lib/package.json @@ -21,7 +21,7 @@ "scripts": { "dev": "vite", "typings": "tsc --emitDeclarationOnly", - "build": "tsup src/index.ts --format cjs,esm,iife --legacy-output --inject ./react-import.js --clean --no-splitting --sourcemap", + "build": "tsup src/index.ts src/widgets/index.ts --format cjs,esm,iife --legacy-output --inject ./react-import.js --clean --no-splitting --sourcemap", "serve": "vite preview", "lint": "eslint ./src --ext .ts --ext .tsx", "fix-lint": "eslint --fix ./src --ext .ts --ext .tsx", diff --git a/packages/arco-lib/src/constants/widgets.ts b/packages/arco-lib/src/constants/widgets.ts new file mode 100644 index 00000000..0f7ad129 --- /dev/null +++ b/packages/arco-lib/src/constants/widgets.ts @@ -0,0 +1 @@ +export const ARCO_V1_VERSION = 'arco/v1'; diff --git a/packages/arco-lib/src/widgets/TablePrimaryKeyWidget.tsx b/packages/arco-lib/src/widgets/TablePrimaryKeyWidget.tsx new file mode 100644 index 00000000..fd2c1440 --- /dev/null +++ b/packages/arco-lib/src/widgets/TablePrimaryKeyWidget.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import { WidgetProps, implementWidget } from '@sunmao-ui/editor-sdk'; +import { ColumnSpec } from '../generated/types/Table'; +import { Static } from '@sinclair/typebox'; +import { Select } from '@arco-design/web-react'; +import { ARCO_V1_VERSION } from '../constants/widgets'; + +type TablePrimaryKeyWidget = 'arco/v1/primaryKey'; + +declare module '@sunmao-ui/editor-sdk' { + interface WidgetOptionsMap { + 'arco/v1/primaryKey': Record; + } +} + +export const TablePrimaryKeyWidget: React.FC< + WidgetProps +> = props => { + const { value, onChange, component } = props; + const { properties } = component; + const columns = properties.columns as Static[]; + + const keys = ['auto', ...columns.map(c => c.dataIndex)]; + + return ( + + ); +}; + +export default implementWidget({ + version: ARCO_V1_VERSION, + metadata: { + name: 'primaryKey', + }, +})(TablePrimaryKeyWidget); diff --git a/packages/arco-lib/src/widgets/index.ts b/packages/arco-lib/src/widgets/index.ts new file mode 100644 index 00000000..5febe976 --- /dev/null +++ b/packages/arco-lib/src/widgets/index.ts @@ -0,0 +1,3 @@ +import TablePrimaryKeyWidget from './TablePrimaryKeyWidget'; + +export const widgets = [TablePrimaryKeyWidget]; diff --git a/packages/editor/src/main.tsx b/packages/editor/src/main.tsx index fa22bfca..e958100c 100644 --- a/packages/editor/src/main.tsx +++ b/packages/editor/src/main.tsx @@ -3,6 +3,7 @@ import ReactDOM from 'react-dom'; import { RegistryInterface } from '@sunmao-ui/runtime'; import { sunmaoChakraUILib } from '@sunmao-ui/chakra-ui-lib'; import { widgets as chakraWidgets } from '@sunmao-ui/chakra-ui-lib/dist/esm/widgets/index'; +import { widgets as arcoWidgets } from '@sunmao-ui/arco-lib/dist/esm/widgets/index'; import { ArcoDesignLib } from '@sunmao-ui/arco-lib'; import { initSunmaoUIEditor } from './init'; import { LocalStorageManager } from './LocalStorageManager'; @@ -17,7 +18,7 @@ type Options = Partial<{ const lsManager = new LocalStorageManager(); const { Editor, registry } = initSunmaoUIEditor({ - widgets: [...chakraWidgets], + widgets: [...chakraWidgets, ...arcoWidgets], storageHandler: { onSaveApp(app) { lsManager.saveAppInLS(app); diff --git a/packages/editor/types/widgets.d.ts b/packages/editor/types/widgets.d.ts index a86b1479..2db0f345 100644 --- a/packages/editor/types/widgets.d.ts +++ b/packages/editor/types/widgets.d.ts @@ -1,3 +1,6 @@ declare module '@sunmao-ui/chakra-ui-lib/dist/esm/widgets/index' { export * from '@sunmao-ui/chakra-ui-lib/lib/widgets/index'; } +declare module '@sunmao-ui/arco-lib/dist/esm/widgets/index' { + export * from '@sunmao-ui/arco-lib/lib/widgets/index'; +} From 265ba4a6c1e3dcd88c30f80d76d14cd64ce4da50 Mon Sep 17 00:00:00 2001 From: xzdry Date: Sun, 17 Jul 2022 17:16:25 +0800 Subject: [PATCH 04/36] feat(arco/Table): support for automatic generation of `rowKey` or setting from columns --- .../arco-lib/src/components/Table/Table.tsx | 51 ++++++++++++++++--- .../arco-lib/src/generated/types/Table.ts | 7 +++ 2 files changed, 52 insertions(+), 6 deletions(-) diff --git a/packages/arco-lib/src/components/Table/Table.tsx b/packages/arco-lib/src/components/Table/Table.tsx index 2c4f3ed2..b943e766 100644 --- a/packages/arco-lib/src/components/Table/Table.tsx +++ b/packages/arco-lib/src/components/Table/Table.tsx @@ -72,6 +72,8 @@ const rowClickStyle = css` } `; +const tableRowKey = Symbol.for('table.rowKey'); + export const exampleProperties: Static = { columns: [ { @@ -107,11 +109,11 @@ export const exampleProperties: Static = { data: Array(13) .fill('') .map((_, index) => ({ - key: `key ${index}`, name: `${Math.random() > 0.5 ? 'Kevin Sandra' : 'Naomi Cook'}${index}`, link: `link${Math.random() > 0.5 ? '-A' : '-B'}`, salary: Math.floor(Math.random() * 1000), })), + rowKey: 'auto', checkCrossPage: true, pagination: { enablePagination: true, @@ -172,8 +174,15 @@ export const Table = implementRuntimeComponent({ } = props; const ref = useRef(null); - const { pagination, rowClick, useDefaultFilter, useDefaultSort, data, ...cProps } = - getComponentProps(props); + const { + pagination, + rowKey, + rowClick, + useDefaultFilter, + useDefaultSort, + data, + ...cProps + } = getComponentProps(props); const { pageSize, @@ -186,6 +195,7 @@ export const Table = implementRuntimeComponent({ } = pagination; const rowSelectionType = rowSelectionTypeMap[cProps.rowSelectionType]; + const currentChecked = useRef<(string | number)[]>([]); const [currentPage, setCurrentPage] = useStateValue( defaultCurrent ?? 1, @@ -196,6 +206,7 @@ export const Table = implementRuntimeComponent({ useEffect(() => { mergeState({ pageSize }); + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const [sortRule, setSortRule] = useState(null); @@ -231,13 +242,39 @@ export const Table = implementRuntimeComponent({ }, [data, sortRule]); const currentPageData = useMemo(() => { + let data = sortedData; if (enablePagination && useCustomPagination) { // If the `useCustomPagination` is true, then no pagination will be done and data over the pagesize will be sliced // Otherwise it will automatically paginate on the front end based on the current page - return sortedData?.slice(0, pageSize); + data = sortedData?.slice(0, pageSize); } - return sortedData; - }, [pageSize, sortedData, enablePagination, useCustomPagination]); + + // auto-generated row key + return rowKey === 'auto' + ? data?.map((el, idx) => { + el[tableRowKey] = idx; + return el; + }) + : data; + }, [pageSize, sortedData, rowKey, enablePagination, useCustomPagination]); + + // reset state when data changed + useEffect(() => { + if (!currentPageData.length) { + mergeState({ + selectedRowKeys: [], + selectedRows: [], + sortRule: {}, + filterRule: undefined, + currentPage: undefined, + }); + } + + const key = rowKey === 'auto' ? tableRowKey : rowKey; + mergeState({ + selectedRows: currentPageData.filter(d => currentChecked.current.includes(d[key])), + }); + }, [currentPageData, mergeState, rowKey]); useEffect(() => { setColumns( @@ -466,6 +503,7 @@ export const Table = implementRuntimeComponent({ return ( Date: Thu, 21 Jul 2022 00:08:37 +0800 Subject: [PATCH 05/36] updated the English version of readme file --- README.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 6811ce9d..5ea420b1 100644 --- a/README.md +++ b/README.md @@ -18,21 +18,21 @@

-Sunmao(榫卯 /suən mɑʊ/) is a front-end low-code framework. Through Sunmao, you can easily encapsulate any front-end UI components into low-code component libraries, so as to build your own low-code UI development platform, making front-end development as tight as Sunmao. +Sunmao(榫卯 /suən mɑʊ/) is a front-end low-code framework. Through Sunmao, you can easily encapsulate any front-end UI components into low-code component libraries to build your own low-code UI development platform, making front-end development as tight as Sunmao("mortise and tenon" in Chinese). [中文](./docs/zh/README.md) ## DEMO -Sunmao‘s website is developed with Sunmao, look here: [Sunmao website editor](https://sunmao-ui.com/dev.html) +The offcial website of Sunmao is developed by Sunmao, try it from here: [Sunmao website editor](https://sunmao-ui.com/dev.html) We also provide an open-to-use template: [Sunmao Starter Kit](https://github.com/webzard-io/sunmao-start) ## Why Sunmao? -### Reactive low-code framework +### Responsive low-code framework -Sunmao chooses a reactive solution that is easy to understand and has excellent performance, making Sunmao intuitive and quick to use. +Sunmao chooses a responsive solution that is easy to understand and has excellent performance, making Sunmao intuitive and quick to start. ### Powerful low-code GUI editor @@ -40,7 +40,7 @@ Sunmao has a built-in GUI editor, which almost includes all the capabilities tha ### Extremely Extensible -Both the UI component library itself and the low-code editor support custom extensions. Developers can register various components to cover application requirements and continue to use the existing visual design system. +Both the UI component library itself and the low-code editor support custom extensions. Developers can register various components to meet the needs of application and continue to use the existing visual design system. ### Type Safety @@ -50,13 +50,13 @@ For more details, read [Sunmao: A truly extensible low-code UI framework](./docs ## Tutorial -Sunmao users are divided into two roles, one is a developer and the other is a user. +Sunmao users are divided into two roles, one is developer and the other is user. -The responsibilities of developers are similar to those of common front-end developers. They are responsible for developing UI components and encapsulating common UI components to Sunmao components. Developers need to write code to implement the logic of the component. +The responsibilities of developers are similar to those of common front-end developers. They are responsible for developing UI components and encapsulating common UI components to Sunmao components. Developers need to write code to implement the logic of components. -The user's responsibility is to use the Sunmao components encapsulated by developers to build front-end applications in the Sunmao low-code editor. Users do not need front-end knowledge and programming skills. They can complete application construction only through UI interaction. +The user's responsibility is to use the Sunmao components encapsulated by developers to build front-end applications in the Sunmao low-code editor. Users do not need front-end knowledge and programming skills. They can finish building the application through UI interaction only. -We have prepared two tutorials for different roles. The user only needs to read the user's tutorial, but the developer has to read both. +We have prepared two tutorials for user and developer. The user only needs to read the user's tutorial, while the developer must read both. - [User's Tutorial](./docs/en/user.md) - [Developer's Tutorial](./docs/en/developer.md) From 25c819db3a18690136a36106c012aee154de401d Mon Sep 17 00:00:00 2001 From: GouXi Date: Thu, 21 Jul 2022 02:17:46 -0400 Subject: [PATCH 06/36] use 'Reactive rendering' instead of 'Responsive'. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5ea420b1..c223a350 100644 --- a/README.md +++ b/README.md @@ -30,9 +30,9 @@ We also provide an open-to-use template: [Sunmao Starter Kit](https://github.com ## Why Sunmao? -### Responsive low-code framework +### Reactive rendering low-code framework -Sunmao chooses a responsive solution that is easy to understand and has excellent performance, making Sunmao intuitive and quick to start. +Sunmao chooses a reactive rendering solution that is easy to understand and has excellent performance, making Sunmao intuitive and quick to start. ### Powerful low-code GUI editor From f234b11bcf0736d4e3c38b7823e0a43d8e7b13f9 Mon Sep 17 00:00:00 2001 From: xzdry Date: Thu, 21 Jul 2022 22:26:09 +0800 Subject: [PATCH 07/36] docs: fix typo --- docs/en/component.md | 2 +- docs/zh/component.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/en/component.md b/docs/en/component.md index 19bc50cc..57ad4b59 100644 --- a/docs/en/component.md +++ b/docs/en/component.md @@ -29,7 +29,7 @@ First, let's look at this Input Component *Spec* example. displayName: "Input", exampleProperties: { placeholder: "Input here", - disabled: fasle, + disabled: false, }, }, spec: { diff --git a/docs/zh/component.md b/docs/zh/component.md index c015890b..aec988a2 100644 --- a/docs/zh/component.md +++ b/docs/zh/component.md @@ -29,7 +29,7 @@ Spec 本质上是一个 JSON,它的作用是描述组件的参数、行为等 displayName: "Input", exampleProperties: { placeholder: "Input here", - disabled: fasle, + disabled: false, }, }, spec: { @@ -135,7 +135,7 @@ const InputSpec = { displayName: 'Input', exampleProperties: { placeholder: 'Input here', - disabled: fasle, + disabled: false, }, }, spec: { From e82774757f176bff163d20215e769fd7ca38edac Mon Sep 17 00:00:00 2001 From: Bowen Tan Date: Fri, 22 Jul 2022 17:04:43 +0800 Subject: [PATCH 08/36] fix(structureTree): if a component item is already in viewport, then don't scroll it into view --- .../StructureTree/StructureTree.tsx | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/packages/editor/src/components/StructureTree/StructureTree.tsx b/packages/editor/src/components/StructureTree/StructureTree.tsx index a2b433d0..778c3f25 100644 --- a/packages/editor/src/components/StructureTree/StructureTree.tsx +++ b/packages/editor/src/components/StructureTree/StructureTree.tsx @@ -35,8 +35,7 @@ export const StructureTree: React.FC = props => { const [search, setSearch] = useState(''); const { components, onSelectComponent, services } = props; const { editorStore } = services; - const wrapperRef = useRef(null); - + const scrollWrapper = useRef(null); const onSelectOption = useCallback( ({ item }: { item: Item }) => { onSelectComponent(item.value); @@ -49,10 +48,19 @@ export const StructureTree: React.FC = props => { // wait the component tree to be expanded setTimeout(() => { const selectedElement: HTMLElement | undefined | null = - wrapperRef.current?.querySelector(`#tree-item-${selectedId}`); + scrollWrapper.current?.querySelector(`#tree-item-${selectedId}`); - if (selectedElement) { - scrollIntoView(selectedElement, { time: 0, align: { lockX: true } }); + const wrapperRect = scrollWrapper.current?.getBoundingClientRect(); + const eleRect = selectedElement?.getBoundingClientRect(); + if ( + selectedElement && + eleRect && + wrapperRect && + (eleRect.top < wrapperRect.top || + eleRect.top > wrapperRect.top + wrapperRect?.height) + ) { + // check selected element is outside of view + scrollIntoView(selectedElement, { time: 300, align: { lockX: true } }); } }); } @@ -90,7 +98,6 @@ export const StructureTree: React.FC = props => { return ( = props => { - + {componentEles.length > 0 ? ( componentEles ) : ( From 1ddb111a0d71141168f5b44b3ab5c163fd403428 Mon Sep 17 00:00:00 2001 From: Bowen Tan Date: Fri, 22 Jul 2022 18:15:54 +0800 Subject: [PATCH 09/36] fix(shared): use default value instead of undefined when generate default value from spec --- packages/shared/__tests__/spec.spec.ts | 9 ++------- packages/shared/src/utils/spec.ts | 18 ++++++++---------- 2 files changed, 10 insertions(+), 17 deletions(-) diff --git a/packages/shared/__tests__/spec.spec.ts b/packages/shared/__tests__/spec.spec.ts index 16c56e67..0efaacc3 100644 --- a/packages/shared/__tests__/spec.spec.ts +++ b/packages/shared/__tests__/spec.spec.ts @@ -17,14 +17,9 @@ describe('generateDefaultValueFromSpec function', () => { const type = Type.Number(); expect(generateDefaultValueFromSpec(type)).toEqual(0); }); - // Type.Optional can only be judged by the modifier feature provided by the typebox, - // but this would break the consistency of the function, - // and it doesn't seem to make much sense to deal with non-object optional alone like Type.Optional(Type.String()) - // Therefore it is possible to determine whether an object's property is optional using spec.required, - // and if the property is within Type.Object is optional then it is not required. - it('can parse optional', () => { + it('can parse optional and the value is the default value of its type', () => { const type = Type.Optional(Type.Object({ str: Type.Optional(Type.String()) })); - expect(generateDefaultValueFromSpec(type)).toEqual({ str: undefined }); + expect(generateDefaultValueFromSpec(type)).toEqual({ str: '' }); }); it('can parse object', () => { const type = Type.Object({ diff --git a/packages/shared/src/utils/spec.ts b/packages/shared/src/utils/spec.ts index ef496aa1..84865571 100644 --- a/packages/shared/src/utils/spec.ts +++ b/packages/shared/src/utils/spec.ts @@ -31,7 +31,6 @@ function getArray(items: JSONSchema7Definition[]): JSONSchema7Type[] { function getObject(spec: JSONSchema7): JSONSchema7Object { const obj: JSONSchema7Object = {}; - const requiredKeys = spec.required; if (spec.allOf && spec.allOf.length > 0) { return (getArray(spec.allOf) as JSONSchema7Object[]).reduce((prev, cur) => { @@ -40,15 +39,14 @@ function getObject(spec: JSONSchema7): JSONSchema7Object { }, obj); } - requiredKeys && - requiredKeys.forEach(key => { - const subSpec = spec.properties?.[key]; - if (typeof subSpec === 'boolean') { - obj[key] = null; - } else if (subSpec) { - obj[key] = generateDefaultValueFromSpec(subSpec); - } - }); + for (const key in spec.properties) { + const subSpec = spec.properties?.[key]; + if (typeof subSpec === 'boolean') { + obj[key] = null; + } else if (subSpec) { + obj[key] = generateDefaultValueFromSpec(subSpec); + } + } return obj; } From 475e76c8d1248b3e0d19b2d51d8fc660ed1aeb5d Mon Sep 17 00:00:00 2001 From: Bowen Tan Date: Mon, 25 Jul 2022 11:22:42 +0800 Subject: [PATCH 10/36] fix(traitForm): hide create slot trait button in menu --- .../ComponentForm/GeneralTraitFormList/AddTraitButton.tsx | 6 ++++-- packages/editor/src/constants/index.ts | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/editor/src/components/ComponentForm/GeneralTraitFormList/AddTraitButton.tsx b/packages/editor/src/components/ComponentForm/GeneralTraitFormList/AddTraitButton.tsx index 7ebd9fb8..33bd3f25 100644 --- a/packages/editor/src/components/ComponentForm/GeneralTraitFormList/AddTraitButton.tsx +++ b/packages/editor/src/components/ComponentForm/GeneralTraitFormList/AddTraitButton.tsx @@ -10,7 +10,7 @@ import { } from '@chakra-ui/react'; import { RegistryInterface } from '@sunmao-ui/runtime'; import React, { useMemo } from 'react'; -import { ignoreTraitsList } from '../../../constants'; +import { hideCreateTraitsList } from '../../../constants'; import { ComponentSchema } from '@sunmao-ui/core'; type Props = { @@ -30,7 +30,9 @@ export const AddTraitButton: React.FC = props => { [component] ); const traitTypes = useMemo(() => { - return registry.getAllTraitTypes().filter(type => !ignoreTraitsList.includes(type)); + return registry + .getAllTraitTypes() + .filter(type => !hideCreateTraitsList.includes(type)); }, [registry]); const menuItems = traitTypes.map(type => { diff --git a/packages/editor/src/constants/index.ts b/packages/editor/src/constants/index.ts index 6bdca94d..7c4812f4 100644 --- a/packages/editor/src/constants/index.ts +++ b/packages/editor/src/constants/index.ts @@ -4,14 +4,16 @@ import { CORE_VERSION, CoreTraitName } from '@sunmao-ui/shared'; export const unremovableTraits = [`${CORE_VERSION}/${CoreTraitName.Slot}`]; -export const ignoreTraitsList = [ +export const hideCreateTraitsList = [ `${CORE_VERSION}/${CoreTraitName.Event}`, `${CORE_VERSION}/${CoreTraitName.Style}`, `${CORE_VERSION}/${CoreTraitName.Fetch}`, + `${CORE_VERSION}/${CoreTraitName.Slot}`, ]; export const hasSpecialFormTraitList = [ - ...ignoreTraitsList, + `${CORE_VERSION}/${CoreTraitName.Event}`, + `${CORE_VERSION}/${CoreTraitName.Style}`, `${CORE_VERSION}/${CoreTraitName.Fetch}`, ]; From 739ab07ee91117141ff8e23f98b8de4cd420ff2f Mon Sep 17 00:00:00 2001 From: xzdry Date: Mon, 25 Jul 2022 11:31:02 +0800 Subject: [PATCH 11/36] refactor(Table): remove auto-generated rowkey --- .../arco-lib/src/components/Table/Table.tsx | 33 ++++++++++--------- .../arco-lib/src/generated/types/Table.ts | 2 +- .../src/widgets/TablePrimaryKeyWidget.tsx | 2 +- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/packages/arco-lib/src/components/Table/Table.tsx b/packages/arco-lib/src/components/Table/Table.tsx index b943e766..ad0f94b1 100644 --- a/packages/arco-lib/src/components/Table/Table.tsx +++ b/packages/arco-lib/src/components/Table/Table.tsx @@ -72,10 +72,16 @@ const rowClickStyle = css` } `; -const tableRowKey = Symbol.for('table.rowKey'); - export const exampleProperties: Static = { columns: [ + { + title: 'Key', + dataIndex: 'key', + type: 'text', + displayValue: '', + filter: false, + componentSlotIndex: 0, + }, { title: 'Name', dataIndex: 'name', @@ -109,11 +115,12 @@ export const exampleProperties: Static = { data: Array(13) .fill('') .map((_, index) => ({ + key: index, name: `${Math.random() > 0.5 ? 'Kevin Sandra' : 'Naomi Cook'}${index}`, link: `link${Math.random() > 0.5 ? '-A' : '-B'}`, salary: Math.floor(Math.random() * 1000), })), - rowKey: 'auto', + rowKey: 'key', checkCrossPage: true, pagination: { enablePagination: true, @@ -242,21 +249,14 @@ export const Table = implementRuntimeComponent({ }, [data, sortRule]); const currentPageData = useMemo(() => { - let data = sortedData; if (enablePagination && useCustomPagination) { // If the `useCustomPagination` is true, then no pagination will be done and data over the pagesize will be sliced // Otherwise it will automatically paginate on the front end based on the current page - data = sortedData?.slice(0, pageSize); + return sortedData?.slice(0, pageSize); } - // auto-generated row key - return rowKey === 'auto' - ? data?.map((el, idx) => { - el[tableRowKey] = idx; - return el; - }) - : data; - }, [pageSize, sortedData, rowKey, enablePagination, useCustomPagination]); + return sortedData; + }, [pageSize, sortedData, enablePagination, useCustomPagination]); // reset state when data changed useEffect(() => { @@ -270,9 +270,10 @@ export const Table = implementRuntimeComponent({ }); } - const key = rowKey === 'auto' ? tableRowKey : rowKey; mergeState({ - selectedRows: currentPageData.filter(d => currentChecked.current.includes(d[key])), + selectedRows: currentPageData.filter(d => + currentChecked.current.includes(d[rowKey]) + ), }); }, [currentPageData, mergeState, rowKey]); @@ -503,7 +504,7 @@ export const Table = implementRuntimeComponent({ return ( []; - const keys = ['auto', ...columns.map(c => c.dataIndex)]; + const keys = columns.map(c => c.dataIndex); return (