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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 353 additions and 480 deletions

View File

@ -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<string, unknown> = {}
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(
<CloudPlanItem
currentPlan={currentPlan}
plan={plan}
planRange={planRange}
canPay={canPay}
/>,
<>
<ToastHost timeout={0} />
<CloudPlanItem
currentPlan={currentPlan}
plan={plan}
planRange={planRange}
canPay={canPay}
/>
</>,
)
}
@ -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()

View File

@ -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<string, unknown> = {}
const mockToastNotify = vi.fn()
const originalLocation = window.location
let assignedHref = ''
@ -40,10 +40,6 @@ vi.mock('@/app/components/base/icons/src/public/billing', () => ({
AwsMarketplaceDark: () => <span data-testid="icon-aws-dark" />,
}))
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 }) => (
<div data-testid={`self-hosted-list-${plan}`}>Features</div>
@ -57,10 +53,20 @@ const setupAppContext = (overrides: Record<string, unknown> = {}) => {
}
}
const renderSelfHostedPlanItem = (plan: SelfHostedPlan) => {
return render(
<>
<ToastHost timeout={0} />
<SelfHostedPlanItem plan={plan} />
</>,
)
}
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(<SelfHostedPlanItem plan={SelfHostedPlan.community} />)
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(<SelfHostedPlanItem plan={SelfHostedPlan.premium} />)
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(<SelfHostedPlanItem plan={SelfHostedPlan.enterprise} />)
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(<SelfHostedPlanItem plan={SelfHostedPlan.community} />)
renderSelfHostedPlanItem(SelfHostedPlan.community)
expect(screen.queryByText(/plans\.community\.priceTip/i)).not.toBeInTheDocument()
})
it('should show price tip for premium plan', () => {
render(<SelfHostedPlanItem plan={SelfHostedPlan.premium} />)
renderSelfHostedPlanItem(SelfHostedPlan.premium)
expect(screen.getByText(/plans\.premium\.priceTip/i)).toBeInTheDocument()
})
it('should render features list for each plan', () => {
const { unmount: unmount1 } = render(<SelfHostedPlanItem plan={SelfHostedPlan.community} />)
const { unmount: unmount1 } = renderSelfHostedPlanItem(SelfHostedPlan.community)
expect(screen.getByTestId('self-hosted-list-community')).toBeInTheDocument()
unmount1()
const { unmount: unmount2 } = render(<SelfHostedPlanItem plan={SelfHostedPlan.premium} />)
const { unmount: unmount2 } = renderSelfHostedPlanItem(SelfHostedPlan.premium)
expect(screen.getByTestId('self-hosted-list-premium')).toBeInTheDocument()
unmount2()
render(<SelfHostedPlanItem plan={SelfHostedPlan.enterprise} />)
renderSelfHostedPlanItem(SelfHostedPlan.enterprise)
expect(screen.getByTestId('self-hosted-list-enterprise')).toBeInTheDocument()
})
it('should show AWS marketplace icon for premium plan button', () => {
render(<SelfHostedPlanItem plan={SelfHostedPlan.premium} />)
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(<SelfHostedPlanItem plan={SelfHostedPlan.community} />)
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(<SelfHostedPlanItem plan={SelfHostedPlan.premium} />)
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(<SelfHostedPlanItem plan={SelfHostedPlan.enterprise} />)
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(<SelfHostedPlanItem plan={SelfHostedPlan.community} />)
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(<SelfHostedPlanItem plan={SelfHostedPlan.premium} />)
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(<SelfHostedPlanItem plan={SelfHostedPlan.enterprise} />)
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('')
})

View File

@ -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])

View File

@ -39,8 +39,8 @@ vi.mock('../app-card', () => ({
vi.mock('@/app/components/explore/create-app-modal', () => ({
default: () => <div data-testid="create-from-template-modal" />,
}))
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(),

View File

@ -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' }) })
}
}

View File

@ -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<IToastContext>({} as IToastContext)
/** @deprecated Use `@/app/components/base/ui/toast` instead. See issue #32811. */
export const useToastContext = () => useContext(ToastContext)

View File

@ -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,
}: {

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>

View File

@ -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<string, unknown>
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 }) => (
<div data-testid="action-buttons">
@ -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(
<NewSegmentModal
{...defaultProps}
docForm={ChunkingMode.text}
viewNewlyAddedChunk={mockViewNewlyAddedChunk}
/>,
<>
<ToastHost timeout={0} />
<NewSegmentModal
{...defaultProps}
docForm={ChunkingMode.text}
viewNewlyAddedChunk={mockViewNewlyAddedChunk}
/>
</>,
)
// 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(<NewSegmentModal {...defaultProps} onSave={mockOnSave} docForm={ChunkingMode.text} />)
// 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)
})
})
})

View File

