chore(ui): wire sonner Toaster; delegate antd managers to sonner

- Rewrite MessageManager (message_manager.tsx) to delegate to toast.* from
  sonner. Public API preserved (success/error/warning/info/loading/destroy).
  setMessageInstance() is now a no-op back-compat shim.
- Rewrite NotificationManager (notifications_manager.tsx) to delegate to
  sonner. Same classification logic (titleFor/classifyGeneralMessage) and
  same public API (error/warning/info/success/fromBackend/clear) preserved.
  setNotificationInstance() is now a no-op back-compat shim.
- antd duration (seconds) \u2192 sonner duration (ms) conversion at each call site.
- Gut AntdGlobalProvider to a children passthrough (antd message/notification
  provider is no longer needed). Will be deleted in the final cleanup task
  after antd is uninstalled.
- Add <Toaster richColors position="top-right" /> to the root layout.
- Rewrite message_manager and notifications_manager unit tests to mock
  sonner's toast.* rather than antd.message/notification. Use vi.unmock in
  notifications_manager test to bypass the global setupTests.ts mock.
- Fix stale antd.message.error assertion in AddFallbacks.test.tsx: mock
  @/components/molecules/message_manager directly so the assertion is
  agnostic to the underlying toast library.

Build and targeted vitest suites pass. Full vitest suite: 3801/3801 pass
after these fixes (2 pre-existing failures were caused by the migration
and are now fixed via the updated AddFallbacks test mock).

Co-authored-by: yuneng-jiang <yuneng-berri@users.noreply.github.com>
This commit is contained in:
Cursor Agent 2026-04-23 06:41:22 +00:00
parent a507e5514a
commit 966d7e0cc2
No known key found for this signature in database
7 changed files with 241 additions and 237 deletions

View file

