mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-19 21:32:20 +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(
|
export function useProviderConnections(
|
||||||
providerId: string,
|
providerId: string,
|
||||||
isCompatible: boolean,
|
isCompatible: boolean,
|
||||||
isSearchProvider: boolean
|
_isSearchProvider: boolean
|
||||||
): UseProviderConnectionsReturn {
|
): UseProviderConnectionsReturn {
|
||||||
const t = useTranslations("providers");
|
const t = useTranslations("providers");
|
||||||
const notify = useNotificationStore();
|
const notify = useNotificationStore();
|
||||||
@@ -844,7 +844,8 @@ export function useProviderConnections(
|
|||||||
setSelectedIds(new Set());
|
setSelectedIds(new Set());
|
||||||
await fetchConnections();
|
await fetchConnections();
|
||||||
notify.success(t("batchDeleteSuccess", { count }));
|
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 {
|
} else {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
notify.error(data.error || providerText(t, "batchDeleteFailed", "Batch delete failed"));
|
notify.error(data.error || providerText(t, "batchDeleteFailed", "Batch delete failed"));
|
||||||
|
|||||||
@@ -275,7 +275,7 @@ export function ConfirmModal({
|
|||||||
<Button variant="ghost" onClick={onClose} disabled={loading}>
|
<Button variant="ghost" onClick={onClose} disabled={loading}>
|
||||||
{resolvedCancelText}
|
{resolvedCancelText}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant={variant} onClick={onConfirm} loading={loading}>
|
<Button variant={variant} onClick={() => void onConfirm()} loading={loading}>
|
||||||
{resolvedConfirmText}
|
{resolvedConfirmText}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -349,6 +349,7 @@
|
|||||||
"tests/unit/repro-antigravity-404-family-cooldown-hijack.test.ts",
|
"tests/unit/repro-antigravity-404-family-cooldown-hijack.test.ts",
|
||||||
"tests/unit/repro-combo-persisted-cooldown-preskip.test.ts",
|
"tests/unit/repro-combo-persisted-cooldown-preskip.test.ts",
|
||||||
"tests/unit/repro-glm-iso-reset-24h-cap.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/resilience-connections.test.ts",
|
||||||
"tests/unit/responses-handler.test.ts",
|
"tests/unit/responses-handler.test.ts",
|
||||||
"tests/unit/responses-passthrough-openai-compatible.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