diff --git a/web/package-lock.json b/web/package-lock.json index e14bba981..c3c05f9ee 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -87,6 +87,7 @@ "react-virtualized-auto-sizer": "^1.0.25", "rehype-raw": "^7.0.0", "remark-gfm": "^4.0.1", + "sonner": "^2.0.1", "split-pane-react": "^0.1.3", "tailwind-merge": "^3.0.1", "utility-types": "^3.11.0", @@ -2412,6 +2413,7 @@ }, "node_modules/@clack/prompts/node_modules/is-unicode-supported": { "version": "1.3.0", + "extraneous": true, "inBundle": true, "license": "MIT", "engines": { @@ -23496,6 +23498,16 @@ "node": ">=10.0.0" } }, + "node_modules/sonner": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.1.tgz", + "integrity": "sha512-FRBphaehZ5tLdLcQ8g2WOIRE+Y7BCfWi5Zyd8bCvBjiW8TxxAyoWZIxS661Yz6TGPqFQ4VLzOF89WEYhfynSFQ==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", diff --git a/web/package.json b/web/package.json index 051afa98f..bd829cd23 100644 --- a/web/package.json +++ b/web/package.json @@ -96,6 +96,7 @@ "react-virtualized-auto-sizer": "^1.0.25", "rehype-raw": "^7.0.0", "remark-gfm": "^4.0.1", + "sonner": "^2.0.1", "split-pane-react": "^0.1.3", "tailwind-merge": "^3.0.1", "utility-types": "^3.11.0", diff --git a/web/src/components/ui/toaster/AppToaster.tsx b/web/src/components/ui/toaster/AppToaster.tsx new file mode 100644 index 000000000..d18a4d37a --- /dev/null +++ b/web/src/components/ui/toaster/AppToaster.tsx @@ -0,0 +1,30 @@ +'use client'; + +import { useTheme } from 'next-themes'; +import { Toaster } from 'sonner'; + +type ToasterProps = React.ComponentProps; + +const AppToaster = ({ ...props }: ToasterProps) => { + const { theme = 'system' } = useTheme(); + + return ( + + ); +}; + +export { AppToaster }; diff --git a/web/src/components/ui/toaster/index.ts b/web/src/components/ui/toaster/index.ts new file mode 100644 index 000000000..fed0843ba --- /dev/null +++ b/web/src/components/ui/toaster/index.ts @@ -0,0 +1 @@ +export * from './AppToaster'; diff --git a/web/src/context/BusterNotifications/BusterNotifications.tsx b/web/src/context/BusterNotifications/BusterNotifications.tsx index 01d03a2b2..46932180c 100644 --- a/web/src/context/BusterNotifications/BusterNotifications.tsx +++ b/web/src/context/BusterNotifications/BusterNotifications.tsx @@ -1,6 +1,7 @@ import React, { PropsWithChildren } from 'react'; -import { App, ModalFuncProps } from 'antd'; -import { createStyles } from 'antd-style'; +// import { App, ModalFuncProps } from 'antd'; +import { toast, type ExternalToast } from 'sonner'; +// import { createStyles } from 'antd-style'; import { useMemoizedFn } from 'ahooks'; import { useContextSelector, @@ -18,39 +19,57 @@ export interface NotificationProps { duration?: number; } -const useStyles = createStyles(({ token, css }) => ({ - modal: css` - .busterv2-modal-body { - padding: 0px !important; - } +// const useStyles = createStyles(({ token, css }) => ({ +// modal: css` +// .busterv2-modal-body { +// padding: 0px !important; +// } - .busterv2-modal-confirm-body { - padding: 24px 32px 16px 32px !important; - } +// .busterv2-modal-confirm-body { +// padding: 24px 32px 16px 32px !important; +// } - .busterv2-modal-confirm-btns { - margin-top: 0px !important; - padding: 12px 32px !important; - border-top: 0.5px solid ${token.colorBorder}; - display: flex; - align-items: center; - justify-content: flex-end; - } +// .busterv2-modal-confirm-btns { +// margin-top: 0px !important; +// padding: 12px 32px !important; +// border-top: 0.5px solid ${token.colorBorder}; +// display: flex; +// align-items: center; +// justify-content: flex-end; +// } - .busterv2-modal-confirm-content { - color: ${token.colorTextSecondary} !important; - } - ` -})); +// .busterv2-modal-confirm-content { +// color: ${token.colorTextSecondary} !important; +// } +// ` +// })); export const useBusterNotificationsInternal = () => { - const { message, notification, modal } = App.useApp(); - - const { cx, styles } = useStyles(); + // const { message, notification, modal } = App.useApp(); + // const { cx, styles } = useStyles(); const openNotification = useMemoizedFn( (props: { title?: string; message: string; type: NotificationType }) => { - notification?.open?.({ ...props, description: props.message, message: props.title }); + const { title, message, type } = props; + + const toastOptions: ExternalToast = { + description: message, + position: 'top-center' + }; + + switch (type) { + case 'success': + return toast.success(title, toastOptions); + case 'info': + return toast.info(title, toastOptions); + case 'warning': + return toast.warning(title, toastOptions); + case 'error': + return toast.error(title, toastOptions); + default: + const _never: never = type; + return ''; + } } ); @@ -59,24 +78,24 @@ export const useBusterNotificationsInternal = () => { const type = values.type || 'error'; const title = values.title || 'Error'; const message = values.message || 'Something went wrong. Please try again.'; - openNotification({ ...values, message, title, type }); + return openNotification({ ...values, message, title, type }); }); const openInfoNotification = useMemoizedFn( ({ type = 'info', message = 'Info', title = 'Info', ...props }: NotificationProps) => { - openNotification({ ...props, title, message, type }); + return openNotification({ ...props, title, message, type }); } ); const openSuccessNotification = useMemoizedFn( ({ type = 'success', title = 'Success', message = 'success', ...props }: NotificationProps) => { - openNotification({ ...props, message, title, type }); + return openNotification({ ...props, message, title, type }); } ); const openWarningNotification = useMemoizedFn( ({ type = 'warning', title = 'Warning', message = 'Warning', ...props }: NotificationProps) => { - openNotification({ ...props, message, title, type }); + return openNotification({ ...props, message, title, type }); } ); @@ -86,28 +105,27 @@ export const useBusterNotificationsInternal = () => { (props: { type: NotificationType; message: string; - loading?: boolean; onClose?: () => void; duration?: number; }) => { - if (props.loading) { - message.loading(props.message, props.duration, props.onClose); - } else { - message?.[props.type]?.(props.message, props.duration, props.onClose); - } + return openNotification({ + ...props, + title: props.message, + message: '' + }); } ); const openErrorMessage = useMemoizedFn((message: string) => { - openMessage({ type: 'error', message }); + return openMessage({ type: 'error', message }); }); const openInfoMessage = useMemoizedFn((message: string, duration?: number) => { - openMessage({ type: 'info', message, duration }); + return openMessage({ type: 'info', message, duration }); }); const openSuccessMessage = useMemoizedFn((message: string) => { - openMessage({ type: 'success', message }); + return openMessage({ type: 'success', message }); }); const openConfirmModal = useMemoizedFn( @@ -117,36 +135,34 @@ export const useBusterNotificationsInternal = () => { onOk: () => void; onCancel?: () => void; icon?: React.ReactNode; - okButtonProps?: ModalFuncProps['okButtonProps']; - cancelButtonProps?: ModalFuncProps['cancelButtonProps']; width?: string | number; useReject?: boolean; }): Promise => { const useReject = props.useReject ?? true; return new Promise((resolve, reject) => { - modal.confirm({ - icon: props.icon || <>, - ...props, - className: cx(styles.modal, ''), - cancelButtonProps: { - ...props.cancelButtonProps, - type: 'text' - }, - okButtonProps: { - ...props.okButtonProps, - type: 'default' - }, - onOk: async () => { - await props.onOk(); - resolve(); - }, - onCancel: async () => { - await props.onCancel?.(); - if (useReject) reject(); - else resolve(); - } - }); + // modal.confirm({ + // icon: props.icon || <>, + // ...props, + // className: cx(styles.modal, ''), + // cancelButtonProps: { + // ...props.cancelButtonProps, + // type: 'text' + // }, + // okButtonProps: { + // ...props.okButtonProps, + // type: 'default' + // }, + // onOk: async () => { + // await props.onOk(); + // resolve(); + // }, + // onCancel: async () => { + // await props.onCancel?.(); + // if (useReject) reject(); + // else resolve(); + // } + // }); }); } ); diff --git a/web/src/context/BusterStyles/BusterStyles.tsx b/web/src/context/BusterStyles/BusterStyles.tsx index 979f9988f..b432e50e9 100644 --- a/web/src/context/BusterStyles/BusterStyles.tsx +++ b/web/src/context/BusterStyles/BusterStyles.tsx @@ -9,6 +9,7 @@ import { createContext, ContextSelector } from '@fluentui/react-context-selector'; +import { AppToaster } from '@/components/ui/toaster'; export const ENABLE_DARK_MODE = false; @@ -31,7 +32,10 @@ export const BusterStyleProvider: React.FC> = ({ children enableSystem={ENABLE_DARK_MODE} themes={['light', 'dark']} disableTransitionOnChange> - {children} + + {children} + + ); };