Pick combo 修改相关 (#9878)

* fix: 修复 table2 树形数据展示对应错误问题

* fix: 修复 combo 同步父级数据可能存在展示值和实际值不一致的问题 Close: #8773 (#8831)

* fix: 修复 combo 中有 pipeIn & pipeOut 场景时报错 Close: #8970

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

* fix: 修复 combo 可能无限触发 onChange 的问题

* fix: 修复 combo 同步父级数据可能存在展示值和实际值不一致的问题 Close: #8773

* fix: 修复 combo 中有 pipeIn & pipeOut 场景时报错 Close: #8970 (#8980)

* fix: 修复页面设计器重复执行onChange的问题

* fix: 修复数据下发同步问题 (#9625)

* fix: 修复 crud 重置失效的问题 Close: #9686 (#9693)

* fix: crud2条件查询表单重置失效 (#9706)

* chore: combo 中减少表单项重绘

* fix typecheck error

---------

Co-authored-by: wutong25 <wutong25@baidu.com>
Co-authored-by: walkin <wyc19966@hotmail.com>
This commit is contained in:
liaoxuezhi 2024-03-26 14:20:19 +08:00 committed by GitHub
parent b36586253b
commit 6d3cac5920
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 379 additions and 87 deletions

View File

@ -64,7 +64,8 @@ export const RENDERER_TRANSMISSION_OMIT_PROPS = [
'label', 'label',
'renderLabel', 'renderLabel',
'trackExpression', 'trackExpression',
'editorSetting' 'editorSetting',
'updatePristineAfterStoreDataReInit'
]; ];
const componentCache: SimpleMap = new SimpleMap(); const componentCache: SimpleMap = new SimpleMap();

View File

@ -207,7 +207,8 @@ export function HocStoreFactory(renderer: {
...(store.hasRemoteData ? store.data : null), // todo 只保留 remote 数据 ...(store.hasRemoteData ? store.data : null), // todo 只保留 remote 数据
...this.formatData(props.defaultData), ...this.formatData(props.defaultData),
...this.formatData(props.data) ...this.formatData(props.data)
}) }),
props.updatePristineAfterStoreDataReInit === false
); );
} }
} else if ( } else if (
@ -234,7 +235,8 @@ export function HocStoreFactory(renderer: {
store, store,
props.syncSuperStore === true props.syncSuperStore === true
) )
) ),
props.updatePristineAfterStoreDataReInit === false
); );
} else if (props.data && (props.data as any).__super) { } else if (props.data && (props.data as any).__super) {
store.initData( store.initData(
@ -250,16 +252,20 @@ export function HocStoreFactory(renderer: {
props.store?.storeType === 'ComboStore' props.store?.storeType === 'ComboStore'
? undefined ? undefined
: syncDataFromSuper( : syncDataFromSuper(
props.data, {...store.data, ...props.data},
(props.data as any).__super, (props.data as any).__super,
(prevProps.data as any).__super, (prevProps.data as any).__super,
store, store,
false false
) )
) ),
props.updatePristineAfterStoreDataReInit === false
); );
} else { } else {
store.initData(createObject(props.scope, props.data)); store.initData(
createObject(props.scope, props.data),
props.updatePristineAfterStoreDataReInit === false
);
} }
} else if ( } else if (
!props.trackExpression && !props.trackExpression &&
@ -282,8 +288,9 @@ export function HocStoreFactory(renderer: {
...store.data ...store.data
}), }),
store.storeType === 'FormStore' && props.updatePristineAfterStoreDataReInit === false ||
prevProps.store?.storeType === 'CRUDStore' (store.storeType === 'FormStore' &&
prevProps.store?.storeType === 'CRUDStore')
); );
} }
// nextProps.data.__super !== props.data.__super) && // nextProps.data.__super !== props.data.__super) &&
@ -299,7 +306,8 @@ export function HocStoreFactory(renderer: {
createObject(props.scope, { createObject(props.scope, {
// ...nextProps.data, // ...nextProps.data,
...store.data ...store.data
}) }),
props.updatePristineAfterStoreDataReInit === false
); );
} }
} }

