From 789fa8cf1e83418305e3ac3b0ea3c92b87510d93 Mon Sep 17 00:00:00 2001 From: lorne <1991wangliang@gmail.com> Date: Mon, 17 Aug 2026 10:50:23 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E7=A7=BB=E5=8A=A8?= =?UTF-8?q?=E7=AB=AF=E8=BD=AC=E5=8A=9E=E5=8A=A8=E4=BD=9C=E5=B4=A9=E6=BA=83?= =?UTF-8?q?=E5=B9=B6=E8=A1=A5=E9=BD=90=20triggerFrontEvent=20=E8=B7=AF?= =?UTF-8?q?=E5=BE=84=E5=AE=A1=E6=89=B9=E6=93=8D=E4=BD=9C=E6=8B=A6=E6=88=AA?= =?UTF-8?q?=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 问题一:转办动作选人弹框崩溃 - flow-mobile-approval 的 transfer.tsx 渲染业务方覆盖视图时未透传 props,仅转办缺失(add-audit/delegate/PC 端均传 {...props}), 导致覆盖视图 action=undefined,useEffect 依赖 [action.id] 求值崩溃。 - 修复:transfer.tsx 渲染 ActionView 时补传 {...props}。 问题二:triggerFrontEvent 自定义按钮绕过拦截器 - 该路径不调用 action(),移动端此前直接 emit,未执行审批操作拦截器 (PC 端已处理)。新增 dispatchApprovalFrontEvent:全部拦截器放行后 才派发事件,任一拦截返回 false 则终止,与 PC 语义对齐。 - 接入 flow-approval-actions.tsx 的 handlerAction 与 footer 按钮两处派发点。 均为 TDD 先行:新增 4 个用例覆盖 props 透传与拦截器放行/拦截行为。 GitHub: fix #46 Co-Authored-By: Claude --- .../mobile-transfer-props-and-interceptor.md | 5 ++ .../components/action-front-event.ts | 27 +++++++ .../components/action/transfer.tsx | 1 + .../components/flow-approval-actions.tsx | 6 +- .../tests/action-front-event.test.ts | 72 +++++++++++++++++++ .../transfer-action-forward-props.test.tsx | 64 +++++++++++++++++ 6 files changed, 173 insertions(+), 2 deletions(-) create mode 100644 .changeset/mobile-transfer-props-and-interceptor.md create mode 100644 packages/flow-mobile/flow-mobile-approval/src/components/flow-approval/components/action-front-event.ts create mode 100644 packages/flow-mobile/flow-mobile-approval/tests/action-front-event.test.ts create mode 100644 packages/flow-mobile/flow-mobile-approval/tests/transfer-action-forward-props.test.tsx diff --git a/.changeset/mobile-transfer-props-and-interceptor.md b/.changeset/mobile-transfer-props-and-interceptor.md new file mode 100644 index 0000000..75578d1 --- /dev/null +++ b/.changeset/mobile-transfer-props-and-interceptor.md @@ -0,0 +1,5 @@ +--- +"@coding-flow/flow-mobile-approval": patch +--- + +修复移动端转办(transfer)动作渲染覆盖视图时未透传 props 导致选人弹框 `action` 丢失崩溃;并为 `triggerFrontEvent` 自定义按钮补齐审批操作拦截器(先执行 `interceptAction`,全部放行后才派发事件,与 PC 语义对齐)。 \ No newline at end of file diff --git a/packages/flow-mobile/flow-mobile-approval/src/components/flow-approval/components/action-front-event.ts b/packages/flow-mobile/flow-mobile-approval/src/components/flow-approval/components/action-front-event.ts new file mode 100644 index 0000000..fd27ef3 --- /dev/null +++ b/packages/flow-mobile/flow-mobile-approval/src/components/flow-approval/components/action-front-event.ts @@ -0,0 +1,27 @@ +import { EventBus } from "@coding-flow/flow-core"; +import { FlowActionPresenter } from "@coding-flow/flow-approval-presenter"; + +/** + * 派发前端触发事件前执行审批操作拦截器(与 PC 端语义一致)。 + * + * 配置了 `triggerFrontEvent` 的自定义按钮不调用 `action()`,因此不会自动执行拦截器。 + * 本方法手动执行 `interceptAction`:全部拦截器放行后才派发事件, + * 任一拦截器返回 false 则终止本次派发。 + * + * @param actionPresenter 审批动作 Presenter(用于执行拦截器) + * @param actionId 触发的动作 ID + * @param triggerFrontEvent 前端触发事件名 + * @returns 是否放行并已派发事件 + */ +export async function dispatchApprovalFrontEvent( + actionPresenter: FlowActionPresenter, + actionId: string, + triggerFrontEvent: string, +): Promise { + const passed = await actionPresenter.interceptAction(actionId); + if (!passed) { + return false; + } + EventBus.getInstance().emit(triggerFrontEvent); + return true; +} \ No newline at end of file diff --git a/packages/flow-mobile/flow-mobile-approval/src/components/flow-approval/components/action/transfer.tsx b/packages/flow-mobile/flow-mobile-approval/src/components/flow-approval/components/action/transfer.tsx index e32387d..1cb05a0 100644 --- a/packages/flow-mobile/flow-mobile-approval/src/components/flow-approval/components/action/transfer.tsx +++ b/packages/flow-mobile/flow-mobile-approval/src/components/flow-approval/components/action/transfer.tsx @@ -82,6 +82,7 @@ export const TransferAction: React.FC = (props) => { if (ActionView) { return ( ) } diff --git a/packages/flow-mobile/flow-mobile-approval/src/components/flow-approval/components/flow-approval-actions.tsx b/packages/flow-mobile/flow-mobile-approval/src/components/flow-approval/components/flow-approval-actions.tsx index 8bec78c..b401994 100644 --- a/packages/flow-mobile/flow-mobile-approval/src/components/flow-approval/components/flow-approval-actions.tsx +++ b/packages/flow-mobile/flow-mobile-approval/src/components/flow-approval/components/flow-approval-actions.tsx @@ -6,6 +6,7 @@ import { ActionSheet, Button, Space, Toast } from "antd-mobile"; import { RevokeAction } from "@/components/flow-approval/components/action/revoke"; import { UrgeAction } from "@/components/flow-approval/components/action/urge"; import { ActionFactory } from "@/components/flow-approval/components/action/factory"; +import { dispatchApprovalFrontEvent } from "@/components/flow-approval/components/action-front-event"; import { EventBus, ObjectUtils, FlowMessageKey, FlowMessageRegistry } from "@coding-flow/flow-core"; import { useApprovalContext } from "@coding-flow/flow-approval-presenter"; @@ -33,7 +34,8 @@ export const FlowApprovalActions = () => { if (action) { const triggerFrontEvent = action.triggerFrontEvent; if (triggerFrontEvent) { - EventBus.getInstance().emit(triggerFrontEvent); + // 前端触发事件不经过 action(),需手动执行拦截器:全部放行后才派发事件 + dispatchApprovalFrontEvent(actionPresenter, id, triggerFrontEvent); } else { EventBus.getInstance().emit(id); } @@ -67,7 +69,7 @@ export const FlowApprovalActions = () => { onClick={() => { const triggerFrontEvent = action.triggerFrontEvent; if (triggerFrontEvent) { - EventBus.getInstance().emit(triggerFrontEvent); + dispatchApprovalFrontEvent(actionPresenter, action.id, triggerFrontEvent); } else { handlerAction(action.id); } diff --git a/packages/flow-mobile/flow-mobile-approval/tests/action-front-event.test.ts b/packages/flow-mobile/flow-mobile-approval/tests/action-front-event.test.ts new file mode 100644 index 0000000..90f1469 --- /dev/null +++ b/packages/flow-mobile/flow-mobile-approval/tests/action-front-event.test.ts @@ -0,0 +1,72 @@ +import { describe, expect, it, rs } from "@rstest/core"; +import { EventBus } from "@coding-flow/flow-core"; +import { + dispatchApprovalFrontEvent, +} from "@/components/flow-approval/components/action-front-event"; +import { FlowActionPresenter } from "@coding-flow/flow-approval-presenter"; + +/** + * 构造一个只暴露 interceptAction 的假 Presenter。 + * 事件触发逻辑仅依赖拦截器放行结果,无需完整 Presenter 栈。 + */ +const buildPresenter = (interceptAction: () => Promise | boolean) => { + return { + interceptAction: rs.fn(interceptAction), + } as unknown as FlowActionPresenter; +}; + +describe.sequential('移动端前端触发事件(triggerFrontEvent)拦截器', () => { + + it('拦截器全部放行后派发事件', async () => { + // given:一个放行的拦截器 + 事件监听 + const presenter = buildPresenter(async () => true); + let emitted = 0; + EventBus.getInstance().on('front-event-b', () => { + emitted += 1; + }); + + // when + const passed = await dispatchApprovalFrontEvent(presenter, 'custom-1', 'front-event-b'); + + // then:放行且事件已派发 + expect(passed).toBe(true); + expect(presenter.interceptAction).toHaveBeenCalledWith('custom-1'); + expect(emitted).toBe(1); + EventBus.getInstance().off('front-event-b'); + }); + + it('任一拦截器拦截则不派发事件', async () => { + // given:一个拦截的拦截器 + 事件监听 + const presenter = buildPresenter(async () => false); + let emitted = 0; + EventBus.getInstance().on('front-event-blocked', () => { + emitted += 1; + }); + + // when + const passed = await dispatchApprovalFrontEvent(presenter, 'custom-1', 'front-event-blocked'); + + // then:被拦截且未派发事件 + expect(passed).toBe(false); + expect(presenter.interceptAction).toHaveBeenCalledWith('custom-1'); + expect(emitted).toBe(0); + EventBus.getInstance().off('front-event-blocked'); + }); + + it('放行拦截器返回空 payload 也可派发', async () => { + // given + const presenter = buildPresenter(() => true); + let emitted = 0; + EventBus.getInstance().on('front-event-ok', () => { + emitted += 1; + }); + + // when + const passed = await dispatchApprovalFrontEvent(presenter, 'custom-ok', 'front-event-ok'); + + // then + expect(passed).toBe(true); + expect(emitted).toBe(1); + EventBus.getInstance().off('front-event-ok'); + }); +}); \ No newline at end of file diff --git a/packages/flow-mobile/flow-mobile-approval/tests/transfer-action-forward-props.test.tsx b/packages/flow-mobile/flow-mobile-approval/tests/transfer-action-forward-props.test.tsx new file mode 100644 index 0000000..bb0afad --- /dev/null +++ b/packages/flow-mobile/flow-mobile-approval/tests/transfer-action-forward-props.test.tsx @@ -0,0 +1,64 @@ +import { afterEach, describe, expect, test, rs } from "@rstest/core"; +import { Provider } from "react-redux"; +import { render, cleanup } from "@testing-library/react"; +import { configureStore } from "@reduxjs/toolkit"; +import { + ApprovalContext, + approvalSlice, + updateState, +} from "@coding-flow/flow-approval-presenter"; +import { FlowAction } from "@coding-flow/flow-types"; +import { ViewBindPlugin } from "@coding-flow/flow-core"; +import { APPROVAL_ACTION_TRANSFER_KEY } from "@/components/flow-approval"; +import { TransferAction } from "@/components/flow-approval/components/action/transfer"; + +const buildTransferAction = (id: string): FlowAction => ({ + id, + title: '转办', + type: 'TRANSFER', + display: { title: '转办', style: 'primary', icon: '' }, + enable: true, +} as FlowAction); + +/** 覆盖视图收到的 props(结构上等同于 FlowActionProps) */ +interface FlowActionProps { + action: FlowAction; +} + +describe.sequential('移动端转办动作(TransferAction)覆盖视图 props 透传', () => { + + afterEach(() => { + cleanup(); + }); + + test('渲染覆盖视图时应透传 action(回归:未透传导致 PersonSelectModal 崩溃)', () => { + // given:注册一个 spy 覆盖视图,并搭建 redux + ApprovalContext 环境 + const transferAction = buildTransferAction('transfer-1'); + const spy = rs.fn((props: FlowActionProps) =>
转办覆盖视图
); + ViewBindPlugin.getInstance().register(APPROVAL_ACTION_TRANSFER_KEY, spy); + + const store = configureStore({ + reducer: { approval: approvalSlice.reducer }, + }); + store.dispatch(updateState({ flow: { actions: [transferAction] }, actionLoading: false })); + + // 最小可用的 context:TransferAction 仅在选中 ActionView 分支前读取 + // state.actionLoading 与 context.getPresenter().getFlowActionPresenter() + const fakeContext = { + getPresenter: () => ({ getFlowActionPresenter: () => null }), + }; + + render( + + + + + + ); + + // then:覆盖视图被渲染,且收到 action props + expect(spy).toHaveBeenCalledTimes(1); + const receivedProps = spy.mock.calls[0][0]; + expect(receivedProps.action).toBe(transferAction); + }); +}); \ No newline at end of file