@ -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<string, unknown>
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 }) => (
<div data-testid="action-buttons">
@ -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(<NewChildSegmentModal {...defaultProps} />)
render(
<>
<ToastHost timeout={0} />
<NewChildSegmentModal
{...defaultProps}
viewNewlyAddedChildChunk={mockViewNewlyAddedChildChunk}
/>
</>,
)
// 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(<NewChildSegmentModal {...defaultProps} onSave={mockOnSave} />)
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) => {

View File

@ -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<NewChildSegmentModalProps> = ({
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<any>(null)
const isFullDocMode = useMemo(() => {
return parentMode === 'full-doc'
}, [parentMode])
const CustomButton = (
<>
<Divider type="vertical" className="mx-1 h-3 bg-divider-regular" />
<button
type="button"
className="text-text-accent system-xs-semibold"
onClick={() => {
clearTimeout(refreshTimer.current)
viewNewlyAddedChildChunk?.()
}}
>
{t('operation.view', { ns: 'common' })}
</button>
</>
)
const isFullDocMode = parentMode === 'full-doc'
const handleCancel = (actionType: 'esc' | 'add' = 'esc') => {
if (actionType === 'esc' || !addAnother)
@ -80,26 +53,27 @@ const NewChildSegmentModal: FC<NewChildSegmentModalProps> = ({
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<NewChildSegmentModalProps> = ({
})
}
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 (
<div className="flex h-full flex-col">

View File

@ -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<NewSegmentModalProps> = ({
viewNewlyAddedChunk,
}) => {
const { t } = useTranslation()
const { notify } = useContext(ToastContext)
const [question, setQuestion] = useState('')
const [answer, setAnswer] = useState('')
const [attachments, setAttachments] = useState<FileEntity[]>([])
@ -50,27 +46,7 @@ const NewSegmentModal: FC<NewSegmentModalProps> = ({
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<any>(null)
const CustomButton = useMemo(() => (
<>
<Divider type="vertical" className="mx-1 h-3 bg-divider-regular" />
<button
type="button"
className="text-text-accent system-xs-semibold"
onClick={() => {
clearTimeout(refreshTimer.current)
viewNewlyAddedChunk()
}}
>
{t('operation.view', { ns: 'common' })}
</button>
</>
), [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<NewSegmentModalProps> = ({
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<NewSegmentModalProps> = ({
}
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<NewSegmentModalProps> = ({
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<NewSegmentModalProps> = ({
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

View File

@ -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',
})
})
})

View File

@ -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)
}

View File

@ -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)

View File

@ -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<SystemModelSelectorProps> = ({
isLoading,
}) => {
const { t } = useTranslation()
const { notify } = useToastContext()
const { isCurrentWorkspaceManager } = useAppContext()
const { textGenerationModelList } = useProviderContext()
const updateModelList = useUpdateModelList()
@ -124,7 +123,7 @@ const SystemModel: FC<SystemModelSelectorProps> = ({
},
})
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]

View File

@ -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' }))
})
})

View File

@ -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 (
<Confirm
title={t(`${tPrefix}.title`, { ns: 'pluginTrigger', name: currentName })}
confirmText={t(`${tPrefix}.confirm`, { ns: 'pluginTrigger' })}
content={workflowsInUse > 0
? (
<>
{t(`${tPrefix}.contentWithApps`, { ns: 'pluginTrigger', count: workflowsInUse })}
<div className="system-sm-medium mb-2 mt-6 text-text-secondary">{t(`${tPrefix}.confirmInputTip`, { ns: 'pluginTrigger', name: currentName })}</div>
<AlertDialog open={isShow} onOpenChange={handleOpenChange}>
<AlertDialogContent backdropProps={{ forceRender: true }}>
<div className="flex flex-col gap-2 px-6 pb-4 pt-6">
<AlertDialogTitle title={t(`${tPrefix}.title`, { ns: 'pluginTrigger', name: currentName })} className="w-full truncate text-text-primary title-2xl-semi-bold">
{t(`${tPrefix}.title`, { ns: 'pluginTrigger', name: currentName })}
</AlertDialogTitle>
<AlertDialogDescription className="w-full whitespace-pre-wrap break-words text-text-tertiary system-md-regular">
{workflowsInUse > 0
? t(`${tPrefix}.contentWithApps`, { ns: 'pluginTrigger', count: workflowsInUse })
: t(`${tPrefix}.content`, { ns: 'pluginTrigger' })}
</AlertDialogDescription>
{workflowsInUse > 0 && (
<div className="mt-6">
<div className="mb-2 text-text-secondary system-sm-medium">
{t(`${tPrefix}.confirmInputTip`, { ns: 'pluginTrigger', name: currentName })}
</div>
<Input
value={inputName}
onChange={e => 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}
/>
</div>
)}
</div>
<AlertDialogActions>
<AlertDialogCancelButton disabled={isDeleting}>
{t('operation.cancel', { ns: 'common' })}
</AlertDialogCancelButton>
<AlertDialogConfirmButton loading={isDeleting} disabled={isDeleting} onClick={onConfirm}>
{t(`${tPrefix}.confirm`, { ns: 'pluginTrigger' })}
</AlertDialogConfirmButton>
</AlertDialogActions>
</AlertDialogContent>
</AlertDialog>
)
}

View File

@ -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<Props> = ({
onRemove,
}) => {
const { t } = useTranslation()
const [toastHandler, setToastHandler] = useState<ToastHandle>()
const list = outputKeyOrders.map((key) => {
return {
@ -42,20 +40,17 @@ const OutputVarList: FC<Props> = ({
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<Props> = ({
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<Props> = ({
})
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<Props> = ({
})
onChange(newOutputs)
}
}, [list, onChange, outputs, outputKeyOrders])
}, [list, onChange, outputs])
const handleVarRemove = useCallback((index: number) => {
return () => {

View File

@ -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<Props> = ({
isSupportFileVar = true,
}) => {
const { t } = useTranslation()
const [toastHandle, setToastHandle] = useState<ToastHandle>()
const listWithIds = useMemo(() => list.map((item) => {
const id = uuid4()
@ -55,20 +53,17 @@ const VarList: FC<Props> = ({
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<Props> = ({
const newKey = e.target.value
toastHandle?.clear?.()
validateVarInput(list.toSpliced(index, 1), newKey)
onVarNameChange?.(list[index].variable, newKey)

View File

@ -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: () => {

View File

@ -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' }),
})
}
}

View File

@ -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,
})
}
}

View File

@ -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
},

View File

@ -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(),
},
}))

View File

@ -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`