mirror of https://github.com/langgenius/dify.git
refactor(web): migrate core toast call sites to base ui toast (#33643)
This commit is contained in:
parent
db4deb1d6b
commit
93f9546353
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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('')
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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' }) })
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}: {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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' }))
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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: () => {
|
||||
|
|
|
|||
|
|
@ -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' }),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
},
|
||||
}))
|
||||
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
|
|||
Loading…
Reference in New Issue