@ -4,6 +4,7 @@ import "./globals.css";
import AntdGlobalProvider from "@/contexts/AntdGlobalProvider";
import ReactQueryProvider from "@/contexts/ReactQueryProvider";
import { Toaster } from "@/components/ui/sonner";
const inter = Inter({ subsets: ["latin"] });
@ -24,6 +25,7 @@ export default function RootLayout({
<ReactQueryProvider>
<AntdGlobalProvider>{children}</AntdGlobalProvider>
</ReactQueryProvider>
<Toaster richColors position="top-right" />
</body>
</html>
);

View file

@ -8,15 +8,22 @@ vi.mock("../../../playground/llm_calls/fetch_models", () => ({
fetchAvailableModels: vi.fn(),
}));
vi.mock("antd", async (importOriginal) => {
const actual = await importOriginal<typeof import("antd")>();
return {
...actual,
message: {
error: vi.fn(),
},
};
});
// MessageManager is the project's global message wrapper (delegates to sonner
// post phase-1 migration). Mock it as a first-class object so assertions don't
// care which underlying toast library is in use.
const mockMessageManager = vi.hoisted(() => ({
success: vi.fn(),
error: vi.fn(),
warning: vi.fn(),
info: vi.fn(),
loading: vi.fn(),
destroy: vi.fn(),
}));
vi.mock("@/components/molecules/message_manager", () => ({
default: mockMessageManager,
setMessageInstance: vi.fn(),
}));
vi.mock("./FallbackSelectionForm", () => ({
FallbackSelectionForm: ({ groups, onGroupsChange }: any) => {
@ -122,7 +129,6 @@ describe("AddFallbacks", () => {
it("should show error when saving incomplete groups", async () => {
const user = userEvent.setup();
const antd = await import("antd");
render(<AddFallbacks {...defaultProps} />);
const addButton = screen.getByRole("button", { name: /add fallbacks/i });
@ -141,13 +147,12 @@ describe("AddFallbacks", () => {
await user.click(saveButton);
await waitFor(() => {
expect(antd.message.error).toHaveBeenCalled();
expect(mockMessageManager.error).toHaveBeenCalled();
});
});
it("should show error message when saving incomplete groups", async () => {
const user = userEvent.setup();
const antd = await import("antd");
render(<AddFallbacks {...defaultProps} />);
const addButton = screen.getByRole("button", { name: /add fallbacks/i });
@ -161,7 +166,7 @@ describe("AddFallbacks", () => {
await user.click(saveButton);
await waitFor(() => {
expect(antd.message.error).toHaveBeenCalled();
expect(mockMessageManager.error).toHaveBeenCalled();
});
});

View file

@ -1,17 +1,17 @@
import { describe, expect, it, vi, beforeEach } from "vitest";
// Use vi.hoisted so the mock object is available when vi.mock is hoisted
const mockStaticMessage = vi.hoisted(() => ({
// Sonner is a module-level singleton; mock it before importing the manager.
const mockToast = vi.hoisted(() => ({
success: vi.fn(),
error: vi.fn(),
warning: vi.fn(),
info: vi.fn(),
loading: vi.fn(),
destroy: vi.fn(),
dismiss: vi.fn(),
}));
vi.mock("antd", () => ({
message: mockStaticMessage,
vi.mock("sonner", () => ({
toast: mockToast,
}));
import MessageManager, { setMessageInstance } from "./message_manager";
@ -21,87 +21,43 @@ describe("MessageManager", () => {
vi.clearAllMocks();
});
describe("when no instance is set (falls back to static message)", () => {
it("delegates success to static message", () => {
describe("delegates to sonner", () => {
it("delegates success", () => {
MessageManager.success("done!");
expect(mockStaticMessage.success).toHaveBeenCalledWith("done!", undefined);
expect(mockToast.success).toHaveBeenCalledWith("done!", { duration: undefined });
});
it("delegates error to static message", () => {
it("delegates error with duration (converts seconds → ms)", () => {
MessageManager.error("failed!", 5);
expect(mockStaticMessage.error).toHaveBeenCalledWith("failed!", 5);
expect(mockToast.error).toHaveBeenCalledWith("failed!", { duration: 5000 });
});
it("delegates warning to static message", () => {
it("delegates warning", () => {
MessageManager.warning("watch out");
expect(mockStaticMessage.warning).toHaveBeenCalledWith("watch out", undefined);
expect(mockToast.warning).toHaveBeenCalledWith("watch out", { duration: undefined });
});
it("delegates info to static message", () => {
MessageManager.info("fyi");
expect(mockStaticMessage.info).toHaveBeenCalledWith("fyi", undefined);
it("delegates info with duration", () => {
MessageManager.info("fyi", 2);
expect(mockToast.info).toHaveBeenCalledWith("fyi", { duration: 2000 });
});
it("delegates loading to static message", () => {
MessageManager.loading("loading...", 3);
expect(mockStaticMessage.loading).toHaveBeenCalledWith("loading...", 3);
it("delegates loading and returns the toast id", () => {
mockToast.loading.mockReturnValue("toast-id-42");
const result = MessageManager.loading("loading...", 3);
expect(mockToast.loading).toHaveBeenCalledWith("loading...", { duration: 3000 });
expect(result).toBe("toast-id-42");
});
it("delegates destroy to static message", () => {
it("delegates destroy to toast.dismiss()", () => {
MessageManager.destroy();
expect(mockStaticMessage.destroy).toHaveBeenCalled();
expect(mockToast.dismiss).toHaveBeenCalled();
});
});
describe("when a custom instance is set", () => {
const mockInstance = {
success: vi.fn(),
error: vi.fn(),
warning: vi.fn(),
info: vi.fn(),
loading: vi.fn(),
destroy: vi.fn(),
open: vi.fn(),
};
beforeEach(() => {
vi.clearAllMocks();
setMessageInstance(mockInstance as any);
});
it("delegates success to custom instance", () => {
MessageManager.success("done!");
expect(mockInstance.success).toHaveBeenCalledWith("done!", undefined);
expect(mockStaticMessage.success).not.toHaveBeenCalled();
});
it("delegates error with duration to custom instance", () => {
MessageManager.error("failed!", 5);
expect(mockInstance.error).toHaveBeenCalledWith("failed!", 5);
expect(mockStaticMessage.error).not.toHaveBeenCalled();
});
it("delegates warning to custom instance", () => {
MessageManager.warning("watch out");
expect(mockInstance.warning).toHaveBeenCalledWith("watch out", undefined);
});
it("delegates info to custom instance", () => {
MessageManager.info("fyi", 2);
expect(mockInstance.info).toHaveBeenCalledWith("fyi", 2);
});
it("delegates loading to custom instance and returns result", () => {
const mockReturn = { then: vi.fn() };
mockInstance.loading.mockReturnValue(mockReturn);
const result = MessageManager.loading("loading...", 3);
expect(mockInstance.loading).toHaveBeenCalledWith("loading...", 3);
expect(result).toBe(mockReturn);
});
it("delegates destroy to custom instance", () => {
MessageManager.destroy();
expect(mockInstance.destroy).toHaveBeenCalled();
describe("setMessageInstance is a no-op (back-compat shim)", () => {
it("does not throw when called", () => {
expect(() => setMessageInstance({} as unknown)).not.toThrow();
});
});
});

View file

@ -1,37 +1,60 @@
import { message as staticMessage } from "antd";
import type { MessageInstance } from "antd/es/message/interface";
/**
* Global message manager — thin wrapper around sonner's `toast.*` API.
*
* Phase-1 shadcn migration: this file previously delegated to antd's
* `message.*` API. It now delegates to sonner. The public API (including
* `setMessageInstance`) is preserved so existing call sites don't need to
* change; the `setMessageInstance` function is now a no-op kept for
* backwards-compatibility (sonner is singleton-rendered at the root layout
* and does not need per-tree instance registration).
*/
import { toast } from "sonner";
let messageInstance: MessageInstance | null = null;
export const setMessageInstance = (instance: MessageInstance) => {
messageInstance = instance;
/**
* No-op retained for API compatibility with call sites that import
* `setMessageInstance` from this module. Sonner is rendered globally via
* `<Toaster />` in the root layout; no per-tree registration is required.
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const setMessageInstance = (_instance: unknown) => {
// no-op — sonner is singleton
};
const getMessageApi = () => messageInstance || staticMessage;
/** antd durations are seconds; sonner durations are milliseconds. */
function toSonnerDuration(duration?: number): number | undefined {
if (duration == null) return undefined;
return duration * 1000;
}
const MessageManager = {
success(content: string, duration?: number) {
getMessageApi().success(content, duration);
return toast.success(content, { duration: toSonnerDuration(duration) });
},
error(content: string, duration?: number) {
getMessageApi().error(content, duration);
return toast.error(content, { duration: toSonnerDuration(duration) });
},
warning(content: string, duration?: number) {
getMessageApi().warning(content, duration);
return toast.warning(content, { duration: toSonnerDuration(duration) });
},
info(content: string, duration?: number) {
getMessageApi().info(content, duration);
return toast.info(content, { duration: toSonnerDuration(duration) });
},
/**
* Show a loading toast. Returns the sonner toast id, which can be passed
* to `toast.dismiss(id)` (or `MessageManager.destroy()` to clear all).
* Behaves like antd's `message.loading` — duration in seconds; omit to
* keep the toast open until dismissed.
*/
loading(content: string, duration?: number) {
return getMessageApi().loading(content, duration);
return toast.loading(content, { duration: toSonnerDuration(duration) });
},
destroy() {
getMessageApi().destroy();
toast.dismiss();
},
};

View file

@ -1,33 +1,35 @@
import { notification } from "antd";
import { beforeEach, describe, expect, it, vi } from "vitest";
import NotificationManager, { COMMON_NOTIFICATION_PROPS } from "./notifications_manager";
vi.mock("@/components/molecules/notifications_manager", async () => {
const actual = await vi.importActual<typeof import("@/components/molecules/notifications_manager")>(
"@/components/molecules/notifications_manager",
);
// The global `setupTests.ts` mocks `@/components/molecules/notifications_manager`
// as a safety rail for component tests. Unmock it here so we can exercise the
// real manager against sonner.
vi.unmock("@/components/molecules/notifications_manager");
vi.unmock("./notifications_manager");
return actual;
});
// Mock the antd notification module
vi.mock("antd", () => ({
notification: {
error: vi.fn(),
warning: vi.fn(),
info: vi.fn(),
success: vi.fn(),
destroy: vi.fn(),
},
// Sonner is a module-level singleton; mock it before importing the manager.
const mockToast = vi.hoisted(() => ({
success: vi.fn(),
error: vi.fn(),
warning: vi.fn(),
info: vi.fn(),
loading: vi.fn(),
dismiss: vi.fn(),
}));
describe("NotificationManager", () => {
vi.mock("sonner", () => ({
toast: mockToast,
}));
// Dynamic import AFTER vi.unmock so the global mock doesn't take effect.
const { default: NotificationManager } = await import("./notifications_manager");
describe("NotificationManager (sonner-backed)", () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe("Already Exists case", () => {
it("should show error notification for 'already exists' message", () => {
it("should show error toast for 'already exists' backend message", () => {
const error = {
message: "Key with alias 'test10' already exists.",
type: "bad_request_error",
@ -36,35 +38,36 @@ describe("NotificationManager", () => {
NotificationManager.fromBackend(error);
expect(notification.error).toHaveBeenCalledWith(
expect(mockToast.error).toHaveBeenCalledWith(
"Already Exists",
expect.objectContaining({
message: "Already Exists",
description: "Key with alias 'test10' already exists.",
duration: 6,
placement: "topRight",
duration: 6000,
}),
);
});
});
describe("COMMON_NOTIFICATION_PROPS", () => {
const notificationTypes = [
{ type: "error", method: NotificationManager.error, mockFn: notification.error },
{ type: "warning", method: NotificationManager.warning, mockFn: notification.warning },
{ type: "info", method: NotificationManager.info, mockFn: notification.info },
{ type: "success", method: NotificationManager.success, mockFn: notification.success },
describe("direct call routing", () => {
const cases = [
{ label: "error", call: () => NotificationManager.error("Test error"), mockFn: mockToast.error },
{ label: "warning", call: () => NotificationManager.warning("Test warning"), mockFn: mockToast.warning },
{ label: "info", call: () => NotificationManager.info("Test info"), mockFn: mockToast.info },
{ label: "success", call: () => NotificationManager.success("Test success"), mockFn: mockToast.success },
];
notificationTypes.forEach(({ type, method, mockFn }) => {
it(`should pass COMMON_NOTIFICATION_PROPS to ${type} notifications`, () => {
method(`Test ${type}`);
expect(mockFn).toHaveBeenCalledWith(
expect.objectContaining({
...COMMON_NOTIFICATION_PROPS,
}),
);
cases.forEach(({ label, call, mockFn }) => {
it(`should route ${label} to toast.${label}`, () => {
call();
expect(mockFn).toHaveBeenCalled();
});
});
});
describe("clear()", () => {
it("dismisses all toasts", () => {
NotificationManager.clear();
expect(mockToast.dismiss).toHaveBeenCalled();
});
});
});

View file

@ -1,17 +1,32 @@
import React from "react";
import { notification as staticNotification } from "antd";
import type { NotificationInstance } from "antd/es/notification/interface";
import { toast } from "sonner";
import { parseErrorMessage } from "../shared/errorUtils";
import { ArgsProps } from "antd/es/notification";
let notificationInstance: NotificationInstance | null = null;
/**
* Global notification manager — thin wrapper around sonner's `toast.*` API.
*
* Phase-1 shadcn migration: this file previously delegated to antd's
* `notification.*` API. It now delegates to sonner. The public shape of
* the module is preserved (same functions + `setNotificationInstance`)
* so existing call sites don't need to change; `setNotificationInstance`
* is now a no-op kept for backwards-compatibility.
*/
export const setNotificationInstance = (instance: NotificationInstance) => {
notificationInstance = instance;
/**
* No-op retained for API compatibility with call sites that import
* `setNotificationInstance` from this module. Sonner is rendered globally via
* `<Toaster />` in the root layout; no per-tree registration is required.
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const setNotificationInstance = (_instance: unknown) => {
// no-op — sonner is singleton
};
// Helper to get the best available notification instance
const getNotification = () => notificationInstance || staticNotification;
/** Kept public for callers that used to spread it into an antd notification config. */
export const COMMON_NOTIFICATION_PROPS = {
// sonner's equivalent toggles are `richColors` (global on <Toaster>) and
// `closeButton`; individual toasts don't need these fields.
};
type Placement = "top" | "topLeft" | "topRight" | "bottom" | "bottomLeft" | "bottomRight";
@ -25,16 +40,12 @@ type NotificationConfig = {
type NotificationConfigResolved = Omit<NotificationConfig, "message"> & { message: string | React.ReactNode };
function defaultPlacement(): Placement {
return "topRight";
}
function normalize(input: string | NotificationConfig, fallbackTitle: string): NotificationConfigResolved {
if (typeof input === "string") return { message: fallbackTitle, description: input };
return { message: input.message ?? fallbackTitle, ...input };
}
function toIntMaybe(val: any): number | undefined {
function toIntMaybe(val: unknown): number | undefined {
if (typeof val === "number") return val;
if (typeof val === "string" && /^\d+$/.test(val)) return parseInt(val, 10);
return undefined;
@ -114,7 +125,7 @@ const NOT_FOUND_MATCH = [
"team not found",
"organization not found",
"mcp server with id",
"tool '", // will combine with “not found” in message
"tool '",
];
const EXISTS_MATCH = ["already exists", "team member is already in team", "user already exists"];
@ -151,10 +162,10 @@ function titleFor(status?: number, desc?: string): string {
if (AUTH_MATCH.some((s) => d.includes(s))) return "Authentication Error";
if (FORBIDDEN_MATCH.some((s) => d.includes(s))) return "Access Denied";
if (DB_MATCH?.some?.((s: string) => d.includes(s)) || status === 503) return "Service Unavailable";
if (BUDGET_MATCH?.some?.((s: string) => d.includes(s))) return "Budget Exceeded";
if (ENTERPRISE_MATCH?.some?.((s: string) => d.includes(s))) return "Feature Unavailable";
if (ROUTER_MATCH?.some?.((s: string) => d.includes(s))) return "Routing Error";
if (DB_MATCH.some((s) => d.includes(s)) || status === 503) return "Service Unavailable";
if (BUDGET_MATCH.some((s) => d.includes(s))) return "Budget Exceeded";
if (ENTERPRISE_MATCH.some((s) => d.includes(s))) return "Feature Unavailable";
if (ROUTER_MATCH.some((s) => d.includes(s))) return "Routing Error";
if (EXISTS_MATCH.some((s) => d.includes(s))) return "Already Exists";
if (GUARDRAIL_MATCH.some((s) => d.includes(s))) return "Content Blocked";
@ -169,7 +180,7 @@ function titleFor(status?: number, desc?: string): string {
d.includes("rate limit") ||
d.includes("tpm") ||
d.includes("rpm") ||
RATE_LIMIT_EXTRA?.some?.((s: string) => d.includes(s))
RATE_LIMIT_EXTRA.some((s) => d.includes(s))
)
return "Rate Limit Exceeded";
if (status && status >= 500) return "Server Error";
@ -224,99 +235,115 @@ function classifyGeneralMessage(desc?: string): { kind: "success" | "info" | "wa
if (SUCCESS_MATCH.some((s) => d.includes(s))) return { kind: "success", title: "Success" };
if (DEPRECATION_FEATURE_WARN_MATCH.some((s) => d.includes(s))) return { kind: "warning", title: "Feature Notice" };
if (CONFIG_WARN_MATCH.some((s) => d.includes(s))) return { kind: "warning", title: "Configuration Warning" };
if (INFO_MATCH.some((s) => d.includes(s))) return { kind: "warning", title: "Rate Limit" }; // show as warning for visibility
if (INFO_MATCH.some((s) => d.includes(s))) return { kind: "warning", title: "Rate Limit" };
return null;
}
function extractStatus(input: any): number | undefined {
return toIntMaybe(input?.response?.status) ?? toIntMaybe(input?.status_code) ?? toIntMaybe(input?.code);
function extractStatus(input: unknown): number | undefined {
const obj = (input ?? {}) as Record<string, unknown>;
const response = obj?.response as Record<string, unknown> | undefined;
return toIntMaybe(response?.status) ?? toIntMaybe(obj?.status_code) ?? toIntMaybe(obj?.code);
}
function extractDescription(input: any): string {
if (typeof input === "string") return input; // raw error string
function extractDescription(input: unknown): string {
if (typeof input === "string") return input;
const obj = (input ?? {}) as Record<string, unknown>;
const response = obj?.response as Record<string, unknown> | undefined;
const data = response?.data as Record<string, unknown> | undefined;
const dataError = data?.error as Record<string, unknown> | string | undefined;
const backendMsg =
input?.response?.data?.error?.message ??
input?.response?.data?.message ??
input?.response?.data?.error ??
input?.detail ??
input?.message ??
(typeof dataError === "object" ? dataError?.message : dataError) ??
data?.message ??
obj?.detail ??
obj?.message ??
input;
return parseErrorMessage(backendMsg);
}
export const COMMON_NOTIFICATION_PROPS: Partial<ArgsProps> = {
showProgress: true,
pauseOnHover: true,
};
function looksErrorPayload(input: any, status?: number): boolean {
function looksErrorPayload(input: unknown, status?: number): boolean {
if (status !== undefined) return true;
if (input instanceof Error) return true;
if (typeof input === "string") return true; // treat raw strings passed to fromBackend as errors
if (typeof input === "string") return true;
if (input && typeof input === "object" && ("error" in input || "detail" in input)) return true;
return false;
}
/** antd durations are seconds; sonner durations are milliseconds. */
function toMs(d?: number): number | undefined {
if (d == null) return undefined;
return d * 1000;
}
/**
* Sonner accepts a `description` field on every level, so we bundle
* the (message, description) pair by using message as the main text and
* description as the subtitle — same visual shape as the old antd stack.
*/
function callToast(
level: "error" | "warning" | "info" | "success",
cfg: NotificationConfigResolved,
defaultDurationSec: number,
) {
const title =
typeof cfg.message === "string" ? cfg.message : String(cfg.message ?? "");
const description =
typeof cfg.description === "string" || typeof cfg.description === "number"
? String(cfg.description)
: cfg.description
? (cfg.description as React.ReactNode)
: undefined;
const duration = toMs(cfg.duration ?? defaultDurationSec);
const opts: Parameters<typeof toast>[1] = { description, duration };
switch (level) {
case "error":
return toast.error(title, opts);
case "warning":
return toast.warning(title, opts);
case "info":
return toast.info(title, opts);
case "success":
return toast.success(title, opts);
}
}
const NotificationManager = {
error(input: string | NotificationConfig) {
const cfg = normalize(input, "Error");
getNotification().error({
...COMMON_NOTIFICATION_PROPS,
...cfg,
placement: cfg.placement ?? defaultPlacement(),
duration: cfg.duration ?? 6,
});
callToast("error", cfg, 6);
},
warning(input: string | NotificationConfig) {
const cfg = normalize(input, "Warning");
getNotification().warning({
...COMMON_NOTIFICATION_PROPS,
...cfg,
placement: cfg.placement ?? defaultPlacement(),
duration: cfg.duration ?? 5,
});
callToast("warning", cfg, 5);
},
info(input: string | NotificationConfig) {
const cfg = normalize(input, "Info");
getNotification().info({
...COMMON_NOTIFICATION_PROPS,
...cfg,
placement: cfg.placement ?? defaultPlacement(),
duration: cfg.duration ?? 4,
});
callToast("info", cfg, 4);
},
success(input: string | React.ReactNode | NotificationConfig) {
if (React.isValidElement(input)) {
getNotification().success({
...COMMON_NOTIFICATION_PROPS,
message: "Success",
description: input,
placement: defaultPlacement(),
duration: 3.5,
});
toast.success("Success", { description: input, duration: toMs(3.5) });
return;
}
const cfg = normalize(input as string | NotificationConfig, "Success");
getNotification().success({
...COMMON_NOTIFICATION_PROPS,
...cfg,
placement: cfg.placement ?? defaultPlacement(),
duration: cfg.duration ?? 3.5,
});
callToast("success", cfg, 3.5);
},
fromBackend(input: any, extra?: Omit<NotificationConfig, "message" | "description">) {
fromBackend(input: unknown, extra?: Omit<NotificationConfig, "message" | "description">) {
const status = extractStatus(input);
const description = extractDescription(input);
const base = { ...(extra ?? {}), description, placement: extra?.placement ?? defaultPlacement() };
const base: NotificationConfigResolved = {
...(extra ?? {}),
message: "Info",
description,
};
if (looksErrorPayload(input, status)) {
const title = titleFor(status, description);
const payload = { ...base, message: title };
const payload: NotificationConfigResolved = { ...base, message: title };
if (
title === "Rate Limit Exceeded" ||
@ -326,11 +353,11 @@ const NotificationManager = {
title === "Content Blocked" ||
title === "Integration Error"
) {
getNotification().warning({ ...COMMON_NOTIFICATION_PROPS, ...payload, duration: extra?.duration ?? 7 });
callToast("warning", payload, extra?.duration ?? 7);
return;
}
if (title === "Server Error") {
getNotification().error({ ...COMMON_NOTIFICATION_PROPS, ...payload, duration: extra?.duration ?? 8 });
callToast("error", payload, extra?.duration ?? 8);
return;
}
if (
@ -341,30 +368,29 @@ const NotificationManager = {
title === "Error" ||
title === "Already Exists"
) {
getNotification().error({ ...COMMON_NOTIFICATION_PROPS, ...payload, duration: extra?.duration ?? 6 });
callToast("error", payload, extra?.duration ?? 6);
return;
}
getNotification().info({ ...COMMON_NOTIFICATION_PROPS, ...payload, duration: extra?.duration ?? 4 });
callToast("info", payload, extra?.duration ?? 4);
return;
}
// Non-error: success/info/warning classifier
const cls = classifyGeneralMessage(description);
const payload = { ...base, message: cls?.title ?? "Info" };
const payload: NotificationConfigResolved = { ...base, message: cls?.title ?? "Info" };
if (cls?.kind === "success") {
getNotification().success({ ...COMMON_NOTIFICATION_PROPS, ...payload, duration: extra?.duration ?? 3.5 });
callToast("success", payload, extra?.duration ?? 3.5);
return;
}
if (cls?.kind === "warning") {
getNotification().warning({ ...COMMON_NOTIFICATION_PROPS, ...payload, duration: extra?.duration ?? 6 });
callToast("warning", payload, extra?.duration ?? 6);
return;
}
getNotification().info({ ...COMMON_NOTIFICATION_PROPS, ...payload, duration: extra?.duration ?? 4 });
callToast("info", payload, extra?.duration ?? 4);
},
clear() {
getNotification().destroy();
toast.dismiss();
},
};

View file

@ -1,28 +1,17 @@
"use client";
import React, { useEffect, useRef } from "react";
import { notification, message } from "antd";
import { setNotificationInstance } from "@/components/molecules/notifications_manager";
import { setMessageInstance } from "@/components/molecules/message_manager";
import React from "react";
/**
* Legacy wrapper kept for backwards-compatibility during the phase-1
* shadcn migration. Previously registered antd notification/message
* instances with the global managers. The managers now delegate to
* sonner (rendered globally via `<Toaster />` in the root layout), so
* this component is a passthrough.
*
* Will be deleted entirely in the "drop AntdGlobalProvider" cleanup
* task after antd has been uninstalled.
*/
export default function AntdGlobalProvider({ children }: { children: React.ReactNode }) {
const [notificationApi, notificationContextHolder] = notification.useNotification();
const [messageApi, messageContextHolder] = message.useMessage();
const initialized = useRef(false);
useEffect(() => {
if (!initialized.current) {
setNotificationInstance(notificationApi);
setMessageInstance(messageApi);
initialized.current = true;
}
}, [notificationApi, messageApi]);
return (
<>
{notificationContextHolder}
{messageContextHolder}
{children}
</>
);
return <>{children}</>;
}