From 93f95463539868ed74522d5d33cf767475858337 Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Wed, 18 Mar 2026 16:53:55 +0800 Subject: [PATCH] refactor(web): migrate core toast call sites to base ui toast (#33643) --- .../billing/cloud-plan-payment-flow.test.tsx | 28 +++--- .../billing/self-hosted-plan-flow.test.tsx | 58 ++++++------- web/app/account/oauth/authorize/page.tsx | 12 +-- .../create-app-dialog/app-list/index.spec.tsx | 4 +- .../app/create-app-dialog/app-list/index.tsx | 8 +- web/app/components/base/toast/context.ts | 10 +++ web/app/components/base/toast/index.tsx | 9 ++ .../cloud-plan-item/__tests__/index.spec.tsx | 26 +++--- .../pricing/plans/cloud-plan-item/index.tsx | 21 +++-- .../__tests__/index.spec.tsx | 26 +++--- .../plans/self-hosted-plan-item/index.tsx | 17 ++-- .../detail/__tests__/new-segment.spec.tsx | 81 ++++++----------- .../__tests__/new-child-segment.spec.tsx | 70 +++++++++------ .../detail/completed/new-child-segment.tsx | 58 ++++--------- .../datasets/documents/detail/new-segment.tsx | 65 ++++---------- .../connector/__tests__/index.spec.tsx | 18 ++-- .../connector/index.tsx | 7 +- .../__tests__/index.spec.tsx | 10 +-- .../system-model-selector/index.tsx | 5 +- .../__tests__/delete-confirm.spec.tsx | 12 +-- .../subscription-list/delete-confirm.tsx | 86 ++++++++++++------- .../components/variable/output-var-list.tsx | 26 +++--- .../_base/components/variable/var-list.tsx | 22 ++--- .../panel/version-history-panel/index.tsx | 30 +++---- .../components/mail-and-password-auth.tsx | 18 ++-- web/context/provider-context-provider.tsx | 12 ++- web/eslint-suppressions.json | 84 ------------------ web/service/fetch.spec.ts | 6 +- web/service/fetch.ts | 4 +- 29 files changed, 353 insertions(+), 480 deletions(-) diff --git a/web/__tests__/billing/cloud-plan-payment-flow.test.tsx b/web/__tests__/billing/cloud-plan-payment-flow.test.tsx index bd3b6aa8d8..84653cd68c 100644 --- a/web/__tests__/billing/cloud-plan-payment-flow.test.tsx +++ b/web/__tests__/billing/cloud-plan-payment-flow.test.tsx @@ -11,6 +11,7 @@ import type { BasicPlan } from '@/app/components/billing/type' import { cleanup, render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import * as React from 'react' +import { toast, ToastHost } from '@/app/components/base/ui/toast' import { ALL_PLANS } from '@/app/components/billing/config' import { PlanRange } from '@/app/components/billing/pricing/plan-switcher/plan-range-switcher' import CloudPlanItem from '@/app/components/billing/pricing/plans/cloud-plan-item' @@ -21,7 +22,6 @@ let mockAppCtx: Record = {} const mockFetchSubscriptionUrls = vi.fn() const mockInvoices = vi.fn() const mockOpenAsyncWindow = vi.fn() -const mockToastNotify = vi.fn() // ─── Context mocks ─────────────────────────────────────────────────────────── vi.mock('@/context/app-context', () => ({ @@ -49,10 +49,6 @@ vi.mock('@/hooks/use-async-window-open', () => ({ useAsyncWindowOpen: () => mockOpenAsyncWindow, })) -vi.mock('@/app/components/base/toast', () => ({ - default: { notify: (args: unknown) => mockToastNotify(args) }, -})) - // ─── Navigation mocks ─────────────────────────────────────────────────────── vi.mock('@/next/navigation', () => ({ useRouter: () => ({ push: vi.fn() }), @@ -82,12 +78,15 @@ const renderCloudPlanItem = ({ canPay = true, }: RenderCloudPlanItemOptions = {}) => { return render( - , + <> + + + , ) } @@ -96,6 +95,7 @@ describe('Cloud Plan Payment Flow', () => { beforeEach(() => { vi.clearAllMocks() cleanup() + toast.close() setupAppContext() mockFetchSubscriptionUrls.mockResolvedValue({ url: 'https://pay.example.com/checkout' }) mockInvoices.mockResolvedValue({ url: 'https://billing.example.com/invoices' }) @@ -283,11 +283,7 @@ describe('Cloud Plan Payment Flow', () => { await user.click(button) await waitFor(() => { - expect(mockToastNotify).toHaveBeenCalledWith( - expect.objectContaining({ - type: 'error', - }), - ) + expect(screen.getByText('billing.buyPermissionDeniedTip')).toBeInTheDocument() }) // Should not proceed with payment expect(mockFetchSubscriptionUrls).not.toHaveBeenCalled() diff --git a/web/__tests__/billing/self-hosted-plan-flow.test.tsx b/web/__tests__/billing/self-hosted-plan-flow.test.tsx index 810d36da8a..0802b760e1 100644 --- a/web/__tests__/billing/self-hosted-plan-flow.test.tsx +++ b/web/__tests__/billing/self-hosted-plan-flow.test.tsx @@ -10,12 +10,12 @@ import { cleanup, render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' import * as React from 'react' +import { toast, ToastHost } from '@/app/components/base/ui/toast' import { contactSalesUrl, getStartedWithCommunityUrl, getWithPremiumUrl } from '@/app/components/billing/config' import SelfHostedPlanItem from '@/app/components/billing/pricing/plans/self-hosted-plan-item' import { SelfHostedPlan } from '@/app/components/billing/type' let mockAppCtx: Record = {} -const mockToastNotify = vi.fn() const originalLocation = window.location let assignedHref = '' @@ -40,10 +40,6 @@ vi.mock('@/app/components/base/icons/src/public/billing', () => ({ AwsMarketplaceDark: () => , })) -vi.mock('@/app/components/base/toast', () => ({ - default: { notify: (args: unknown) => mockToastNotify(args) }, -})) - vi.mock('@/app/components/billing/pricing/plans/self-hosted-plan-item/list', () => ({ default: ({ plan }: { plan: string }) => (
Features
@@ -57,10 +53,20 @@ const setupAppContext = (overrides: Record = {}) => { } } +const renderSelfHostedPlanItem = (plan: SelfHostedPlan) => { + return render( + <> + + + , + ) +} + describe('Self-Hosted Plan Flow', () => { beforeEach(() => { vi.clearAllMocks() cleanup() + toast.close() setupAppContext() // Mock window.location with minimal getter/setter (Location props are non-enumerable) @@ -85,14 +91,14 @@ describe('Self-Hosted Plan Flow', () => { // ─── 1. Plan Rendering ────────────────────────────────────────────────── describe('Plan rendering', () => { it('should render community plan with name and description', () => { - render() + renderSelfHostedPlanItem(SelfHostedPlan.community) expect(screen.getByText(/plans\.community\.name/i)).toBeInTheDocument() expect(screen.getByText(/plans\.community\.description/i)).toBeInTheDocument() }) it('should render premium plan with cloud provider icons', () => { - render() + renderSelfHostedPlanItem(SelfHostedPlan.premium) expect(screen.getByText(/plans\.premium\.name/i)).toBeInTheDocument() expect(screen.getByTestId('icon-azure')).toBeInTheDocument() @@ -100,39 +106,39 @@ describe('Self-Hosted Plan Flow', () => { }) it('should render enterprise plan without cloud provider icons', () => { - render() + renderSelfHostedPlanItem(SelfHostedPlan.enterprise) expect(screen.getByText(/plans\.enterprise\.name/i)).toBeInTheDocument() expect(screen.queryByTestId('icon-azure')).not.toBeInTheDocument() }) it('should not show price tip for community (free) plan', () => { - render() + renderSelfHostedPlanItem(SelfHostedPlan.community) expect(screen.queryByText(/plans\.community\.priceTip/i)).not.toBeInTheDocument() }) it('should show price tip for premium plan', () => { - render() + renderSelfHostedPlanItem(SelfHostedPlan.premium) expect(screen.getByText(/plans\.premium\.priceTip/i)).toBeInTheDocument() }) it('should render features list for each plan', () => { - const { unmount: unmount1 } = render() + const { unmount: unmount1 } = renderSelfHostedPlanItem(SelfHostedPlan.community) expect(screen.getByTestId('self-hosted-list-community')).toBeInTheDocument() unmount1() - const { unmount: unmount2 } = render() + const { unmount: unmount2 } = renderSelfHostedPlanItem(SelfHostedPlan.premium) expect(screen.getByTestId('self-hosted-list-premium')).toBeInTheDocument() unmount2() - render() + renderSelfHostedPlanItem(SelfHostedPlan.enterprise) expect(screen.getByTestId('self-hosted-list-enterprise')).toBeInTheDocument() }) it('should show AWS marketplace icon for premium plan button', () => { - render() + renderSelfHostedPlanItem(SelfHostedPlan.premium) expect(screen.getByTestId('icon-aws-light')).toBeInTheDocument() }) @@ -142,7 +148,7 @@ describe('Self-Hosted Plan Flow', () => { describe('Navigation flow', () => { it('should redirect to GitHub when clicking community plan button', async () => { const user = userEvent.setup() - render() + renderSelfHostedPlanItem(SelfHostedPlan.community) const button = screen.getByRole('button') await user.click(button) @@ -152,7 +158,7 @@ describe('Self-Hosted Plan Flow', () => { it('should redirect to AWS Marketplace when clicking premium plan button', async () => { const user = userEvent.setup() - render() + renderSelfHostedPlanItem(SelfHostedPlan.premium) const button = screen.getByRole('button') await user.click(button) @@ -162,7 +168,7 @@ describe('Self-Hosted Plan Flow', () => { it('should redirect to Typeform when clicking enterprise plan button', async () => { const user = userEvent.setup() - render() + renderSelfHostedPlanItem(SelfHostedPlan.enterprise) const button = screen.getByRole('button') await user.click(button) @@ -176,15 +182,13 @@ describe('Self-Hosted Plan Flow', () => { it('should show error toast when non-manager clicks community button', async () => { setupAppContext({ isCurrentWorkspaceManager: false }) const user = userEvent.setup() - render() + renderSelfHostedPlanItem(SelfHostedPlan.community) const button = screen.getByRole('button') await user.click(button) await waitFor(() => { - expect(mockToastNotify).toHaveBeenCalledWith( - expect.objectContaining({ type: 'error' }), - ) + expect(screen.getByText('billing.buyPermissionDeniedTip')).toBeInTheDocument() }) // Should NOT redirect expect(assignedHref).toBe('') @@ -193,15 +197,13 @@ describe('Self-Hosted Plan Flow', () => { it('should show error toast when non-manager clicks premium button', async () => { setupAppContext({ isCurrentWorkspaceManager: false }) const user = userEvent.setup() - render() + renderSelfHostedPlanItem(SelfHostedPlan.premium) const button = screen.getByRole('button') await user.click(button) await waitFor(() => { - expect(mockToastNotify).toHaveBeenCalledWith( - expect.objectContaining({ type: 'error' }), - ) + expect(screen.getByText('billing.buyPermissionDeniedTip')).toBeInTheDocument() }) expect(assignedHref).toBe('') }) @@ -209,15 +211,13 @@ describe('Self-Hosted Plan Flow', () => { it('should show error toast when non-manager clicks enterprise button', async () => { setupAppContext({ isCurrentWorkspaceManager: false }) const user = userEvent.setup() - render() + renderSelfHostedPlanItem(SelfHostedPlan.enterprise) const button = screen.getByRole('button') await user.click(button) await waitFor(() => { - expect(mockToastNotify).toHaveBeenCalledWith( - expect.objectContaining({ type: 'error' }), - ) + expect(screen.getByText('billing.buyPermissionDeniedTip')).toBeInTheDocument() }) expect(assignedHref).toBe('') }) diff --git a/web/app/account/oauth/authorize/page.tsx b/web/app/account/oauth/authorize/page.tsx index 5ca920343e..30cfdd25d3 100644 --- a/web/app/account/oauth/authorize/page.tsx +++ b/web/app/account/oauth/authorize/page.tsx @@ -13,7 +13,7 @@ import { useTranslation } from 'react-i18next' import { Avatar } from '@/app/components/base/avatar' import Button from '@/app/components/base/button' import Loading from '@/app/components/base/loading' -import Toast from '@/app/components/base/toast' +import { toast } from '@/app/components/base/ui/toast' import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks' import { setPostLoginRedirect } from '@/app/signin/utils/post-login-redirect' import { useRouter, useSearchParams } from '@/next/navigation' @@ -91,9 +91,9 @@ export default function OAuthAuthorize() { globalThis.location.href = url.toString() } catch (err: any) { - Toast.notify({ + toast.add({ type: 'error', - message: `${t('error.authorizeFailed', { ns: 'oauth' })}: ${err.message}`, + title: `${t('error.authorizeFailed', { ns: 'oauth' })}: ${err.message}`, }) } } @@ -102,10 +102,10 @@ export default function OAuthAuthorize() { const invalidParams = !client_id || !redirect_uri if ((invalidParams || isError) && !hasNotifiedRef.current) { hasNotifiedRef.current = true - Toast.notify({ + toast.add({ type: 'error', - message: invalidParams ? t('error.invalidParams', { ns: 'oauth' }) : t('error.authAppInfoFetchFailed', { ns: 'oauth' }), - duration: 0, + title: invalidParams ? t('error.invalidParams', { ns: 'oauth' }) : t('error.authAppInfoFetchFailed', { ns: 'oauth' }), + timeout: 0, }) } }, [client_id, redirect_uri, isError]) diff --git a/web/app/components/app/create-app-dialog/app-list/index.spec.tsx b/web/app/components/app/create-app-dialog/app-list/index.spec.tsx index e2db3a94f7..a9b65a4ae9 100644 --- a/web/app/components/app/create-app-dialog/app-list/index.spec.tsx +++ b/web/app/components/app/create-app-dialog/app-list/index.spec.tsx @@ -39,8 +39,8 @@ vi.mock('../app-card', () => ({ vi.mock('@/app/components/explore/create-app-modal', () => ({ default: () =>
, })) -vi.mock('@/app/components/base/toast', () => ({ - default: { notify: vi.fn() }, +vi.mock('@/app/components/base/ui/toast', () => ({ + toast: { add: vi.fn() }, })) vi.mock('@/app/components/base/amplitude', () => ({ trackEvent: vi.fn(), diff --git a/web/app/components/app/create-app-dialog/app-list/index.tsx b/web/app/components/app/create-app-dialog/app-list/index.tsx index 5dea3e8aef..8b1876be04 100644 --- a/web/app/components/app/create-app-dialog/app-list/index.tsx +++ b/web/app/components/app/create-app-dialog/app-list/index.tsx @@ -12,7 +12,7 @@ import { trackEvent } from '@/app/components/base/amplitude' import Divider from '@/app/components/base/divider' import Input from '@/app/components/base/input' import Loading from '@/app/components/base/loading' -import Toast from '@/app/components/base/toast' +import { toast } from '@/app/components/base/ui/toast' import CreateAppModal from '@/app/components/explore/create-app-modal' import { usePluginDependencies } from '@/app/components/workflow/plugin-dependency/hooks' import { NEED_REFRESH_APP_LIST_KEY } from '@/config' @@ -137,9 +137,9 @@ const Apps = ({ }) setIsShowCreateModal(false) - Toast.notify({ + toast.add({ type: 'success', - message: t('newApp.appCreated', { ns: 'app' }), + title: t('newApp.appCreated', { ns: 'app' }), }) if (onSuccess) onSuccess() @@ -149,7 +149,7 @@ const Apps = ({ getRedirection(isCurrentWorkspaceEditor, { id: app.app_id!, mode }, push) } catch { - Toast.notify({ type: 'error', message: t('newApp.appCreateFailed', { ns: 'app' }) }) + toast.add({ type: 'error', title: t('newApp.appCreateFailed', { ns: 'app' }) }) } } diff --git a/web/app/components/base/toast/context.ts b/web/app/components/base/toast/context.ts index ddd8f91336..07b4e72602 100644 --- a/web/app/components/base/toast/context.ts +++ b/web/app/components/base/toast/context.ts @@ -1,8 +1,15 @@ 'use client' +/** + * @deprecated Use `@/app/components/base/ui/toast` instead. + * This module will be removed after migration is complete. + * See: https://github.com/langgenius/dify/issues/32811 + */ + import type { ReactNode } from 'react' import { createContext, useContext } from 'use-context-selector' +/** @deprecated Use `@/app/components/base/ui/toast` instead. See issue #32811. */ export type IToastProps = { type?: 'success' | 'error' | 'warning' | 'info' size?: 'md' | 'sm' @@ -19,5 +26,8 @@ type IToastContext = { close: () => void } +/** @deprecated Use `@/app/components/base/ui/toast` instead. See issue #32811. */ export const ToastContext = createContext({} as IToastContext) + +/** @deprecated Use `@/app/components/base/ui/toast` instead. See issue #32811. */ export const useToastContext = () => useContext(ToastContext) diff --git a/web/app/components/base/toast/index.tsx b/web/app/components/base/toast/index.tsx index 897b6039ba..0cb14f3f11 100644 --- a/web/app/components/base/toast/index.tsx +++ b/web/app/components/base/toast/index.tsx @@ -1,4 +1,11 @@ 'use client' + +/** + * @deprecated Use `@/app/components/base/ui/toast` instead. + * This component will be removed after migration is complete. + * See: https://github.com/langgenius/dify/issues/32811 + */ + import type { ReactNode } from 'react' import type { IToastProps } from './context' import { noop } from 'es-toolkit/function' @@ -12,6 +19,7 @@ import { ToastContext, useToastContext } from './context' export type ToastHandle = { clear?: VoidFunction } + const Toast = ({ type = 'info', size = 'md', @@ -74,6 +82,7 @@ const Toast = ({ ) } +/** @deprecated Use `@/app/components/base/ui/toast` instead. See issue #32811. */ export const ToastProvider = ({ children, }: { diff --git a/web/app/components/billing/pricing/plans/cloud-plan-item/__tests__/index.spec.tsx b/web/app/components/billing/pricing/plans/cloud-plan-item/__tests__/index.spec.tsx index 1c7283abeb..bd602df6c1 100644 --- a/web/app/components/billing/pricing/plans/cloud-plan-item/__tests__/index.spec.tsx +++ b/web/app/components/billing/pricing/plans/cloud-plan-item/__tests__/index.spec.tsx @@ -1,22 +1,16 @@ import type { Mock } from 'vitest' import { fireEvent, render, screen, waitFor } from '@testing-library/react' import * as React from 'react' +import { toast, ToastHost } from '@/app/components/base/ui/toast' import { useAppContext } from '@/context/app-context' import { useAsyncWindowOpen } from '@/hooks/use-async-window-open' import { fetchSubscriptionUrls } from '@/service/billing' import { consoleClient } from '@/service/client' -import Toast from '../../../../../base/toast' import { ALL_PLANS } from '../../../../config' import { Plan } from '../../../../type' import { PlanRange } from '../../../plan-switcher/plan-range-switcher' import CloudPlanItem from '../index' -vi.mock('../../../../../base/toast', () => ({ - default: { - notify: vi.fn(), - }, -})) - vi.mock('@/context/app-context', () => ({ useAppContext: vi.fn(), })) @@ -47,11 +41,19 @@ const mockUseAppContext = useAppContext as Mock const mockUseAsyncWindowOpen = useAsyncWindowOpen as Mock const mockBillingInvoices = consoleClient.billing.invoices as Mock const mockFetchSubscriptionUrls = fetchSubscriptionUrls as Mock -const mockToastNotify = Toast.notify as Mock let assignedHref = '' const originalLocation = window.location +const renderWithToastHost = (ui: React.ReactNode) => { + return render( + <> + + {ui} + , + ) +} + beforeAll(() => { Object.defineProperty(window, 'location', { configurable: true, @@ -68,6 +70,7 @@ beforeAll(() => { beforeEach(() => { vi.clearAllMocks() + toast.close() mockUseAppContext.mockReturnValue({ isCurrentWorkspaceManager: true }) mockUseAsyncWindowOpen.mockReturnValue(vi.fn(async open => await open())) mockBillingInvoices.mockResolvedValue({ url: 'https://billing.example' }) @@ -163,7 +166,7 @@ describe('CloudPlanItem', () => { it('should show toast when non-manager tries to buy a plan', () => { mockUseAppContext.mockReturnValue({ isCurrentWorkspaceManager: false }) - render( + renderWithToastHost( { ) fireEvent.click(screen.getByRole('button', { name: 'billing.plansCommon.startBuilding' })) - expect(mockToastNotify).toHaveBeenCalledWith(expect.objectContaining({ - type: 'error', - message: 'billing.buyPermissionDeniedTip', - })) + expect(screen.getByText('billing.buyPermissionDeniedTip')).toBeInTheDocument() expect(mockBillingInvoices).not.toHaveBeenCalled() }) diff --git a/web/app/components/billing/pricing/plans/cloud-plan-item/index.tsx b/web/app/components/billing/pricing/plans/cloud-plan-item/index.tsx index 0807381bcd..56856ccb77 100644 --- a/web/app/components/billing/pricing/plans/cloud-plan-item/index.tsx +++ b/web/app/components/billing/pricing/plans/cloud-plan-item/index.tsx @@ -4,11 +4,11 @@ import type { BasicPlan } from '../../../type' import * as React from 'react' import { useMemo } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from '@/app/components/base/ui/toast' import { useAppContext } from '@/context/app-context' import { useAsyncWindowOpen } from '@/hooks/use-async-window-open' import { fetchSubscriptionUrls } from '@/service/billing' import { consoleClient } from '@/service/client' -import Toast from '../../../../base/toast' import { ALL_PLANS } from '../../../config' import { Plan } from '../../../type' import { Professional, Sandbox, Team } from '../../assets' @@ -66,10 +66,9 @@ const CloudPlanItem: FC = ({ return if (!isCurrentWorkspaceManager) { - Toast.notify({ + toast.add({ type: 'error', - message: t('buyPermissionDeniedTip', { ns: 'billing' }), - className: 'z-[1001]', + title: t('buyPermissionDeniedTip', { ns: 'billing' }), }) return } @@ -83,7 +82,7 @@ const CloudPlanItem: FC = ({ throw new Error('Failed to open billing page') }, { onError: (err) => { - Toast.notify({ type: 'error', message: err.message || String(err) }) + toast.add({ type: 'error', title: err.message || String(err) }) }, }) return @@ -111,34 +110,34 @@ const CloudPlanItem: FC = ({ { isMostPopularPlan && (
- + {t('plansCommon.mostPopular', { ns: 'billing' })}
) }
-
{t(`${i18nPrefix}.description`, { ns: 'billing' })}
+
{t(`${i18nPrefix}.description`, { ns: 'billing' })}
{/* Price */}
{isFreePlan && ( - {t('plansCommon.free', { ns: 'billing' })} + {t('plansCommon.free', { ns: 'billing' })} )} {!isFreePlan && ( <> {isYear && ( - + $ {planInfo.price * 12} )} - + $ {isYear ? planInfo.price * 10 : planInfo.price} - + {t('plansCommon.priceTip', { ns: 'billing' })} {t(`plansCommon.${!isYear ? 'month' : 'year'}`, { ns: 'billing' })} diff --git a/web/app/components/billing/pricing/plans/self-hosted-plan-item/__tests__/index.spec.tsx b/web/app/components/billing/pricing/plans/self-hosted-plan-item/__tests__/index.spec.tsx index 9507cdef3c..d086b6ed9a 100644 --- a/web/app/components/billing/pricing/plans/self-hosted-plan-item/__tests__/index.spec.tsx +++ b/web/app/components/billing/pricing/plans/self-hosted-plan-item/__tests__/index.spec.tsx @@ -1,8 +1,8 @@ import type { Mock } from 'vitest' import { fireEvent, render, screen } from '@testing-library/react' import * as React from 'react' +import { toast, ToastHost } from '@/app/components/base/ui/toast' import { useAppContext } from '@/context/app-context' -import Toast from '../../../../../base/toast' import { contactSalesUrl, getStartedWithCommunityUrl, getWithPremiumUrl } from '../../../../config' import { SelfHostedPlan } from '../../../../type' import SelfHostedPlanItem from '../index' @@ -16,12 +16,6 @@ vi.mock('../list', () => ({ ), })) -vi.mock('../../../../../base/toast', () => ({ - default: { - notify: vi.fn(), - }, -})) - vi.mock('@/context/app-context', () => ({ useAppContext: vi.fn(), })) @@ -35,11 +29,19 @@ vi.mock('../../../assets', () => ({ })) const mockUseAppContext = useAppContext as Mock -const mockToastNotify = Toast.notify as Mock let assignedHref = '' const originalLocation = window.location +const renderWithToastHost = (ui: React.ReactNode) => { + return render( + <> + + {ui} + , + ) +} + beforeAll(() => { Object.defineProperty(window, 'location', { configurable: true, @@ -56,6 +58,7 @@ beforeAll(() => { beforeEach(() => { vi.clearAllMocks() + toast.close() mockUseAppContext.mockReturnValue({ isCurrentWorkspaceManager: true }) assignedHref = '' }) @@ -90,13 +93,10 @@ describe('SelfHostedPlanItem', () => { it('should show toast when non-manager tries to proceed', () => { mockUseAppContext.mockReturnValue({ isCurrentWorkspaceManager: false }) - render() + renderWithToastHost() fireEvent.click(screen.getByRole('button', { name: /billing\.plans\.premium\.btnText/ })) - expect(mockToastNotify).toHaveBeenCalledWith(expect.objectContaining({ - type: 'error', - message: 'billing.buyPermissionDeniedTip', - })) + expect(screen.getByText('billing.buyPermissionDeniedTip')).toBeInTheDocument() }) it('should redirect to community url when community plan button clicked', () => { diff --git a/web/app/components/billing/pricing/plans/self-hosted-plan-item/index.tsx b/web/app/components/billing/pricing/plans/self-hosted-plan-item/index.tsx index eaee5082ff..e1fabef96e 100644 --- a/web/app/components/billing/pricing/plans/self-hosted-plan-item/index.tsx +++ b/web/app/components/billing/pricing/plans/self-hosted-plan-item/index.tsx @@ -4,9 +4,9 @@ import * as React from 'react' import { useCallback } from 'react' import { useTranslation } from 'react-i18next' import { Azure, GoogleCloud } from '@/app/components/base/icons/src/public/billing' +import { toast } from '@/app/components/base/ui/toast' import { useAppContext } from '@/context/app-context' import { cn } from '@/utils/classnames' -import Toast from '../../../../base/toast' import { contactSalesUrl, getStartedWithCommunityUrl, getWithPremiumUrl } from '../../../config' import { SelfHostedPlan } from '../../../type' import { Community, Enterprise, EnterpriseNoise, Premium, PremiumNoise } from '../../assets' @@ -56,10 +56,9 @@ const SelfHostedPlanItem: FC = ({ const handleGetPayUrl = useCallback(() => { // Only workspace manager can buy plan if (!isCurrentWorkspaceManager) { - Toast.notify({ + toast.add({ type: 'error', - message: t('buyPermissionDeniedTip', { ns: 'billing' }), - className: 'z-[1001]', + title: t('buyPermissionDeniedTip', { ns: 'billing' }), }) return } @@ -82,18 +81,18 @@ const SelfHostedPlanItem: FC = ({ {/* Noise Effect */} {STYLE_MAP[plan].noise}
-
+
{STYLE_MAP[plan].icon}
{t(`${i18nPrefix}.name`, { ns: 'billing' })}
-
{t(`${i18nPrefix}.description`, { ns: 'billing' })}
+
{t(`${i18nPrefix}.description`, { ns: 'billing' })}
{/* Price */}
-
{t(`${i18nPrefix}.price`, { ns: 'billing' })}
+
{t(`${i18nPrefix}.price`, { ns: 'billing' })}
{!isFreePlan && ( - + {t(`${i18nPrefix}.priceTip`, { ns: 'billing' })} )} @@ -114,7 +113,7 @@ const SelfHostedPlanItem: FC = ({
- + {t('plans.premium.comingSoon', { ns: 'billing' })}
diff --git a/web/app/components/datasets/documents/detail/__tests__/new-segment.spec.tsx b/web/app/components/datasets/documents/detail/__tests__/new-segment.spec.tsx index dd0cc3cd16..97287822c3 100644 --- a/web/app/components/datasets/documents/detail/__tests__/new-segment.spec.tsx +++ b/web/app/components/datasets/documents/detail/__tests__/new-segment.spec.tsx @@ -1,6 +1,6 @@ -import type * as React from 'react' import { fireEvent, render, screen, waitFor } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vitest' +import { toast, ToastHost } from '@/app/components/base/ui/toast' import { ChunkingMode } from '@/models/datasets' import { IndexingType } from '../../../create/step-two' @@ -13,14 +13,7 @@ vi.mock('@/next/navigation', () => ({ }), })) -const mockNotify = vi.fn() -vi.mock('use-context-selector', async (importOriginal) => { - const actual = await importOriginal() as Record - return { - ...actual, - useContext: () => ({ notify: mockNotify }), - } -}) +const toastAddSpy = vi.spyOn(toast, 'add') // Mock dataset detail context let mockIndexingTechnique = IndexingType.QUALIFIED @@ -51,11 +44,6 @@ vi.mock('@/service/knowledge/use-segment', () => ({ }), })) -// Mock app store -vi.mock('@/app/components/app/store', () => ({ - useStore: () => ({ appSidebarExpand: 'expand' }), -})) - vi.mock('../completed/common/action-buttons', () => ({ default: ({ handleCancel, handleSave, loading, actionType }: { handleCancel: () => void, handleSave: () => void, loading: boolean, actionType: string }) => (
@@ -139,6 +127,8 @@ vi.mock('@/app/components/datasets/common/image-uploader/image-uploader-in-chunk describe('NewSegmentModal', () => { beforeEach(() => { vi.clearAllMocks() + vi.useRealTimers() + toast.close() mockFullScreen = false mockIndexingTechnique = IndexingType.QUALIFIED }) @@ -258,7 +248,7 @@ describe('NewSegmentModal', () => { fireEvent.click(screen.getByTestId('save-btn')) await waitFor(() => { - expect(mockNotify).toHaveBeenCalledWith( + expect(toastAddSpy).toHaveBeenCalledWith( expect.objectContaining({ type: 'error', }), @@ -272,7 +262,7 @@ describe('NewSegmentModal', () => { fireEvent.click(screen.getByTestId('save-btn')) await waitFor(() => { - expect(mockNotify).toHaveBeenCalledWith( + expect(toastAddSpy).toHaveBeenCalledWith( expect.objectContaining({ type: 'error', }), @@ -287,7 +277,7 @@ describe('NewSegmentModal', () => { fireEvent.click(screen.getByTestId('save-btn')) await waitFor(() => { - expect(mockNotify).toHaveBeenCalledWith( + expect(toastAddSpy).toHaveBeenCalledWith( expect.objectContaining({ type: 'error', }), @@ -337,7 +327,7 @@ describe('NewSegmentModal', () => { fireEvent.click(screen.getByTestId('save-btn')) await waitFor(() => { - expect(mockNotify).toHaveBeenCalledWith( + expect(toastAddSpy).toHaveBeenCalledWith( expect.objectContaining({ type: 'success', }), @@ -430,10 +420,9 @@ describe('NewSegmentModal', () => { }) }) - describe('CustomButton in success notification', () => { - it('should call viewNewlyAddedChunk when custom button is clicked', async () => { + describe('Action button in success notification', () => { + it('should call viewNewlyAddedChunk when the toast action is clicked', async () => { const mockViewNewlyAddedChunk = vi.fn() - mockNotify.mockImplementation(() => {}) mockAddSegment.mockImplementation((_params: unknown, options: { onSuccess: () => void, onSettled: () => void }) => { options.onSuccess() @@ -442,37 +431,25 @@ describe('NewSegmentModal', () => { }) render( - , + <> + + + , ) - // Enter content and save fireEvent.change(screen.getByTestId('question-input'), { target: { value: 'Test content' } }) fireEvent.click(screen.getByTestId('save-btn')) + const actionButton = await screen.findByRole('button', { name: 'common.operation.view' }) + fireEvent.click(actionButton) + await waitFor(() => { - expect(mockNotify).toHaveBeenCalledWith( - expect.objectContaining({ - type: 'success', - customComponent: expect.anything(), - }), - ) + expect(mockViewNewlyAddedChunk).toHaveBeenCalledTimes(1) }) - - // Extract customComponent from the notify call args - const notifyCallArgs = mockNotify.mock.calls[0][0] as { customComponent?: React.ReactElement } - expect(notifyCallArgs.customComponent).toBeDefined() - const customComponent = notifyCallArgs.customComponent! - const { container: btnContainer } = render(customComponent) - const viewButton = btnContainer.querySelector('.system-xs-semibold.text-text-accent') as HTMLElement - expect(viewButton).toBeInTheDocument() - fireEvent.click(viewButton) - - // Assert that viewNewlyAddedChunk was called via the onClick handler (lines 66-67) - expect(mockViewNewlyAddedChunk).toHaveBeenCalled() }) }) @@ -599,9 +576,8 @@ describe('NewSegmentModal', () => { }) }) - describe('onSave delayed call', () => { - it('should call onSave after timeout in success handler', async () => { - vi.useFakeTimers() + describe('onSave after success', () => { + it('should call onSave immediately after save succeeds', async () => { const mockOnSave = vi.fn() mockAddSegment.mockImplementation((_params: unknown, options: { onSuccess: () => void, onSettled: () => void }) => { options.onSuccess() @@ -611,15 +587,12 @@ describe('NewSegmentModal', () => { render() - // Enter content and save fireEvent.change(screen.getByTestId('question-input'), { target: { value: 'Test content' } }) fireEvent.click(screen.getByTestId('save-btn')) - // Fast-forward timer - vi.advanceTimersByTime(3000) - - expect(mockOnSave).toHaveBeenCalled() - vi.useRealTimers() + await waitFor(() => { + expect(mockOnSave).toHaveBeenCalledTimes(1) + }) }) }) diff --git a/web/app/components/datasets/documents/detail/completed/__tests__/new-child-segment.spec.tsx b/web/app/components/datasets/documents/detail/completed/__tests__/new-child-segment.spec.tsx index 48e8782740..b9c8bf80ba 100644 --- a/web/app/components/datasets/documents/detail/completed/__tests__/new-child-segment.spec.tsx +++ b/web/app/components/datasets/documents/detail/completed/__tests__/new-child-segment.spec.tsx @@ -1,5 +1,6 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vitest' +import { toast, ToastHost } from '@/app/components/base/ui/toast' import NewChildSegmentModal from '../new-child-segment' @@ -10,14 +11,7 @@ vi.mock('@/next/navigation', () => ({ }), })) -const mockNotify = vi.fn() -vi.mock('use-context-selector', async (importOriginal) => { - const actual = await importOriginal() as Record - return { - ...actual, - useContext: () => ({ notify: mockNotify }), - } -}) +const toastAddSpy = vi.spyOn(toast, 'add') // Mock document context let mockParentMode = 'paragraph' @@ -48,11 +42,6 @@ vi.mock('@/service/knowledge/use-segment', () => ({ }), })) -// Mock app store -vi.mock('@/app/components/app/store', () => ({ - useStore: () => ({ appSidebarExpand: 'expand' }), -})) - vi.mock('../common/action-buttons', () => ({ default: ({ handleCancel, handleSave, loading, actionType, isChildChunk }: { handleCancel: () => void, handleSave: () => void, loading: boolean, actionType: string, isChildChunk?: boolean }) => (
@@ -103,6 +92,8 @@ vi.mock('../common/segment-index-tag', () => ({ describe('NewChildSegmentModal', () => { beforeEach(() => { vi.clearAllMocks() + vi.useRealTimers() + toast.close() mockFullScreen = false mockParentMode = 'paragraph' }) @@ -198,7 +189,7 @@ describe('NewChildSegmentModal', () => { fireEvent.click(screen.getByTestId('save-btn')) await waitFor(() => { - expect(mockNotify).toHaveBeenCalledWith( + expect(toastAddSpy).toHaveBeenCalledWith( expect.objectContaining({ type: 'error', }), @@ -253,7 +244,7 @@ describe('NewChildSegmentModal', () => { fireEvent.click(screen.getByTestId('save-btn')) await waitFor(() => { - expect(mockNotify).toHaveBeenCalledWith( + expect(toastAddSpy).toHaveBeenCalledWith( expect.objectContaining({ type: 'success', }), @@ -374,35 +365,62 @@ describe('NewChildSegmentModal', () => { // View newly added chunk describe('View Newly Added Chunk', () => { - it('should show custom button in full-doc mode after save', async () => { + it('should call viewNewlyAddedChildChunk when the toast action is clicked', async () => { mockParentMode = 'full-doc' + const mockViewNewlyAddedChildChunk = vi.fn() mockAddChildSegment.mockImplementation((_params, options) => { options.onSuccess({ data: { id: 'new-child-id' } }) options.onSettled() return Promise.resolve() }) - render() + render( + <> + + + , + ) - // Enter valid content fireEvent.change(screen.getByTestId('content-input'), { target: { value: 'Valid content' }, }) fireEvent.click(screen.getByTestId('save-btn')) - // Assert - success notification with custom component + const actionButton = await screen.findByRole('button', { name: 'common.operation.view' }) + fireEvent.click(actionButton) + await waitFor(() => { - expect(mockNotify).toHaveBeenCalledWith( - expect.objectContaining({ - type: 'success', - customComponent: expect.anything(), - }), - ) + expect(mockViewNewlyAddedChildChunk).toHaveBeenCalledTimes(1) }) }) - it('should not show custom button in paragraph mode after save', async () => { + it('should call onSave immediately in full-doc mode after save succeeds', async () => { + mockParentMode = 'full-doc' + const mockOnSave = vi.fn() + mockAddChildSegment.mockImplementation((_params, options) => { + options.onSuccess({ data: { id: 'new-child-id' } }) + options.onSettled() + return Promise.resolve() + }) + + render() + + fireEvent.change(screen.getByTestId('content-input'), { + target: { value: 'Valid content' }, + }) + + fireEvent.click(screen.getByTestId('save-btn')) + + await waitFor(() => { + expect(mockOnSave).toHaveBeenCalledTimes(1) + }) + }) + + it('should call onSave with the new child chunk in paragraph mode', async () => { mockParentMode = 'paragraph' const mockOnSave = vi.fn() mockAddChildSegment.mockImplementation((_params, options) => { diff --git a/web/app/components/datasets/documents/detail/completed/new-child-segment.tsx b/web/app/components/datasets/documents/detail/completed/new-child-segment.tsx index edc0fca04c..bc9200c5be 100644 --- a/web/app/components/datasets/documents/detail/completed/new-child-segment.tsx +++ b/web/app/components/datasets/documents/detail/completed/new-child-segment.tsx @@ -1,13 +1,10 @@ import type { FC } from 'react' import type { ChildChunkDetail, SegmentUpdater } from '@/models/datasets' import { RiCloseLine, RiExpandDiagonalLine } from '@remixicon/react' -import { memo, useMemo, useRef, useState } from 'react' +import { memo, useState } from 'react' import { useTranslation } from 'react-i18next' -import { useContext } from 'use-context-selector' -import { useShallow } from 'zustand/react/shallow' -import { useStore as useAppStore } from '@/app/components/app/store' import Divider from '@/app/components/base/divider' -import { ToastContext } from '@/app/components/base/toast/context' +import { toast } from '@/app/components/base/ui/toast' import { ChunkingMode } from '@/models/datasets' import { useParams } from '@/next/navigation' import { useAddChildSegment } from '@/service/knowledge/use-segment' @@ -35,39 +32,15 @@ const NewChildSegmentModal: FC = ({ viewNewlyAddedChildChunk, }) => { const { t } = useTranslation() - const { notify } = useContext(ToastContext) const [content, setContent] = useState('') const { datasetId, documentId } = useParams<{ datasetId: string, documentId: string }>() const [loading, setLoading] = useState(false) const [addAnother, setAddAnother] = useState(true) const fullScreen = useSegmentListContext(s => s.fullScreen) const toggleFullScreen = useSegmentListContext(s => s.toggleFullScreen) - const { appSidebarExpand } = useAppStore(useShallow(state => ({ - appSidebarExpand: state.appSidebarExpand, - }))) const parentMode = useDocumentContext(s => s.parentMode) - const refreshTimer = useRef(null) - - const isFullDocMode = useMemo(() => { - return parentMode === 'full-doc' - }, [parentMode]) - - const CustomButton = ( - <> - - - - ) + const isFullDocMode = parentMode === 'full-doc' const handleCancel = (actionType: 'esc' | 'add' = 'esc') => { if (actionType === 'esc' || !addAnother) @@ -80,26 +53,27 @@ const NewChildSegmentModal: FC = ({ const params: SegmentUpdater = { content: '' } if (!content.trim()) - return notify({ type: 'error', message: t('segment.contentEmpty', { ns: 'datasetDocuments' }) }) + return toast.add({ type: 'error', title: t('segment.contentEmpty', { ns: 'datasetDocuments' }) }) params.content = content setLoading(true) await addChildSegment({ datasetId, documentId, segmentId: chunkId, body: params }, { onSuccess(res) { - notify({ + toast.add({ type: 'success', - message: t('segment.childChunkAdded', { ns: 'datasetDocuments' }), - className: `!w-[296px] !bottom-0 ${appSidebarExpand === 'expand' ? '!left-[216px]' : '!left-14'} - !top-auto !right-auto !mb-[52px] !ml-11`, - customComponent: isFullDocMode && CustomButton, + title: t('segment.childChunkAdded', { ns: 'datasetDocuments' }), + actionProps: isFullDocMode + ? { + children: t('operation.view', { ns: 'common' }), + onClick: viewNewlyAddedChildChunk, + } + : undefined, }) handleCancel('add') setContent('') if (isFullDocMode) { - refreshTimer.current = setTimeout(() => { - onSave() - }, 3000) + onSave() } else { onSave(res.data) @@ -111,10 +85,8 @@ const NewChildSegmentModal: FC = ({ }) } - const wordCountText = useMemo(() => { - const count = content.length - return `${formatNumber(count)} ${t('segment.characters', { ns: 'datasetDocuments', count })}` - }, [content.length]) + const count = content.length + const wordCountText = `${formatNumber(count)} ${t('segment.characters', { ns: 'datasetDocuments', count })}` return (
diff --git a/web/app/components/datasets/documents/detail/new-segment.tsx b/web/app/components/datasets/documents/detail/new-segment.tsx index 8db909f889..9cbb4746f9 100644 --- a/web/app/components/datasets/documents/detail/new-segment.tsx +++ b/web/app/components/datasets/documents/detail/new-segment.tsx @@ -2,13 +2,10 @@ import type { FC } from 'react' import type { FileEntity } from '@/app/components/datasets/common/image-uploader/types' import type { SegmentUpdater } from '@/models/datasets' import { RiCloseLine, RiExpandDiagonalLine } from '@remixicon/react' -import { memo, useCallback, useMemo, useRef, useState } from 'react' +import { memo, useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' -import { useContext } from 'use-context-selector' -import { useShallow } from 'zustand/react/shallow' -import { useStore as useAppStore } from '@/app/components/app/store' import Divider from '@/app/components/base/divider' -import { ToastContext } from '@/app/components/base/toast/context' +import { toast } from '@/app/components/base/ui/toast' import ImageUploaderInChunk from '@/app/components/datasets/common/image-uploader/image-uploader-in-chunk' import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail' import { ChunkingMode } from '@/models/datasets' @@ -39,7 +36,6 @@ const NewSegmentModal: FC = ({ viewNewlyAddedChunk, }) => { const { t } = useTranslation() - const { notify } = useContext(ToastContext) const [question, setQuestion] = useState('') const [answer, setAnswer] = useState('') const [attachments, setAttachments] = useState([]) @@ -50,27 +46,7 @@ const NewSegmentModal: FC = ({ const fullScreen = useSegmentListContext(s => s.fullScreen) const toggleFullScreen = useSegmentListContext(s => s.toggleFullScreen) const indexingTechnique = useDatasetDetailContextWithSelector(s => s.dataset?.indexing_technique) - const { appSidebarExpand } = useAppStore(useShallow(state => ({ - appSidebarExpand: state.appSidebarExpand, - }))) - const [imageUploaderKey, setImageUploaderKey] = useState(Date.now()) - const refreshTimer = useRef(null) - - const CustomButton = useMemo(() => ( - <> - - - - ), [viewNewlyAddedChunk, t]) + const [imageUploaderKey, setImageUploaderKey] = useState(() => Date.now()) const handleCancel = useCallback((actionType: 'esc' | 'add' = 'esc') => { if (actionType === 'esc' || !addAnother) @@ -87,15 +63,15 @@ const NewSegmentModal: FC = ({ const params: SegmentUpdater = { content: '', attachment_ids: [] } if (docForm === ChunkingMode.qa) { if (!question.trim()) { - return notify({ + return toast.add({ type: 'error', - message: t('segment.questionEmpty', { ns: 'datasetDocuments' }), + title: t('segment.questionEmpty', { ns: 'datasetDocuments' }), }) } if (!answer.trim()) { - return notify({ + return toast.add({ type: 'error', - message: t('segment.answerEmpty', { ns: 'datasetDocuments' }), + title: t('segment.answerEmpty', { ns: 'datasetDocuments' }), }) } @@ -104,9 +80,9 @@ const NewSegmentModal: FC = ({ } else { if (!question.trim()) { - return notify({ + return toast.add({ type: 'error', - message: t('segment.contentEmpty', { ns: 'datasetDocuments' }), + title: t('segment.contentEmpty', { ns: 'datasetDocuments' }), }) } @@ -122,12 +98,13 @@ const NewSegmentModal: FC = ({ setLoading(true) await addSegment({ datasetId, documentId, body: params }, { onSuccess() { - notify({ + toast.add({ type: 'success', - message: t('segment.chunkAdded', { ns: 'datasetDocuments' }), - className: `!w-[296px] !bottom-0 ${appSidebarExpand === 'expand' ? '!left-[216px]' : '!left-14'} - !top-auto !right-auto !mb-[52px] !ml-11`, - customComponent: CustomButton, + title: t('segment.chunkAdded', { ns: 'datasetDocuments' }), + actionProps: { + children: t('operation.view', { ns: 'common' }), + onClick: viewNewlyAddedChunk, + }, }) handleCancel('add') setQuestion('') @@ -135,20 +112,16 @@ const NewSegmentModal: FC = ({ setAttachments([]) setImageUploaderKey(Date.now()) setKeywords([]) - refreshTimer.current = setTimeout(() => { - onSave() - }, 3000) + onSave() }, onSettled() { setLoading(false) }, }) - }, [docForm, keywords, addSegment, datasetId, documentId, question, answer, attachments, notify, t, appSidebarExpand, CustomButton, handleCancel, onSave]) + }, [docForm, keywords, addSegment, datasetId, documentId, question, answer, attachments, t, handleCancel, onSave, viewNewlyAddedChunk]) - const wordCountText = useMemo(() => { - const count = docForm === ChunkingMode.qa ? (question.length + answer.length) : question.length - return `${formatNumber(count)} ${t('segment.characters', { ns: 'datasetDocuments', count })}` - }, [question.length, answer.length, docForm, t]) + const count = docForm === ChunkingMode.qa ? (question.length + answer.length) : question.length + const wordCountText = `${formatNumber(count)} ${t('segment.characters', { ns: 'datasetDocuments', count })}` const isECOIndexing = indexingTechnique === IndexingType.ECONOMICAL diff --git a/web/app/components/datasets/external-knowledge-base/connector/__tests__/index.spec.tsx b/web/app/components/datasets/external-knowledge-base/connector/__tests__/index.spec.tsx index 64b24fb08f..c948450f1b 100644 --- a/web/app/components/datasets/external-knowledge-base/connector/__tests__/index.spec.tsx +++ b/web/app/components/datasets/external-knowledge-base/connector/__tests__/index.spec.tsx @@ -21,11 +21,11 @@ vi.mock('@/context/i18n', () => ({ useDocLink: () => (path?: string) => `https://docs.dify.ai/en${path || ''}`, })) -const mockNotify = vi.fn() -vi.mock('@/app/components/base/toast/context', () => ({ - useToastContext: () => ({ - notify: mockNotify, - }), +const mockNotify = vi.hoisted(() => vi.fn()) +vi.mock('@/app/components/base/ui/toast', () => ({ + toast: { + add: mockNotify, + }, })) // Mock modal context @@ -164,7 +164,7 @@ describe('ExternalKnowledgeBaseConnector', () => { // Verify success notification expect(mockNotify).toHaveBeenCalledWith({ type: 'success', - message: 'External Knowledge Base Connected Successfully', + title: 'External Knowledge Base Connected Successfully', }) // Verify navigation back @@ -206,7 +206,7 @@ describe('ExternalKnowledgeBaseConnector', () => { await waitFor(() => { expect(mockNotify).toHaveBeenCalledWith({ type: 'error', - message: 'Failed to connect External Knowledge Base', + title: 'Failed to connect External Knowledge Base', }) }) @@ -228,7 +228,7 @@ describe('ExternalKnowledgeBaseConnector', () => { await waitFor(() => { expect(mockNotify).toHaveBeenCalledWith({ type: 'error', - message: 'Failed to connect External Knowledge Base', + title: 'Failed to connect External Knowledge Base', }) }) @@ -274,7 +274,7 @@ describe('ExternalKnowledgeBaseConnector', () => { await waitFor(() => { expect(mockNotify).toHaveBeenCalledWith({ type: 'success', - message: 'External Knowledge Base Connected Successfully', + title: 'External Knowledge Base Connected Successfully', }) }) }) diff --git a/web/app/components/datasets/external-knowledge-base/connector/index.tsx b/web/app/components/datasets/external-knowledge-base/connector/index.tsx index 789e92c668..6ff7014f47 100644 --- a/web/app/components/datasets/external-knowledge-base/connector/index.tsx +++ b/web/app/components/datasets/external-knowledge-base/connector/index.tsx @@ -4,13 +4,12 @@ import type { CreateKnowledgeBaseReq } from '@/app/components/datasets/external- import * as React from 'react' import { useState } from 'react' import { trackEvent } from '@/app/components/base/amplitude' -import { useToastContext } from '@/app/components/base/toast/context' +import { toast } from '@/app/components/base/ui/toast' import ExternalKnowledgeBaseCreate from '@/app/components/datasets/external-knowledge-base/create' import { useRouter } from '@/next/navigation' import { createExternalKnowledgeBase } from '@/service/datasets' const ExternalKnowledgeBaseConnector = () => { - const { notify } = useToastContext() const [loading, setLoading] = useState(false) const router = useRouter() @@ -19,7 +18,7 @@ const ExternalKnowledgeBaseConnector = () => { setLoading(true) const result = await createExternalKnowledgeBase({ body: formValue }) if (result && result.id) { - notify({ type: 'success', message: 'External Knowledge Base Connected Successfully' }) + toast.add({ type: 'success', title: 'External Knowledge Base Connected Successfully' }) trackEvent('create_external_knowledge_base', { provider: formValue.provider, name: formValue.name, @@ -30,7 +29,7 @@ const ExternalKnowledgeBaseConnector = () => { } catch (error) { console.error('Error creating external knowledge base:', error) - notify({ type: 'error', message: 'Failed to connect External Knowledge Base' }) + toast.add({ type: 'error', title: 'Failed to connect External Knowledge Base' }) } setLoading(false) } diff --git a/web/app/components/header/account-setting/model-provider-page/system-model-selector/__tests__/index.spec.tsx b/web/app/components/header/account-setting/model-provider-page/system-model-selector/__tests__/index.spec.tsx index fd3577a7cf..a3f20bed8f 100644 --- a/web/app/components/header/account-setting/model-provider-page/system-model-selector/__tests__/index.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/system-model-selector/__tests__/index.spec.tsx @@ -43,10 +43,10 @@ vi.mock('@/context/provider-context', () => ({ }), })) -vi.mock('@/app/components/base/toast/context', () => ({ - useToastContext: () => ({ - notify: mockNotify, - }), +vi.mock('@/app/components/base/ui/toast', () => ({ + toast: { + add: mockNotify, + }, })) vi.mock('../../hooks', () => ({ @@ -150,7 +150,7 @@ describe('SystemModel', () => { expect(mockUpdateDefaultModel).toHaveBeenCalledTimes(1) expect(mockNotify).toHaveBeenCalledWith({ type: 'success', - message: 'Modified successfully', + title: 'Modified successfully', }) expect(mockInvalidateDefaultModel).toHaveBeenCalledTimes(5) expect(mockUpdateModelList).toHaveBeenCalledTimes(5) diff --git a/web/app/components/header/account-setting/model-provider-page/system-model-selector/index.tsx b/web/app/components/header/account-setting/model-provider-page/system-model-selector/index.tsx index a103cafd91..f311d82b57 100644 --- a/web/app/components/header/account-setting/model-provider-page/system-model-selector/index.tsx +++ b/web/app/components/header/account-setting/model-provider-page/system-model-selector/index.tsx @@ -6,13 +6,13 @@ import type { import { useState } from 'react' import { useTranslation } from 'react-i18next' import Button from '@/app/components/base/button' -import { useToastContext } from '@/app/components/base/toast/context' import { Dialog, DialogCloseButton, DialogContent, DialogTitle, } from '@/app/components/base/ui/dialog' +import { toast } from '@/app/components/base/ui/toast' import { Tooltip, TooltipContent, @@ -64,7 +64,6 @@ const SystemModel: FC = ({ isLoading, }) => { const { t } = useTranslation() - const { notify } = useToastContext() const { isCurrentWorkspaceManager } = useAppContext() const { textGenerationModelList } = useProviderContext() const updateModelList = useUpdateModelList() @@ -124,7 +123,7 @@ const SystemModel: FC = ({ }, }) if (res.result === 'success') { - notify({ type: 'success', message: t('actionMsg.modifiedSuccessfully', { ns: 'common' }) }) + toast.add({ type: 'success', title: t('actionMsg.modifiedSuccessfully', { ns: 'common' }) }) setOpen(false) const allModelTypes = [ModelTypeEnum.textGeneration, ModelTypeEnum.textEmbedding, ModelTypeEnum.rerank, ModelTypeEnum.speech2text, ModelTypeEnum.tts] diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/__tests__/delete-confirm.spec.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/__tests__/delete-confirm.spec.tsx index 2f5dfe4256..af9018694f 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/__tests__/delete-confirm.spec.tsx +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/__tests__/delete-confirm.spec.tsx @@ -4,7 +4,7 @@ import { DeleteConfirm } from '../delete-confirm' const mockRefetch = vi.fn() const mockDelete = vi.fn() -const mockToast = vi.fn() +const mockToastAdd = vi.hoisted(() => vi.fn()) vi.mock('../use-subscription-list', () => ({ useSubscriptionList: () => ({ refetch: mockRefetch }), @@ -14,9 +14,9 @@ vi.mock('@/service/use-triggers', () => ({ useDeleteTriggerSubscription: () => ({ mutate: mockDelete, isPending: false }), })) -vi.mock('@/app/components/base/toast', () => ({ - default: { - notify: (args: { type: string, message: string }) => mockToast(args), +vi.mock('@/app/components/base/ui/toast', () => ({ + toast: { + add: mockToastAdd, }, })) @@ -42,7 +42,7 @@ describe('DeleteConfirm', () => { fireEvent.click(screen.getByRole('button', { name: /pluginTrigger\.subscription\.list\.item\.actions\.deleteConfirm\.confirm/ })) expect(mockDelete).not.toHaveBeenCalled() - expect(mockToast).toHaveBeenCalledWith(expect.objectContaining({ type: 'error' })) + expect(mockToastAdd).toHaveBeenCalledWith(expect.objectContaining({ type: 'error' })) }) it('should allow deletion after matching input name', () => { @@ -87,6 +87,6 @@ describe('DeleteConfirm', () => { fireEvent.click(screen.getByRole('button', { name: /pluginTrigger\.subscription\.list\.item\.actions\.deleteConfirm\.confirm/ })) - expect(mockToast).toHaveBeenCalledWith(expect.objectContaining({ type: 'error', message: 'network error' })) + expect(mockToastAdd).toHaveBeenCalledWith(expect.objectContaining({ type: 'error', title: 'network error' })) }) }) diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/delete-confirm.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/delete-confirm.tsx index 4cf8362b26..0c5fff0b82 100644 --- a/web/app/components/plugins/plugin-detail-panel/subscription-list/delete-confirm.tsx +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/delete-confirm.tsx @@ -1,8 +1,16 @@ import { useState } from 'react' import { useTranslation } from 'react-i18next' -import Confirm from '@/app/components/base/confirm' import Input from '@/app/components/base/input' -import Toast from '@/app/components/base/toast' +import { + AlertDialog, + AlertDialogActions, + AlertDialogCancelButton, + AlertDialogConfirmButton, + AlertDialogContent, + AlertDialogDescription, + AlertDialogTitle, +} from '@/app/components/base/ui/alert-dialog' +import { toast } from '@/app/components/base/ui/toast' import { useDeleteTriggerSubscription } from '@/service/use-triggers' import { useSubscriptionList } from './use-subscription-list' @@ -23,58 +31,74 @@ export const DeleteConfirm = (props: Props) => { const { t } = useTranslation() const [inputName, setInputName] = useState('') + const handleOpenChange = (open: boolean) => { + if (isDeleting) + return + + if (!open) + onClose(false) + } + const onConfirm = () => { if (workflowsInUse > 0 && inputName !== currentName) { - Toast.notify({ + toast.add({ type: 'error', - message: t(`${tPrefix}.confirmInputWarning`, { ns: 'pluginTrigger' }), - // temporarily - className: 'z-[10000001]', + title: t(`${tPrefix}.confirmInputWarning`, { ns: 'pluginTrigger' }), }) return } deleteSubscription(currentId, { onSuccess: () => { - Toast.notify({ + toast.add({ type: 'success', - message: t(`${tPrefix}.success`, { ns: 'pluginTrigger', name: currentName }), - className: 'z-[10000001]', + title: t(`${tPrefix}.success`, { ns: 'pluginTrigger', name: currentName }), }) refetch?.() onClose(true) }, - onError: (error: any) => { - Toast.notify({ + onError: (error: unknown) => { + toast.add({ type: 'error', - message: error?.message || t(`${tPrefix}.error`, { ns: 'pluginTrigger', name: currentName }), - className: 'z-[10000001]', + title: error instanceof Error ? error.message : t(`${tPrefix}.error`, { ns: 'pluginTrigger', name: currentName }), }) }, }) } + return ( - 0 - ? ( - <> - {t(`${tPrefix}.contentWithApps`, { ns: 'pluginTrigger', count: workflowsInUse })} -
{t(`${tPrefix}.confirmInputTip`, { ns: 'pluginTrigger', name: currentName })}
+ + +
+ + {t(`${tPrefix}.title`, { ns: 'pluginTrigger', name: currentName })} + + + {workflowsInUse > 0 + ? t(`${tPrefix}.contentWithApps`, { ns: 'pluginTrigger', count: workflowsInUse }) + : t(`${tPrefix}.content`, { ns: 'pluginTrigger' })} + + {workflowsInUse > 0 && ( +
+
+ {t(`${tPrefix}.confirmInputTip`, { ns: 'pluginTrigger', name: currentName })} +
setInputName(e.target.value)} placeholder={t(`${tPrefix}.confirmInputPlaceholder`, { ns: 'pluginTrigger', name: currentName })} /> - - ) - : t(`${tPrefix}.content`, { ns: 'pluginTrigger' })} - isShow={isShow} - isLoading={isDeleting} - isDisabled={isDeleting} - onConfirm={onConfirm} - onCancel={() => onClose(false)} - maskClosable={false} - /> +
+ )} +
+ + + {t('operation.cancel', { ns: 'common' })} + + + {t(`${tPrefix}.confirm`, { ns: 'pluginTrigger' })} + + +
+
) } diff --git a/web/app/components/workflow/nodes/_base/components/variable/output-var-list.tsx b/web/app/components/workflow/nodes/_base/components/variable/output-var-list.tsx index a486721be5..338651c147 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/output-var-list.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/output-var-list.tsx @@ -1,15 +1,14 @@ 'use client' import type { FC } from 'react' import type { OutputVar } from '../../../code/types' -import type { ToastHandle } from '@/app/components/base/toast' import type { VarType } from '@/app/components/workflow/types' import { useDebounceFn } from 'ahooks' import { produce } from 'immer' import * as React from 'react' -import { useCallback, useState } from 'react' +import { useCallback } from 'react' import { useTranslation } from 'react-i18next' import Input from '@/app/components/base/input' -import Toast from '@/app/components/base/toast' +import { toast } from '@/app/components/base/ui/toast' import { checkKeys, replaceSpaceWithUnderscoreInVarNameInput } from '@/utils/var' import RemoveButton from '../remove-button' import VarTypePicker from './var-type-picker' @@ -30,7 +29,6 @@ const OutputVarList: FC = ({ onRemove, }) => { const { t } = useTranslation() - const [toastHandler, setToastHandler] = useState() const list = outputKeyOrders.map((key) => { return { @@ -42,20 +40,17 @@ const OutputVarList: FC = ({ const { run: validateVarInput } = useDebounceFn((existingVariables: typeof list, newKey: string) => { const result = checkKeys([newKey], true) if (!result.isValid) { - setToastHandler(Toast.notify({ + toast.add({ type: 'error', - message: t(`varKeyError.${result.errorMessageKey}`, { ns: 'appDebug', key: result.errorKey }), - })) + title: t(`varKeyError.${result.errorMessageKey}`, { ns: 'appDebug', key: result.errorKey }), + }) return } if (existingVariables.some(key => key.variable?.trim() === newKey.trim())) { - setToastHandler(Toast.notify({ + toast.add({ type: 'error', - message: t('varKeyError.keyAlreadyExists', { ns: 'appDebug', key: newKey }), - })) - } - else { - toastHandler?.clear?.() + title: t('varKeyError.keyAlreadyExists', { ns: 'appDebug', key: newKey }), + }) } }, { wait: 500 }) @@ -66,7 +61,6 @@ const OutputVarList: FC = ({ replaceSpaceWithUnderscoreInVarNameInput(e.target) const newKey = e.target.value - toastHandler?.clear?.() validateVarInput(list.toSpliced(index, 1), newKey) const newOutputs = produce(outputs, (draft) => { @@ -75,7 +69,7 @@ const OutputVarList: FC = ({ }) onChange(newOutputs, index, newKey) } - }, [list, onChange, outputs, outputKeyOrders, validateVarInput]) + }, [list, onChange, outputs, validateVarInput]) const handleVarTypeChange = useCallback((index: number) => { return (value: string) => { @@ -85,7 +79,7 @@ const OutputVarList: FC = ({ }) onChange(newOutputs) } - }, [list, onChange, outputs, outputKeyOrders]) + }, [list, onChange, outputs]) const handleVarRemove = useCallback((index: number) => { return () => { diff --git a/web/app/components/workflow/nodes/_base/components/variable/var-list.tsx b/web/app/components/workflow/nodes/_base/components/variable/var-list.tsx index b5104377e1..0cb80f453f 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/var-list.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/var-list.tsx @@ -1,17 +1,16 @@ 'use client' import type { FC } from 'react' -import type { ToastHandle } from '@/app/components/base/toast' import type { ValueSelector, Var, Variable } from '@/app/components/workflow/types' import { RiDraggable } from '@remixicon/react' import { useDebounceFn } from 'ahooks' import { produce } from 'immer' import * as React from 'react' -import { useCallback, useMemo, useState } from 'react' +import { useCallback, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { ReactSortable } from 'react-sortablejs' import { v4 as uuid4 } from 'uuid' import Input from '@/app/components/base/input' -import Toast from '@/app/components/base/toast' +import { toast } from '@/app/components/base/ui/toast' import { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types' import { cn } from '@/utils/classnames' import { checkKeys, replaceSpaceWithUnderscoreInVarNameInput } from '@/utils/var' @@ -42,7 +41,6 @@ const VarList: FC = ({ isSupportFileVar = true, }) => { const { t } = useTranslation() - const [toastHandle, setToastHandle] = useState() const listWithIds = useMemo(() => list.map((item) => { const id = uuid4() @@ -55,20 +53,17 @@ const VarList: FC = ({ const { run: validateVarInput } = useDebounceFn((list: Variable[], newKey: string) => { const result = checkKeys([newKey], true) if (!result.isValid) { - setToastHandle(Toast.notify({ + toast.add({ type: 'error', - message: t(`varKeyError.${result.errorMessageKey}`, { ns: 'appDebug', key: result.errorKey }), - })) + title: t(`varKeyError.${result.errorMessageKey}`, { ns: 'appDebug', key: result.errorKey }), + }) return } if (list.some(item => item.variable?.trim() === newKey.trim())) { - setToastHandle(Toast.notify({ + toast.add({ type: 'error', - message: t('varKeyError.keyAlreadyExists', { ns: 'appDebug', key: newKey }), - })) - } - else { - toastHandle?.clear?.() + title: t('varKeyError.keyAlreadyExists', { ns: 'appDebug', key: newKey }), + }) } }, { wait: 500 }) @@ -78,7 +73,6 @@ const VarList: FC = ({ const newKey = e.target.value - toastHandle?.clear?.() validateVarInput(list.toSpliced(index, 1), newKey) onVarNameChange?.(list[index].variable, newKey) diff --git a/web/app/components/workflow/panel/version-history-panel/index.tsx b/web/app/components/workflow/panel/version-history-panel/index.tsx index 0ad3ef0549..9439efc918 100644 --- a/web/app/components/workflow/panel/version-history-panel/index.tsx +++ b/web/app/components/workflow/panel/version-history-panel/index.tsx @@ -7,7 +7,7 @@ import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import VersionInfoModal from '@/app/components/app/app-publisher/version-info-modal' import Divider from '@/app/components/base/divider' -import Toast from '@/app/components/base/toast' +import { toast } from '@/app/components/base/ui/toast' import { useSelector as useAppContextSelector } from '@/context/app-context' import { useDeleteWorkflow, useInvalidAllLastRun, useResetWorkflowVersionHistory, useUpdateWorkflow, useWorkflowVersionHistory } from '@/service/use-workflow' import { useDSL, useNodesSyncDraft, useWorkflowRun } from '../../hooks' @@ -118,9 +118,9 @@ export const VersionHistoryPanel = ({ break case VersionHistoryContextMenuOptions.copyId: copy(item.id) - Toast.notify({ + toast.add({ type: 'success', - message: t('versionHistory.action.copyIdSuccess', { ns: 'workflow' }), + title: t('versionHistory.action.copyIdSuccess', { ns: 'workflow' }), }) break case VersionHistoryContextMenuOptions.exportDSL: @@ -152,17 +152,17 @@ export const VersionHistoryPanel = ({ workflowStore.setState({ backupDraft: undefined }) handleSyncWorkflowDraft(true, false, { onSuccess: () => { - Toast.notify({ + toast.add({ type: 'success', - message: t('versionHistory.action.restoreSuccess', { ns: 'workflow' }), + title: t('versionHistory.action.restoreSuccess', { ns: 'workflow' }), }) deleteAllInspectVars() invalidAllLastRun() }, onError: () => { - Toast.notify({ + toast.add({ type: 'error', - message: t('versionHistory.action.restoreFailure', { ns: 'workflow' }), + title: t('versionHistory.action.restoreFailure', { ns: 'workflow' }), }) }, onSettled: () => { @@ -177,18 +177,18 @@ export const VersionHistoryPanel = ({ await deleteWorkflow(deleteVersionUrl?.(id) || '', { onSuccess: () => { setDeleteConfirmOpen(false) - Toast.notify({ + toast.add({ type: 'success', - message: t('versionHistory.action.deleteSuccess', { ns: 'workflow' }), + title: t('versionHistory.action.deleteSuccess', { ns: 'workflow' }), }) resetWorkflowVersionHistory() deleteAllInspectVars() invalidAllLastRun() }, onError: () => { - Toast.notify({ + toast.add({ type: 'error', - message: t('versionHistory.action.deleteFailure', { ns: 'workflow' }), + title: t('versionHistory.action.deleteFailure', { ns: 'workflow' }), }) }, onSettled: () => { @@ -207,16 +207,16 @@ export const VersionHistoryPanel = ({ }, { onSuccess: () => { setEditModalOpen(false) - Toast.notify({ + toast.add({ type: 'success', - message: t('versionHistory.action.updateSuccess', { ns: 'workflow' }), + title: t('versionHistory.action.updateSuccess', { ns: 'workflow' }), }) resetWorkflowVersionHistory() }, onError: () => { - Toast.notify({ + toast.add({ type: 'error', - message: t('versionHistory.action.updateFailure', { ns: 'workflow' }), + title: t('versionHistory.action.updateFailure', { ns: 'workflow' }), }) }, onSettled: () => { diff --git a/web/app/signin/components/mail-and-password-auth.tsx b/web/app/signin/components/mail-and-password-auth.tsx index 7ce4c9054f..e12c3da4df 100644 --- a/web/app/signin/components/mail-and-password-auth.tsx +++ b/web/app/signin/components/mail-and-password-auth.tsx @@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next' import { trackEvent } from '@/app/components/base/amplitude' import Button from '@/app/components/base/button' import Input from '@/app/components/base/input' -import Toast from '@/app/components/base/toast' +import { toast } from '@/app/components/base/ui/toast' import { emailRegex } from '@/config' import { useLocale } from '@/context/i18n' import Link from '@/next/link' @@ -35,18 +35,18 @@ export default function MailAndPasswordAuth({ isInvite, isEmailSetup, allowRegis const handleEmailPasswordLogin = async () => { if (!email) { - Toast.notify({ type: 'error', message: t('error.emailEmpty', { ns: 'login' }) }) + toast.add({ type: 'error', title: t('error.emailEmpty', { ns: 'login' }) }) return } if (!emailRegex.test(email)) { - Toast.notify({ + toast.add({ type: 'error', - message: t('error.emailInValid', { ns: 'login' }), + title: t('error.emailInValid', { ns: 'login' }), }) return } if (!password?.trim()) { - Toast.notify({ type: 'error', message: t('error.passwordEmpty', { ns: 'login' }) }) + toast.add({ type: 'error', title: t('error.passwordEmpty', { ns: 'login' }) }) return } @@ -83,17 +83,17 @@ export default function MailAndPasswordAuth({ isInvite, isEmailSetup, allowRegis } } else { - Toast.notify({ + toast.add({ type: 'error', - message: res.data, + title: res.data, }) } } catch (error) { if ((error as ResponseError).code === 'authentication_failed') { - Toast.notify({ + toast.add({ type: 'error', - message: t('error.invalidEmailOrPassword', { ns: 'login' }), + title: t('error.invalidEmailOrPassword', { ns: 'login' }), }) } } diff --git a/web/context/provider-context-provider.tsx b/web/context/provider-context-provider.tsx index ce7f2ba40c..0101dc69c8 100644 --- a/web/context/provider-context-provider.tsx +++ b/web/context/provider-context-provider.tsx @@ -5,7 +5,7 @@ import { useQueryClient } from '@tanstack/react-query' import dayjs from 'dayjs' import { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' -import Toast from '@/app/components/base/toast' +import { toast } from '@/app/components/base/ui/toast' import { setZendeskConversationFields } from '@/app/components/base/zendesk/utils' import { defaultPlan } from '@/app/components/billing/config' import { parseCurrentPlan } from '@/app/components/billing/utils' @@ -132,13 +132,11 @@ export const ProviderContextProvider = ({ if (anthropic && anthropic.system_configuration.current_quota_type === CurrentSystemQuotaTypeEnum.trial) { const quota = anthropic.system_configuration.quota_configurations.find(item => item.quota_type === anthropic.system_configuration.current_quota_type) if (quota && quota.is_valid && quota.quota_used < quota.quota_limit) { - Toast.notify({ + localStorage.setItem('anthropic_quota_notice', 'true') + toast.add({ type: 'info', - message: t('provider.anthropicHosted.trialQuotaTip', { ns: 'common' }), - duration: 60000, - onClose: () => { - localStorage.setItem('anthropic_quota_notice', 'true') - }, + title: t('provider.anthropicHosted.trialQuotaTip', { ns: 'common' }), + timeout: 60000, }) } } diff --git a/web/eslint-suppressions.json b/web/eslint-suppressions.json index a678b53eba..b613b64691 100644 --- a/web/eslint-suppressions.json +++ b/web/eslint-suppressions.json @@ -335,9 +335,6 @@ } }, "app/account/oauth/authorize/page.tsx": { - "no-restricted-imports": { - "count": 1 - }, "ts/no-explicit-any": { "count": 1 } @@ -1127,9 +1124,6 @@ } }, "app/components/app/create-app-dialog/app-list/index.tsx": { - "no-restricted-imports": { - "count": 1 - }, "tailwindcss/enforce-consistent-class-order": { "count": 5 } @@ -2924,14 +2918,6 @@ "count": 1 } }, - "app/components/billing/pricing/plans/cloud-plan-item/index.tsx": { - "no-restricted-imports": { - "count": 1 - }, - "tailwindcss/enforce-consistent-class-order": { - "count": 6 - } - }, "app/components/billing/pricing/plans/cloud-plan-item/list/item/index.tsx": { "tailwindcss/enforce-consistent-class-order": { "count": 1 @@ -2947,17 +2933,6 @@ "count": 1 } }, - "app/components/billing/pricing/plans/self-hosted-plan-item/index.tsx": { - "no-restricted-imports": { - "count": 1 - }, - "tailwindcss/enforce-consistent-class-order": { - "count": 4 - }, - "tailwindcss/no-unnecessary-whitespace": { - "count": 1 - } - }, "app/components/billing/pricing/plans/self-hosted-plan-item/list/index.tsx": { "tailwindcss/enforce-consistent-class-order": { "count": 1 @@ -3786,14 +3761,6 @@ "count": 1 } }, - "app/components/datasets/documents/detail/completed/new-child-segment.tsx": { - "no-restricted-imports": { - "count": 1 - }, - "ts/no-explicit-any": { - "count": 1 - } - }, "app/components/datasets/documents/detail/completed/segment-card/chunk-content.tsx": { "tailwindcss/enforce-consistent-class-order": { "count": 2 @@ -3862,14 +3829,6 @@ "count": 1 } }, - "app/components/datasets/documents/detail/new-segment.tsx": { - "no-restricted-imports": { - "count": 1 - }, - "ts/no-explicit-any": { - "count": 1 - } - }, "app/components/datasets/documents/detail/segment-add/index.tsx": { "no-restricted-imports": { "count": 1 @@ -3930,11 +3889,6 @@ "count": 1 } }, - "app/components/datasets/external-knowledge-base/connector/index.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "app/components/datasets/external-knowledge-base/create/ExternalApiSelect.tsx": { "react-hooks-extra/no-direct-set-state-in-use-effect": { "count": 1 @@ -4859,11 +4813,6 @@ "count": 1 } }, - "app/components/header/account-setting/model-provider-page/system-model-selector/index.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "app/components/header/account-setting/plugin-page/SerpapiPlugin.tsx": { "no-restricted-imports": { "count": 1 @@ -5394,17 +5343,6 @@ "count": 3 } }, - "app/components/plugins/plugin-detail-panel/subscription-list/delete-confirm.tsx": { - "no-restricted-imports": { - "count": 2 - }, - "tailwindcss/enforce-consistent-class-order": { - "count": 1 - }, - "ts/no-explicit-any": { - "count": 1 - } - }, "app/components/plugins/plugin-detail-panel/subscription-list/edit/apikey-edit-modal.tsx": { "no-restricted-imports": { "count": 2 @@ -7105,11 +7043,6 @@ "count": 5 } }, - "app/components/workflow/nodes/_base/components/variable/output-var-list.tsx": { - "no-restricted-imports": { - "count": 2 - } - }, "app/components/workflow/nodes/_base/components/variable/utils.ts": { "ts/no-explicit-any": { "count": 32 @@ -7123,11 +7056,6 @@ "count": 1 } }, - "app/components/workflow/nodes/_base/components/variable/var-list.tsx": { - "no-restricted-imports": { - "count": 2 - } - }, "app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx": { "no-restricted-imports": { "count": 2 @@ -8877,9 +8805,6 @@ } }, "app/components/workflow/panel/version-history-panel/index.tsx": { - "no-restricted-imports": { - "count": 1 - }, "tailwindcss/enforce-consistent-class-order": { "count": 2 } @@ -9450,9 +9375,6 @@ } }, "app/signin/components/mail-and-password-auth.tsx": { - "no-restricted-imports": { - "count": 1 - }, "ts/no-explicit-any": { "count": 1 } @@ -9564,9 +9486,6 @@ } }, "context/provider-context-provider.tsx": { - "no-restricted-imports": { - "count": 1 - }, "ts/no-explicit-any": { "count": 1 } @@ -9752,9 +9671,6 @@ } }, "service/fetch.ts": { - "no-restricted-imports": { - "count": 1 - }, "regexp/no-unused-capturing-group": { "count": 1 }, diff --git a/web/service/fetch.spec.ts b/web/service/fetch.spec.ts index ef38a4c510..0c01d32438 100644 --- a/web/service/fetch.spec.ts +++ b/web/service/fetch.spec.ts @@ -1,9 +1,9 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { base } from './fetch' -vi.mock('@/app/components/base/toast', () => ({ - default: { - notify: vi.fn(), +vi.mock('@/app/components/base/ui/toast', () => ({ + toast: { + add: vi.fn(), }, })) diff --git a/web/service/fetch.ts b/web/service/fetch.ts index d5934d4a57..664280cd8e 100644 --- a/web/service/fetch.ts +++ b/web/service/fetch.ts @@ -2,7 +2,7 @@ import type { AfterResponseHook, BeforeRequestHook, Hooks } from 'ky' import type { IOtherOptions } from './base' import Cookies from 'js-cookie' import ky, { HTTPError } from 'ky' -import Toast from '@/app/components/base/toast' +import { toast } from '@/app/components/base/ui/toast' import { API_PREFIX, APP_VERSION, CSRF_COOKIE_NAME, CSRF_HEADER_NAME, IS_MARKETPLACE, MARKETPLACE_API_PREFIX, PASSPORT_HEADER_NAME, PUBLIC_API_PREFIX, WEB_APP_SHARE_CODE_HEADER_NAME } from '@/config' import { getWebAppAccessToken, getWebAppPassport } from './webapp-auth' @@ -48,7 +48,7 @@ const afterResponseErrorCode = (otherOptions: IOtherOptions): AfterResponseHook const shouldNotifyError = response.status !== 401 && errorData && !otherOptions.silent if (shouldNotifyError) - Toast.notify({ type: 'error', message: errorData.message }) + toast.add({ type: 'error', title: errorData.message }) if (response.status === 403 && errorData?.code === 'already_setup') globalThis.location.href = `${globalThis.location.origin}/signin`