refactor(web): migrate plugin toast usage to new UI toast API and update tests (#34001)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
yyh 2026-03-24 14:02:52 +08:00 committed by GitHub
parent 8b634a9bee
commit b0920ecd17
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
41 changed files with 390 additions and 339 deletions

View File

@ -12,8 +12,16 @@ vi.mock('@/config', () => ({
})) }))
const mockToastNotify = vi.fn() const mockToastNotify = vi.fn()
vi.mock('@/app/components/base/toast', () => ({ vi.mock('@/app/components/base/ui/toast', () => ({
default: { notify: (...args: unknown[]) => mockToastNotify(...args) }, toast: Object.assign((message: string, options?: { type?: string }) => mockToastNotify({ type: options?.type, message }), {
success: (message: string) => mockToastNotify({ type: 'success', message }),
error: (message: string) => mockToastNotify({ type: 'error', message }),
warning: (message: string) => mockToastNotify({ type: 'warning', message }),
info: (message: string) => mockToastNotify({ type: 'info', message }),
dismiss: vi.fn(),
update: vi.fn(),
promise: vi.fn(),
}),
})) }))
const mockUploadGitHub = vi.fn() const mockUploadGitHub = vi.fn()

View File

@ -3,8 +3,16 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
import { useGitHubReleases, useGitHubUpload } from '../hooks' import { useGitHubReleases, useGitHubUpload } from '../hooks'
const mockNotify = vi.fn() const mockNotify = vi.fn()
vi.mock('@/app/components/base/toast', () => ({ vi.mock('@/app/components/base/ui/toast', () => ({
default: { notify: (...args: unknown[]) => mockNotify(...args) }, toast: Object.assign((...args: unknown[]) => mockNotify(...args), {
success: (...args: unknown[]) => mockNotify(...args),
error: (...args: unknown[]) => mockNotify(...args),
warning: (...args: unknown[]) => mockNotify(...args),
info: (...args: unknown[]) => mockNotify(...args),
dismiss: vi.fn(),
update: vi.fn(),
promise: vi.fn(),
}),
})) }))
vi.mock('@/config', () => ({ vi.mock('@/config', () => ({
@ -56,9 +64,7 @@ describe('install-plugin/hooks', () => {
const releases = await result.current.fetchReleases('owner', 'repo') const releases = await result.current.fetchReleases('owner', 'repo')
expect(releases).toEqual([]) expect(releases).toEqual([])
expect(mockNotify).toHaveBeenCalledWith( expect(mockNotify).toHaveBeenCalledWith('Failed to fetch repository releases')
expect.objectContaining({ type: 'error' }),
)
}) })
}) })
@ -130,9 +136,7 @@ describe('install-plugin/hooks', () => {
await expect( await expect(
result.current.handleUpload('url', 'v1', 'pkg'), result.current.handleUpload('url', 'v1', 'pkg'),
).rejects.toThrow('Upload failed') ).rejects.toThrow('Upload failed')
expect(mockNotify).toHaveBeenCalledWith( expect(mockNotify).toHaveBeenCalledWith('Error uploading package')
expect.objectContaining({ type: 'error', message: 'Error uploading package' }),
)
}) })
}) })
}) })

View File

@ -1,6 +1,5 @@
import type { GitHubRepoReleaseResponse } from '../types' import type { GitHubRepoReleaseResponse } from '../types'
import type { IToastProps } from '@/app/components/base/toast' import { toast } from '@/app/components/base/ui/toast'
import Toast from '@/app/components/base/toast'
import { GITHUB_ACCESS_TOKEN } from '@/config' import { GITHUB_ACCESS_TOKEN } from '@/config'
import { uploadGitHub } from '@/service/plugins' import { uploadGitHub } from '@/service/plugins'
import { compareVersion, getLatestVersion } from '@/utils/semver' import { compareVersion, getLatestVersion } from '@/utils/semver'
@ -37,16 +36,10 @@ export const useGitHubReleases = () => {
} }
catch (error) { catch (error) {
if (error instanceof Error) { if (error instanceof Error) {
Toast.notify({ toast.error(error.message)
type: 'error',
message: error.message,
})
} }
else { else {
Toast.notify({ toast.error('Failed to fetch repository releases')
type: 'error',
message: 'Failed to fetch repository releases',
})
} }
return [] return []
} }
@ -54,7 +47,7 @@ export const useGitHubReleases = () => {
const checkForUpdates = (fetchedReleases: GitHubRepoReleaseResponse[], currentVersion: string) => { const checkForUpdates = (fetchedReleases: GitHubRepoReleaseResponse[], currentVersion: string) => {
let needUpdate = false let needUpdate = false
const toastProps: IToastProps = { const toastProps: { type?: 'success' | 'error' | 'info' | 'warning', message: string } = {
type: 'info', type: 'info',
message: 'No new version available', message: 'No new version available',
} }
@ -99,10 +92,7 @@ export const useGitHubUpload = () => {
return GitHubPackage return GitHubPackage
} }
catch (error) { catch (error) {
Toast.notify({ toast.error('Error uploading package')
type: 'error',
message: 'Error uploading package',
})
throw error throw error
} }
} }

View File

@ -57,10 +57,16 @@ const createUpdatePayload = (overrides: Partial<UpdateFromGitHubPayload> = {}):
// Mock external dependencies // Mock external dependencies
const mockNotify = vi.fn() const mockNotify = vi.fn()
vi.mock('@/app/components/base/toast', () => ({ vi.mock('@/app/components/base/ui/toast', () => ({
default: { toast: Object.assign((props: { type: string, message: string }) => mockNotify(props), {
notify: (props: { type: string, message: string }) => mockNotify(props), success: (message: string) => mockNotify({ type: 'success', message }),
}, error: (message: string) => mockNotify({ type: 'error', message }),
warning: (message: string) => mockNotify({ type: 'warning', message }),
info: (message: string) => mockNotify({ type: 'info', message }),
dismiss: vi.fn(),
update: vi.fn(),
promise: vi.fn(),
}),
})) }))
const mockGetIconUrl = vi.fn() const mockGetIconUrl = vi.fn()

View File