View File

@ -50,6 +50,7 @@ import {isAlive} from 'mobx-state-tree';
import type {LabelAlign} from './Item'; import type {LabelAlign} from './Item';
import {injectObjectChain} from '../utils'; import {injectObjectChain} from '../utils';
import {reaction} from 'mobx';
export interface FormHorizontal { export interface FormHorizontal {
left?: number; left?: number;
@ -371,6 +372,7 @@ export interface FormProps
onFailed?: (reason: string, errors: any) => any; onFailed?: (reason: string, errors: any) => any;
onFinished: (values: object, action: any) => any; onFinished: (values: object, action: any) => any;
onValidate: (values: object, form: any) => any; onValidate: (values: object, form: any) => any;
onValidChange?: (valid: boolean, props: any) => void; // 表单数据合法性变更
messages: { messages: {
fetchSuccess?: string; fetchSuccess?: string;
fetchFailed?: string; fetchFailed?: string;
@ -443,6 +445,8 @@ export default class Form extends React.Component<FormProps, object> {
'onChange', 'onChange',
'onFailed', 'onFailed',
'onFinished', 'onFinished',
'onValidate',
'onValidChange',
'onSaved', 'onSaved',
'canAccessSuperData', 'canAccessSuperData',
'lazyChange', 'lazyChange',
@ -460,8 +464,7 @@ export default class Form extends React.Component<FormProps, object> {
[propName: string]: Array<() => Promise<any>>; [propName: string]: Array<() => Promise<any>>;
} = {}; } = {};
asyncCancel: () => void; asyncCancel: () => void;
disposeOnValidate: () => void; toDispose: Array<() => void> = [];
disposeRulesValidate: () => void;
shouldLoadInitApi: boolean = false; shouldLoadInitApi: boolean = false;
timer: ReturnType<typeof setTimeout>; timer: ReturnType<typeof setTimeout>;
mounted: boolean; mounted: boolean;
@ -518,6 +521,18 @@ export default class Form extends React.Component<FormProps, object> {
) )
); );
} }
// withStore 里面与上层数据会做同步
// 这个时候变更的数据没有同步 onChange 出去,出现数据不一致的问题。
// https://github.com/baidu/amis/issues/8773
this.toDispose.push(
reaction(
() => store.initedAt,
() => {
store.inited && this.emitChange(!!this.props.submitOnChange, true);
}
)
);
} }
componentDidMount() { componentDidMount() {
@ -531,6 +546,7 @@ export default class Form extends React.Component<FormProps, object> {
store, store,
messages: {fetchSuccess, fetchFailed}, messages: {fetchSuccess, fetchFailed},
onValidate, onValidate,
onValidChange,
promptPageLeave, promptPageLeave,
env, env,
rules rules
@ -540,49 +556,63 @@ export default class Form extends React.Component<FormProps, object> {
if (onValidate) { if (onValidate) {
const finalValidate = promisify(onValidate); const finalValidate = promisify(onValidate);
this.disposeOnValidate = this.addHook(async () => { this.toDispose.push(
const result = await finalValidate(store.data, store); this.addHook(async () => {
const result = await finalValidate(store.data, store);
if (result && isObject(result)) { if (result && isObject(result)) {
Object.keys(result).forEach(key => { Object.keys(result).forEach(key => {
let msg = result[key]; let msg = result[key];
const items = store.getItemsByPath(key); const items = store.getItemsByPath(key);
// 没有找到 // 没有找到
if (!Array.isArray(items) || !items.length) { if (!Array.isArray(items) || !items.length) {
return; return;
} }
// 在setError之前提前把残留的error信息清除掉否则每次onValidate后都会一直把报错 append 上去 // 在setError之前提前把残留的error信息清除掉否则每次onValidate后都会一直把报错 append 上去
items.forEach(item => item.clearError()); items.forEach(item => item.clearError());
if (msg) { if (msg) {
msg = Array.isArray(msg) ? msg : [msg]; msg = Array.isArray(msg) ? msg : [msg];
items.forEach(item => item.addError(msg)); items.forEach(item => item.addError(msg));
} }
delete result[key]; delete result[key];
}); });
isEmpty(result) isEmpty(result)
? store.clearRestError() ? store.clearRestError()
: store.setRestError(Object.keys(result).map(key => result[key])); : 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) { if (Array.isArray(rules) && rules.length) {
this.disposeRulesValidate = this.addHook(() => { this.toDispose.push(
if (!store.valid) { this.addHook(() => {
return; if (!store.valid) {
} return;
}
rules.forEach( rules.forEach(
item => item =>
!evalExpression(item.rule, store.data) && !evalExpression(item.rule, store.data) &&
store.addRestError(item.message, item.name) store.addRestError(item.message, item.name)
); );
}); })
);
} }
if (isEffectiveApi(initApi, store.data, initFetch, initFetchOn)) { if (isEffectiveApi(initApi, store.data, initFetch, initFetchOn)) {
@ -654,8 +684,8 @@ export default class Form extends React.Component<FormProps, object> {
// this.lazyHandleChange.flush(); // this.lazyHandleChange.flush();
this.lazyEmitChange.cancel(); this.lazyEmitChange.cancel();
this.asyncCancel && this.asyncCancel(); this.asyncCancel && this.asyncCancel();
this.disposeOnValidate && this.disposeOnValidate(); this.toDispose.forEach(fn => fn());
this.disposeRulesValidate && this.disposeRulesValidate(); this.toDispose = [];
window.removeEventListener('beforeunload', this.beforePageUnload); window.removeEventListener('beforeunload', this.beforePageUnload);
this.unBlockRouting?.(); this.unBlockRouting?.();
} }
@ -984,21 +1014,21 @@ export default class Form extends React.Component<FormProps, object> {
}; };
} }
async emitChange(submit: boolean) { async emitChange(submit: boolean, skipIfNothingChanges: boolean = false) {
const {onChange, store, submitOnChange, dispatchEvent, data} = this.props; const {onChange, store, submitOnChange, dispatchEvent, data} = this.props;
if (!isAlive(store)) { if (!isAlive(store)) {
return; return;
} }
const diff = difference(store.data, store.pristine);
if (skipIfNothingChanges && !Object.keys(diff).length) {
return;
}
// 提前准备好 onChange 的参数。 // 提前准备好 onChange 的参数。
// 因为 store.data 会在 await 期间被 WithStore.componentDidUpdate 中的 store.initData 改变。导致数据丢失 // 因为 store.data 会在 await 期间被 WithStore.componentDidUpdate 中的 store.initData 改变。导致数据丢失
const changeProps = [ const changeProps = [store.data, diff, this.props];
store.data,
difference(store.data, store.pristine),
this.props
];
const dispatcher = await dispatchEvent( const dispatcher = await dispatchEvent(
'change', 'change',
createObject(data, store.data) createObject(data, store.data)

View File

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

View File

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

View File

@ -242,6 +242,10 @@
border-color: var(--Tabs-onActive-borderColor); border-color: var(--Tabs-onActive-borderColor);
border-bottom-color: transparent; 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-paddingRight) var(--combo-vertical-paddingBottom)
var(--combo-vertical-paddingLeft); var(--combo-vertical-paddingLeft);
position: relative; 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 { > .#{$ns}Combo-items > .#{$ns}Combo-item {

View File

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

View File

@ -0,0 +1,85 @@
import {fireEvent, render} from '@testing-library/react';
import '../../../src';
import {render as amisRender} from '../../../src';
import {makeEnv, wait} from '../../helper';
test('paginationWrapper: service + crud', async () => {
const fetcher = jest.fn().mockImplementation(() =>
Promise.resolve({
data: {
status: 0,
data: {
items: [
{
label: '110101',
name: '东城区',
sale: 46861
},
{
label: '110102',
name: '西城区',
sale: 44882
}
]
}
}
})
);
const {container} = render(
amisRender(
{
type: 'page',
body: [
{
type: 'service',
id: 'u:ff652047d747',
api: {
method: 'get',
url: 'https://yapi.baidu-int.com/mock/42601/amis-chart/chart/sales/data2'
},
body: [
{
type: 'pagination-wrapper',
body: [
{
type: 'crud',
source: '${items}',
columns: [
{
name: 'label',
label: '地区',
type: 'text',
id: 'u:331ab3342710'
},
{
name: 'sale',
label: '销售',
type: 'text',
id: 'u:3dba120eda1d'
}
],
id: 'u:b3c77cb44fc8',
perPageAvailable: [10]
}
],
inputName: 'items',
outputName: 'items',
perPage: 20,
position: 'bottom'
}
]
}
]
},
{},
makeEnv({
fetcher
})
)
);
await wait(200);
const tds = [].slice
.call(container.querySelectorAll('tbody td'))
.map((td: any) => td.textContent);
expect(tds).toEqual(['110101', '46861', '110102', '44882']);
});

View File

@ -659,6 +659,17 @@ test('Renderer:combo with canAccessSuperData & strictMode & syncFields', async (
expect(comboInputs[0]!.value).toBe(''); expect(comboInputs[0]!.value).toBe('');
expect(comboInputs[1]!.value).toBe('123'); expect(comboInputs[1]!.value).toBe('123');
expect(comboInputs[2]!.value).toBe('123456'); expect(comboInputs[2]!.value).toBe('123456');
fireEvent.click(submitBtn);
await wait(300);
expect(onSubmit).toHaveBeenCalled();
expect(onSubmit.mock.calls[0][0]).toMatchObject({
super_text: '123456',
combo1: [{}],
combo2: [{super_text: '123'}],
combo3: [{super_text: '123456'}]
});
}); });
// 9. tabsMode // 9. tabsMode

View File

@ -194,6 +194,7 @@ test('Renderer:inputArray with minLength & maxLength', async () => {
expect(container.querySelector('.cxd-Combo-addBtn')).toBeInTheDocument(); expect(container.querySelector('.cxd-Combo-addBtn')).toBeInTheDocument();
// 最大值 // 最大值
fireEvent.click(container.querySelector('.cxd-Combo-addBtn')!); fireEvent.click(container.querySelector('.cxd-Combo-addBtn')!);
await wait(300);
await waitFor(() => { await waitFor(() => {
expect(container.querySelector('a.cxd-Combo-delBtn')).toBeInTheDocument(); expect(container.querySelector('a.cxd-Combo-delBtn')).toBeInTheDocument();
expect( expect(

View File

@ -915,8 +915,13 @@ export default class CRUD extends React.Component<CRUDProps, any> {
handleFilterReset(values: object, action: any) { handleFilterReset(values: object, action: any) {
const {store, syncLocation, env, pageField, perPageField} = this.props; const {store, syncLocation, env, pageField, perPageField} = this.props;
const resetQuery: any = {};
Object.keys(values).forEach(key => (resetQuery[key] = ''));
store.updateQuery( store.updateQuery(
store.pristineQuery, {
...resetQuery,
...store.pristineQuery
},
syncLocation && env && env.updateLocation syncLocation && env && env.updateLocation
? (location: any) => env.updateLocation(location) ? (location: any) => env.updateLocation(location)
: undefined, : undefined,

View File

@ -465,7 +465,7 @@ export default class CRUD2 extends React.Component<CRUD2Props, any> {
: query; : query;
store.updateQuery( store.updateQuery(
resetQuery ? this.props.store.pristineQuery : query, resetQuery ? {...query, ...this.props.store.pristineQuery} : query,
syncLocation && env && env.updateLocation syncLocation && env && env.updateLocation
? (location: any) => env.updateLocation(location, true) ? (location: any) => env.updateLocation(location, true)
: undefined, : undefined,
@ -1086,11 +1086,16 @@ export default class CRUD2 extends React.Component<CRUD2Props, any> {
key: index + 'filter', key: index + 'filter',
data: this.props.store.filterData, data: this.props.store.filterData,
onSubmit: (data: any) => this.handleSearch({query: data}), onSubmit: (data: any) => this.handleSearch({query: data}),
onReset: () => onReset: (data: any) => {
const resetQueries: any = {};
Object.keys(data!).forEach(key => (resetQueries[key] = ''));
this.handleSearch({ this.handleSearch({
query: resetQueries,
resetQuery: true, resetQuery: true,
replaceQuery: true replaceQuery: true
}) });
}
}) })
); );
} }

View File

@ -7,7 +7,12 @@ import {
FormBaseControl, FormBaseControl,
resolveEventData, resolveEventData,
ApiObject, ApiObject,
FormHorizontal FormHorizontal,
evalExpressionWithConditionBuilder,
IFormStore,
getVariable,
IFormItemStore,
deleteVariable
} from 'amis-core'; } from 'amis-core';
import {ActionObject, Api} from 'amis-core'; import {ActionObject, Api} from 'amis-core';
import {ComboStore, IComboStore} from 'amis-core'; import {ComboStore, IComboStore} from 'amis-core';
@ -36,7 +41,11 @@ import {isEffectiveApi, str2AsyncFunction} from 'amis-core';
import {Alert2} from 'amis-ui'; import {Alert2} from 'amis-ui';
import memoize from 'lodash/memoize'; import memoize from 'lodash/memoize';
import {Icon} from 'amis-ui'; import {Icon} from 'amis-ui';
import {isAlive} from 'mobx-state-tree'; import {
isAlive,
clone as cloneModel,
destroy as destroyModel
} from 'mobx-state-tree';
import { import {
FormBaseControlSchema, FormBaseControlSchema,
SchemaApi, SchemaApi,
@ -47,7 +56,6 @@ import {
import {ListenerAction} from 'amis-core'; import {ListenerAction} from 'amis-core';
import type {SchemaTokenizeableString} from '../../Schema'; import type {SchemaTokenizeableString} from '../../Schema';
import isPlainObject from 'lodash/isPlainObject'; import isPlainObject from 'lodash/isPlainObject';
import {isMobile} from 'amis-core';
export type ComboCondition = { export type ComboCondition = {
test: string; test: string;
@ -393,6 +401,7 @@ export default class ComboControl extends React.Component<ComboProps> {
this.dragTipRef = this.dragTipRef.bind(this); this.dragTipRef = this.dragTipRef.bind(this);
this.flush = this.flush.bind(this); this.flush = this.flush.bind(this);
this.handleComboTypeChange = this.handleComboTypeChange.bind(this); this.handleComboTypeChange = this.handleComboTypeChange.bind(this);
this.handleSubFormValid = this.handleSubFormValid.bind(this);
this.defaultValue = { this.defaultValue = {
...props.scaffold ...props.scaffold
}; };
@ -532,8 +541,11 @@ export default class ComboControl extends React.Component<ComboProps> {
} }
getValueAsArray(props = this.props) { getValueAsArray(props = this.props) {
const {flat, joinValues, delimiter, type} = props; const {flat, joinValues, delimiter, type, formItem} = props;
let value = props.value; // 因为 combo 多个子表单可能同时发生变化。
// onChagne 触发多次,上次变更还没应用到 props.value 上来,这次触发变更就会包含历史数据,把上次触发的数据给重置成旧的了。
// 通过 props.getValue() 拿到的是最新的
let value = props.getValue();
if (joinValues && flat && typeof value === 'string') { if (joinValues && flat && typeof value === 'string') {
value = value.split(delimiter || ','); value = value.split(delimiter || ',');
@ -704,13 +716,32 @@ export default class ComboControl extends React.Component<ComboProps> {
} }
handleChange(values: any, diff: any, {index}: any) { handleChange(values: any, diff: any, {index}: any) {
const {flat, store, joinValues, delimiter, disabled, submitOnChange, type} = const {
this.props; flat,
store,
joinValues,
delimiter,
disabled,
submitOnChange,
type,
syncFields,
name
} = this.props;
if (disabled) { if (disabled) {
return; return;
} }
// 不要递归更新自己
if (Array.isArray(syncFields)) {
syncFields.forEach(field => {
if (name?.startsWith(field)) {
values = {...values};
deleteVariable(values, name);
}
});
}
let value = this.getValueAsArray(); let value = this.getValueAsArray();
value[index] = flat ? values.flat : {...values}; value[index] = flat ? values.flat : {...values};
@ -795,6 +826,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) { handleFormInit(values: any, {index}: any) {
const { const {
syncDefaultValue, syncDefaultValue,
@ -804,9 +840,27 @@ export default class ComboControl extends React.Component<ComboProps> {
formInited, formInited,
onChange, onChange,
submitOnChange, submitOnChange,
setPrinstineValue setPrinstineValue,
formItem,
name,
syncFields
} = this.props; } = this.props;
// 不要递归更新自己
if (Array.isArray(syncFields)) {
syncFields.forEach(field => {
if (name?.startsWith(field)) {
values = {...values};
deleteVariable(values, name);
}
});
}
// 已经开始验证了,那么打开成员的时候,就要验证一下。
if (formItem?.validated) {
this.subForms[index]?.validate(true, false, false);
}
this.subFormDefaultValues.push({ this.subFormDefaultValues.push({
index, index,
values, values,
@ -879,7 +933,13 @@ export default class ComboControl extends React.Component<ComboProps> {
} }
validate(): any { validate(): any {
const {messages, nullable, translate: __} = this.props; const {
messages,
nullable,
value: rawValue,
translate: __,
store
} = this.props;
const value = this.getValueAsArray(); const value = this.getValueAsArray();
const minLength = this.resolveVariableProps(this.props, 'minLength'); const minLength = this.resolveVariableProps(this.props, 'minLength');
const maxLength = this.resolveVariableProps(this.props, 'maxLength'); const maxLength = this.resolveVariableProps(this.props, 'maxLength');
@ -894,18 +954,62 @@ export default class ComboControl extends React.Component<ComboProps> {
(messages && messages.maxLengthValidateFailed) || 'Combo.maxLength', (messages && messages.maxLengthValidateFailed) || 'Combo.maxLength',
{maxLength} {maxLength}
); );
} else if (this.subForms.length && (!nullable || value)) { } else if (nullable && !rawValue) {
return Promise.all(this.subForms.map(item => item.validate())).then( return; // 不校验
values => { } else if (value.length) {
if (~values.indexOf(false)) { return Promise.all(
return __( value.map(async (values: any, index: number) => {
(messages && messages.validateFailed) || 'validateFailed' 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;
});
} }
} }
@ -1251,6 +1355,12 @@ export default class ComboControl extends React.Component<ComboProps> {
// 不能按需渲染,因为 unique 会失效。 // 不能按需渲染,因为 unique 会失效。
mountOnEnter={!hasUnique} mountOnEnter={!hasUnique}
unmountOnExit={false} unmountOnExit={false}
className={
store.memberValidMap[index] === false ? 'has-error' : ''
}
tabClassName={
store.memberValidMap[index] === false ? 'has-error' : ''
}
> >
{condition && typeSwitchable !== false ? ( {condition && typeSwitchable !== false ? (
<div className={cx('Combo-itemTag')}> <div className={cx('Combo-itemTag')}>
@ -1483,7 +1593,8 @@ export default class ComboControl extends React.Component<ComboProps> {
itemClassName, itemClassName,
itemsWrapperClassName, itemsWrapperClassName,
static: isStatic, static: isStatic,
mobileUI mobileUI,
store
} = this.props; } = this.props;
let items = this.props.items; let items = this.props.items;
@ -1541,7 +1652,11 @@ export default class ComboControl extends React.Component<ComboProps> {
return ( return (
<div <div
className={cx(`Combo-item`, itemClassName)} className={cx(
`Combo-item`,
itemClassName,
store.memberValidMap[index] === false ? 'has-error' : ''
)}
key={this.keys[index]} key={this.keys[index]}
> >
{!isStatic && !disabled && draggable && thelist.length > 1 ? ( {!isStatic && !disabled && draggable && thelist.length > 1 ? (
@ -1620,7 +1735,8 @@ export default class ComboControl extends React.Component<ComboProps> {
nullable, nullable,
translate: __, translate: __,
itemClassName, itemClassName,
mobileUI mobileUI,
store
} = this.props; } = this.props;
let items = this.props.items; let items = this.props.items;
@ -1644,7 +1760,13 @@ export default class ComboControl extends React.Component<ComboProps> {
disabled ? 'is-disabled' : '' disabled ? 'is-disabled' : ''
)} )}
> >
<div className={cx(`Combo-item`, itemClassName)}> <div
className={cx(
`Combo-item`,
itemClassName,
store.memberValidMap[0] === false ? 'has-error' : ''
)}
>
{condition && typeSwitchable !== false ? ( {condition && typeSwitchable !== false ? (
<div className={cx('Combo-itemTag')}> <div className={cx('Combo-itemTag')}>
<label>{__('Combo.type')}</label> <label>{__('Combo.type')}</label>
@ -1712,14 +1834,17 @@ export default class ComboControl extends React.Component<ComboProps> {
className: cx(`Combo-form`, formClassName) className: cx(`Combo-form`, formClassName)
}, },
{ {
index: 0,
disabled: disabled, disabled: disabled,
static: isStatic, static: isStatic,
data, data,
onChange: this.handleSingleFormChange, onChange: this.handleSingleFormChange,
ref: this.makeFormRef(0), ref: this.makeFormRef(0),
onValidChange: this.handleSubFormValid,
onInit: this.handleSingleFormInit, onInit: this.handleSingleFormInit,
canAccessSuperData, canAccessSuperData,
formStore: undefined formStore: undefined,
updatePristineAfterStoreDataReInit: false
} }
); );
} else if (multiple && index !== undefined && index >= 0) { } else if (multiple && index !== undefined && index >= 0) {
@ -1744,13 +1869,15 @@ export default class ComboControl extends React.Component<ComboProps> {
onAction: this.handleAction, onAction: this.handleAction,
onRadioChange: this.handleRadioChange, onRadioChange: this.handleRadioChange,
ref: this.makeFormRef(index), ref: this.makeFormRef(index),
onValidChange: this.handleSubFormValid,
canAccessSuperData, canAccessSuperData,
lazyChange: changeImmediately ? false : true, lazyChange: changeImmediately ? false : true,
formLazyChange: false, formLazyChange: false,
value: undefined, value: undefined,
formItemValue: undefined, formItemValue: undefined,
formStore: undefined, formStore: undefined,
...(tabsMode ? {} : {lazyLoad}) ...(tabsMode ? {} : {lazyLoad}),
updatePristineAfterStoreDataReInit: false
} }
); );
} }

View File

@ -355,15 +355,12 @@ export default observer((props: TableRowProps) => {
id={item.id} id={item.id}
newIndex={item.newIndex} newIndex={item.newIndex}
isHover={item.isHover} isHover={item.isHover}
partial={item.partial}
checked={item.checked} checked={item.checked}
modified={item.modified} modified={item.modified}
moved={item.moved} moved={item.moved}
depth={item.depth} depth={item.depth}
expandable={item.expandable} expandable={item.expandable}
checkdisable={item.checkdisable} checkdisable={item.checkdisable}
loading={item.loading}
error={item.error}
// data 在 TableRow 里面没有使用,这里写上是为了当列数据变化的时候 TableRow 重新渲染, // data 在 TableRow 里面没有使用,这里写上是为了当列数据变化的时候 TableRow 重新渲染,
// 不是 item.locals 的原因是 item.locals 会变化多次,比如父级上下文变化也会进来,但是 item.data 只会变化一次。 // 不是 item.locals 的原因是 item.locals 会变化多次,比如父级上下文变化也会进来,但是 item.data 只会变化一次。
data={canAccessSuperData ? item.locals : item.data} data={canAccessSuperData ? item.locals : item.data}