Skip to content

Commit 9aec681

Browse files
authored
fix(app-shell): stop double-toasting failed script/modal action errors (#2177)
* 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 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. * chore: changeset for dedupe action error toast
1 parent 46af001 commit 9aec681

3 files changed

Lines changed: 56 additions & 2 deletions

File tree

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
"@object-ui/app-shell": patch
3+
---
4+
5+
fix(app-shell): stop double-toasting failed script/modal action errors
6+
7+
`serverActionHandler` toasted the action error itself **and** returned
8+
`{ success: false, error }`, which `ActionRunner.handlePostExecution` also
9+
surfaces as a toast — so a failed script action (e.g. a validation throw)
10+
showed two identical red toasts.
11+
12+
`apiHandler` and `flowHandler` already only return the error and let the
13+
runner own the toast; `serverActionHandler` now does the same, so a failed
14+
action toasts exactly once.

packages/app-shell/src/hooks/__tests__/useConsoleActionRuntime.test.tsx

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,26 @@ vi.mock('../../views/ActionParamDialog', () => ({ ActionParamDialog: () => null
4444
vi.mock('../../views/ActionResultDialog', () => ({ ActionResultDialog: () => null }));
4545
vi.mock('../../views/FlowRunner', () => ({ FlowRunner: () => null }));
4646

47+
// Spy on the toast library so we can assert the handlers DON'T toast errors
48+
// themselves (the ActionRunner's post-execution hook owns the error toast —
49+
// toasting here too double-fires it).
50+
vi.mock('sonner', () => {
51+
const fn: any = vi.fn();
52+
fn.error = vi.fn();
53+
fn.success = vi.fn();
54+
return { toast: fn };
55+
});
56+
57+
import { toast } from 'sonner';
4758
import { useConsoleActionRuntime, ConsoleActionRuntimeProvider } from '../useConsoleActionRuntime';
4859
import { useAction, usePageVariables, PageVariablesProvider, PageVariableActionBridge } from '@object-ui/react';
4960

5061
beforeEach(() => {
5162
authFetchSpy.mockReset();
5263
navigateSpy.mockReset();
64+
(toast as any).mockClear?.();
65+
(toast as any).error.mockClear();
66+
(toast as any).success.mockClear();
5367
});
5468

5569
describe('useConsoleActionRuntime — authenticated handlers', () => {
@@ -170,6 +184,29 @@ describe('useConsoleActionRuntime — authenticated handlers', () => {
170184
expect(res).toMatchObject({ success: true });
171185
});
172186

187+
it('serverActionHandler returns a failed action error WITHOUT toasting it (the ActionRunner owns the error toast — no double toast)', async () => {
188+
// A script action that throws (e.g. lead_apply_convert validation) returns
189+
// { success:false, error } from the server. The handler must NOT toast it —
190+
// ActionRunner.handlePostExecution does, and toasting here too showed the
191+
// error twice (the reported bug).
192+
authFetchSpy.mockResolvedValue({
193+
ok: true,
194+
status: 200,
195+
json: async () => ({ success: false, error: '线索信息不完整,提交转商机申请前请补全:终端客户' }),
196+
});
197+
const { result } = renderHook(() =>
198+
useConsoleActionRuntime({ dataSource: {}, objects: [], objectName: 'mtc_lead' }),
199+
);
200+
201+
let res: any;
202+
await act(async () => {
203+
res = await result.current.serverActionHandler({ type: 'script', name: 'lead_apply_convert' } as any);
204+
});
205+
206+
expect(res).toEqual({ success: false, error: '线索信息不完整,提交转商机申请前请补全:终端客户' });
207+
expect((toast as any).error).not.toHaveBeenCalled();
208+
});
209+
173210
it('exposes ActionProvider props with the api/flow/script/modal handlers wired', () => {
174211
const { result } = renderHook(() =>
175212
useConsoleActionRuntime({ dataSource: {}, objects: [], objectName: 'inv' }),

packages/app-shell/src/hooks/useConsoleActionRuntime.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -510,7 +510,9 @@ export function useConsoleActionRuntime(opts: ConsoleActionRuntimeOptions): Cons
510510
if (!res.ok || (json && json.success === false)) {
511511
const errMsg = json?.error || `Action "${targetName}" failed (HTTP ${res.status})`;
512512
if (preOpenedTab) { try { preOpenedTab.close(); } catch { /* ignore */ } }
513-
toast.error(errMsg);
513+
// Don't toast here — the ActionRunner's post-execution hook surfaces
514+
// `error` as a toast (see apiHandler/flowHandler, which likewise only
515+
// return). Toasting here too double-fires the error (two identical toasts).
514516
return { success: false, error: errMsg };
515517
}
516518
const shouldRefresh = action.refreshAfter !== false;
@@ -544,7 +546,8 @@ export function useConsoleActionRuntime(opts: ConsoleActionRuntimeOptions): Cons
544546
} catch (error) {
545547
if (preOpenedTab) { try { preOpenedTab.close(); } catch { /* ignore */ } }
546548
const msg = (error as Error).message;
547-
toast.error(msg);
549+
// The ActionRunner's post-execution hook toasts `error`; returning it here
550+
// (without a local toast.error) avoids the double toast.
548551
return { success: false, error: msg };
549552
} finally {
550553
serverActionInFlight.current.delete(inflightKey);

0 commit comments

Comments
 (0)