@ -7,7 +7,7 @@ import * as React from 'react'
import { useCallback, useState } from 'react' import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Modal from '@/app/components/base/modal' import Modal from '@/app/components/base/modal'
import Toast from '@/app/components/base/toast' import { toast } from '@/app/components/base/ui/toast'
import useGetIcon from '@/app/components/plugins/install-plugin/base/use-get-icon' import useGetIcon from '@/app/components/plugins/install-plugin/base/use-get-icon'
import { cn } from '@/utils/classnames' import { cn } from '@/utils/classnames'
import { InstallStepFromGitHub } from '../../types' import { InstallStepFromGitHub } from '../../types'
@ -81,10 +81,7 @@ const InstallFromGitHub: React.FC<InstallFromGitHubProps> = ({ updatePayload, on
const handleUrlSubmit = async () => { const handleUrlSubmit = async () => {
const { isValid, owner, repo } = parseGitHubUrl(state.repoUrl) const { isValid, owner, repo } = parseGitHubUrl(state.repoUrl)
if (!isValid || !owner || !repo) { if (!isValid || !owner || !repo) {
Toast.notify({ toast.error(t('error.inValidGitHubUrl', { ns: 'plugin' }))
type: 'error',
message: t('error.inValidGitHubUrl', { ns: 'plugin' }),
})
return return
} }
try { try {
@ -97,17 +94,11 @@ const InstallFromGitHub: React.FC<InstallFromGitHubProps> = ({ updatePayload, on
})) }))
} }
else { else {
Toast.notify({ toast.error(t('error.noReleasesFound', { ns: 'plugin' }))
type: 'error',
message: t('error.noReleasesFound', { ns: 'plugin' }),
})
} }
} }
catch { catch {
Toast.notify({ toast.error(t('error.fetchReleasesError', { ns: 'plugin' }))
type: 'error',
message: t('error.fetchReleasesError', { ns: 'plugin' }),
})
} }
} }
@ -175,10 +166,10 @@ const InstallFromGitHub: React.FC<InstallFromGitHubProps> = ({ updatePayload, on
> >
<div className="flex items-start gap-2 self-stretch pb-3 pl-6 pr-14 pt-6"> <div className="flex items-start gap-2 self-stretch pb-3 pl-6 pr-14 pt-6">
<div className="flex grow flex-col items-start gap-1"> <div className="flex grow flex-col items-start gap-1">
<div className="title-2xl-semi-bold self-stretch text-text-primary"> <div className="self-stretch text-text-primary title-2xl-semi-bold">
{getTitle()} {getTitle()}
</div> </div>
<div className="system-xs-regular self-stretch text-text-tertiary"> <div className="self-stretch text-text-tertiary system-xs-regular">
{!([InstallStepFromGitHub.uploadFailed, InstallStepFromGitHub.installed, InstallStepFromGitHub.installFailed].includes(state.step)) && t('installFromGitHub.installNote', { ns: 'plugin' })} {!([InstallStepFromGitHub.uploadFailed, InstallStepFromGitHub.installed, InstallStepFromGitHub.installFailed].includes(state.step)) && t('installFromGitHub.installNote', { ns: 'plugin' })}
</div> </div>
</div> </div>

View File

@ -2,10 +2,25 @@ import type { PluginDetail } from '../../types'
import { fireEvent, render, screen, waitFor } from '@testing-library/react' import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest' import { beforeEach, describe, expect, it, vi } from 'vitest'
import * as amplitude from '@/app/components/base/amplitude' import * as amplitude from '@/app/components/base/amplitude'
import Toast from '@/app/components/base/toast'
import { PluginSource } from '../../types' import { PluginSource } from '../../types'
import DetailHeader from '../detail-header' import DetailHeader from '../detail-header'
const { mockToast } = vi.hoisted(() => ({
mockToast: Object.assign(vi.fn(), {
success: vi.fn(),
error: vi.fn(),
warning: vi.fn(),
info: vi.fn(),
dismiss: vi.fn(),
update: vi.fn(),
promise: vi.fn(),
}),
}))
vi.mock('@/app/components/base/ui/toast', () => ({
toast: mockToast,
}))
const { const {
mockSetShowUpdatePluginModal, mockSetShowUpdatePluginModal,
mockRefreshModelProviders, mockRefreshModelProviders,
@ -272,7 +287,7 @@ describe('DetailHeader', () => {
vi.clearAllMocks() vi.clearAllMocks()
mockAutoUpgradeInfo = null mockAutoUpgradeInfo = null
mockEnableMarketplace = true mockEnableMarketplace = true
vi.spyOn(Toast, 'notify').mockImplementation(() => ({ clear: vi.fn() })) vi.clearAllMocks()
vi.spyOn(amplitude, 'trackEvent').mockImplementation(() => {}) vi.spyOn(amplitude, 'trackEvent').mockImplementation(() => {})
}) })

View File

@ -1,7 +1,6 @@
import type { EndpointListItem, PluginDetail } from '../../types' import type { EndpointListItem, PluginDetail } from '../../types'
import { act, fireEvent, render, screen } from '@testing-library/react' import { act, fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest' import { beforeEach, describe, expect, it, vi } from 'vitest'
import Toast from '@/app/components/base/toast'
import EndpointCard from '../endpoint-card' import EndpointCard from '../endpoint-card'
const mockHandleChange = vi.fn() const mockHandleChange = vi.fn()
@ -9,6 +8,22 @@ const mockEnableEndpoint = vi.fn()
const mockDisableEndpoint = vi.fn() const mockDisableEndpoint = vi.fn()
const mockDeleteEndpoint = vi.fn() const mockDeleteEndpoint = vi.fn()
const mockUpdateEndpoint = vi.fn() const mockUpdateEndpoint = vi.fn()
const mockToastNotify = vi.fn()
vi.mock('@/app/components/base/ui/toast', () => ({
toast: Object.assign(
(message: string, options?: { type?: string }) => mockToastNotify({ type: options?.type, message }),
{
success: (message: string) => mockToastNotify({ type: 'success', message }),
error: (message: string) => mockToastNotify({ type: 'error', message }),
warning: (message: string) => mockToastNotify({ type: 'warning', message }),
info: (message: string) => mockToastNotify({ type: 'info', message }),
dismiss: vi.fn(),
update: vi.fn(),
promise: vi.fn(),
},
),
}))
// Flags to control whether operations should fail // Flags to control whether operations should fail
const failureFlags = { const failureFlags = {
@ -127,8 +142,6 @@ describe('EndpointCard', () => {
failureFlags.disable = false failureFlags.disable = false
failureFlags.delete = false failureFlags.delete = false
failureFlags.update = false failureFlags.update = false
// Mock Toast.notify to prevent toast elements from accumulating in DOM
vi.spyOn(Toast, 'notify').mockImplementation(() => ({ clear: vi.fn() }))
// Polyfill document.execCommand for copy-to-clipboard in jsdom // Polyfill document.execCommand for copy-to-clipboard in jsdom
if (typeof document.execCommand !== 'function') { if (typeof document.execCommand !== 'function') {
document.execCommand = vi.fn().mockReturnValue(true) document.execCommand = vi.fn().mockReturnValue(true)

View File

@ -2,9 +2,25 @@ import type { FormSchema } from '../../../base/form/types'
import type { PluginDetail } from '../../types' import type { PluginDetail } from '../../types'
import { fireEvent, render, screen } from '@testing-library/react' import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest' import { beforeEach, describe, expect, it, vi } from 'vitest'
import Toast from '@/app/components/base/toast'
import EndpointModal from '../endpoint-modal' import EndpointModal from '../endpoint-modal'
const mockToastNotify = vi.fn()
vi.mock('@/app/components/base/ui/toast', () => ({
toast: Object.assign(
(message: string, options?: { type?: string }) => mockToastNotify({ type: options?.type, message }),
{
success: (message: string) => mockToastNotify({ type: 'success', message }),
error: (message: string) => mockToastNotify({ type: 'error', message }),
warning: (message: string) => mockToastNotify({ type: 'warning', message }),
info: (message: string) => mockToastNotify({ type: 'info', message }),
dismiss: vi.fn(),
update: vi.fn(),
promise: vi.fn(),
},
),
}))
vi.mock('@/hooks/use-i18n', () => ({ vi.mock('@/hooks/use-i18n', () => ({
useRenderI18nObject: () => (obj: Record<string, string> | string) => useRenderI18nObject: () => (obj: Record<string, string> | string) =>
typeof obj === 'string' ? obj : obj?.en_US || '', typeof obj === 'string' ? obj : obj?.en_US || '',
@ -69,11 +85,9 @@ const mockPluginDetail: PluginDetail = {
describe('EndpointModal', () => { describe('EndpointModal', () => {
const mockOnCancel = vi.fn() const mockOnCancel = vi.fn()
const mockOnSaved = vi.fn() const mockOnSaved = vi.fn()
let mockToastNotify: ReturnType<typeof vi.spyOn>
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks() vi.clearAllMocks()
mockToastNotify = vi.spyOn(Toast, 'notify').mockImplementation(() => ({ clear: vi.fn() }))
}) })
describe('Rendering', () => { describe('Rendering', () => {

View File

@ -3,7 +3,6 @@ import type { ModalStates, VersionTarget } from '../use-detail-header-state'
import { act, renderHook } from '@testing-library/react' import { act, renderHook } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest' import { beforeEach, describe, expect, it, vi } from 'vitest'
import * as amplitude from '@/app/components/base/amplitude' import * as amplitude from '@/app/components/base/amplitude'
import Toast from '@/app/components/base/toast'
import { PluginSource } from '../../../../types' import { PluginSource } from '../../../../types'
import { usePluginOperations } from '../use-plugin-operations' import { usePluginOperations } from '../use-plugin-operations'
@ -20,6 +19,7 @@ const {
mockUninstallPlugin, mockUninstallPlugin,
mockFetchReleases, mockFetchReleases,
mockCheckForUpdates, mockCheckForUpdates,
mockToastNotify,
} = vi.hoisted(() => { } = vi.hoisted(() => {
return { return {
mockSetShowUpdatePluginModal: vi.fn(), mockSetShowUpdatePluginModal: vi.fn(),
@ -29,9 +29,25 @@ const {
mockUninstallPlugin: vi.fn(() => Promise.resolve({ success: true })), mockUninstallPlugin: vi.fn(() => Promise.resolve({ success: true })),
mockFetchReleases: vi.fn(() => Promise.resolve([{ tag_name: 'v2.0.0' }])), mockFetchReleases: vi.fn(() => Promise.resolve([{ tag_name: 'v2.0.0' }])),
mockCheckForUpdates: vi.fn(() => ({ needUpdate: true, toastProps: { type: 'success', message: 'Update available' } })), mockCheckForUpdates: vi.fn(() => ({ needUpdate: true, toastProps: { type: 'success', message: 'Update available' } })),
mockToastNotify: vi.fn(),
} }
}) })
vi.mock('@/app/components/base/ui/toast', () => ({
toast: Object.assign(
(message: string, options?: { type?: string }) => mockToastNotify({ type: options?.type, message }),
{
success: (message: string) => mockToastNotify({ type: 'success', message }),
error: (message: string) => mockToastNotify({ type: 'error', message }),
warning: (message: string) => mockToastNotify({ type: 'warning', message }),
info: (message: string) => mockToastNotify({ type: 'info', message }),
dismiss: vi.fn(),
update: vi.fn(),
promise: vi.fn(),
},
),
}))
vi.mock('@/context/modal-context', () => ({ vi.mock('@/context/modal-context', () => ({
useModalContext: () => ({ useModalContext: () => ({
setShowUpdatePluginModal: mockSetShowUpdatePluginModal, setShowUpdatePluginModal: mockSetShowUpdatePluginModal,
@ -124,7 +140,6 @@ describe('usePluginOperations', () => {
modalStates = createModalStatesMock() modalStates = createModalStatesMock()
versionPicker = createVersionPickerMock() versionPicker = createVersionPickerMock()
mockOnUpdate = vi.fn() mockOnUpdate = vi.fn()
vi.spyOn(Toast, 'notify').mockImplementation(() => ({ clear: vi.fn() }))
vi.spyOn(amplitude, 'trackEvent').mockImplementation(() => {}) vi.spyOn(amplitude, 'trackEvent').mockImplementation(() => {})
}) })
@ -233,7 +248,7 @@ describe('usePluginOperations', () => {
}) })
expect(mockCheckForUpdates).toHaveBeenCalled() expect(mockCheckForUpdates).toHaveBeenCalled()
expect(Toast.notify).toHaveBeenCalled() expect(mockToastNotify).toHaveBeenCalledWith({ type: 'success', message: 'Update available' })
}) })
it('should show update plugin modal when update is needed', async () => { it('should show update plugin modal when update is needed', async () => {

View File

@ -5,7 +5,7 @@ import type { ModalStates, VersionTarget } from './use-detail-header-state'
import { useCallback } from 'react' import { useCallback } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { trackEvent } from '@/app/components/base/amplitude' import { trackEvent } from '@/app/components/base/amplitude'
import Toast from '@/app/components/base/toast' import { toast } from '@/app/components/base/ui/toast'
import { useModalContext } from '@/context/modal-context' import { useModalContext } from '@/context/modal-context'
import { useProviderContext } from '@/context/provider-context' import { useProviderContext } from '@/context/provider-context'
import { uninstallPlugin } from '@/service/plugins' import { uninstallPlugin } from '@/service/plugins'
@ -60,10 +60,7 @@ export const usePluginOperations = ({
} }
if (!meta?.repo || !meta?.version || !meta?.package) { if (!meta?.repo || !meta?.version || !meta?.package) {
Toast.notify({ toast.error('Missing plugin metadata for GitHub update')
type: 'error',
message: 'Missing plugin metadata for GitHub update',
})
return return
} }
@ -74,7 +71,7 @@ export const usePluginOperations = ({
return return
const { needUpdate, toastProps } = checkForUpdates(fetchedReleases, meta.version) const { needUpdate, toastProps } = checkForUpdates(fetchedReleases, meta.version)
Toast.notify(toastProps) toast(toastProps.message, { type: toastProps.type })
if (needUpdate) { if (needUpdate) {
setShowUpdatePluginModal({ setShowUpdatePluginModal({
@ -122,10 +119,7 @@ export const usePluginOperations = ({
if (res.success) { if (res.success) {
modalStates.hideDeleteConfirm() modalStates.hideDeleteConfirm()
Toast.notify({ toast.success(t('action.deleteSuccess', { ns: 'plugin' }))
type: 'success',
message: t('action.deleteSuccess', { ns: 'plugin' }),
})
handlePluginUpdated(true) handlePluginUpdated(true)
if (PluginCategoryEnum.model.includes(category)) if (PluginCategoryEnum.model.includes(category))

View File

@ -9,8 +9,8 @@ import ActionButton from '@/app/components/base/action-button'
import Confirm from '@/app/components/base/confirm' import Confirm from '@/app/components/base/confirm'
import { CopyCheck } from '@/app/components/base/icons/src/vender/line/files' import { CopyCheck } from '@/app/components/base/icons/src/vender/line/files'
import Switch from '@/app/components/base/switch' import Switch from '@/app/components/base/switch'
import Toast from '@/app/components/base/toast'
import Tooltip from '@/app/components/base/tooltip' import Tooltip from '@/app/components/base/tooltip'
import { toast } from '@/app/components/base/ui/toast'
import Indicator from '@/app/components/header/indicator' import Indicator from '@/app/components/header/indicator'
import { addDefaultValue, toolCredentialToFormSchemas } from '@/app/components/tools/utils/to-form-schema' import { addDefaultValue, toolCredentialToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
import { import {
@ -47,7 +47,7 @@ const EndpointCard = ({
await handleChange() await handleChange()
}, },
onError: () => { onError: () => {
Toast.notify({ type: 'error', message: t('actionMsg.modifiedUnsuccessfully', { ns: 'common' }) }) toast.error(t('actionMsg.modifiedUnsuccessfully', { ns: 'common' }))
setActive(false) setActive(false)
}, },
}) })
@ -57,7 +57,7 @@ const EndpointCard = ({
hideDisableConfirm() hideDisableConfirm()
}, },
onError: () => { onError: () => {
Toast.notify({ type: 'error', message: t('actionMsg.modifiedUnsuccessfully', { ns: 'common' }) }) toast.error(t('actionMsg.modifiedUnsuccessfully', { ns: 'common' }))
setActive(false) setActive(false)
}, },
}) })
@ -83,7 +83,7 @@ const EndpointCard = ({
hideDeleteConfirm() hideDeleteConfirm()
}, },
onError: () => { onError: () => {
Toast.notify({ type: 'error', message: t('actionMsg.modifiedUnsuccessfully', { ns: 'common' }) }) toast.error(t('actionMsg.modifiedUnsuccessfully', { ns: 'common' }))
}, },
}) })
@ -108,7 +108,7 @@ const EndpointCard = ({
hideEndpointModalConfirm() hideEndpointModalConfirm()
}, },
onError: () => { onError: () => {
Toast.notify({ type: 'error', message: t('actionMsg.modifiedUnsuccessfully', { ns: 'common' }) }) toast.error(t('actionMsg.modifiedUnsuccessfully', { ns: 'common' }))
}, },
}) })
const handleUpdate = (state: Record<string, any>) => updateEndpoint({ const handleUpdate = (state: Record<string, any>) => updateEndpoint({
@ -139,7 +139,7 @@ const EndpointCard = ({
<div className="rounded-xl bg-background-section-burn p-0.5"> <div className="rounded-xl bg-background-section-burn p-0.5">
<div className="group rounded-[10px] border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg p-2.5 pl-3"> <div className="group rounded-[10px] border-[0.5px] border-components-panel-border bg-components-panel-on-panel-item-bg p-2.5 pl-3">
<div className="flex items-center"> <div className="flex items-center">
<div className="system-md-semibold mb-1 flex h-6 grow items-center gap-1 text-text-secondary"> <div className="mb-1 flex h-6 grow items-center gap-1 text-text-secondary system-md-semibold">
<RiLoginCircleLine className="h-4 w-4" /> <RiLoginCircleLine className="h-4 w-4" />
<div>{data.name}</div> <div>{data.name}</div>
</div> </div>
@ -154,8 +154,8 @@ const EndpointCard = ({
</div> </div>
{data.declaration.endpoints.filter(endpoint => !endpoint.hidden).map((endpoint, index) => ( {data.declaration.endpoints.filter(endpoint => !endpoint.hidden).map((endpoint, index) => (
<div key={index} className="flex h-6 items-center"> <div key={index} className="flex h-6 items-center">
<div className="system-xs-regular w-12 shrink-0 text-text-tertiary">{endpoint.method}</div> <div className="w-12 shrink-0 text-text-tertiary system-xs-regular">{endpoint.method}</div>
<div className="group/item system-xs-regular flex grow items-center truncate text-text-secondary"> <div className="group/item flex grow items-center truncate text-text-secondary system-xs-regular">
<div title={`${data.url}${endpoint.path}`} className="truncate">{`${data.url}${endpoint.path}`}</div> <div title={`${data.url}${endpoint.path}`} className="truncate">{`${data.url}${endpoint.path}`}</div>
<Tooltip popupContent={t(`operation.${isCopied ? 'copied' : 'copy'}`, { ns: 'common' })} position="top"> <Tooltip popupContent={t(`operation.${isCopied ? 'copied' : 'copy'}`, { ns: 'common' })} position="top">
<ActionButton className="ml-2 hidden shrink-0 group-hover/item:flex" onClick={() => handleCopy(`${data.url}${endpoint.path}`)}> <ActionButton className="ml-2 hidden shrink-0 group-hover/item:flex" onClick={() => handleCopy(`${data.url}${endpoint.path}`)}>
@ -168,13 +168,13 @@ const EndpointCard = ({
</div> </div>
<div className="flex items-center justify-between p-2 pl-3"> <div className="flex items-center justify-between p-2 pl-3">
{active && ( {active && (
<div className="system-xs-semibold-uppercase flex items-center gap-1 text-util-colors-green-green-600"> <div className="flex items-center gap-1 text-util-colors-green-green-600 system-xs-semibold-uppercase">
<Indicator color="green" /> <Indicator color="green" />
{t('detailPanel.serviceOk', { ns: 'plugin' })} {t('detailPanel.serviceOk', { ns: 'plugin' })}
</div> </div>
)} )}
{!active && ( {!active && (
<div className="system-xs-semibold-uppercase flex items-center gap-1 text-text-tertiary"> <div className="flex items-center gap-1 text-text-tertiary system-xs-semibold-uppercase">
<Indicator color="gray" /> <Indicator color="gray" />
{t('detailPanel.disabled', { ns: 'plugin' })} {t('detailPanel.disabled', { ns: 'plugin' })}
</div> </div>

View File

@ -9,8 +9,8 @@ import * as React from 'react'
import { useMemo } from 'react' import { useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button' import ActionButton from '@/app/components/base/action-button'
import Toast from '@/app/components/base/toast'
import Tooltip from '@/app/components/base/tooltip' import Tooltip from '@/app/components/base/tooltip'
import { toast } from '@/app/components/base/ui/toast'
import { toolCredentialToFormSchemas } from '@/app/components/tools/utils/to-form-schema' import { toolCredentialToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
import { useDocLink } from '@/context/i18n' import { useDocLink } from '@/context/i18n'
import { import {
@ -50,7 +50,7 @@ const EndpointList = ({ detail }: Props) => {
hideEndpointModal() hideEndpointModal()
}, },
onError: () => { onError: () => {
Toast.notify({ type: 'error', message: t('actionMsg.modifiedUnsuccessfully', { ns: 'common' }) }) toast.error(t('actionMsg.modifiedUnsuccessfully', { ns: 'common' }))
}, },
}) })
@ -64,7 +64,7 @@ const EndpointList = ({ detail }: Props) => {
return ( return (
<div className={cn('border-divider-subtle px-4 py-2', showTopBorder && 'border-t')}> <div className={cn('border-divider-subtle px-4 py-2', showTopBorder && 'border-t')}>
<div className="system-sm-semibold-uppercase mb-1 flex h-6 items-center justify-between text-text-secondary"> <div className="mb-1 flex h-6 items-center justify-between text-text-secondary system-sm-semibold-uppercase">
<div className="flex items-center gap-0.5"> <div className="flex items-center gap-0.5">
{t('detailPanel.endpoints', { ns: 'plugin' })} {t('detailPanel.endpoints', { ns: 'plugin' })}
<Tooltip <Tooltip
@ -75,13 +75,13 @@ const EndpointList = ({ detail }: Props) => {
<div className="flex h-8 w-8 items-center justify-center rounded-lg border-[0.5px] border-components-panel-border-subtle bg-background-default-subtle"> <div className="flex h-8 w-8 items-center justify-center rounded-lg border-[0.5px] border-components-panel-border-subtle bg-background-default-subtle">
<RiApps2AddLine className="h-4 w-4 text-text-tertiary" /> <RiApps2AddLine className="h-4 w-4 text-text-tertiary" />
</div> </div>
<div className="system-xs-regular text-text-tertiary">{t('detailPanel.endpointsTip', { ns: 'plugin' })}</div> <div className="text-text-tertiary system-xs-regular">{t('detailPanel.endpointsTip', { ns: 'plugin' })}</div>
<a <a
href={docLink('/develop-plugin/getting-started/getting-started-dify-plugin')} href={docLink('/develop-plugin/getting-started/getting-started-dify-plugin')}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
<div className="system-xs-regular inline-flex cursor-pointer items-center gap-1 text-text-accent"> <div className="inline-flex cursor-pointer items-center gap-1 text-text-accent system-xs-regular">
<RiBookOpenLine className="h-3 w-3" /> <RiBookOpenLine className="h-3 w-3" />
{t('detailPanel.endpointsDocLink', { ns: 'plugin' })} {t('detailPanel.endpointsDocLink', { ns: 'plugin' })}
</div> </div>
@ -95,7 +95,7 @@ const EndpointList = ({ detail }: Props) => {
</ActionButton> </ActionButton>
</div> </div>
{data.endpoints.length === 0 && ( {data.endpoints.length === 0 && (
<div className="system-xs-regular mb-1 flex justify-center rounded-[10px] bg-background-section p-3 text-text-tertiary">{t('detailPanel.endpointsEmpty', { ns: 'plugin' })}</div> <div className="mb-1 flex justify-center rounded-[10px] bg-background-section p-3 text-text-tertiary system-xs-regular">{t('detailPanel.endpointsEmpty', { ns: 'plugin' })}</div>
)} )}
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
{data.endpoints.map((item, index) => ( {data.endpoints.map((item, index) => (

View File

@ -8,7 +8,7 @@ import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button' import ActionButton from '@/app/components/base/action-button'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import Drawer from '@/app/components/base/drawer' import Drawer from '@/app/components/base/drawer'
import Toast from '@/app/components/base/toast' import { toast } from '@/app/components/base/ui/toast'
import Form from '@/app/components/header/account-setting/model-provider-page/model-modal/Form' import Form from '@/app/components/header/account-setting/model-provider-page/model-modal/Form'
import { useRenderI18nObject } from '@/hooks/use-i18n' import { useRenderI18nObject } from '@/hooks/use-i18n'
import { cn } from '@/utils/classnames' import { cn } from '@/utils/classnames'
@ -48,7 +48,10 @@ const EndpointModal: FC<Props> = ({
const handleSave = () => { const handleSave = () => {
for (const field of formSchemas) { for (const field of formSchemas) {
if (field.required && !tempCredential[field.name]) { if (field.required && !tempCredential[field.name]) {
Toast.notify({ type: 'error', message: t('errorMsg.fieldRequired', { ns: 'common', field: typeof field.label === 'string' ? field.label : getValueFromI18nObject(field.label as Record<string, string>) }) }) toast.error(t('errorMsg.fieldRequired', {
ns: 'common',
field: typeof field.label === 'string' ? field.label : getValueFromI18nObject(field.label as Record<string, string>),
}))
return return
} }
} }
@ -83,12 +86,12 @@ const EndpointModal: FC<Props> = ({
<> <>
<div className="p-4 pb-2"> <div className="p-4 pb-2">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="system-xl-semibold text-text-primary">{t('detailPanel.endpointModalTitle', { ns: 'plugin' })}</div> <div className="text-text-primary system-xl-semibold">{t('detailPanel.endpointModalTitle', { ns: 'plugin' })}</div>
<ActionButton onClick={onCancel}> <ActionButton onClick={onCancel}>
<RiCloseLine className="h-4 w-4" /> <RiCloseLine className="h-4 w-4" />
</ActionButton> </ActionButton>
</div> </div>
<div className="system-xs-regular mt-0.5 text-text-tertiary">{t('detailPanel.endpointModalDesc', { ns: 'plugin' })}</div> <div className="mt-0.5 text-text-tertiary system-xs-regular">{t('detailPanel.endpointModalDesc', { ns: 'plugin' })}</div>
<ReadmeEntrance pluginDetail={pluginDetail} className="px-0 pt-3" /> <ReadmeEntrance pluginDetail={pluginDetail} className="px-0 pt-3" />
</div> </div>
<div className="grow overflow-y-auto"> <div className="grow overflow-y-auto">
@ -109,7 +112,7 @@ const EndpointModal: FC<Props> = ({
href={item.url} href={item.url}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="body-xs-regular inline-flex items-center text-text-accent-secondary" className="inline-flex items-center text-text-accent-secondary body-xs-regular"
> >
{t('howToGet', { ns: 'tools' })} {t('howToGet', { ns: 'tools' })}
<RiArrowRightUpLine className="ml-1 h-3 w-3" /> <RiArrowRightUpLine className="ml-1 h-3 w-3" />

View File

@ -1,14 +1,29 @@
import type { Model, ModelItem } from '@/app/components/header/account-setting/model-provider-page/declarations' import type { Model, ModelItem } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { fireEvent, render, screen, waitFor } from '@testing-library/react' import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest' import { beforeEach, describe, expect, it, vi } from 'vitest'
// Import component after mocks
import Toast from '@/app/components/base/toast'
import { ConfigurationMethodEnum, ModelStatusEnum, ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import { ConfigurationMethodEnum, ModelStatusEnum, ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
// Import component after mocks
import ModelParameterModal from '../index' import ModelParameterModal from '../index'
// ==================== Mock Setup ==================== // ==================== Mock Setup ====================
const mockToastNotify = vi.fn()
vi.mock('@/app/components/base/ui/toast', () => ({
toast: Object.assign(
(message: string, options?: { type?: string }) => mockToastNotify({ type: options?.type, message }),
{
success: (message: string) => mockToastNotify({ type: 'success', message }),
error: (message: string) => mockToastNotify({ type: 'error', message }),
warning: (message: string) => mockToastNotify({ type: 'warning', message }),
info: (message: string) => mockToastNotify({ type: 'info', message }),
dismiss: vi.fn(),
update: vi.fn(),
promise: vi.fn(),
},
),
}))
// Mock provider context // Mock provider context
const mockProviderContextValue = { const mockProviderContextValue = {
isAPIKeySet: true, isAPIKeySet: true,
@ -53,8 +68,6 @@ vi.mock('@/utils/completion-params', () => ({
fetchAndMergeValidCompletionParams: (...args: unknown[]) => mockFetchAndMergeValidCompletionParams(...args), fetchAndMergeValidCompletionParams: (...args: unknown[]) => mockFetchAndMergeValidCompletionParams(...args),
})) }))
const mockToastNotify = vi.spyOn(Toast, 'notify')
// Mock child components // Mock child components
vi.mock('@/app/components/header/account-setting/model-provider-page/model-selector', () => ({ vi.mock('@/app/components/header/account-setting/model-provider-page/model-selector', () => ({
default: ({ defaultModel, modelList, scopeFeatures, onSelect }: { default: ({ defaultModel, modelList, scopeFeatures, onSelect }: {
@ -244,7 +257,6 @@ const setupModelLists = (config: {
describe('ModelParameterModal', () => { describe('ModelParameterModal', () => {
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks() vi.clearAllMocks()
mockToastNotify.mockReturnValue({})
mockProviderContextValue.isAPIKeySet = true mockProviderContextValue.isAPIKeySet = true
mockProviderContextValue.modelProviders = [] mockProviderContextValue.modelProviders = []
setupModelLists() setupModelLists()
@ -865,9 +877,7 @@ describe('ModelParameterModal', () => {
// Assert // Assert
await waitFor(() => { await waitFor(() => {
expect(Toast.notify).toHaveBeenCalledWith( expect(mockToastNotify).toHaveBeenCalledWith(expect.objectContaining({ type: 'warning' }))
expect.objectContaining({ type: 'warning' }),
)
}) })
}) })
@ -892,9 +902,7 @@ describe('ModelParameterModal', () => {
// Assert // Assert
await waitFor(() => { await waitFor(() => {
expect(Toast.notify).toHaveBeenCalledWith( expect(mockToastNotify).toHaveBeenCalledWith(expect.objectContaining({ type: 'error' }))
expect.objectContaining({ type: 'error' }),
)
}) })
}) })
}) })

View File

@ -10,12 +10,12 @@ import type {
import type { TriggerProps } from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal/trigger' import type { TriggerProps } from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal/trigger'
import { useMemo, useState } from 'react' import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Toast from '@/app/components/base/toast'
import { import {
Popover, Popover,
PopoverContent, PopoverContent,
PopoverTrigger, PopoverTrigger,
} from '@/app/components/base/ui/popover' } from '@/app/components/base/ui/popover'
import { toast } from '@/app/components/base/ui/toast'
import { ModelStatusEnum, ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import { ModelStatusEnum, ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { import {
useModelList, useModelList,
@ -134,14 +134,11 @@ const ModelParameterModal: FC<ModelParameterModalProps> = ({
const keys = Object.keys(removedDetails || {}) const keys = Object.keys(removedDetails || {})
if (keys.length) { if (keys.length) {
Toast.notify({ toast.warning(`${t('modelProvider.parametersInvalidRemoved', { ns: 'common' })}: ${keys.map(k => `${k} (${removedDetails[k]})`).join(', ')}`)
type: 'warning',
message: `${t('modelProvider.parametersInvalidRemoved', { ns: 'common' })}: ${keys.map(k => `${k} (${removedDetails[k]})`).join(', ')}`,
})
} }
} }
catch { catch {
Toast.notify({ type: 'error', message: t('error', { ns: 'common' }) }) toast.error(t('error', { ns: 'common' }))
} }
} }

View File

@ -1,12 +1,26 @@
import type { TriggerLogEntity } from '@/app/components/workflow/block-selector/types' import type { TriggerLogEntity } from '@/app/components/workflow/block-selector/types'
import { cleanup, fireEvent, render, screen } from '@testing-library/react' import { cleanup, fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest' import { beforeEach, describe, expect, it, vi } from 'vitest'
import Toast from '@/app/components/base/toast'
import LogViewer from '../log-viewer' import LogViewer from '../log-viewer'
const mockToastNotify = vi.fn() const mockToastNotify = vi.fn()
const mockWriteText = vi.fn() const mockWriteText = vi.fn()
vi.mock('@/app/components/base/ui/toast', () => ({
toast: Object.assign(
(message: string, options?: { type?: string }) => mockToastNotify({ type: options?.type, message }),
{
success: (message: string) => mockToastNotify({ type: 'success', message }),
error: (message: string) => mockToastNotify({ type: 'error', message }),
warning: (message: string) => mockToastNotify({ type: 'warning', message }),
info: (message: string) => mockToastNotify({ type: 'info', message }),
dismiss: vi.fn(),
update: vi.fn(),
promise: vi.fn(),
},
),
}))
vi.mock('@/app/components/workflow/nodes/_base/components/editor/code-editor', () => ({ vi.mock('@/app/components/workflow/nodes/_base/components/editor/code-editor', () => ({
default: ({ value }: { value: unknown }) => ( default: ({ value }: { value: unknown }) => (
<div data-testid="code-editor">{JSON.stringify(value)}</div> <div data-testid="code-editor">{JSON.stringify(value)}</div>
@ -57,10 +71,6 @@ beforeEach(() => {
}, },
configurable: true, configurable: true,
}) })
vi.spyOn(Toast, 'notify').mockImplementation((args) => {
mockToastNotify(args)
return { clear: vi.fn() }
})
}) })
describe('LogViewer', () => { describe('LogViewer', () => {

View File

@ -26,10 +26,16 @@ vi.mock('@/service/use-triggers', () => ({
useDeleteTriggerSubscription: () => ({ mutate: vi.fn(), isPending: false }), useDeleteTriggerSubscription: () => ({ mutate: vi.fn(), isPending: false }),
})) }))
vi.mock('@/app/components/base/toast', () => ({ vi.mock('@/app/components/base/ui/toast', () => ({
default: { toast: Object.assign(vi.fn(), {
notify: vi.fn(), success: vi.fn(),
}, error: vi.fn(),
warning: vi.fn(),
info: vi.fn(),
dismiss: vi.fn(),
update: vi.fn(),
promise: vi.fn(),
}),
})) }))
const createSubscription = (overrides: Partial<TriggerSubscription> = {}): TriggerSubscription => ({ const createSubscription = (overrides: Partial<TriggerSubscription> = {}): TriggerSubscription => ({

View File

@ -1,7 +1,6 @@
import type { TriggerSubscription } from '@/app/components/workflow/block-selector/types' import type { TriggerSubscription } from '@/app/components/workflow/block-selector/types'
import { fireEvent, render, screen } from '@testing-library/react' import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest' import { beforeEach, describe, expect, it, vi } from 'vitest'
import Toast from '@/app/components/base/toast'
import { TriggerCredentialTypeEnum } from '@/app/components/workflow/block-selector/types' import { TriggerCredentialTypeEnum } from '@/app/components/workflow/block-selector/types'
import { SubscriptionSelectorView } from '../selector-view' import { SubscriptionSelectorView } from '../selector-view'
@ -26,6 +25,18 @@ vi.mock('@/service/use-triggers', () => ({
useDeleteTriggerSubscription: () => ({ mutate: mockDelete, isPending: false }), useDeleteTriggerSubscription: () => ({ mutate: mockDelete, isPending: false }),
})) }))
vi.mock('@/app/components/base/ui/toast', () => ({
toast: Object.assign(vi.fn(), {
success: vi.fn(),
error: vi.fn(),
warning: vi.fn(),
info: vi.fn(),
dismiss: vi.fn(),
update: vi.fn(),
promise: vi.fn(),
}),
}))
const createSubscription = (overrides: Partial<TriggerSubscription> = {}): TriggerSubscription => ({ const createSubscription = (overrides: Partial<TriggerSubscription> = {}): TriggerSubscription => ({
id: 'sub-1', id: 'sub-1',
name: 'Subscription One', name: 'Subscription One',
@ -42,7 +53,6 @@ const createSubscription = (overrides: Partial<TriggerSubscription> = {}): Trigg
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks() vi.clearAllMocks()
mockSubscriptions = [createSubscription()] mockSubscriptions = [createSubscription()]
vi.spyOn(Toast, 'notify').mockImplementation(() => ({ clear: vi.fn() }))
}) })
describe('SubscriptionSelectorView', () => { describe('SubscriptionSelectorView', () => {

View File

@ -1,7 +1,6 @@
import type { TriggerSubscription } from '@/app/components/workflow/block-selector/types' import type { TriggerSubscription } from '@/app/components/workflow/block-selector/types'
import { fireEvent, render, screen } from '@testing-library/react' import { fireEvent, render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest' import { beforeEach, describe, expect, it, vi } from 'vitest'
import Toast from '@/app/components/base/toast'
import { TriggerCredentialTypeEnum } from '@/app/components/workflow/block-selector/types' import { TriggerCredentialTypeEnum } from '@/app/components/workflow/block-selector/types'
import SubscriptionCard from '../subscription-card' import SubscriptionCard from '../subscription-card'
@ -30,6 +29,18 @@ vi.mock('@/service/use-triggers', () => ({
useDeleteTriggerSubscription: () => ({ mutate: vi.fn(), isPending: false }), useDeleteTriggerSubscription: () => ({ mutate: vi.fn(), isPending: false }),
})) }))
vi.mock('@/app/components/base/ui/toast', () => ({
toast: Object.assign(vi.fn(), {
success: vi.fn(),
error: vi.fn(),
warning: vi.fn(),
info: vi.fn(),
dismiss: vi.fn(),
update: vi.fn(),
promise: vi.fn(),
}),
}))
const createSubscription = (overrides: Partial<TriggerSubscription> = {}): TriggerSubscription => ({ const createSubscription = (overrides: Partial<TriggerSubscription> = {}): TriggerSubscription => ({
id: 'sub-1', id: 'sub-1',
name: 'Subscription One', name: 'Subscription One',
@ -45,7 +56,6 @@ const createSubscription = (overrides: Partial<TriggerSubscription> = {}): Trigg
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks() vi.clearAllMocks()
vi.spyOn(Toast, 'notify').mockImplementation(() => ({ clear: vi.fn() }))
}) })
describe('SubscriptionCard', () => { describe('SubscriptionCard', () => {

View File

@ -122,10 +122,16 @@ vi.mock('@/utils/urlValidation', () => ({
})) }))
const mockToastNotify = vi.fn() const mockToastNotify = vi.fn()
vi.mock('@/app/components/base/toast', () => ({ vi.mock('@/app/components/base/ui/toast', () => ({
default: { toast: Object.assign((params: unknown) => mockToastNotify(params), {
notify: (params: unknown) => mockToastNotify(params), success: (message: unknown) => mockToastNotify({ type: 'success', message }),
}, error: (message: unknown) => mockToastNotify({ type: 'error', message }),
warning: (message: unknown) => mockToastNotify({ type: 'warning', message }),
info: (message: unknown) => mockToastNotify({ type: 'info', message }),
dismiss: vi.fn(),
update: vi.fn(),
promise: vi.fn(),
}),
})) }))
vi.mock('@/app/components/base/modal/modal', () => ({ vi.mock('@/app/components/base/modal/modal', () => ({

View File

@ -2,6 +2,7 @@ import type { SimpleDetail } from '../../../store'
import type { TriggerOAuthConfig, TriggerProviderApiEntity, TriggerSubscription, TriggerSubscriptionBuilder } from '@/app/components/workflow/block-selector/types' import type { TriggerOAuthConfig, TriggerProviderApiEntity, TriggerSubscription, TriggerSubscriptionBuilder } from '@/app/components/workflow/block-selector/types'
import { fireEvent, render, screen, waitFor } from '@testing-library/react' import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest' import { beforeEach, describe, expect, it, vi } from 'vitest'
import { toast } from '@/app/components/base/ui/toast'
import { SupportedCreationMethods } from '@/app/components/plugins/types' import { SupportedCreationMethods } from '@/app/components/plugins/types'
import { TriggerCredentialTypeEnum } from '@/app/components/workflow/block-selector/types' import { TriggerCredentialTypeEnum } from '@/app/components/workflow/block-selector/types'
import { CreateButtonType, CreateSubscriptionButton, DEFAULT_METHOD } from '../index' import { CreateButtonType, CreateSubscriptionButton, DEFAULT_METHOD } from '../index'
@ -33,10 +34,16 @@ vi.mock('@/app/components/base/portal-to-follow-elem', () => ({
}, },
})) }))
vi.mock('@/app/components/base/toast', () => ({ vi.mock('@/app/components/base/ui/toast', () => ({
default: { toast: Object.assign(vi.fn(), {
notify: vi.fn(), success: vi.fn(),
}, error: vi.fn(),
warning: vi.fn(),
info: vi.fn(),
dismiss: vi.fn(),
update: vi.fn(),
promise: vi.fn(),
}),
})) }))
let mockStoreDetail: SimpleDetail | undefined let mockStoreDetail: SimpleDetail | undefined
@ -908,8 +915,6 @@ describe('CreateSubscriptionButton', () => {
it('should handle OAuth initiation error', async () => { it('should handle OAuth initiation error', async () => {
// Arrange // Arrange
const Toast = await import('@/app/components/base/toast')
mockInitiateOAuth.mockImplementation((_provider: string, callbacks: { onError: () => void }) => { mockInitiateOAuth.mockImplementation((_provider: string, callbacks: { onError: () => void }) => {
callbacks.onError() callbacks.onError()
}) })
@ -932,9 +937,7 @@ describe('CreateSubscriptionButton', () => {
// Assert // Assert
await waitFor(() => { await waitFor(() => {
expect(Toast.default.notify).toHaveBeenCalledWith( expect(toast.error).toHaveBeenCalled()
expect.objectContaining({ type: 'error' }),
)
}) })
}) })
}) })

View File

@ -86,10 +86,19 @@ vi.mock('@/hooks/use-oauth', () => ({
})) }))
const mockToastNotify = vi.fn() const mockToastNotify = vi.fn()
vi.mock('@/app/components/base/toast', () => ({ vi.mock('@/app/components/base/ui/toast', () => ({
default: { toast: Object.assign(
notify: (params: unknown) => mockToastNotify(params), (message: string, options?: { type?: string }) => mockToastNotify({ type: options?.type, message }),
}, {
success: (message: string) => mockToastNotify({ type: 'success', message }),
error: (message: string) => mockToastNotify({ type: 'error', message }),
warning: (message: string) => mockToastNotify({ type: 'warning', message }),
info: (message: string) => mockToastNotify({ type: 'info', message }),
dismiss: vi.fn(),
update: vi.fn(),
promise: vi.fn(),
},
),
})) }))
const mockClipboardWriteText = vi.fn() const mockClipboardWriteText = vi.fn()

View File

@ -77,10 +77,19 @@ vi.mock('@/hooks/use-oauth', () => ({
})) }))
const mockToastNotify = vi.fn() const mockToastNotify = vi.fn()
vi.mock('@/app/components/base/toast', () => ({ vi.mock('@/app/components/base/ui/toast', () => ({
default: { toast: Object.assign(
notify: (params: unknown) => mockToastNotify(params), (message: string, options?: { type?: string }) => mockToastNotify({ type: options?.type, message }),
}, {
success: (message: string) => mockToastNotify({ type: 'success', message }),
error: (message: string) => mockToastNotify({ type: 'error', message }),
warning: (message: string) => mockToastNotify({ type: 'warning', message }),
info: (message: string) => mockToastNotify({ type: 'info', message }),
dismiss: vi.fn(),
update: vi.fn(),
promise: vi.fn(),
},
),
})) }))
// ============================================================================ // ============================================================================

View File

@ -7,7 +7,7 @@ import type { BuildTriggerSubscriptionPayload } from '@/service/use-triggers'
import { debounce } from 'es-toolkit/compat' import { debounce } from 'es-toolkit/compat'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Toast from '@/app/components/base/toast' import { toast } from '@/app/components/base/ui/toast'
import { SupportedCreationMethods } from '@/app/components/plugins/types' import { SupportedCreationMethods } from '@/app/components/plugins/types'
import { TriggerCredentialTypeEnum } from '@/app/components/workflow/block-selector/types' import { TriggerCredentialTypeEnum } from '@/app/components/workflow/block-selector/types'
import { import {
@ -154,10 +154,7 @@ export const useCommonModalState = ({
onError: async (error: unknown) => { onError: async (error: unknown) => {
const errorMessage = await parsePluginErrorMessage(error) || t('modal.errors.updateFailed', { ns: 'pluginTrigger' }) const errorMessage = await parsePluginErrorMessage(error) || t('modal.errors.updateFailed', { ns: 'pluginTrigger' })
console.error('Failed to update subscription builder:', error) console.error('Failed to update subscription builder:', error)
Toast.notify({ toast.error(errorMessage)
type: 'error',
message: errorMessage,
})
}, },
}, },
) )
@ -178,10 +175,7 @@ export const useCommonModalState = ({
} }
catch (error) { catch (error) {
console.error('createBuilder error:', error) console.error('createBuilder error:', error)
Toast.notify({ toast.error(t('modal.errors.createFailed', { ns: 'pluginTrigger' }))
type: 'error',
message: t('modal.errors.createFailed', { ns: 'pluginTrigger' }),
})
} }
} }
if (!isInitializedRef.current && !subscriptionBuilder && detail?.provider) if (!isInitializedRef.current && !subscriptionBuilder && detail?.provider)
@ -239,10 +233,7 @@ export const useCommonModalState = ({
const handleVerify = useCallback(() => { const handleVerify = useCallback(() => {
// Guard against uninitialized state // Guard against uninitialized state
if (!detail?.provider || !subscriptionBuilder?.id) { if (!detail?.provider || !subscriptionBuilder?.id) {
Toast.notify({ toast.error('Subscription builder not initialized')
type: 'error',
message: 'Subscription builder not initialized',
})
return return
} }
@ -250,10 +241,7 @@ export const useCommonModalState = ({
const credentials = apiKeyCredentialsFormValues.values const credentials = apiKeyCredentialsFormValues.values
if (!Object.keys(credentials).length) { if (!Object.keys(credentials).length) {
Toast.notify({ toast.error('Please fill in all required credentials')
type: 'error',
message: 'Please fill in all required credentials',
})
return return
} }
@ -270,10 +258,7 @@ export const useCommonModalState = ({
}, },
{ {
onSuccess: () => { onSuccess: () => {
Toast.notify({ toast.success(t('modal.apiKey.verify.success', { ns: 'pluginTrigger' }))
type: 'success',
message: t('modal.apiKey.verify.success', { ns: 'pluginTrigger' }),
})
setCurrentStep(ApiKeyStep.Configuration) setCurrentStep(ApiKeyStep.Configuration)
}, },
onError: async (error: unknown) => { onError: async (error: unknown) => {
@ -290,10 +275,7 @@ export const useCommonModalState = ({
// Handle create // Handle create
const handleCreate = useCallback(() => { const handleCreate = useCallback(() => {
if (!subscriptionBuilder) { if (!subscriptionBuilder) {
Toast.notify({ toast.error('Subscription builder not found')
type: 'error',
message: 'Subscription builder not found',
})
return return
} }
@ -327,19 +309,13 @@ export const useCommonModalState = ({
params, params,
{ {
onSuccess: () => { onSuccess: () => {
Toast.notify({ toast.success(t('subscription.createSuccess', { ns: 'pluginTrigger' }))
type: 'success',
message: t('subscription.createSuccess', { ns: 'pluginTrigger' }),
})
onClose() onClose()
refetch?.() refetch?.()
}, },
onError: async (error: unknown) => { onError: async (error: unknown) => {
const errorMessage = await parsePluginErrorMessage(error) || t('subscription.createFailed', { ns: 'pluginTrigger' }) const errorMessage = await parsePluginErrorMessage(error) || t('subscription.createFailed', { ns: 'pluginTrigger' })
Toast.notify({ toast.error(errorMessage)
type: 'error',
message: errorMessage,
})
}, },
}, },
) )

View File

@ -4,7 +4,7 @@ import type { TriggerOAuthClientParams, TriggerOAuthConfig, TriggerSubscriptionB
import type { ConfigureTriggerOAuthPayload } from '@/service/use-triggers' import type { ConfigureTriggerOAuthPayload } from '@/service/use-triggers'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Toast from '@/app/components/base/toast' import { toast } from '@/app/components/base/ui/toast'
import { openOAuthPopup } from '@/hooks/use-oauth' import { openOAuthPopup } from '@/hooks/use-oauth'
import { import {
useConfigureTriggerOAuth, useConfigureTriggerOAuth,
@ -118,20 +118,14 @@ export const useOAuthClientState = ({
openOAuthPopup(response.authorization_url, (callbackData) => { openOAuthPopup(response.authorization_url, (callbackData) => {
if (!callbackData) if (!callbackData)
return return
Toast.notify({ toast.success(t('modal.oauth.authorization.authSuccess', { ns: 'pluginTrigger' }))
type: 'success',
message: t('modal.oauth.authorization.authSuccess', { ns: 'pluginTrigger' }),
})
onClose() onClose()
showOAuthCreateModal(response.subscription_builder) showOAuthCreateModal(response.subscription_builder)
}) })
}, },
onError: () => { onError: () => {
setAuthorizationStatus(AuthorizationStatusEnum.Failed) setAuthorizationStatus(AuthorizationStatusEnum.Failed)
Toast.notify({ toast.error(t('modal.oauth.authorization.authFailed', { ns: 'pluginTrigger' }))
type: 'error',
message: t('modal.oauth.authorization.authFailed', { ns: 'pluginTrigger' }),
})
}, },
}) })
}, [providerName, initiateOAuth, onClose, showOAuthCreateModal, t]) }, [providerName, initiateOAuth, onClose, showOAuthCreateModal, t])
@ -141,16 +135,10 @@ export const useOAuthClientState = ({
deleteOAuth(providerName, { deleteOAuth(providerName, {
onSuccess: () => { onSuccess: () => {
onClose() onClose()
Toast.notify({ toast.success(t('modal.oauth.remove.success', { ns: 'pluginTrigger' }))
type: 'success',
message: t('modal.oauth.remove.success', { ns: 'pluginTrigger' }),
})
}, },
onError: (error: unknown) => { onError: (error: unknown) => {
Toast.notify({ toast.error(getErrorMessage(error, t('modal.oauth.remove.failed', { ns: 'pluginTrigger' })))
type: 'error',
message: getErrorMessage(error, t('modal.oauth.remove.failed', { ns: 'pluginTrigger' })),
})
}, },
}) })
}, [providerName, deleteOAuth, onClose, t]) }, [providerName, deleteOAuth, onClose, t])
@ -187,10 +175,7 @@ export const useOAuthClientState = ({
return return
} }
onClose() onClose()
Toast.notify({ toast.success(t('modal.oauth.save.success', { ns: 'pluginTrigger' }))
type: 'success',
message: t('modal.oauth.save.success', { ns: 'pluginTrigger' }),
})
}, },
}) })
}, [clientType, providerName, oauthClientSchema, oauthConfig?.params, configureOAuth, handleAuthorization, onClose, t]) }, [clientType, providerName, oauthClientSchema, oauthConfig?.params, configureOAuth, handleAuthorization, onClose, t])

View File

@ -8,8 +8,8 @@ import { ActionButton, ActionButtonState } from '@/app/components/base/action-bu
import Badge from '@/app/components/base/badge' import Badge from '@/app/components/base/badge'
import { Button } from '@/app/components/base/button' import { Button } from '@/app/components/base/button'
import CustomSelect from '@/app/components/base/select/custom' import CustomSelect from '@/app/components/base/select/custom'
import Toast from '@/app/components/base/toast'
import Tooltip from '@/app/components/base/tooltip' import Tooltip from '@/app/components/base/tooltip'
import { toast } from '@/app/components/base/ui/toast'
import { openOAuthPopup } from '@/hooks/use-oauth' import { openOAuthPopup } from '@/hooks/use-oauth'
import { useInitiateTriggerOAuth, useTriggerOAuthConfig, useTriggerProviderInfo } from '@/service/use-triggers' import { useInitiateTriggerOAuth, useTriggerOAuthConfig, useTriggerProviderInfo } from '@/service/use-triggers'
import { cn } from '@/utils/classnames' import { cn } from '@/utils/classnames'
@ -107,19 +107,13 @@ export const CreateSubscriptionButton = ({ buttonType = CreateButtonType.FULL_BU
onSuccess: (response) => { onSuccess: (response) => {
openOAuthPopup(response.authorization_url, (callbackData) => { openOAuthPopup(response.authorization_url, (callbackData) => {
if (callbackData) { if (callbackData) {
Toast.notify({ toast.success(t('modal.oauth.authorization.authSuccess', { ns: 'pluginTrigger' }))
type: 'success',
message: t('modal.oauth.authorization.authSuccess', { ns: 'pluginTrigger' }),
})
setSelectedCreateInfo({ type: SupportedCreationMethods.OAUTH, builder: response.subscription_builder }) setSelectedCreateInfo({ type: SupportedCreationMethods.OAUTH, builder: response.subscription_builder })
} }
}) })
}, },
onError: () => { onError: () => {
Toast.notify({ toast.error(t('modal.oauth.authorization.authFailed', { ns: 'pluginTrigger' }))
type: 'error',
message: t('modal.oauth.authorization.authFailed', { ns: 'pluginTrigger' }),
})
}, },
}) })
} }

View File

@ -8,7 +8,7 @@ import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import { BaseForm } from '@/app/components/base/form/components/base' import { BaseForm } from '@/app/components/base/form/components/base'
import Modal from '@/app/components/base/modal/modal' import Modal from '@/app/components/base/modal/modal'
import Toast from '@/app/components/base/toast' import { toast } from '@/app/components/base/ui/toast'
import OptionCard from '@/app/components/workflow/nodes/_base/components/option-card' import OptionCard from '@/app/components/workflow/nodes/_base/components/option-card'
import { usePluginStore } from '../../store' import { usePluginStore } from '../../store'
import { ClientTypeEnum, useOAuthClientState } from './hooks/use-oauth-client-state' import { ClientTypeEnum, useOAuthClientState } from './hooks/use-oauth-client-state'
@ -48,10 +48,7 @@ export const OAuthClientSettingsModal = ({ oauthConfig, onClose, showOAuthCreate
const handleCopyRedirectUri = () => { const handleCopyRedirectUri = () => {
navigator.clipboard.writeText(oauthConfig?.redirect_uri || '') navigator.clipboard.writeText(oauthConfig?.redirect_uri || '')
Toast.notify({ toast.success(t('actionMsg.copySuccessfully', { ns: 'common' }))
type: 'success',
message: t('actionMsg.copySuccessfully', { ns: 'common' }),
})
} }
return ( return (

View File

@ -47,13 +47,19 @@ vi.mock('@/service/use-triggers', () => ({
useTriggerPluginDynamicOptions: () => ({ data: [], isLoading: false }), useTriggerPluginDynamicOptions: () => ({ data: [], isLoading: false }),
})) }))
vi.mock('@/app/components/base/toast', async (importOriginal) => { vi.mock('@/app/components/base/ui/toast', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/app/components/base/toast')>() const actual = await importOriginal<typeof import('@/app/components/base/ui/toast')>()
return { return {
...actual, ...actual,
default: { toast: Object.assign((args: { type: string, message: string }) => mockToast(args), {
notify: (args: { type: string, message: string }) => mockToast(args), success: (message: string) => mockToast({ type: 'success', message }),
}, error: (message: string) => mockToast({ type: 'error', message }),
warning: (message: string) => mockToast({ type: 'warning', message }),
info: (message: string) => mockToast({ type: 'info', message }),
dismiss: vi.fn(),
update: vi.fn(),
promise: vi.fn(),
}),
} }
}) })

View File

@ -13,8 +13,16 @@ import { OAuthEditModal } from '../oauth-edit-modal'
// ==================== Mock Setup ==================== // ==================== Mock Setup ====================
const mockToastNotify = vi.fn() const mockToastNotify = vi.fn()
vi.mock('@/app/components/base/toast', () => ({ vi.mock('@/app/components/base/ui/toast', () => ({
default: { notify: (params: unknown) => mockToastNotify(params) }, toast: Object.assign((message: string, options?: { type?: string }) => mockToastNotify({ type: options?.type, message }), {
success: (message: string) => mockToastNotify({ type: 'success', message }),
error: (message: string) => mockToastNotify({ type: 'error', message }),
warning: (message: string) => mockToastNotify({ type: 'warning', message }),
info: (message: string) => mockToastNotify({ type: 'info', message }),
dismiss: vi.fn(),
update: vi.fn(),
promise: vi.fn(),
}),
})) }))
const mockParsePluginErrorMessage = vi.fn() const mockParsePluginErrorMessage = vi.fn()

View File

@ -30,13 +30,19 @@ vi.mock('@/service/use-triggers', () => ({
useTriggerPluginDynamicOptions: () => ({ data: [], isLoading: false }), useTriggerPluginDynamicOptions: () => ({ data: [], isLoading: false }),
})) }))
vi.mock('@/app/components/base/toast', async (importOriginal) => { vi.mock('@/app/components/base/ui/toast', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/app/components/base/toast')>() const actual = await importOriginal<typeof import('@/app/components/base/ui/toast')>()
return { return {
...actual, ...actual,
default: { toast: Object.assign((args: { type: string, message: string }) => mockToast(args), {
notify: (args: { type: string, message: string }) => mockToast(args), success: (message: string) => mockToast({ type: 'success', message }),
}, error: (message: string) => mockToast({ type: 'error', message }),
warning: (message: string) => mockToast({ type: 'warning', message }),
info: (message: string) => mockToast({ type: 'info', message }),
dismiss: vi.fn(),
update: vi.fn(),
promise: vi.fn(),
}),
} }
}) })

View File

@ -30,13 +30,19 @@ vi.mock('@/service/use-triggers', () => ({
useTriggerPluginDynamicOptions: () => ({ data: [], isLoading: false }), useTriggerPluginDynamicOptions: () => ({ data: [], isLoading: false }),
})) }))
vi.mock('@/app/components/base/toast', async (importOriginal) => { vi.mock('@/app/components/base/ui/toast', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/app/components/base/toast')>() const actual = await importOriginal<typeof import('@/app/components/base/ui/toast')>()
return { return {
...actual, ...actual,
default: { toast: Object.assign((args: { type: string, message: string }) => mockToast(args), {
notify: (args: { type: string, message: string }) => mockToast(args), success: (message: string) => mockToast({ type: 'success', message }),
}, error: (message: string) => mockToast({ type: 'error', message }),
warning: (message: string) => mockToast({ type: 'warning', message }),
info: (message: string) => mockToast({ type: 'info', message }),
dismiss: vi.fn(),
update: vi.fn(),
promise: vi.fn(),
}),
} }
}) })

View File

@ -9,7 +9,7 @@ import { EncryptedBottom } from '@/app/components/base/encrypted-bottom'
import { BaseForm } from '@/app/components/base/form/components/base' import { BaseForm } from '@/app/components/base/form/components/base'
import { FormTypeEnum } from '@/app/components/base/form/types' import { FormTypeEnum } from '@/app/components/base/form/types'
import Modal from '@/app/components/base/modal/modal' import Modal from '@/app/components/base/modal/modal'
import Toast from '@/app/components/base/toast' import { toast } from '@/app/components/base/ui/toast'
import { ReadmeEntrance } from '@/app/components/plugins/readme-panel/entrance' import { ReadmeEntrance } from '@/app/components/plugins/readme-panel/entrance'
import { useUpdateTriggerSubscription, useVerifyTriggerSubscription } from '@/service/use-triggers' import { useUpdateTriggerSubscription, useVerifyTriggerSubscription } from '@/service/use-triggers'
import { parsePluginErrorMessage } from '@/utils/error-parser' import { parsePluginErrorMessage } from '@/utils/error-parser'
@ -65,7 +65,7 @@ const StatusStep = ({ isActive, text, onClick, clickable }: {
}) => { }) => {
return ( return (
<div <div
className={`system-2xs-semibold-uppercase flex items-center gap-1 ${isActive className={`flex items-center gap-1 system-2xs-semibold-uppercase ${isActive
? 'text-state-accent-solid' ? 'text-state-accent-solid'
: 'text-text-tertiary'} ${clickable ? 'cursor-pointer hover:text-text-secondary' : ''}`} : 'text-text-tertiary'} ${clickable ? 'cursor-pointer hover:text-text-secondary' : ''}`}
onClick={clickable ? onClick : undefined} onClick={clickable ? onClick : undefined}
@ -143,20 +143,14 @@ export const ApiKeyEditModal = ({ onClose, subscription, pluginDetail }: Props)
}, },
{ {
onSuccess: () => { onSuccess: () => {
Toast.notify({ toast.success(t('modal.apiKey.verify.success', { ns: 'pluginTrigger' }))
type: 'success',
message: t('modal.apiKey.verify.success', { ns: 'pluginTrigger' }),
})
// Only save credentials if any field was modified (not all hidden) // Only save credentials if any field was modified (not all hidden)
setVerifiedCredentials(areAllCredentialsHidden(credentials) ? null : credentials) setVerifiedCredentials(areAllCredentialsHidden(credentials) ? null : credentials)
setCurrentStep(EditStep.EditConfiguration) setCurrentStep(EditStep.EditConfiguration)
}, },
onError: async (error: unknown) => { onError: async (error: unknown) => {
const errorMessage = await parsePluginErrorMessage(error) || t('modal.apiKey.verify.error', { ns: 'pluginTrigger' }) const errorMessage = await parsePluginErrorMessage(error) || t('modal.apiKey.verify.error', { ns: 'pluginTrigger' })
Toast.notify({ toast.error(errorMessage)
type: 'error',
message: errorMessage,
})
}, },
}, },
) )
@ -192,19 +186,13 @@ export const ApiKeyEditModal = ({ onClose, subscription, pluginDetail }: Props)
}, },
{ {
onSuccess: () => { onSuccess: () => {
Toast.notify({ toast.success(t('subscription.list.item.actions.edit.success', { ns: 'pluginTrigger' }))
type: 'success',
message: t('subscription.list.item.actions.edit.success', { ns: 'pluginTrigger' }),
})
refetch?.() refetch?.()
onClose() onClose()
}, },
onError: async (error: unknown) => { onError: async (error: unknown) => {
const errorMessage = await parsePluginErrorMessage(error) || t('subscription.list.item.actions.edit.error', { ns: 'pluginTrigger' }) const errorMessage = await parsePluginErrorMessage(error) || t('subscription.list.item.actions.edit.error', { ns: 'pluginTrigger' })
Toast.notify({ toast.error(errorMessage)
type: 'error',
message: errorMessage,
})
}, },
}, },
) )

View File

@ -8,7 +8,7 @@ import { useTranslation } from 'react-i18next'
import { BaseForm } from '@/app/components/base/form/components/base' import { BaseForm } from '@/app/components/base/form/components/base'
import { FormTypeEnum } from '@/app/components/base/form/types' import { FormTypeEnum } from '@/app/components/base/form/types'
import Modal from '@/app/components/base/modal/modal' import Modal from '@/app/components/base/modal/modal'
import Toast from '@/app/components/base/toast' import { toast } from '@/app/components/base/ui/toast'
import { ReadmeEntrance } from '@/app/components/plugins/readme-panel/entrance' import { ReadmeEntrance } from '@/app/components/plugins/readme-panel/entrance'
import { useUpdateTriggerSubscription } from '@/service/use-triggers' import { useUpdateTriggerSubscription } from '@/service/use-triggers'
import { ReadmeShowType } from '../../../readme-panel/store' import { ReadmeShowType } from '../../../readme-panel/store'
@ -94,18 +94,12 @@ export const ManualEditModal = ({ onClose, subscription, pluginDetail }: Props)
}, },
{ {
onSuccess: () => { onSuccess: () => {
Toast.notify({ toast.success(t('subscription.list.item.actions.edit.success', { ns: 'pluginTrigger' }))
type: 'success',
message: t('subscription.list.item.actions.edit.success', { ns: 'pluginTrigger' }),
})
refetch?.() refetch?.()
onClose() onClose()
}, },
onError: (error: unknown) => { onError: (error: unknown) => {
Toast.notify({ toast.error(getErrorMessage(error, t('subscription.list.item.actions.edit.error', { ns: 'pluginTrigger' })))
type: 'error',
message: getErrorMessage(error, t('subscription.list.item.actions.edit.error', { ns: 'pluginTrigger' })),
})
}, },
}, },
) )

View File

@ -8,7 +8,7 @@ import { useTranslation } from 'react-i18next'
import { BaseForm } from '@/app/components/base/form/components/base' import { BaseForm } from '@/app/components/base/form/components/base'
import { FormTypeEnum } from '@/app/components/base/form/types' import { FormTypeEnum } from '@/app/components/base/form/types'
import Modal from '@/app/components/base/modal/modal' import Modal from '@/app/components/base/modal/modal'
import Toast from '@/app/components/base/toast' import { toast } from '@/app/components/base/ui/toast'
import { ReadmeEntrance } from '@/app/components/plugins/readme-panel/entrance' import { ReadmeEntrance } from '@/app/components/plugins/readme-panel/entrance'
import { useUpdateTriggerSubscription } from '@/service/use-triggers' import { useUpdateTriggerSubscription } from '@/service/use-triggers'
import { ReadmeShowType } from '../../../readme-panel/store' import { ReadmeShowType } from '../../../readme-panel/store'
@ -94,18 +94,12 @@ export const OAuthEditModal = ({ onClose, subscription, pluginDetail }: Props) =
}, },
{ {
onSuccess: () => { onSuccess: () => {
Toast.notify({ toast.success(t('subscription.list.item.actions.edit.success', { ns: 'pluginTrigger' }))
type: 'success',
message: t('subscription.list.item.actions.edit.success', { ns: 'pluginTrigger' }),
})
refetch?.() refetch?.()
onClose() onClose()
}, },
onError: (error: unknown) => { onError: (error: unknown) => {
Toast.notify({ toast.error(getErrorMessage(error, t('subscription.list.item.actions.edit.error', { ns: 'pluginTrigger' })))
type: 'error',
message: getErrorMessage(error, t('subscription.list.item.actions.edit.error', { ns: 'pluginTrigger' })),
})
}, },
}, },
) )

View File

@ -11,7 +11,7 @@ import dayjs from 'dayjs'
import * as React from 'react' import * as React from 'react'
import { useState } from 'react' import { useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Toast from '@/app/components/base/toast' import { toast } from '@/app/components/base/ui/toast'
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor' import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types' import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
import { cn } from '@/utils/classnames' import { cn } from '@/utils/classnames'
@ -89,10 +89,7 @@ const LogViewer = ({ logs, className }: Props) => {
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()
navigator.clipboard.writeText(String(parsedData)) navigator.clipboard.writeText(String(parsedData))
Toast.notify({ toast.success(t('actionMsg.copySuccessfully', { ns: 'common' }))
type: 'success',
message: t('actionMsg.copySuccessfully', { ns: 'common' }),
})
}} }}
className="rounded-md p-0.5 hover:bg-components-panel-border" className="rounded-md p-0.5 hover:bg-components-panel-border"
> >

View File

@ -298,8 +298,16 @@ vi.mock('@/app/components/header/account-setting/model-provider-page/model-modal
// Mock Toast - need to track notify calls for assertions // Mock Toast - need to track notify calls for assertions
const mockToastNotify = vi.fn() const mockToastNotify = vi.fn()
vi.mock('@/app/components/base/toast', () => ({ vi.mock('@/app/components/base/ui/toast', () => ({
default: { notify: (...args: unknown[]) => mockToastNotify(...args) }, toast: Object.assign((message: string, options?: { type?: string }) => mockToastNotify({ type: options?.type, message }), {
success: (message: string) => mockToastNotify({ type: 'success', message }),
error: (message: string) => mockToastNotify({ type: 'error', message }),
warning: (message: string) => mockToastNotify({ type: 'warning', message }),
info: (message: string) => mockToastNotify({ type: 'info', message }),
dismiss: vi.fn(),
update: vi.fn(),
promise: vi.fn(),
}),
})) }))
// ==================== Test Utilities ==================== // ==================== Test Utilities ====================
@ -1943,7 +1951,7 @@ describe('ToolCredentialsForm Component', () => {
const saveBtn = screen.getByText(/save/i) const saveBtn = screen.getByText(/save/i)
fireEvent.click(saveBtn) fireEvent.click(saveBtn)
// Toast.notify should have been called with error (lines 49-50) // notifyToast should have been called with error (lines 49-50)
expect(mockToastNotify).toHaveBeenCalledWith(expect.objectContaining({ type: 'error' })) expect(mockToastNotify).toHaveBeenCalledWith(expect.objectContaining({ type: 'error' }))
// onSaved should not be called because validation fails // onSaved should not be called because validation fails
expect(onSaved).not.toHaveBeenCalled() expect(onSaved).not.toHaveBeenCalled()

View File

@ -10,12 +10,16 @@ vi.mock('@/utils/classnames', () => ({
cn: (...args: unknown[]) => args.filter(Boolean).join(' '), cn: (...args: unknown[]) => args.filter(Boolean).join(' '),
})) }))
vi.mock('@/app/components/base/toast', () => ({ vi.mock('@/app/components/base/ui/toast', () => ({
default: { notify: vi.fn() }, toast: Object.assign(vi.fn(), {
})) success: vi.fn(),
error: vi.fn(),
vi.mock('@/app/components/base/toast/context', () => ({ warning: vi.fn(),
useToastContext: () => ({ notify: vi.fn() }), info: vi.fn(),
dismiss: vi.fn(),
update: vi.fn(),
promise: vi.fn(),
}),
})) }))
const mockFormSchemas = [ const mockFormSchemas = [

View File

@ -10,7 +10,7 @@ import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button' import Button from '@/app/components/base/button'
import Loading from '@/app/components/base/loading' import Loading from '@/app/components/base/loading'
import Toast from '@/app/components/base/toast' import { toast } from '@/app/components/base/ui/toast'
import Form from '@/app/components/header/account-setting/model-provider-page/model-modal/Form' import Form from '@/app/components/header/account-setting/model-provider-page/model-modal/Form'
import { addDefaultValue, toolCredentialToFormSchemas } from '@/app/components/tools/utils/to-form-schema' import { addDefaultValue, toolCredentialToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
import { useRenderI18nObject } from '@/hooks/use-i18n' import { useRenderI18nObject } from '@/hooks/use-i18n'
@ -49,7 +49,10 @@ const ToolCredentialForm: FC<Props> = ({
return return
for (const field of credentialSchema) { for (const field of credentialSchema) {
if (field.required && !tempCredential[field.name]) { if (field.required && !tempCredential[field.name]) {
Toast.notify({ type: 'error', message: t('errorMsg.fieldRequired', { ns: 'common', field: getValueFromI18nObject(field.label) }) }) toast.error(t('errorMsg.fieldRequired', {
ns: 'common',
field: getValueFromI18nObject(field.label),
}))
return return
} }
} }

View File

@ -1,7 +1,6 @@
import type { MetaData, PluginCategoryEnum } from '../../types' import type { MetaData, PluginCategoryEnum } from '../../types'
import { fireEvent, render, screen, waitFor } from '@testing-library/react' import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest' import { beforeEach, describe, expect, it, vi } from 'vitest'
import Toast from '@/app/components/base/toast'
// ==================== Imports (after mocks) ==================== // ==================== Imports (after mocks) ====================
@ -17,12 +16,29 @@ const {
mockCheckForUpdates, mockCheckForUpdates,
mockSetShowUpdatePluginModal, mockSetShowUpdatePluginModal,
mockInvalidateInstalledPluginList, mockInvalidateInstalledPluginList,
mockToastNotify,
} = vi.hoisted(() => ({ } = vi.hoisted(() => ({
mockUninstallPlugin: vi.fn(), mockUninstallPlugin: vi.fn(),
mockFetchReleases: vi.fn(), mockFetchReleases: vi.fn(),
mockCheckForUpdates: vi.fn(), mockCheckForUpdates: vi.fn(),
mockSetShowUpdatePluginModal: vi.fn(), mockSetShowUpdatePluginModal: vi.fn(),
mockInvalidateInstalledPluginList: vi.fn(), mockInvalidateInstalledPluginList: vi.fn(),
mockToastNotify: vi.fn(),
}))
vi.mock('@/app/components/base/ui/toast', () => ({
toast: Object.assign(
(message: string, options?: { type?: string }) => mockToastNotify({ type: options?.type, message }),
{
success: (message: string) => mockToastNotify({ type: 'success', message }),
error: (message: string) => mockToastNotify({ type: 'error', message }),
warning: (message: string) => mockToastNotify({ type: 'warning', message }),
info: (message: string) => mockToastNotify({ type: 'info', message }),
dismiss: vi.fn(),
update: vi.fn(),
promise: vi.fn(),
},
),
})) }))
// Mock uninstall plugin service // Mock uninstall plugin service
@ -140,13 +156,8 @@ const getActionButtons = () => screen.getAllByRole('button')
const queryActionButtons = () => screen.queryAllByRole('button') const queryActionButtons = () => screen.queryAllByRole('button')
describe('Action Component', () => { describe('Action Component', () => {
// Spy on Toast.notify - real component but we track calls
let toastNotifySpy: ReturnType<typeof vi.spyOn>
beforeEach(() => { beforeEach(() => {
vi.clearAllMocks() vi.clearAllMocks()
// Spy on Toast.notify and mock implementation to avoid DOM side effects
toastNotifySpy = vi.spyOn(Toast, 'notify').mockImplementation(() => ({ clear: vi.fn() }))
mockUninstallPlugin.mockResolvedValue({ success: true }) mockUninstallPlugin.mockResolvedValue({ success: true })
mockFetchReleases.mockResolvedValue([]) mockFetchReleases.mockResolvedValue([])
mockCheckForUpdates.mockReturnValue({ mockCheckForUpdates.mockReturnValue({
@ -155,10 +166,6 @@ describe('Action Component', () => {
}) })
}) })
afterEach(() => {
toastNotifySpy.mockRestore()
})
// ==================== Rendering Tests ==================== // ==================== Rendering Tests ====================
describe('Rendering', () => { describe('Rendering', () => {
it('should render delete button when isShowDelete is true', () => { it('should render delete button when isShowDelete is true', () => {
@ -563,9 +570,9 @@ describe('Action Component', () => {
render(<Action {...props} />) render(<Action {...props} />)
fireEvent.click(getActionButtons()[0]) fireEvent.click(getActionButtons()[0])
// Assert - Toast.notify is called with the toast props // Assert - toast is called with the translated payload
await waitFor(() => { await waitFor(() => {
expect(toastNotifySpy).toHaveBeenCalledWith({ type: 'success', message: 'Already up to date' }) expect(mockToastNotify).toHaveBeenCalledWith({ type: 'success', message: 'Already up to date' })
}) })
}) })

View File

@ -7,7 +7,7 @@ import { useBoolean } from 'ahooks'
import * as React from 'react' import * as React from 'react'
import { useCallback } from 'react' import { useCallback } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import Toast from '@/app/components/base/toast' import { toast } from '@/app/components/base/ui/toast'
import { useModalContext } from '@/context/modal-context' import { useModalContext } from '@/context/modal-context'
import { uninstallPlugin } from '@/service/plugins' import { uninstallPlugin } from '@/service/plugins'
import { useInvalidateInstalledPluginList } from '@/service/use-plugins' import { useInvalidateInstalledPluginList } from '@/service/use-plugins'
@ -65,7 +65,7 @@ const Action: FC<Props> = ({
if (fetchedReleases.length === 0) if (fetchedReleases.length === 0)
return return
const { needUpdate, toastProps } = checkForUpdates(fetchedReleases, meta!.version) const { needUpdate, toastProps } = checkForUpdates(fetchedReleases, meta!.version)
Toast.notify(toastProps) toast(toastProps.message, { type: toastProps.type })
if (needUpdate) { if (needUpdate) {
setShowUpdatePluginModal({ setShowUpdatePluginModal({
onSaveCallback: () => { onSaveCallback: () => {

View File

@ -5062,9 +5062,6 @@
} }
}, },
"app/components/plugins/install-plugin/hooks.ts": { "app/components/plugins/install-plugin/hooks.ts": {
"no-restricted-imports": {
"count": 2
},
"ts/no-explicit-any": { "ts/no-explicit-any": {
"count": 4 "count": 4
} }
@ -5100,9 +5097,6 @@
}, },
"app/components/plugins/install-plugin/install-from-github/index.tsx": { "app/components/plugins/install-plugin/install-from-github/index.tsx": {
"no-restricted-imports": { "no-restricted-imports": {
"count": 3
},
"tailwindcss/enforce-consistent-class-order": {
"count": 2 "count": 2
}, },
"ts/no-explicit-any": { "ts/no-explicit-any": {
@ -5367,17 +5361,9 @@
"count": 1 "count": 1
} }
}, },
"app/components/plugins/plugin-detail-panel/detail-header/hooks/use-plugin-operations.ts": {
"no-restricted-imports": {
"count": 1
}
},
"app/components/plugins/plugin-detail-panel/endpoint-card.tsx": { "app/components/plugins/plugin-detail-panel/endpoint-card.tsx": {
"no-restricted-imports": { "no-restricted-imports": {
"count": 3 "count": 2
},
"tailwindcss/enforce-consistent-class-order": {
"count": 5
}, },
"ts/no-explicit-any": { "ts/no-explicit-any": {
"count": 2 "count": 2
@ -5385,22 +5371,13 @@
}, },
"app/components/plugins/plugin-detail-panel/endpoint-list.tsx": { "app/components/plugins/plugin-detail-panel/endpoint-list.tsx": {
"no-restricted-imports": { "no-restricted-imports": {
"count": 2 "count": 1
},
"tailwindcss/enforce-consistent-class-order": {
"count": 4
}, },
"ts/no-explicit-any": { "ts/no-explicit-any": {
"count": 2 "count": 2
} }
}, },
"app/components/plugins/plugin-detail-panel/endpoint-modal.tsx": { "app/components/plugins/plugin-detail-panel/endpoint-modal.tsx": {
"no-restricted-imports": {
"count": 1
},
"tailwindcss/enforce-consistent-class-order": {
"count": 3
},
"ts/no-explicit-any": { "ts/no-explicit-any": {
"count": 7 "count": 7
} }
@ -5414,9 +5391,6 @@
} }
}, },
"app/components/plugins/plugin-detail-panel/model-selector/index.tsx": { "app/components/plugins/plugin-detail-panel/model-selector/index.tsx": {
"no-restricted-imports": {
"count": 1
},
"ts/no-explicit-any": { "ts/no-explicit-any": {
"count": 3 "count": 3
} }
@ -5471,27 +5445,21 @@
"app/components/plugins/plugin-detail-panel/subscription-list/create/hooks/use-common-modal-state.ts": { "app/components/plugins/plugin-detail-panel/subscription-list/create/hooks/use-common-modal-state.ts": {
"erasable-syntax-only/enums": { "erasable-syntax-only/enums": {
"count": 1 "count": 1
},
"no-restricted-imports": {
"count": 1
} }
}, },
"app/components/plugins/plugin-detail-panel/subscription-list/create/hooks/use-oauth-client-state.ts": { "app/components/plugins/plugin-detail-panel/subscription-list/create/hooks/use-oauth-client-state.ts": {
"erasable-syntax-only/enums": { "erasable-syntax-only/enums": {
"count": 2 "count": 2
},
"no-restricted-imports": {
"count": 1
} }
}, },
"app/components/plugins/plugin-detail-panel/subscription-list/create/index.tsx": { "app/components/plugins/plugin-detail-panel/subscription-list/create/index.tsx": {
"no-restricted-imports": { "no-restricted-imports": {
"count": 4 "count": 3
} }
}, },
"app/components/plugins/plugin-detail-panel/subscription-list/create/oauth-client.tsx": { "app/components/plugins/plugin-detail-panel/subscription-list/create/oauth-client.tsx": {
"no-restricted-imports": { "no-restricted-imports": {
"count": 2 "count": 1
}, },
"tailwindcss/enforce-consistent-class-order": { "tailwindcss/enforce-consistent-class-order": {
"count": 3 "count": 3
@ -5507,20 +5475,17 @@
"count": 1 "count": 1
}, },
"no-restricted-imports": { "no-restricted-imports": {
"count": 2
},
"tailwindcss/enforce-consistent-class-order": {
"count": 1 "count": 1
} }
}, },
"app/components/plugins/plugin-detail-panel/subscription-list/edit/manual-edit-modal.tsx": { "app/components/plugins/plugin-detail-panel/subscription-list/edit/manual-edit-modal.tsx": {
"no-restricted-imports": { "no-restricted-imports": {
"count": 2 "count": 1
} }
}, },
"app/components/plugins/plugin-detail-panel/subscription-list/edit/oauth-edit-modal.tsx": { "app/components/plugins/plugin-detail-panel/subscription-list/edit/oauth-edit-modal.tsx": {
"no-restricted-imports": { "no-restricted-imports": {
"count": 2 "count": 1
} }
}, },
"app/components/plugins/plugin-detail-panel/subscription-list/index.tsx": { "app/components/plugins/plugin-detail-panel/subscription-list/index.tsx": {
@ -5540,9 +5505,6 @@
"erasable-syntax-only/enums": { "erasable-syntax-only/enums": {
"count": 1 "count": 1
}, },
"no-restricted-imports": {
"count": 1
},
"tailwindcss/enforce-consistent-class-order": { "tailwindcss/enforce-consistent-class-order": {
"count": 5 "count": 5
}, },
@ -5600,11 +5562,6 @@
"count": 2 "count": 2
} }
}, },
"app/components/plugins/plugin-detail-panel/tool-selector/components/tool-credentials-form.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"app/components/plugins/plugin-detail-panel/tool-selector/components/tool-item.tsx": { "app/components/plugins/plugin-detail-panel/tool-selector/components/tool-item.tsx": {
"no-restricted-imports": { "no-restricted-imports": {
"count": 2 "count": 2
@ -5643,7 +5600,7 @@
}, },
"app/components/plugins/plugin-item/action.tsx": { "app/components/plugins/plugin-item/action.tsx": {
"no-restricted-imports": { "no-restricted-imports": {
"count": 3 "count": 2
} }
}, },
"app/components/plugins/plugin-item/index.tsx": { "app/components/plugins/plugin-item/index.tsx": {