mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-14 02:42:24 +03:00
fix(dashboard): batch delete no longer toasts failure after success (#12711)
Validado numa worktree combinada com as 16 PRs desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-file-size e check-changelog-integrity OK, complexity 2788/3218 e cognitive 1261/1437 (ambos sob a baseline), ESLint 0 erros nos 152 arquivos alterados, 771 testes unitários focados, 49 de integração e a suíte vitest:ui completa (2149) verdes. Além do bug do toast, esta PR foi a que derrubou os três base-reds vivos do tip: o fragmento `changelog.d/fixes/reset-aware-model-family.md` sem o `- ` inicial, o registro do `tests/unit/reset-aware-request-scope-12600.test.ts` no `stryker.conf.json` e o `TS2554` do glm. O `check-changelog-integrity` voltou a passar aqui por causa dela. O diagnóstico do MouseEvent é o que dá o valor: `onConfirm` chegava como handler de clique nativo e `handleBatchDeleteConfirm` tratava qualquer primeiro argumento truthy como callback. O cinto (`typeof`) e o suspensório (o wrap no ConfirmModal) juntos estão certos — só um dos dois deixaria a porta aberta para o próximo caller.
This commit is contained in:
2
changelog.d/fixes/12711-batch-delete-click-event.md
Normal file
2
changelog.d/fixes/12711-batch-delete-click-event.md
Normal file
@@ -0,0 +1,2 @@
|
||||
- **fix(dashboard):** batch-deleting provider keys no longer toasts failure after a successful delete when the confirm button's click event is forwarded as `onAfter` ([#12711](https://github.com/diegosouzapw/OmniRoute/pull/12711))
|
||||
- **fix(glm):** drop the extra 16th argument to `createSSETransformStreamWithLogger` that TypeScript rejected (TS2554) and that never reached the TransformStream
|
||||
@@ -1 +1 @@
|
||||
Keep Antigravity Gemini usable when the same connection's Claude weekly quota is empty; generic quota cache stays per-connection for every other provider.
|
||||
- Keep Antigravity Gemini usable when the same connection's Claude weekly quota is empty; generic quota cache stays per-connection for every other provider.
|
||||
|
||||
@@ -248,7 +248,7 @@ export interface UseProviderConnectionsReturn {
|
||||
export function useProviderConnections(
|
||||
providerId: string,
|
||||
isCompatible: boolean,
|
||||
isSearchProvider: boolean
|
||||
_isSearchProvider: boolean
|
||||
): UseProviderConnectionsReturn {
|
||||
const t = useTranslations("providers");
|
||||
const notify = useNotificationStore();
|
||||
@@ -844,7 +844,8 @@ export function useProviderConnections(
|
||||
setSelectedIds(new Set());
|
||||
await fetchConnections();
|
||||
notify.success(t("batchDeleteSuccess", { count }));
|
||||
if (onAfter) await onAfter();
|
||||
// ConfirmModal's onClick forwards a MouseEvent; only a real callback runs.
|
||||
if (typeof onAfter === "function") await onAfter();
|
||||
} else {
|
||||
const data = await res.json();
|
||||
notify.error(data.error || providerText(t, "batchDeleteFailed", "Batch delete failed"));
|
||||
|
||||
@@ -275,7 +275,7 @@ export function ConfirmModal({
|
||||
<Button variant="ghost" onClick={onClose} disabled={loading}>
|
||||
{resolvedCancelText}
|
||||
</Button>
|
||||
<Button variant={variant} onClick={onConfirm} loading={loading}>
|
||||
<Button variant={variant} onClick={() => void onConfirm()} loading={loading}>
|
||||
{resolvedConfirmText}
|
||||
</Button>
|
||||
</>
|
||||
|
||||
@@ -349,6 +349,7 @@
|
||||
"tests/unit/repro-antigravity-404-family-cooldown-hijack.test.ts",
|
||||
"tests/unit/repro-combo-persisted-cooldown-preskip.test.ts",
|
||||
"tests/unit/repro-glm-iso-reset-24h-cap.test.ts",
|
||||
"tests/unit/reset-aware-request-scope-12600.test.ts",
|
||||
"tests/unit/resilience-connections.test.ts",
|
||||
"tests/unit/responses-handler.test.ts",
|
||||
"tests/unit/responses-passthrough-openai-compatible.test.ts",
|
||||
|
||||
159
tests/unit/ui/batch-delete-click-event.test.tsx
Normal file
159
tests/unit/ui/batch-delete-click-event.test.tsx
Normal file
@@ -0,0 +1,159 @@
|
||||
// @vitest-environment jsdom
|
||||
/**
|
||||
* ConfirmModal wires onConfirm to <Button onClick={onConfirm}>. Native
|
||||
* buttons pass a MouseEvent. handleBatchDeleteConfirm's optional onAfter
|
||||
* used to treat any truthy first arg as a post-delete callback, so the
|
||||
* click event ran as `await onAfter()` after the success toast and the
|
||||
* catch block fired a second "batch delete failed" toast.
|
||||
*/
|
||||
import React, { act, useEffect } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("next/navigation", () => ({
|
||||
useParams: () => ({ id: "moonshot-native" }),
|
||||
useRouter: () => ({ push: vi.fn(), replace: vi.fn() }),
|
||||
usePathname: () => "/providers/moonshot-native",
|
||||
}));
|
||||
|
||||
vi.mock("next-intl", () => ({
|
||||
useTranslations: () => (key: string, values?: Record<string, unknown>) => {
|
||||
if (values) {
|
||||
return Object.entries(values).reduce(
|
||||
(acc, [k, v]) => acc.replace(`{${k}}`, String(v)),
|
||||
key
|
||||
);
|
||||
}
|
||||
return key;
|
||||
},
|
||||
}));
|
||||
|
||||
const notify = {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warning: vi.fn(),
|
||||
};
|
||||
vi.mock("@/store/notificationStore", () => ({
|
||||
useNotificationStore: () => notify,
|
||||
}));
|
||||
|
||||
const CONNECTIONS = [
|
||||
{ id: "conn-a", provider: "moonshot-native", name: "Key A" },
|
||||
{ id: "conn-b", provider: "moonshot-native", name: "Key B" },
|
||||
];
|
||||
|
||||
function jsonResponse(status: number, body: unknown) {
|
||||
return {
|
||||
ok: status >= 200 && status < 300,
|
||||
status,
|
||||
json: async () => body,
|
||||
text: async () => JSON.stringify(body),
|
||||
headers: { get: () => null },
|
||||
} as Response;
|
||||
}
|
||||
|
||||
function installFetchMock() {
|
||||
const fn = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const url = String(input);
|
||||
const method = (init?.method || "GET").toUpperCase();
|
||||
if ((url === "/api/providers" || url.startsWith("/api/providers?")) && method === "GET") {
|
||||
return jsonResponse(200, { connections: CONNECTIONS });
|
||||
}
|
||||
if (url === "/api/provider-nodes" && method === "GET") {
|
||||
return jsonResponse(200, { nodes: [] });
|
||||
}
|
||||
if (url === "/api/providers" && method === "DELETE") {
|
||||
return jsonResponse(200, { message: "Deleted 2 connection(s)", deleted: 2 });
|
||||
}
|
||||
return jsonResponse(200, {});
|
||||
});
|
||||
vi.stubGlobal("fetch", fn);
|
||||
return fn;
|
||||
}
|
||||
|
||||
const { useProviderConnections } =
|
||||
await import("@/app/(dashboard)/dashboard/providers/[id]/hooks/useProviderConnections");
|
||||
type HookResult = ReturnType<typeof useProviderConnections>;
|
||||
|
||||
describe("useProviderConnections — batch delete click event must not toast failure after success", () => {
|
||||
let container: HTMLElement;
|
||||
let root: ReturnType<typeof createRoot>;
|
||||
|
||||
beforeEach(() => {
|
||||
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT =
|
||||
true;
|
||||
container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
notify.success.mockClear();
|
||||
notify.error.mockClear();
|
||||
notify.info.mockClear();
|
||||
notify.warning.mockClear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
act(() => {
|
||||
root.unmount();
|
||||
});
|
||||
container.remove();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
async function mountHook() {
|
||||
let result: HookResult | null = null;
|
||||
|
||||
function TestWrapper() {
|
||||
const hookResult = useProviderConnections("moonshot-native", false, true);
|
||||
useEffect(() => {
|
||||
result = hookResult;
|
||||
}, [hookResult]);
|
||||
return <span />;
|
||||
}
|
||||
|
||||
await act(async () => {
|
||||
root.render(<TestWrapper />);
|
||||
});
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
});
|
||||
return () => result as HookResult;
|
||||
}
|
||||
|
||||
it("does not toast batchDeleteNetworkError when ConfirmModal forwards the click event", async () => {
|
||||
installFetchMock();
|
||||
const getHook = await mountHook();
|
||||
|
||||
await act(async () => {
|
||||
getHook().handleToggleSelectAll();
|
||||
});
|
||||
expect(getHook().selectedIds.size).toBe(2);
|
||||
|
||||
const clickEvent = { type: "click", preventDefault() {} } as unknown as MouseEvent;
|
||||
|
||||
await act(async () => {
|
||||
await getHook().handleBatchDeleteConfirm(clickEvent as never);
|
||||
});
|
||||
|
||||
expect(notify.success).toHaveBeenCalledTimes(1);
|
||||
expect(notify.error).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("still runs a real onAfter callback after a successful delete", async () => {
|
||||
installFetchMock();
|
||||
const getHook = await mountHook();
|
||||
const onAfter = vi.fn(async () => {});
|
||||
|
||||
await act(async () => {
|
||||
getHook().handleToggleSelectAll();
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await getHook().handleBatchDeleteConfirm(onAfter);
|
||||
});
|
||||
|
||||
expect(onAfter).toHaveBeenCalledTimes(1);
|
||||
expect(notify.success).toHaveBeenCalledTimes(1);
|
||||
expect(notify.error).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user