Skip to content

Commit

Permalink
feat: Action支持动作结束前禁用,Form支持清除校验错误动作、支持校验错误时返回RawError、api存在,且SendOn生…
Browse files Browse the repository at this point in the history
…效才触发提交成功动作、words/tags支持labelTpl
  • Loading branch information
hezhihang committed Dec 18, 2024
1 parent 856539e commit 0861289
Show file tree
Hide file tree
Showing 15 changed files with 288 additions and 99 deletions.
42 changes: 42 additions & 0 deletions docs/zh-CN/components/form/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -2521,3 +2521,45 @@ Form 支持轮询初始化接口,步骤如下:
]
}
```

### clearError

> 6.11.1 及以上版本
> 清除表单校验产生的错误状态
```schema
{
type: 'page',
title: '清除表单错误',
body: [
{
type: 'form',
title: '表单',
id: 'form',
body: [
{
type: 'input-text',
label: '必填项',
name: 'required',
required: true
}
]
},
{
type: 'button',
label: '清除错误',
level: 'primary',
onEvent: {
click: {
actions: [
{
actionType: 'clearError',
componentId: 'form'
}
]
}
}
}
]
}
```
63 changes: 37 additions & 26 deletions packages/amis-core/src/renderers/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,11 @@ import {

import {IComboStore} from '../store/combo';
import {dataMapping} from '../utils/tpl-builtin';
import {isApiOutdated, isEffectiveApi} from '../utils/api';
import {
isApiOutdated,
isEffectiveApi,
shouldBlockedBySendOnApi
} from '../utils/api';
import LazyComponent from '../components/LazyComponent';
import {isAlive} from 'mobx-state-tree';

Expand Down Expand Up @@ -1319,7 +1323,7 @@ export default class Form extends React.Component<FormProps, object> {
}

// 走到这里代表校验成功了
dispatchEvent('validateSucc', this.props.data);
dispatchEvent('validateSucc', createObject(this.props.data, values));

if (target) {
this.submitToTarget(filterTarget(target, values), values);
Expand Down Expand Up @@ -1439,6 +1443,9 @@ export default class Form extends React.Component<FormProps, object> {
__response: response
});
});
} else if (shouldBlockedBySendOnApi(action.api || api, values)) {
// api存在,但是不满足sendOn时,走这里,不派发submitSucc事件
return;
} else {
clearPersistDataAfterSubmit && store.clearLocalPersistData();
// type为submit,但是没有配api以及target时,只派发事件
Expand Down Expand Up @@ -2314,30 +2321,7 @@ export default class Form extends React.Component<FormProps, object> {
}
}

@Renderer({
type: 'form',
storeType: FormStore.name,
isolateScope: true,
storeExtendsData: (props: any) => props.inheritData,
shouldSyncSuperStore: (store, props, prevProps) => {
// 如果是 QuickEdit,让 store 同步 __super 数据。
if (
props.quickEditFormRef &&
props.onQuickChange &&
(isObjectShallowModified(prevProps.data, props.data) ||
isObjectShallowModified(prevProps.data.__super, props.data.__super) ||
isObjectShallowModified(
prevProps.data.__super?.__super,
props.data.__super?.__super
))
) {
return true;
}

return undefined;
}
})
export class FormRenderer extends Form {
export class _FormRenderer extends Form {
static contextType = ScopedContext;

constructor(props: FormProps, context: IScopedContext) {
Expand Down Expand Up @@ -2408,6 +2392,8 @@ export class FormRenderer extends Form {
);
})
);
} else if (action.actionType === 'clearError') {
return super.clearErrors();
} else {
return super.handleAction(e, action, ctx, throwErrors, delegate);
}
Expand Down Expand Up @@ -2528,3 +2514,28 @@ export class FormRenderer extends Form {
return this.getValues();
}
}

@Renderer({
type: 'form',
storeType: FormStore.name,
isolateScope: true,
storeExtendsData: (props: any) => props.inheritData,
shouldSyncSuperStore: (store, props, prevProps) => {
// 如果是 QuickEdit,让 store 同步 __super 数据。
if (
props.quickEditFormRef &&
props.onQuickChange &&
(isObjectShallowModified(prevProps.data, props.data) ||
isObjectShallowModified(prevProps.data.__super, props.data.__super) ||
isObjectShallowModified(
prevProps.data.__super?.__super,
props.data.__super?.__super
))
) {
return true;
}

return undefined;
}
})
export class FormRenderer extends _FormRenderer {}
19 changes: 16 additions & 3 deletions packages/amis-core/src/store/form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -549,9 +549,11 @@ export const FormStore = ServiceStore.named('FormStore')

// 10s 内不要重复弹同一个错误
const toastValidateError = throttle(
msg => {
(msg, validateError?: ValidateError) => {
const env = getEnv(self);
env.notify('error', msg);
env.notify('error', msg, {
validateError
});
},
10000,
{
Expand Down Expand Up @@ -699,7 +701,18 @@ export const FormStore = ServiceStore.named('FormStore')
dispatcher = yield dispatcher;
}
if (!dispatcher?.prevented) {
msg && toastValidateError(msg);
const validateError = new ValidateError(
failedMessage || self.__('Form.validateFailed'),
self.errors,
{
items: self.items,
msg: {
customMsg: failedMessage,
defaultMsg: self.__('Form.validateFailed')
}
}
);
msg && toastValidateError(msg, validateError);
}
}

Expand Down
6 changes: 4 additions & 2 deletions packages/amis-core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import type {JSONSchema7} from 'json-schema';
import {ListenerAction} from './actions/Action';
import {debounceConfig, trackConfig} from './utils/renderer-event';
import type {TestIdBuilder} from './utils/helper';
import type {TestIdBuilder, ValidateError} from './utils/helper';
import {AnimationsProps} from './utils/animations';

export interface Option {
Expand Down Expand Up @@ -373,7 +373,8 @@ export interface ActionObject extends ButtonObject {
| 'initDrag'
| 'cancelDrag'
| 'toggleExpanded'
| 'setExpanded';
| 'setExpanded'
| 'clearError';

api?: BaseApiObject | string;
asyncApi?: BaseApiObject | string;
Expand Down Expand Up @@ -542,6 +543,7 @@ export type ToastConf = {
className?: string;
items?: Array<any>;
useMobileUI?: boolean;
validateError?: ValidateError;
};

export interface OptionProps {
Expand Down
10 changes: 10 additions & 0 deletions packages/amis-core/src/utils/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -947,6 +947,16 @@ export function isEffectiveApi(
return false;
}

// 判断api是否存在,且SendOn为不发送
export function shouldBlockedBySendOnApi(api?: Api, data?: any) {
if (isObject(api) && (api as ApiObject).url) {
if ((api as ApiObject).sendOn && data) {
return !evalExpression((api as ApiObject).sendOn as string, data);
}
}
return false;
}

export function isSameApi(
apiA: ApiObject | ApiCacheConfig,
apiB: ApiObject | ApiCacheConfig
Expand Down
5 changes: 4 additions & 1 deletion packages/amis-core/src/utils/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1762,15 +1762,18 @@ export class SkipOperation extends Error {}
export class ValidateError extends Error {
name: 'ValidateError';
detail: {[propName: string]: Array<string> | string};
rawError?: {[propName: string]: any};

constructor(
message: string,
error: {[propName: string]: Array<string> | string}
error: {[propName: string]: Array<string> | string},
rawError?: {[propName: string]: any}
) {
super();
this.name = 'ValidateError';
this.message = message;
this.detail = error;
this.rawError = rawError;
}
}

Expand Down
7 changes: 6 additions & 1 deletion packages/amis-editor/src/plugin/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,12 @@ export class ButtonPlugin extends BasePlugin {
name: 'rightIcon',
label: '右侧图标'
}),
getSchemaTpl('badge')
getSchemaTpl('badge'),
getSchemaTpl('switch', {
name: 'disabledOnAction',
label: '动作完成前禁用',
value: false
})
]
},
getSchemaTpl('status', {
Expand Down
9 changes: 8 additions & 1 deletion packages/amis-editor/src/plugin/Form/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,11 @@ export class FormPlugin extends BasePlugin {
actionType: 'setValue',
description: '触发组件数据更新',
...getActionCommonProps('setValue')
},
{
actionLabel: '清除校验状态',
actionType: 'clearError',
description: '清除表单校验产生的错误状态'
}
];

Expand Down Expand Up @@ -651,7 +656,9 @@ export class FormPlugin extends BasePlugin {
/\/crud2\/filter\/form$/.test(context.path) ||
/body\/0\/filter$/.test(context.schemaPath);
/** 表单是否位于Dialog内 */
const isInDialog: boolean = context.path?.includes?.('dialog/');
const isInDialog: boolean =
context.path?.includes?.('dialog/') ||
context.path?.includes?.('drawer/');
/** 是否使用Panel包裹 */
const isWrapped = 'this.wrapWithPanel !== false';
const justifyLayout = (left: number = 2) => ({
Expand Down
9 changes: 8 additions & 1 deletion packages/amis-editor/src/plugin/Tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,14 @@ export class TabsPlugin extends BasePlugin {
body: []
}
],
mountOnEnter: true
mountOnEnter: true,
themeCss: {
titleControlClassName: {
'font:default': {
'text-align': 'center'
}
}
}
};
previewSchema = {
...this.scaffold
Expand Down
30 changes: 27 additions & 3 deletions packages/amis/__tests__/renderers/Form/formitem.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React = require('react');
import {render, fireEvent, cleanup} from '@testing-library/react';
import '../../../src';
import {render as amisRender} from '../../../src';
import {render as amisRender, ValidateError} from '../../../src';
import {wait, makeEnv} from '../../helper';
import {clearStoresCache} from '../../../src';
import moment from 'moment';
Expand Down Expand Up @@ -61,7 +61,19 @@ test('Renderer:FormItem:validateApi:success', async () => {
expect(onSubmit).not.toHaveBeenCalled();

await wait(300);
expect(notify).toHaveBeenCalledWith('error', '依赖的部分字段没有通过验证');
expect(notify).toHaveBeenCalledWith('error', '依赖的部分字段没有通过验证', {
validateError: new ValidateError(
'依赖的部分字段没有通过验证',
{},
{
items: [],
msg: {
customMsg: '依赖的部分字段没有通过验证',
defaultMsg: '依赖的部分字段没有通过验证'
}
}
)
});

const input = container.querySelector('input[name=a]');
expect(input).toBeTruthy();
Expand Down Expand Up @@ -130,7 +142,19 @@ test('Renderer:FormItem:validateApi:failed', async () => {
expect(onSubmit).not.toHaveBeenCalled();

await wait(300);
expect(notify).toHaveBeenCalledWith('error', '依赖的部分字段没有通过验证');
expect(notify).toHaveBeenCalledWith('error', '依赖的部分字段没有通过验证', {
validateError: new ValidateError(
'依赖的部分字段没有通过验证',
{},
{
items: [],
msg: {
customMsg: '依赖的部分字段没有通过验证',
defaultMsg: '依赖的部分字段没有通过验证'
}
}
)
});

const input = container.querySelector('input[name=a]');
expect(input).toBeTruthy();
Expand Down
30 changes: 27 additions & 3 deletions packages/amis/__tests__/renderers/Form/index.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {render, fireEvent, cleanup, waitFor} from '@testing-library/react';
import '../../../src';
import {render as amisRender} from '../../../src';
import {render as amisRender, ValidateError} from '../../../src';
import {wait, makeEnv} from '../../helper';
import {clearStoresCache} from '../../../src';

Expand Down Expand Up @@ -103,7 +103,19 @@ test('Renderer:Form:valdiate', async () => {
expect(container).toMatchSnapshot();
expect(onSubmit).not.toHaveBeenCalled();

expect(notify).toHaveBeenCalledWith('error', '依赖的部分字段没有通过验证');
expect(notify).toHaveBeenCalledWith('error', '依赖的部分字段没有通过验证', {
validateError: new ValidateError(
'依赖的部分字段没有通过验证',
{},
{
items: [],
msg: {
customMsg: '依赖的部分字段没有通过验证',
defaultMsg: '依赖的部分字段没有通过验证'
}
}
)
});

const input = container.querySelector('input[name=a]');
expect(input).toBeTruthy();
Expand Down Expand Up @@ -239,7 +251,19 @@ test('Renderer:Form:onValidate', async () => {
expect(onValidate).toHaveBeenCalled();
expect(onValidate.mock.calls[0][0]).toMatchSnapshot();

expect(notify).toHaveBeenCalledWith('error', '依赖的部分字段没有通过验证');
expect(notify).toHaveBeenCalledWith('error', '依赖的部分字段没有通过验证', {
validateError: new ValidateError(
'依赖的部分字段没有通过验证',
{},
{
items: [],
msg: {
customMsg: '依赖的部分字段没有通过验证',
defaultMsg: '依赖的部分字段没有通过验证'
}
}
)
});

fireEvent.click(getByText('Submit'));

Expand Down
Loading

0 comments on commit 0861289

Please sign in to comment.