From 5af57f77f256b0d4ceadfe0fdf48ce2b30cf6312 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8C=85=E5=91=A8=E6=B6=9B?= Date: Fri, 3 Jul 2026 13:41:59 -0700 Subject: [PATCH 1/2] fix(app-shell): stop double-toasting failed script/modal action errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit serverActionHandler toasted the action error itself AND returned { success:false, error }, which ActionRunner.handlePostExecution ALSO toasts — so a failed script action (e.g. a validation throw) showed two identical red toasts. apiHandler and flowHandler already only return the error and let the runner toast it; align serverActionHandler with them. --- .../useConsoleActionRuntime.test.tsx | 37 +++++++++++++++++++ .../src/hooks/useConsoleActionRuntime.tsx | 7 +++- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/packages/app-shell/src/hooks/__tests__/useConsoleActionRuntime.test.tsx b/packages/app-shell/src/hooks/__tests__/useConsoleActionRuntime.test.tsx index 393dbbd1a..c7007036f 100644 --- a/packages/app-shell/src/hooks/__tests__/useConsoleActionRuntime.test.tsx +++ b/packages/app-shell/src/hooks/__tests__/useConsoleActionRuntime.test.tsx @@ -44,12 +44,26 @@ vi.mock('../../views/ActionParamDialog', () => ({ ActionParamDialog: () => null vi.mock('../../views/ActionResultDialog', () => ({ ActionResultDialog: () => null })); vi.mock('../../views/FlowRunner', () => ({ FlowRunner: () => null })); +// Spy on the toast library so we can assert the handlers DON'T toast errors +// themselves (the ActionRunner's post-execution hook owns the error toast — +// toasting here too double-fires it). +vi.mock('sonner', () => { + const fn: any = vi.fn(); + fn.error = vi.fn(); + fn.success = vi.fn(); + return { toast: fn }; +}); + +import { toast } from 'sonner'; import { useConsoleActionRuntime, ConsoleActionRuntimeProvider } from '../useConsoleActionRuntime'; import { useAction, usePageVariables, PageVariablesProvider, PageVariableActionBridge } from '@object-ui/react'; beforeEach(() => { authFetchSpy.mockReset(); navigateSpy.mockReset(); + (toast as any).mockClear?.(); + (toast as any).error.mockClear(); + (toast as any).success.mockClear(); }); describe('useConsoleActionRuntime — authenticated handlers', () => { @@ -170,6 +184,29 @@ describe('useConsoleActionRuntime — authenticated handlers', () => { expect(res).toMatchObject({ success: true }); }); + it('serverActionHandler returns a failed action error WITHOUT toasting it (the ActionRunner owns the error toast — no double toast)', async () => { + // A script action that throws (e.g. lead_apply_convert validation) returns + // { success:false, error } from the server. The handler must NOT toast it — + // ActionRunner.handlePostExecution does, and toasting here too showed the + // error twice (the reported bug). + authFetchSpy.mockResolvedValue({ + ok: true, + status: 200, + json: async () => ({ success: false, error: '线索信息不完整,提交转商机申请前请补全:终端客户' }), + }); + const { result } = renderHook(() => + useConsoleActionRuntime({ dataSource: {}, objects: [], objectName: 'mtc_lead' }), + ); + + let res: any; + await act(async () => { + res = await result.current.serverActionHandler({ type: 'script', name: 'lead_apply_convert' } as any); + }); + + expect(res).toEqual({ success: false, error: '线索信息不完整,提交转商机申请前请补全:终端客户' }); + expect((toast as any).error).not.toHaveBeenCalled(); + }); + it('exposes ActionProvider props with the api/flow/script/modal handlers wired', () => { const { result } = renderHook(() => useConsoleActionRuntime({ dataSource: {}, objects: [], objectName: 'inv' }), diff --git a/packages/app-shell/src/hooks/useConsoleActionRuntime.tsx b/packages/app-shell/src/hooks/useConsoleActionRuntime.tsx index d54dcec31..eb4485417 100644 --- a/packages/app-shell/src/hooks/useConsoleActionRuntime.tsx +++ b/packages/app-shell/src/hooks/useConsoleActionRuntime.tsx @@ -510,7 +510,9 @@ export function useConsoleActionRuntime(opts: ConsoleActionRuntimeOptions): Cons if (!res.ok || (json && json.success === false)) { const errMsg = json?.error || `Action "${targetName}" failed (HTTP ${res.status})`; if (preOpenedTab) { try { preOpenedTab.close(); } catch { /* ignore */ } } - toast.error(errMsg); + // Don't toast here — the ActionRunner's post-execution hook surfaces + // `error` as a toast (see apiHandler/flowHandler, which likewise only + // return). Toasting here too double-fires the error (two identical toasts). return { success: false, error: errMsg }; } const shouldRefresh = action.refreshAfter !== false; @@ -544,7 +546,8 @@ export function useConsoleActionRuntime(opts: ConsoleActionRuntimeOptions): Cons } catch (error) { if (preOpenedTab) { try { preOpenedTab.close(); } catch { /* ignore */ } } const msg = (error as Error).message; - toast.error(msg); + // The ActionRunner's post-execution hook toasts `error`; returning it here + // (without a local toast.error) avoids the double toast. return { success: false, error: msg }; } finally { serverActionInFlight.current.delete(inflightKey); From d09ba7c727188487941907dfc6ada652ae017f13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8C=85=E5=91=A8=E6=B6=9B?= Date: Fri, 3 Jul 2026 13:42:53 -0700 Subject: [PATCH 2/2] chore: changeset for dedupe action error toast --- .changeset/dedupe-script-action-error-toast.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .changeset/dedupe-script-action-error-toast.md diff --git a/.changeset/dedupe-script-action-error-toast.md b/.changeset/dedupe-script-action-error-toast.md new file mode 100644 index 000000000..0ea1e2efa --- /dev/null +++ b/.changeset/dedupe-script-action-error-toast.md @@ -0,0 +1,14 @@ +--- +"@object-ui/app-shell": patch +--- + +fix(app-shell): stop double-toasting failed script/modal action errors + +`serverActionHandler` toasted the action error itself **and** returned +`{ success: false, error }`, which `ActionRunner.handlePostExecution` also +surfaces as a toast — so a failed script action (e.g. a validation throw) +showed two identical red toasts. + +`apiHandler` and `flowHandler` already only return the error and let the +runner own the toast; `serverActionHandler` now does the same, so a failed +action toasts exactly once.