fix: 修复 combo tabs 模式新成员中有必填字段未填写也能通过校验的问题

This commit is contained in:
2betop 2023-11-09 16:01:32 +08:00
parent 3e1e8a6189
commit c869ea3fbf
7 changed files with 203 additions and 77 deletions

View File

@ -50,6 +50,7 @@ import {isAlive} from 'mobx-state-tree';
import type {LabelAlign} from './Item';
import {injectObjectChain} from '../utils';
import {reaction} from 'mobx';
export interface FormHorizontal {
left?: number;
@ -371,6 +372,7 @@ export interface FormProps
onFailed?: (reason: string, errors: any) => any;
onFinished: (values: object, action: any) => any;
onValidate: (values: object, form: any) => any;
onValidChange?: (valid: boolean, props: any) => void; // 表单数据合法性变更
messages: {
fetchSuccess?: string;
fetchFailed?: string;
@ -443,6 +445,8 @@ export default class Form extends React.Component<FormProps, object> {
'onChange',
'onFailed',
'onFinished',
'onValidate',
'onValidChange',
'onSaved',
'canAccessSuperData',
'lazyChange',
@ -460,8 +464,7 @@ export default class Form extends React.Component<FormProps, object> {
[propName: string]: Array<() => Promise<any>>;
} = {};
asyncCancel: () => void;
disposeOnValidate: () => void;
disposeRulesValidate: () => void;
toDispose: Array<() => void> = [];
shouldLoadInitApi: boolean = false;
timer: ReturnType<typeof setTimeout>;
mounted: boolean;
@ -532,6 +535,7 @@ export default class Form extends React.Component<FormProps, object> {
store,
messages: {fetchSuccess, fetchFailed},
onValidate,
onValidChange,
promptPageLeave,
env,
rules
@ -541,49 +545,63 @@ export default class Form extends React.Component<FormProps, object> {
if (onValidate) {
const finalValidate = promisify(onValidate);
this.disposeOnValidate = this.addHook(async () => {
const result = await finalValidate(store.data, store);
this.toDispose.push(
this.addHook(async () => {
const result = await finalValidate(store.data, store);
if (result && isObject(result)) {
Object.keys(result).forEach(key => {
let msg = result[key];
const items = store.getItemsByPath(key);
if (result && isObject(result)) {
Object.keys(result).forEach(key => {
let msg = result[key];
const items = store.getItemsByPath(key);
// 没有找到
if (!Array.isArray(items) || !items.length) {
return;
}
// 没有找到
if (!Array.isArray(items) || !items.length) {
return;
}
// 在setError之前提前把残留的error信息清除掉否则每次onValidate后都会一直把报错 append 上去
items.forEach(item => item.clearError());
// 在setError之前提前把残留的error信息清除掉否则每次onValidate后都会一直把报错 append 上去
items.forEach(item => item.clearError());
if (msg) {
msg = Array.isArray(msg) ? msg : [msg];
items.forEach(item => item.addError(msg));
}
if (msg) {
msg = Array.isArray(msg) ? msg : [msg];
items.forEach(item => item.addError(msg));
}
delete result[key];
});
delete result[key];
});
isEmpty(result)
? store.clearRestError()
: store.setRestError(Object.keys(result).map(key => result[key]));
}
});
isEmpty(result)
? store.clearRestError()
: store.setRestError(Object.keys(result).map(key => result[key]));
}
})
);
}
// 表单校验结果发生变化时,触发 onValidChange
if (onValidChange) {
this.toDispose.push(
reaction(
() => store.valid,
valid => onValidChange(valid, this.props)
)
);
}
if (Array.isArray(rules) && rules.length) {
this.disposeRulesValidate = this.addHook(() => {
if (!store.valid) {
return;
}
this.toDispose.push(
this.addHook(() => {
if (!store.valid) {
return;
}
rules.forEach(
item =>
!evalExpression(item.rule, store.data) &&
store.addRestError(item.message, item.name)
);
});
rules.forEach(
item =>
!evalExpression(item.rule, store.data) &&
store.addRestError(item.message, item.name)
);
})
);
}
if (isEffectiveApi(initApi, store.data, initFetch, initFetchOn)) {
@ -655,8 +673,8 @@ export default class Form extends React.Component<FormProps, object> {
// this.lazyHandleChange.flush();
this.lazyEmitChange.cancel();
this.asyncCancel && this.asyncCancel();
this.disposeOnValidate && this.disposeOnValidate();
this.disposeRulesValidate && this.disposeRulesValidate();
this.toDispose.forEach(fn => fn());
this.toDispose = [];
window.removeEventListener('beforeunload', this.beforePageUnload);
this.unBlockRouting?.();
}
@ -836,30 +854,27 @@ export default class Form extends React.Component<FormProps, object> {
return this.props.store.validated;
}
validate(
async validate(
forceValidate?: boolean,
throwErrors: boolean = false
throwErrors: boolean = false,
toastErrors: boolean = true
): Promise<boolean> {
const {store, dispatchEvent, data, messages, translate: __} = this.props;
this.flush();
return store
.validate(
this.hooks['validate'] || [],
forceValidate,
throwErrors,
typeof messages?.validateFailed === 'string'
? __(filter(messages.validateFailed, store.data))
: undefined
)
.then((result: boolean) => {
if (result) {
dispatchEvent('validateSucc', data);
} else {
dispatchEvent('validateError', data);
}
return result;
});
const result = await store.validate(
this.hooks['validate'] || [],
forceValidate,
throwErrors,
toastErrors === false
? ''
: typeof messages?.validateFailed === 'string'
? __(filter(messages.validateFailed, store.data))
: undefined
);
dispatchEvent(result ? 'validateSucc' : 'validateError', data);
return result;
}
setErrors(errors: {[propName: string]: string}, tag = 'remote') {

View File

@ -35,7 +35,8 @@ export const ComboStore = iRendererStore
minLength: 0,
maxLength: 0,
length: 0,
activeKey: 0
activeKey: 0,
memberValidMap: types.optional(types.frozen(), {})
})
.views(self => {
function getForms() {
@ -170,13 +171,21 @@ export const ComboStore = iRendererStore
self.activeKey = key;
}
function setMemberValid(valid: boolean, index: number) {
self.memberValidMap = {
...self.memberValidMap,
[index]: valid
};
}
return {
config,
setActiveKey,
bindUniuqueItem,
unBindUniuqueItem,
addForm,
onChildStoreDispose
onChildStoreDispose,
setMemberValid
};
});

View File

@ -2053,6 +2053,7 @@
--Tabs-onActive-bg: var(--background);
--Tabs-onActive-borderColor: var(--borderColor);
--Tabs-onActive-color: var(--colors-neutral-text-2);
--Tabs-onError-color: var(--colors-error-5);
--Tabs-onDisabled-color: var(--colors-neutral-text-7);
--Tabs-onHover-borderColor: var(--colors-neutral-line-8);
--Tabs-add-icon-size: #{px2rem(15px)};
@ -4131,6 +4132,7 @@
var(--combo-vertical-right-border-color)
var(--combo-vertical-bottom-border-color)
var(--combo-vertical-left-border-color);
--Combo--vertical-item--onError-borderColor: var(--colors-error-5);
--Combo--vertical-item-borderRadius: var(
--combo-vertical-top-left-border-radius
)

View File

@ -242,6 +242,10 @@
border-color: var(--Tabs-onActive-borderColor);
border-bottom-color: transparent;
}
&.has-error > a:first-child {
color: var(--Tabs-onError-color) !important;
}
}
}

View File

@ -258,6 +258,12 @@
var(--combo-vertical-paddingRight) var(--combo-vertical-paddingBottom)
var(--combo-vertical-paddingLeft);
position: relative;
&.has-error {
border-color: var(
--Combo--vertical-item--onError-borderColor
) !important; // 因为下面的规则权重更高 &:not(.is-disabled) > .#{$ns}Combo-items > .#{$ns}Combo-item:hover
}
}
> .#{$ns}Combo-items > .#{$ns}Combo-item {

View File

@ -50,6 +50,7 @@ export interface TabProps extends ThemeProps {
tip?: string;
tab?: Schema;
className?: string;
tabClassName?: string;
activeKey?: string | number;
reload?: boolean;
mountOnEnter?: boolean;

View File

@ -8,7 +8,10 @@ import {
resolveEventData,
ApiObject,
FormHorizontal,
evalExpressionWithConditionBuilder
evalExpressionWithConditionBuilder,
IFormStore,
getVariable,
IFormItemStore
} from 'amis-core';
import {ActionObject, Api} from 'amis-core';
import {ComboStore, IComboStore} from 'amis-core';
@ -37,7 +40,11 @@ import {isEffectiveApi, str2AsyncFunction} from 'amis-core';
import {Alert2} from 'amis-ui';
import memoize from 'lodash/memoize';
import {Icon} from 'amis-ui';
import {isAlive} from 'mobx-state-tree';
import {
isAlive,
clone as cloneModel,
destroy as destroyModel
} from 'mobx-state-tree';
import {
FormBaseControlSchema,
SchemaApi,
@ -48,7 +55,6 @@ import {
import {ListenerAction} from 'amis-core';
import type {SchemaTokenizeableString} from '../../Schema';
import isPlainObject from 'lodash/isPlainObject';
import {isMobile} from 'amis-core';
export type ComboCondition = {
test: string;
@ -395,6 +401,7 @@ export default class ComboControl extends React.Component<ComboProps> {
this.dragTipRef = this.dragTipRef.bind(this);
this.flush = this.flush.bind(this);
this.handleComboTypeChange = this.handleComboTypeChange.bind(this);
this.handleSubFormValid = this.handleSubFormValid.bind(this);
this.defaultValue = {
...props.scaffold
};
@ -797,6 +804,11 @@ export default class ComboControl extends React.Component<ComboProps> {
);
}
handleSubFormValid(valid: boolean, {index}: any) {
const {store} = this.props;
store.setMemberValid(valid, index);
}
handleFormInit(values: any, {index}: any) {
const {
syncDefaultValue,
@ -806,9 +818,15 @@ export default class ComboControl extends React.Component<ComboProps> {
formInited,
onChange,
submitOnChange,
setPrinstineValue
setPrinstineValue,
formItem
} = this.props;
// 已经开始验证了,那么打开成员的时候,就要验证一下。
if (formItem?.validated) {
this.subForms[index]?.validate(true, false, false);
}
this.subFormDefaultValues.push({
index,
values,
@ -881,7 +899,13 @@ export default class ComboControl extends React.Component<ComboProps> {
}
validate(): any {
const {messages, nullable, translate: __} = this.props;
const {
messages,
nullable,
value: rawValue,
translate: __,
store
} = this.props;
const value = this.getValueAsArray();
const minLength = this.resolveVariableProps(this.props, 'minLength');
const maxLength = this.resolveVariableProps(this.props, 'maxLength');
@ -896,18 +920,62 @@ export default class ComboControl extends React.Component<ComboProps> {
(messages && messages.maxLengthValidateFailed) || 'Combo.maxLength',
{maxLength}
);
} else if (this.subForms.length && (!nullable || value)) {
return Promise.all(this.subForms.map(item => item.validate())).then(
values => {
if (~values.indexOf(false)) {
return __(
(messages && messages.validateFailed) || 'validateFailed'
);
}
} else if (nullable && !rawValue) {
return; // 不校验
} else if (value.length) {
return Promise.all(
value.map(async (values: any, index: number) => {
const subForm = this.subForms[index];
if (subForm) {
return subForm.validate(true, false, false);
} else {
// 那些还没有渲染出来的数据
// 因为有可能存在分页,有可能存在懒加载,所以没办法直接用 subForm 去校验了
const subForm = this.subForms[Object.keys(this.subForms)[0] as any];
if (subForm) {
const form: IFormStore = subForm.props.store;
let valid = false;
for (let formitem of form.items) {
const cloned: IFormItemStore = cloneModel(formitem);
let value: any = getVariable(values, formitem.name, false);
return;
if (formitem.extraName) {
value = [
getVariable(values, formitem.name, false),
getVariable(values, formitem.extraName, false)
];
}
cloned.changeTmpValue(value, 'dataChanged');
valid = await cloned.validate(values);
destroyModel(cloned);
if (valid === false) {
break;
}
}
store.setMemberValid(valid, index);
return valid;
}
}
})
).then(values => {
if (~values.indexOf(false)) {
return __((messages && messages.validateFailed) || 'validateFailed');
}
);
return;
});
} else if (this.subForms.length) {
return Promise.all(
this.subForms.map(item => item.validate(true, false, false))
).then(values => {
if (~values.indexOf(false)) {
return __((messages && messages.validateFailed) || 'validateFailed');
}
return;
});
}
}
@ -1253,6 +1321,12 @@ export default class ComboControl extends React.Component<ComboProps> {
// 不能按需渲染,因为 unique 会失效。
mountOnEnter={!hasUnique}
unmountOnExit={false}
className={
store.memberValidMap[index] === false ? 'has-error' : ''
}
tabClassName={
store.memberValidMap[index] === false ? 'has-error' : ''
}
>
{condition && typeSwitchable !== false ? (
<div className={cx('Combo-itemTag')}>
@ -1485,7 +1559,8 @@ export default class ComboControl extends React.Component<ComboProps> {
itemClassName,
itemsWrapperClassName,
static: isStatic,
mobileUI
mobileUI,
store
} = this.props;
let items = this.props.items;
@ -1543,7 +1618,11 @@ export default class ComboControl extends React.Component<ComboProps> {
return (
<div
className={cx(`Combo-item`, itemClassName)}
className={cx(
`Combo-item`,
itemClassName,
store.memberValidMap[index] === false ? 'has-error' : ''
)}
key={this.keys[index]}
>
{!isStatic && !disabled && draggable && thelist.length > 1 ? (
@ -1622,7 +1701,8 @@ export default class ComboControl extends React.Component<ComboProps> {
nullable,
translate: __,
itemClassName,
mobileUI
mobileUI,
store
} = this.props;
let items = this.props.items;
@ -1646,7 +1726,13 @@ export default class ComboControl extends React.Component<ComboProps> {
disabled ? 'is-disabled' : ''
)}
>
<div className={cx(`Combo-item`, itemClassName)}>
<div
className={cx(
`Combo-item`,
itemClassName,
store.memberValidMap[0] === false ? 'has-error' : ''
)}
>
{condition && typeSwitchable !== false ? (
<div className={cx('Combo-itemTag')}>
<label>{__('Combo.type')}</label>
@ -1715,11 +1801,13 @@ export default class ComboControl extends React.Component<ComboProps> {
className: cx(`Combo-form`, formClassName)
},
{
index: 0,
disabled: disabled,
static: isStatic,
data,
onChange: this.handleSingleFormChange,
ref: this.makeFormRef(0),
onValidChange: this.handleSubFormValid,
onInit: this.handleSingleFormInit,
canAccessSuperData,
formStore: undefined,
@ -1749,6 +1837,7 @@ export default class ComboControl extends React.Component<ComboProps> {
onAction: this.handleAction,
onRadioChange: this.handleRadioChange,
ref: this.makeFormRef(index),
onValidChange: this.handleSubFormValid,
canAccessSuperData,
lazyChange: changeImmediately ? false : true,
formLazyChange: false,