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 (