diff --git a/docs/zh-CN/concepts/event-action.md b/docs/zh-CN/concepts/event-action.md index c43402a53..e2a1ca1dd 100644 --- a/docs/zh-CN/concepts/event-action.md +++ b/docs/zh-CN/concepts/event-action.md @@ -3194,7 +3194,7 @@ http 请求动作执行结束后,后面的动作可以通过 `${responseResult | id | 组件 ID,即组件的 id 属性的值 | | path | 数据路径,即数据变量的路径 | -# 事件动作干预 +# 干预动作执行 事件动作干预是指执行完当前动作后,干预所监听事件默认处理逻辑和后续其他动作的执行。通过`preventDefault`、`stopPropagation`分别阻止监听事件默认行为和停止下一个动作执行。 @@ -3305,6 +3305,65 @@ http 请求动作执行结束后,后面的动作可以通过 `${responseResult } ``` +## 忽略动作报错继续执行 + +> `3.3.1` 及以上版本 + +默认情况下,尝试执行一个不存在的目标组件动作、JS 脚本执行错误等程序错误都会导致动作执行终止,可以通过`ignoreError: true`来忽略动作报错继续执行后面的动作。 + +```schema +{ + "type": "page", + "title": "第一个按钮发生错误直接阻塞执行,第二个按钮发生错误后仍然执行", + "body": [ + { + type: 'button', + label: '无法弹出提示', + level: 'primary', + className: 'mr-2', + onEvent: { + click: { + actions: [ + { + actionType: 'reload', + componentId: 'notfound' + }, + { + actionType: 'toast', + args: { + msg: 'okk' + } + } + ] + } + } + }, + { + type: 'button', + label: '可以弹出提示', + level: 'primary', + onEvent: { + click: { + actions: [ + { + actionType: 'reload', + componentId: 'notfound', + ignoreError: true + }, + { + actionType: 'toast', + args: { + msg: 'okk' + } + } + ] + } + } + } + ] +} +``` + # 自定义组件接入事件动作 需求场景主要是想要自定义组件的内部事件暴露出去,能够通过对事件的监听来执行所需动作,并希望自定义组件自身的动作能够被其他组件调用。接入方法是通过`props.dispatchEvent`派发自身的各种事件,使其具备更灵活的交互设计能力;通过实现`doAction`方法实现其他组件对其专属动作的调用,需要注意的是,此处依赖内部的 `Scoped Context`来实现自身的注册,可以参考 [组件间通信](../../docs/extend/custom-react#组件间通信)。 @@ -3321,3 +3380,4 @@ http 请求动作执行结束后,后面的动作可以通过 `${responseResult | stopPropagation | `boolean`\|[表达式](../concepts/expression)\|[ConditionBuilder](../../components/form/condition-builder) | false | 停止后续动作执行,`> 1.10.0 及以上版本支持表达式,> 2.9.0 及以上版本支持ConditionBuilder` | | expression | `boolean`\|[表达式](../concepts/expression)\|[ConditionBuilder](../../components/form/condition-builder) | - | 执行条件,不设置表示默认执行,`> 1.10.0 及以上版本支持表达式,> 2.9.0 及以上版本支持ConditionBuilder` | | outputVar | `string` | - | 输出数据变量名 | +| ignoreError | `boolean` | false | 当动作执行出错后,是否忽略错误继续执行。`3.3.1 及以上版本支持` | diff --git a/packages/amis-core/src/actions/Action.ts b/packages/amis-core/src/actions/Action.ts index 71043182c..0a8f23382 100644 --- a/packages/amis-core/src/actions/Action.ts +++ b/packages/amis-core/src/actions/Action.ts @@ -24,6 +24,7 @@ export interface ListenerAction { description?: string; // 事件描述,actionType: broadcast componentId?: string; // 组件ID,用于直接执行指定组件的动作,指定多个组件时使用英文逗号分隔 componentName?: string; // 组件Name,用于直接执行指定组件的动作,指定多个组件时使用英文逗号分隔 + ignoreError?: boolean; // 当执行动作发生错误时,是否忽略并继续执行 args?: Record; // 动作配置,可以配置数据映射。注意:存在schema配置的动作都不能放在args里面,避免数据域不同导致的解析错误问题 data?: Record | null; // 动作数据参数,可以配置数据映射 dataMergeMode?: 'merge' | 'override'; // 参数模式,合并或者覆盖 @@ -181,8 +182,19 @@ export const runActions = async ( actionInstrance = getActionByType('component'); } - // 这些节点的子节点运行逻辑由节点内部实现 - await runAction(actionInstrance, actionConfig, renderer, event); + try { + // 这些节点的子节点运行逻辑由节点内部实现 + await runAction(actionInstrance, actionConfig, renderer, event); + } catch (e) { + const ignore = actionConfig.ignoreError ?? false; + if (!ignore) { + throw Error( + `${actionConfig.actionType} 动作执行失败,原因:${ + e.message || '未知' + }` + ); + } + } if (event.stoped) { break; diff --git a/packages/amis-core/src/actions/CmptAction.ts b/packages/amis-core/src/actions/CmptAction.ts index d47a3b449..c5cf2e495 100644 --- a/packages/amis-core/src/actions/CmptAction.ts +++ b/packages/amis-core/src/actions/CmptAction.ts @@ -43,7 +43,9 @@ export class CmptAction implements RendererAction { : null; if (key && !component) { - throw Error('目标组件没有找到,请检查componentId或componentName是否正确'); + throw Error( + '尝试执行一个不存在的目标组件动作,请检查目标组件非隐藏状态,且正确指定了componentId或componentName' + ); } if (action.actionType === 'setValue') { diff --git a/packages/amis/__tests__/event-action/prevent.test.tsx b/packages/amis/__tests__/event-action/prevent.test.tsx index 28278abbf..84b640aad 100644 --- a/packages/amis/__tests__/event-action/prevent.test.tsx +++ b/packages/amis/__tests__/event-action/prevent.test.tsx @@ -97,3 +97,70 @@ test('EventAction:prevent', async () => { expect(container).toMatchSnapshot(); expect(fetcher).not.toHaveBeenCalled(); }); + +test('EventAction:ignoreError', async () => { + const notify = jest.fn(); + const fetcher = jest.fn().mockImplementation(() => + Promise.resolve({ + data: { + status: 0, + msg: 'ok', + data: { + age: 18 + } + } + }) + ); + const {getByText, container}: any = render( + amisRender( + { + type: 'page', + body: [ + { + type: 'button', + label: '按钮', + level: 'primary', + onEvent: { + click: { + actions: [ + { + actionType: 'reload', + componentId: 'notfound', + ignoreError: true + }, + { + actionType: 'ajax', + api: '/api/test3' + }, + { + actionType: 'custom', + script: + "const myMsg = '我是自定义JS';\nsdfsdfsdf();\ndoAction({\n actionType: 'toast',\n args: {\n msg: myMsg\n }\n});\n", + ignoreError: true + }, + { + actionType: 'ajax', + api: '/api/test4' + } + ] + } + } + } + ] + }, + {}, + makeEnv({ + getModalContainer: () => container, + notify, + fetcher + }) + ) + ); + + fireEvent.click(getByText('按钮')); + await waitFor(() => { + expect(fetcher).toHaveBeenCalledTimes(2); + expect(fetcher.mock.calls[0][0].url).toEqual('/api/test3'); + expect(fetcher.mock.calls[1][0].url).toEqual('/api/test4'); + }); +}); diff --git a/packages/amis/src/renderers/Table/ColGroup.tsx b/packages/amis/src/renderers/Table/ColGroup.tsx index a631d8b1b..2b47a4826 100644 --- a/packages/amis/src/renderers/Table/ColGroup.tsx +++ b/packages/amis/src/renderers/Table/ColGroup.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import type {IColumn, ITableStore} from 'amis-core/lib/store/table'; +import type {IColumn, ITableStore} from 'amis-core'; import {observer} from 'mobx-react'; export function ColGroup({