refactor(web): migrate core toast call sites to base ui toast (#33643)

This commit is contained in:
yyh
2026-03-18 16:53:55 +08:00
committed by GitHub
parent db4deb1d6b
commit 93f9546353
29 changed files with 353 additions and 480 deletions

View File

@ -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(
<>
<ToastHost timeout={0} />
{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(
<CloudPlanItem
plan={Plan.professional}
currentPlan={Plan.sandbox}
@ -173,10 +176,7 @@ describe('CloudPlanItem', () => {
)
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()
})

View File

@ -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<CloudPlanItemProps> = ({
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<CloudPlanItemProps> = ({
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<CloudPlanItemProps> = ({
{
isMostPopularPlan && (
<div className="flex items-center justify-center bg-saas-dify-blue-static px-1.5 py-1">
<span className="system-2xs-semibold-uppercase text-text-primary-on-surface">
<span className="text-text-primary-on-surface system-2xs-semibold-uppercase">
{t('plansCommon.mostPopular', { ns: 'billing' })}
</span>
</div>
)
}
</div>
<div className="system-sm-regular text-text-secondary">{t(`${i18nPrefix}.description`, { ns: 'billing' })}</div>
<div className="text-text-secondary system-sm-regular">{t(`${i18nPrefix}.description`, { ns: 'billing' })}</div>
</div>
</div>
{/* Price */}
<div className="flex items-end gap-x-2 px-1 pb-8 pt-4">
{isFreePlan && (
<span className="title-4xl-semi-bold text-text-primary">{t('plansCommon.free', { ns: 'billing' })}</span>
<span className="text-text-primary title-4xl-semi-bold">{t('plansCommon.free', { ns: 'billing' })}</span>
)}
{!isFreePlan && (
<>
{isYear && (
<span className="title-4xl-semi-bold text-text-quaternary line-through">
<span className="text-text-quaternary line-through title-4xl-semi-bold">
$
{planInfo.price * 12}
</span>
)}
<span className="title-4xl-semi-bold text-text-primary">
<span className="text-text-primary title-4xl-semi-bold">
$
{isYear ? planInfo.price * 10 : planInfo.price}
</span>
<span className="system-md-regular pb-0.5 text-text-tertiary">
<span className="pb-0.5 text-text-tertiary system-md-regular">
{t('plansCommon.priceTip', { ns: 'billing' })}
{t(`plansCommon.${!isYear ? 'month' : 'year'}`, { ns: 'billing' })}
</span>

View File

@ -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(
<>
<ToastHost timeout={0} />
{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(<SelfHostedPlanItem plan={SelfHostedPlan.premium} />)
renderWithToastHost(<SelfHostedPlanItem plan={SelfHostedPlan.premium} />)
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', () => {

View File

@ -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<SelfHostedPlanItemProps> = ({
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<SelfHostedPlanItemProps> = ({
{/* Noise Effect */}
{STYLE_MAP[plan].noise}
<div className="flex flex-col px-5 py-4">
<div className=" flex flex-col gap-y-6 px-1 pt-10">
<div className="flex flex-col gap-y-6 px-1 pt-10">
{STYLE_MAP[plan].icon}
<div className="flex min-h-[104px] flex-col gap-y-2">
<div className="text-[30px] font-medium leading-[1.2] text-text-primary">{t(`${i18nPrefix}.name`, { ns: 'billing' })}</div>
<div className="system-md-regular line-clamp-2 text-text-secondary">{t(`${i18nPrefix}.description`, { ns: 'billing' })}</div>
<div className="line-clamp-2 text-text-secondary system-md-regular">{t(`${i18nPrefix}.description`, { ns: 'billing' })}</div>
</div>
</div>
{/* Price */}
<div className="flex items-end gap-x-2 px-1 pb-8 pt-4">
<div className="title-4xl-semi-bold shrink-0 text-text-primary">{t(`${i18nPrefix}.price`, { ns: 'billing' })}</div>
<div className="shrink-0 text-text-primary title-4xl-semi-bold">{t(`${i18nPrefix}.price`, { ns: 'billing' })}</div>
{!isFreePlan && (
<span className="system-md-regular pb-0.5 text-text-tertiary">
<span className="pb-0.5 text-text-tertiary system-md-regular">
{t(`${i18nPrefix}.priceTip`, { ns: 'billing' })}
</span>
)}
@ -114,7 +113,7 @@ const SelfHostedPlanItem: FC<SelfHostedPlanItemProps> = ({
<GoogleCloud />
</div>
</div>
<span className="system-xs-regular text-text-tertiary">
<span className="text-text-tertiary system-xs-regular">
{t('plans.premium.comingSoon', { ns: 'billing' })}
</span>
</div>