diff --git a/web/__tests__/datasets/dataset-settings-flow.test.tsx b/web/__tests__/datasets/dataset-settings-flow.test.tsx index 607cd8c2d5..b4a5e78326 100644 --- a/web/__tests__/datasets/dataset-settings-flow.test.tsx +++ b/web/__tests__/datasets/dataset-settings-flow.test.tsx @@ -19,6 +19,10 @@ import { RETRIEVE_METHOD } from '@/types/app' // --- Mocks --- +const { mockToastError } = vi.hoisted(() => ({ + mockToastError: vi.fn(), +})) + const mockMutateDatasets = vi.fn() const mockInvalidDatasetList = vi.fn() const mockUpdateDatasetSetting = vi.fn().mockResolvedValue({}) @@ -55,8 +59,11 @@ vi.mock('@/app/components/datasets/common/check-rerank-model', () => ({ isReRankModelSelected: () => true, })) -vi.mock('@/app/components/base/toast', () => ({ - default: { notify: vi.fn() }, +vi.mock('@/app/components/base/ui/toast', () => ({ + toast: { + error: mockToastError, + success: vi.fn(), + }, })) // --- Dataset factory --- @@ -311,7 +318,7 @@ describe('Dataset Settings Flow - Cross-Module Configuration Cascade', () => { describe('Form Submission Validation → All Fields Together', () => { it('should reject empty name on save', async () => { - const Toast = await import('@/app/components/base/toast') + const { toast } = await import('@/app/components/base/ui/toast') const { result } = renderHook(() => useFormState()) act(() => { @@ -322,10 +329,7 @@ describe('Dataset Settings Flow - Cross-Module Configuration Cascade', () => { await result.current.handleSave() }) - expect(Toast.default.notify).toHaveBeenCalledWith({ - type: 'error', - message: expect.any(String), - }) + expect(toast.error).toHaveBeenCalledWith(expect.any(String)) expect(mockUpdateDatasetSetting).not.toHaveBeenCalled() }) diff --git a/web/app/components/app/configuration/dataset-config/params-config/index.spec.tsx b/web/app/components/app/configuration/dataset-config/params-config/index.spec.tsx index 9366039414..024432112d 100644 --- a/web/app/components/app/configuration/dataset-config/params-config/index.spec.tsx +++ b/web/app/components/app/configuration/dataset-config/params-config/index.spec.tsx @@ -3,7 +3,7 @@ import type { DatasetConfigs } from '@/models/debug' import { render, screen, waitFor, within } from '@testing-library/react' import userEvent from '@testing-library/user-event' import * as React from 'react' -import Toast from '@/app/components/base/toast' +import { toast } from '@/app/components/base/ui/toast' import { useCurrentProviderAndModel, useModelListAndDefaultModelAndCurrentProviderAndModel, @@ -75,7 +75,7 @@ vi.mock('@/app/components/header/account-setting/model-provider-page/model-param const mockedUseModelListAndDefaultModelAndCurrentProviderAndModel = useModelListAndDefaultModelAndCurrentProviderAndModel as MockedFunction const mockedUseCurrentProviderAndModel = useCurrentProviderAndModel as MockedFunction -let toastNotifySpy: MockInstance +let toastErrorSpy: MockInstance const createDatasetConfigs = (overrides: Partial = {}): DatasetConfigs => { return { @@ -140,7 +140,7 @@ describe('dataset-config/params-config', () => { beforeEach(() => { vi.clearAllMocks() vi.useRealTimers() - toastNotifySpy = vi.spyOn(Toast, 'notify').mockImplementation(() => ({})) + toastErrorSpy = vi.spyOn(toast, 'error').mockImplementation(() => '') mockedUseModelListAndDefaultModelAndCurrentProviderAndModel.mockReturnValue({ modelList: [], defaultModel: undefined, @@ -154,7 +154,7 @@ describe('dataset-config/params-config', () => { }) afterEach(() => { - toastNotifySpy.mockRestore() + toastErrorSpy.mockRestore() }) // Rendering tests (REQUIRED) @@ -254,10 +254,7 @@ describe('dataset-config/params-config', () => { await user.click(dialogScope.getByRole('button', { name: 'common.operation.save' })) // Assert - expect(toastNotifySpy).toHaveBeenCalledWith({ - type: 'error', - message: 'appDebug.datasetConfig.rerankModelRequired', - }) + expect(toastErrorSpy).toHaveBeenCalledWith('appDebug.datasetConfig.rerankModelRequired') expect(screen.getByRole('dialog')).toBeInTheDocument() }) }) diff --git a/web/app/components/app/configuration/dataset-config/params-config/index.tsx b/web/app/components/app/configuration/dataset-config/params-config/index.tsx index 692ae12022..89410203df 100644 --- a/web/app/components/app/configuration/dataset-config/params-config/index.tsx +++ b/web/app/components/app/configuration/dataset-config/params-config/index.tsx @@ -7,7 +7,7 @@ import { useTranslation } from 'react-i18next' import { useContext } from 'use-context-selector' import Button from '@/app/components/base/button' import Modal from '@/app/components/base/modal' -import Toast from '@/app/components/base/toast' +import { toast } from '@/app/components/base/ui/toast' import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import { useCurrentProviderAndModel, useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks' import { @@ -66,10 +66,7 @@ const ParamsConfig = ({ } } if (errMsg) { - Toast.notify({ - type: 'error', - message: errMsg, - }) + toast.error(errMsg) } return !errMsg } diff --git a/web/app/components/datasets/common/document-status-with-action/__tests__/auto-disabled-document.spec.tsx b/web/app/components/datasets/common/document-status-with-action/__tests__/auto-disabled-document.spec.tsx index 1103da3f36..fcaca86e89 100644 --- a/web/app/components/datasets/common/document-status-with-action/__tests__/auto-disabled-document.spec.tsx +++ b/web/app/components/datasets/common/document-status-with-action/__tests__/auto-disabled-document.spec.tsx @@ -1,10 +1,14 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react' import { beforeEach, describe, expect, it, vi } from 'vitest' -import Toast from '@/app/components/base/toast' +import { toast } from '@/app/components/base/ui/toast' import { useAutoDisabledDocuments } from '@/service/knowledge/use-document' import AutoDisabledDocument from '../auto-disabled-document' +const { mockToastSuccess } = vi.hoisted(() => ({ + mockToastSuccess: vi.fn(), +})) + type AutoDisabledDocumentsResponse = { document_ids: string[] } const createMockQueryResult = ( @@ -26,9 +30,9 @@ vi.mock('@/service/knowledge/use-document', () => ({ useInvalidDisabledDocument: vi.fn(() => mockInvalidDisabledDocument), })) -vi.mock('@/app/components/base/toast', () => ({ - default: { - notify: vi.fn(), +vi.mock('@/app/components/base/ui/toast', () => ({ + toast: { + success: mockToastSuccess, }, })) @@ -134,10 +138,7 @@ describe('AutoDisabledDocument', () => { fireEvent.click(actionButton) await waitFor(() => { - expect(Toast.notify).toHaveBeenCalledWith({ - type: 'success', - message: expect.any(String), - }) + expect(toast.success).toHaveBeenCalledWith(expect.any(String)) }) }) }) diff --git a/web/app/components/datasets/common/document-status-with-action/auto-disabled-document.tsx b/web/app/components/datasets/common/document-status-with-action/auto-disabled-document.tsx index a67c110849..c6c7e03bd1 100644 --- a/web/app/components/datasets/common/document-status-with-action/auto-disabled-document.tsx +++ b/web/app/components/datasets/common/document-status-with-action/auto-disabled-document.tsx @@ -3,7 +3,7 @@ import type { FC } from 'react' import * as React from 'react' import { useCallback } from 'react' import { useTranslation } from 'react-i18next' -import Toast from '@/app/components/base/toast' +import { toast } from '@/app/components/base/ui/toast' import { useAutoDisabledDocuments, useDocumentEnable, useInvalidDisabledDocument } from '@/service/knowledge/use-document' import StatusWithAction from './status-with-action' @@ -23,7 +23,7 @@ const AutoDisabledDocument: FC = ({ const handleEnableDocuments = useCallback(async () => { await enableDocument({ datasetId, documentIds }) invalidDisabledDocument() - Toast.notify({ type: 'success', message: t('actionMsg.modifiedSuccessfully', { ns: 'common' }) }) + toast.success(t('actionMsg.modifiedSuccessfully', { ns: 'common' })) }, []) if (!hasDisabledDocument || isLoading) return null diff --git a/web/app/components/datasets/common/image-uploader/hooks/__tests__/use-upload.spec.tsx b/web/app/components/datasets/common/image-uploader/hooks/__tests__/use-upload.spec.tsx index f37dbd41f4..47a29fcfa1 100644 --- a/web/app/components/datasets/common/image-uploader/hooks/__tests__/use-upload.spec.tsx +++ b/web/app/components/datasets/common/image-uploader/hooks/__tests__/use-upload.spec.tsx @@ -3,10 +3,14 @@ import type { FileEntity } from '../../types' import { act, fireEvent, render, renderHook, screen, waitFor } from '@testing-library/react' import * as React from 'react' import { beforeEach, describe, expect, it, vi } from 'vitest' -import Toast from '@/app/components/base/toast' +import { toast } from '@/app/components/base/ui/toast' import { FileContextProvider } from '../../store' import { useUpload } from '../use-upload' +const { mockToastError } = vi.hoisted(() => ({ + mockToastError: vi.fn(), +})) + vi.mock('@/service/use-common', () => ({ useFileUploadConfig: vi.fn(() => ({ data: { @@ -17,9 +21,9 @@ vi.mock('@/service/use-common', () => ({ })), })) -vi.mock('@/app/components/base/toast', () => ({ - default: { - notify: vi.fn(), +vi.mock('@/app/components/base/ui/toast', () => ({ + toast: { + error: mockToastError, }, })) @@ -177,10 +181,7 @@ describe('useUpload hook', () => { }) await waitFor(() => { - expect(Toast.notify).toHaveBeenCalledWith({ - type: 'error', - message: expect.any(String), - }) + expect(toast.error).toHaveBeenCalledWith(expect.any(String)) }) }) @@ -204,13 +205,11 @@ describe('useUpload hook', () => { result.current.fileChangeHandle(mockEvent) }) - // Should not show type error for valid image type - type ToastCall = [{ type: string, message: string }] - const mockNotify = vi.mocked(Toast.notify) + // Should not show file-extension error for valid image type + type ToastCall = [string] + const mockNotify = vi.mocked(toast.error) const calls = mockNotify.mock.calls as ToastCall[] - const typeErrorCalls = calls.filter( - (call: ToastCall) => call[0].type === 'error' && call[0].message.includes('Extension'), - ) + const typeErrorCalls = calls.filter(call => call[0].includes('common.fileUploader.fileExtensionNotSupport')) expect(typeErrorCalls.length).toBe(0) }) }) @@ -261,7 +260,7 @@ describe('useUpload hook', () => { }) // Should not throw and not show error - expect(Toast.notify).not.toHaveBeenCalled() + expect(toast.error).not.toHaveBeenCalled() }) it('should handle null files', () => { @@ -314,10 +313,7 @@ describe('useUpload hook', () => { }) await waitFor(() => { - expect(Toast.notify).toHaveBeenCalledWith({ - type: 'error', - message: expect.any(String), - }) + expect(toast.error).toHaveBeenCalledWith(expect.any(String)) }) }) }) @@ -419,10 +415,7 @@ describe('useUpload hook', () => { }) await waitFor(() => { - expect(Toast.notify).toHaveBeenCalledWith({ - type: 'error', - message: 'Upload error', - }) + expect(toast.error).toHaveBeenCalledWith('Upload error') }) }) }) @@ -481,10 +474,7 @@ describe('useUpload hook', () => { }) await waitFor(() => { - expect(Toast.notify).toHaveBeenCalledWith({ - type: 'error', - message: 'Upload error', - }) + expect(toast.error).toHaveBeenCalledWith('Upload error') }) }) }) @@ -522,10 +512,7 @@ describe('useUpload hook', () => { }) await waitFor(() => { - expect(Toast.notify).toHaveBeenCalledWith({ - type: 'error', - message: expect.any(String), - }) + expect(toast.error).toHaveBeenCalledWith(expect.any(String)) }) }) }) @@ -610,10 +597,7 @@ describe('useUpload hook', () => { }) await waitFor(() => { - expect(Toast.notify).toHaveBeenCalledWith({ - type: 'error', - message: expect.any(String), - }) + expect(toast.error).toHaveBeenCalledWith(expect.any(String)) }) // Restore original MockFileReader @@ -773,10 +757,7 @@ describe('useUpload hook', () => { // Should show error toast for invalid file type await waitFor(() => { - expect(Toast.notify).toHaveBeenCalledWith({ - type: 'error', - message: expect.any(String), - }) + expect(toast.error).toHaveBeenCalledWith(expect.any(String)) }) }) diff --git a/web/app/components/datasets/common/image-uploader/hooks/use-upload.ts b/web/app/components/datasets/common/image-uploader/hooks/use-upload.ts index ab7b8cbf28..d262401f4b 100644 --- a/web/app/components/datasets/common/image-uploader/hooks/use-upload.ts +++ b/web/app/components/datasets/common/image-uploader/hooks/use-upload.ts @@ -4,7 +4,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { v4 as uuid4 } from 'uuid' import { fileUpload, getFileUploadErrorMessage } from '@/app/components/base/file-uploader/utils' -import Toast from '@/app/components/base/toast' +import { toast } from '@/app/components/base/ui/toast' import { useFileUploadConfig } from '@/service/use-common' import { ACCEPT_TYPES } from '../constants' import { useFileStore } from '../store' @@ -54,9 +54,9 @@ export const useUpload = () => { const showErrorMessage = useCallback((type: 'type' | 'size') => { if (type === 'type') - Toast.notify({ type: 'error', message: t('fileUploader.fileExtensionNotSupport', { ns: 'common' }) }) + toast.error(t('fileUploader.fileExtensionNotSupport', { ns: 'common' })) else - Toast.notify({ type: 'error', message: t('imageUploader.fileSizeLimitExceeded', { ns: 'dataset', size: fileUploadConfig.imageFileSizeLimit }) }) + toast.error(t('imageUploader.fileSizeLimitExceeded', { ns: 'dataset', size: fileUploadConfig.imageFileSizeLimit })) }, [fileUploadConfig, t]) const getValidFiles = useCallback((files: File[]) => { @@ -146,7 +146,7 @@ export const useUpload = () => { }, onErrorCallback: (error?: any) => { const errorMessage = getFileUploadErrorMessage(error, t('fileUploader.uploadFromComputerUploadError', { ns: 'common' }), t) - Toast.notify({ type: 'error', message: errorMessage }) + toast.error(errorMessage) handleUpdateFile({ ...uploadingFile, progress: -1 }) }, }) @@ -188,7 +188,7 @@ export const useUpload = () => { }, onErrorCallback: (error?: any) => { const errorMessage = getFileUploadErrorMessage(error, t('fileUploader.uploadFromComputerUploadError', { ns: 'common' }), t) - Toast.notify({ type: 'error', message: errorMessage }) + toast.error(errorMessage) handleUpdateFile({ ...uploadingFile, progress: -1 }) }, }) @@ -198,7 +198,7 @@ export const useUpload = () => { reader.addEventListener( 'error', () => { - Toast.notify({ type: 'error', message: t('fileUploader.uploadFromComputerReadError', { ns: 'common' }) }) + toast.error(t('fileUploader.uploadFromComputerReadError', { ns: 'common' })) }, false, ) @@ -211,10 +211,7 @@ export const useUpload = () => { if (newFiles.length === 0) return if (files.length + newFiles.length > singleChunkAttachmentLimit) { - Toast.notify({ - type: 'error', - message: t('imageUploader.singleChunkAttachmentLimitTooltip', { ns: 'datasetHitTesting', limit: singleChunkAttachmentLimit }), - }) + toast.error(t('imageUploader.singleChunkAttachmentLimitTooltip', { ns: 'datasetHitTesting', limit: singleChunkAttachmentLimit })) return } for (const file of newFiles) diff --git a/web/app/components/datasets/common/retrieval-param-config/__tests__/index.spec.tsx b/web/app/components/datasets/common/retrieval-param-config/__tests__/index.spec.tsx index f5b41688e1..db6086ca34 100644 --- a/web/app/components/datasets/common/retrieval-param-config/__tests__/index.spec.tsx +++ b/web/app/components/datasets/common/retrieval-param-config/__tests__/index.spec.tsx @@ -5,9 +5,9 @@ import { RETRIEVE_METHOD } from '@/types/app' import RetrievalParamConfig from '../index' const mockNotify = vi.fn() -vi.mock('@/app/components/base/toast', () => ({ - default: { - notify: (params: { type: string, message: string }) => mockNotify(params), +vi.mock('@/app/components/base/ui/toast', () => ({ + toast: { + error: (message: string) => mockNotify(message), }, })) @@ -260,10 +260,7 @@ describe('RetrievalParamConfig', () => { fireEvent.click(screen.getByTestId('rerank-switch')) - expect(mockNotify).toHaveBeenCalledWith({ - type: 'error', - message: 'workflow.errorMsg.rerankModelRequired', - }) + expect(mockNotify).toHaveBeenCalledWith('workflow.errorMsg.rerankModelRequired') }) it('should update reranking model on selection', () => { @@ -618,10 +615,7 @@ describe('RetrievalParamConfig', () => { const rerankModelCard = radioCards.find(card => card.getAttribute('data-title') === 'common.modelProvider.rerankModel.key') fireEvent.click(rerankModelCard!) - expect(mockNotify).toHaveBeenCalledWith({ - type: 'error', - message: 'workflow.errorMsg.rerankModelRequired', - }) + expect(mockNotify).toHaveBeenCalledWith('workflow.errorMsg.rerankModelRequired') }) it('should update weights when WeightedScore changes', () => { diff --git a/web/app/components/datasets/common/retrieval-param-config/index.tsx b/web/app/components/datasets/common/retrieval-param-config/index.tsx index 2414c29a8c..e0fd245ef5 100644 --- a/web/app/components/datasets/common/retrieval-param-config/index.tsx +++ b/web/app/components/datasets/common/retrieval-param-config/index.tsx @@ -11,8 +11,8 @@ import ScoreThresholdItem from '@/app/components/base/param-item/score-threshold import TopKItem from '@/app/components/base/param-item/top-k-item' import RadioCard from '@/app/components/base/radio-card' import Switch from '@/app/components/base/switch' -import Toast from '@/app/components/base/toast' import Tooltip from '@/app/components/base/tooltip' +import { toast } from '@/app/components/base/ui/toast' import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import { useCurrentProviderAndModel, useModelListAndDefaultModel } from '@/app/components/header/account-setting/model-provider-page/hooks' import ModelSelector from '@/app/components/header/account-setting/model-provider-page/model-selector' @@ -59,7 +59,7 @@ const RetrievalParamConfig: FC = ({ const handleToggleRerankEnable = useCallback((enable: boolean) => { if (enable && !currentModel) - Toast.notify({ type: 'error', message: t('errorMsg.rerankModelRequired', { ns: 'workflow' }) }) + toast.error(t('errorMsg.rerankModelRequired', { ns: 'workflow' })) onChange({ ...value, reranking_enable: enable, @@ -96,7 +96,7 @@ const RetrievalParamConfig: FC = ({ } } if (v === RerankingModeEnum.RerankingModel && !currentModel) - Toast.notify({ type: 'error', message: t('errorMsg.rerankModelRequired', { ns: 'workflow' }) }) + toast.error(t('errorMsg.rerankModelRequired', { ns: 'workflow' })) onChange(result) } diff --git a/web/app/components/datasets/documents/components/__tests__/rename-modal.spec.tsx b/web/app/components/datasets/documents/components/__tests__/rename-modal.spec.tsx index 9ed61a66e0..ad40e52752 100644 --- a/web/app/components/datasets/documents/components/__tests__/rename-modal.spec.tsx +++ b/web/app/components/datasets/documents/components/__tests__/rename-modal.spec.tsx @@ -5,11 +5,23 @@ import { renameDocumentName } from '@/service/datasets' import RenameModal from '../rename-modal' +const { mockToastSuccess, mockToastError } = vi.hoisted(() => ({ + mockToastSuccess: vi.fn(), + mockToastError: vi.fn(), +})) + // Mock the service vi.mock('@/service/datasets', () => ({ renameDocumentName: vi.fn(), })) +vi.mock('@/app/components/base/ui/toast', () => ({ + toast: { + success: mockToastSuccess, + error: mockToastError, + }, +})) + const mockRenameDocumentName = vi.mocked(renameDocumentName) describe('RenameModal', () => { @@ -118,6 +130,7 @@ describe('RenameModal', () => { await waitFor(() => { expect(handleSaved).toHaveBeenCalledTimes(1) expect(handleClose).toHaveBeenCalledTimes(1) + expect(mockToastSuccess).toHaveBeenCalledWith(expect.any(String)) }) }) }) @@ -163,6 +176,7 @@ describe('RenameModal', () => { // onSaved and onClose should not be called on error expect(handleSaved).not.toHaveBeenCalled() expect(handleClose).not.toHaveBeenCalled() + expect(mockToastError).toHaveBeenCalledWith('Error: API Error') }) }) }) diff --git a/web/app/components/datasets/documents/components/document-list/hooks/__tests__/use-document-actions.spec.ts b/web/app/components/datasets/documents/components/document-list/hooks/__tests__/use-document-actions.spec.ts index 5f48be084e..449478eb7b 100644 --- a/web/app/components/datasets/documents/components/document-list/hooks/__tests__/use-document-actions.spec.ts +++ b/web/app/components/datasets/documents/components/document-list/hooks/__tests__/use-document-actions.spec.ts @@ -3,6 +3,11 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { DocumentActionType } from '@/models/datasets' import { useDocumentActions } from '../use-document-actions' +const { mockToastSuccess, mockToastError } = vi.hoisted(() => ({ + mockToastSuccess: vi.fn(), + mockToastError: vi.fn(), +})) + const mockArchive = vi.fn() const mockSummary = vi.fn() const mockEnable = vi.fn() @@ -22,9 +27,11 @@ vi.mock('@/service/knowledge/use-document', () => ({ useDocumentDownloadZip: () => ({ mutateAsync: mockDownloadZip, isPending: mockIsDownloadingZip }), })) -const mockToastNotify = vi.fn() -vi.mock('@/app/components/base/toast', () => ({ - default: { notify: (...args: unknown[]) => mockToastNotify(...args) }, +vi.mock('@/app/components/base/ui/toast', () => ({ + toast: { + success: mockToastSuccess, + error: mockToastError, + }, })) const mockDownloadBlob = vi.fn() @@ -67,9 +74,7 @@ describe('useDocumentActions', () => { datasetId: 'ds-1', documentIds: ['doc-1', 'doc-2'], }) - expect(mockToastNotify).toHaveBeenCalledWith( - expect.objectContaining({ type: 'success' }), - ) + expect(mockToastSuccess).toHaveBeenCalledWith(expect.any(String)) expect(defaultOptions.onUpdate).toHaveBeenCalled() }) @@ -142,9 +147,7 @@ describe('useDocumentActions', () => { await result.current.handleAction(DocumentActionType.archive)() }) - expect(mockToastNotify).toHaveBeenCalledWith( - expect.objectContaining({ type: 'error' }), - ) + expect(mockToastError).toHaveBeenCalledWith(expect.any(String)) expect(defaultOptions.onUpdate).not.toHaveBeenCalled() }) }) @@ -174,9 +177,7 @@ describe('useDocumentActions', () => { await result.current.handleBatchReIndex() }) - expect(mockToastNotify).toHaveBeenCalledWith( - expect.objectContaining({ type: 'error' }), - ) + expect(mockToastError).toHaveBeenCalledWith(expect.any(String)) }) }) @@ -210,9 +211,7 @@ describe('useDocumentActions', () => { await result.current.handleBatchDownload() }) - expect(mockToastNotify).toHaveBeenCalledWith( - expect.objectContaining({ type: 'error' }), - ) + expect(mockToastError).toHaveBeenCalledWith(expect.any(String)) }) it('should show error toast when blob is null', async () => { @@ -223,9 +222,7 @@ describe('useDocumentActions', () => { await result.current.handleBatchDownload() }) - expect(mockToastNotify).toHaveBeenCalledWith( - expect.objectContaining({ type: 'error' }), - ) + expect(mockToastError).toHaveBeenCalledWith(expect.any(String)) }) }) }) diff --git a/web/app/components/datasets/documents/components/document-list/hooks/use-document-actions.ts b/web/app/components/datasets/documents/components/document-list/hooks/use-document-actions.ts index 56553faa9e..8b6c40e2be 100644 --- a/web/app/components/datasets/documents/components/document-list/hooks/use-document-actions.ts +++ b/web/app/components/datasets/documents/components/document-list/hooks/use-document-actions.ts @@ -1,7 +1,7 @@ import type { CommonResponse } from '@/models/common' import { useCallback, useMemo } from 'react' import { useTranslation } from 'react-i18next' -import Toast from '@/app/components/base/toast' +import { toast } from '@/app/components/base/ui/toast' import { DocumentActionType } from '@/models/datasets' import { useDocumentArchive, @@ -79,11 +79,11 @@ export const useDocumentActions = ({ if (!e) { if (actionName === DocumentActionType.delete) onClearSelection() - Toast.notify({ type: 'success', message: t('actionMsg.modifiedSuccessfully', { ns: 'common' }) }) + toast.success(t('actionMsg.modifiedSuccessfully', { ns: 'common' })) onUpdate() } else { - Toast.notify({ type: 'error', message: t('actionMsg.modifiedUnsuccessfully', { ns: 'common' }) }) + toast.error(t('actionMsg.modifiedUnsuccessfully', { ns: 'common' })) } } }, [actionMutationMap, datasetId, selectedIds, onClearSelection, onUpdate, t]) @@ -94,11 +94,11 @@ export const useDocumentActions = ({ ) if (!e) { onClearSelection() - Toast.notify({ type: 'success', message: t('actionMsg.modifiedSuccessfully', { ns: 'common' }) }) + toast.success(t('actionMsg.modifiedSuccessfully', { ns: 'common' })) onUpdate() } else { - Toast.notify({ type: 'error', message: t('actionMsg.modifiedUnsuccessfully', { ns: 'common' }) }) + toast.error(t('actionMsg.modifiedUnsuccessfully', { ns: 'common' })) } }, [retryIndexDocument, datasetId, selectedIds, onClearSelection, onUpdate, t]) @@ -110,7 +110,7 @@ export const useDocumentActions = ({ requestDocumentsZip({ datasetId, documentIds: downloadableSelectedIds }), ) if (e || !blob) { - Toast.notify({ type: 'error', message: t('actionMsg.downloadUnsuccessfully', { ns: 'common' }) }) + toast.error(t('actionMsg.downloadUnsuccessfully', { ns: 'common' })) return } diff --git a/web/app/components/datasets/documents/components/rename-modal.tsx b/web/app/components/datasets/documents/components/rename-modal.tsx index a119a2da9e..364aaf48e6 100644 --- a/web/app/components/datasets/documents/components/rename-modal.tsx +++ b/web/app/components/datasets/documents/components/rename-modal.tsx @@ -7,7 +7,7 @@ import { useTranslation } from 'react-i18next' import Button from '@/app/components/base/button' import Input from '@/app/components/base/input' import Modal from '@/app/components/base/modal' -import Toast from '@/app/components/base/toast' +import { toast } from '@/app/components/base/ui/toast' import { renameDocumentName } from '@/service/datasets' type Props = { @@ -41,13 +41,13 @@ const RenameModal: FC = ({ documentId, name: newName, }) - Toast.notify({ type: 'success', message: t('actionMsg.modifiedSuccessfully', { ns: 'common' }) }) + toast.success(t('actionMsg.modifiedSuccessfully', { ns: 'common' })) onSaved() onClose() } catch (error) { if (error) - Toast.notify({ type: 'error', message: error.toString() }) + toast.error(error.toString()) } finally { setSaveLoadingFalse() diff --git a/web/app/components/datasets/list/dataset-card/hooks/__tests__/use-dataset-card-state.spec.ts b/web/app/components/datasets/list/dataset-card/hooks/__tests__/use-dataset-card-state.spec.ts index 63ac30630e..f29d85b460 100644 --- a/web/app/components/datasets/list/dataset-card/hooks/__tests__/use-dataset-card-state.spec.ts +++ b/web/app/components/datasets/list/dataset-card/hooks/__tests__/use-dataset-card-state.spec.ts @@ -5,9 +5,15 @@ import { IndexingType } from '@/app/components/datasets/create/step-two' import { ChunkingMode, DatasetPermission, DataSourceType } from '@/models/datasets' import { useDatasetCardState } from '../use-dataset-card-state' -vi.mock('@/app/components/base/toast', () => ({ - default: { - notify: vi.fn(), +const { mockToastSuccess, mockToastError } = vi.hoisted(() => ({ + mockToastSuccess: vi.fn(), + mockToastError: vi.fn(), +})) + +vi.mock('@/app/components/base/ui/toast', () => ({ + toast: { + success: mockToastSuccess, + error: mockToastError, }, })) @@ -299,7 +305,7 @@ describe('useDatasetCardState', () => { describe('Error Handling', () => { it('should show error toast when export pipeline fails', async () => { - const Toast = await import('@/app/components/base/toast') + const { toast } = await import('@/app/components/base/ui/toast') mockExportPipeline.mockRejectedValue(new Error('Export failed')) const dataset = createMockDataset({ pipeline_id: 'pipeline-1' }) @@ -311,14 +317,11 @@ describe('useDatasetCardState', () => { await result.current.handleExportPipeline() }) - expect(Toast.default.notify).toHaveBeenCalledWith({ - type: 'error', - message: expect.any(String), - }) + expect(toast.error).toHaveBeenCalledWith(expect.any(String)) }) it('should handle Response error in detectIsUsedByApp', async () => { - const Toast = await import('@/app/components/base/toast') + const { toast } = await import('@/app/components/base/ui/toast') const mockResponse = new Response(JSON.stringify({ message: 'API Error' }), { status: 400, }) @@ -333,14 +336,11 @@ describe('useDatasetCardState', () => { await result.current.detectIsUsedByApp() }) - expect(Toast.default.notify).toHaveBeenCalledWith({ - type: 'error', - message: expect.stringContaining('API Error'), - }) + expect(toast.error).toHaveBeenCalledWith(expect.stringContaining('API Error')) }) it('should handle generic Error in detectIsUsedByApp', async () => { - const Toast = await import('@/app/components/base/toast') + const { toast } = await import('@/app/components/base/ui/toast') mockCheckUsage.mockRejectedValue(new Error('Network error')) const dataset = createMockDataset() @@ -352,14 +352,11 @@ describe('useDatasetCardState', () => { await result.current.detectIsUsedByApp() }) - expect(Toast.default.notify).toHaveBeenCalledWith({ - type: 'error', - message: 'Network error', - }) + expect(toast.error).toHaveBeenCalledWith('Network error') }) it('should handle error without message in detectIsUsedByApp', async () => { - const Toast = await import('@/app/components/base/toast') + const { toast } = await import('@/app/components/base/ui/toast') mockCheckUsage.mockRejectedValue({}) const dataset = createMockDataset() @@ -371,10 +368,7 @@ describe('useDatasetCardState', () => { await result.current.detectIsUsedByApp() }) - expect(Toast.default.notify).toHaveBeenCalledWith({ - type: 'error', - message: 'Unknown error', - }) + expect(toast.error).toHaveBeenCalledWith('dataset.unknownError') }) it('should handle exporting state correctly', async () => { diff --git a/web/app/components/datasets/list/dataset-card/hooks/use-dataset-card-state.ts b/web/app/components/datasets/list/dataset-card/hooks/use-dataset-card-state.ts index 4bd8357f1c..850eee4364 100644 --- a/web/app/components/datasets/list/dataset-card/hooks/use-dataset-card-state.ts +++ b/web/app/components/datasets/list/dataset-card/hooks/use-dataset-card-state.ts @@ -2,7 +2,7 @@ import type { Tag } from '@/app/components/base/tag-management/constant' import type { DataSet } from '@/models/datasets' import { useCallback, 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 { useCheckDatasetUsage, useDeleteDataset } from '@/service/use-dataset-card' import { useExportPipelineDSL } from '@/service/use-pipeline' import { downloadBlob } from '@/utils/download' @@ -70,7 +70,7 @@ export const useDatasetCardState = ({ dataset, onSuccess }: UseDatasetCardStateO downloadBlob({ data: file, fileName: `${name}.pipeline` }) } catch { - Toast.notify({ type: 'error', message: t('exportFailed', { ns: 'app' }) }) + toast.error(t('exportFailed', { ns: 'app' })) } finally { setExporting(false) @@ -93,10 +93,10 @@ export const useDatasetCardState = ({ dataset, onSuccess }: UseDatasetCardStateO catch (e: unknown) { if (e instanceof Response) { const res = await e.json() - Toast.notify({ type: 'error', message: res?.message || 'Unknown error' }) + toast.error(res?.message || t('unknownError', { ns: 'dataset' })) } else { - Toast.notify({ type: 'error', message: (e as Error)?.message || 'Unknown error' }) + toast.error((e as Error)?.message || t('unknownError', { ns: 'dataset' })) } } }, [dataset.id, checkUsage, t]) @@ -104,7 +104,7 @@ export const useDatasetCardState = ({ dataset, onSuccess }: UseDatasetCardStateO const onConfirmDelete = useCallback(async () => { try { await deleteDatasetMutation(dataset.id) - Toast.notify({ type: 'success', message: t('datasetDeleted', { ns: 'dataset' }) }) + toast.success(t('datasetDeleted', { ns: 'dataset' })) onSuccess?.() } finally { diff --git a/web/app/components/datasets/settings/form/__tests__/index.spec.tsx b/web/app/components/datasets/settings/form/__tests__/index.spec.tsx index 9eeb62b8e8..a3a22e000b 100644 --- a/web/app/components/datasets/settings/form/__tests__/index.spec.tsx +++ b/web/app/components/datasets/settings/form/__tests__/index.spec.tsx @@ -6,6 +6,10 @@ import { RETRIEVE_METHOD } from '@/types/app' import { IndexingType } from '../../../create/step-two' import Form from '../index' +const { mockToastError } = vi.hoisted(() => ({ + mockToastError: vi.fn(), +})) + // Mock contexts const mockMutateDatasets = vi.fn() const mockInvalidDatasetList = vi.fn() @@ -189,9 +193,10 @@ vi.mock('@/app/components/datasets/common/check-rerank-model', () => ({ isReRankModelSelected: () => true, })) -vi.mock('@/app/components/base/toast', () => ({ - default: { - notify: vi.fn(), +vi.mock('@/app/components/base/ui/toast', () => ({ + toast: { + error: mockToastError, + success: vi.fn(), }, })) @@ -391,7 +396,7 @@ describe('Form', () => { }) it('should show error when trying to save with empty name', async () => { - const Toast = await import('@/app/components/base/toast') + const { toast } = await import('@/app/components/base/ui/toast') render(
) // Clear the name @@ -403,10 +408,7 @@ describe('Form', () => { fireEvent.click(saveButton) await waitFor(() => { - expect(Toast.default.notify).toHaveBeenCalledWith({ - type: 'error', - message: expect.any(String), - }) + expect(toast.error).toHaveBeenCalledWith(expect.any(String)) }) }) diff --git a/web/app/components/datasets/settings/form/hooks/__tests__/use-form-state.spec.ts b/web/app/components/datasets/settings/form/hooks/__tests__/use-form-state.spec.ts index f27b542b1e..00462619aa 100644 --- a/web/app/components/datasets/settings/form/hooks/__tests__/use-form-state.spec.ts +++ b/web/app/components/datasets/settings/form/hooks/__tests__/use-form-state.spec.ts @@ -6,6 +6,11 @@ import { RETRIEVE_METHOD } from '@/types/app' import { IndexingType } from '../../../../create/step-two' import { useFormState } from '../use-form-state' +const { mockToastSuccess, mockToastError } = vi.hoisted(() => ({ + mockToastSuccess: vi.fn(), + mockToastError: vi.fn(), +})) + // Mock contexts const mockMutateDatasets = vi.fn() const mockInvalidDatasetList = vi.fn() @@ -122,9 +127,10 @@ vi.mock('@/app/components/datasets/common/check-rerank-model', () => ({ isReRankModelSelected: () => true, })) -vi.mock('@/app/components/base/toast', () => ({ - default: { - notify: vi.fn(), +vi.mock('@/app/components/base/ui/toast', () => ({ + toast: { + success: mockToastSuccess, + error: mockToastError, }, })) @@ -423,7 +429,7 @@ describe('useFormState', () => { describe('handleSave', () => { it('should show error toast when name is empty', async () => { - const Toast = await import('@/app/components/base/toast') + const { toast } = await import('@/app/components/base/ui/toast') const { result } = renderHook(() => useFormState()) act(() => { @@ -434,14 +440,11 @@ describe('useFormState', () => { await result.current.handleSave() }) - expect(Toast.default.notify).toHaveBeenCalledWith({ - type: 'error', - message: expect.any(String), - }) + expect(toast.error).toHaveBeenCalledWith(expect.any(String)) }) it('should show error toast when name is whitespace only', async () => { - const Toast = await import('@/app/components/base/toast') + const { toast } = await import('@/app/components/base/ui/toast') const { result } = renderHook(() => useFormState()) act(() => { @@ -452,10 +455,7 @@ describe('useFormState', () => { await result.current.handleSave() }) - expect(Toast.default.notify).toHaveBeenCalledWith({ - type: 'error', - message: expect.any(String), - }) + expect(toast.error).toHaveBeenCalledWith(expect.any(String)) }) it('should call updateDatasetSetting with correct params', async () => { @@ -477,7 +477,7 @@ describe('useFormState', () => { }) it('should show success toast on successful save', async () => { - const Toast = await import('@/app/components/base/toast') + const { toast } = await import('@/app/components/base/ui/toast') const { result } = renderHook(() => useFormState()) await act(async () => { @@ -485,10 +485,7 @@ describe('useFormState', () => { }) await waitFor(() => { - expect(Toast.default.notify).toHaveBeenCalledWith({ - type: 'success', - message: expect.any(String), - }) + expect(toast.success).toHaveBeenCalledWith(expect.any(String)) }) }) @@ -553,7 +550,7 @@ describe('useFormState', () => { it('should show error toast on save failure', async () => { const { updateDatasetSetting } = await import('@/service/datasets') - const Toast = await import('@/app/components/base/toast') + const { toast } = await import('@/app/components/base/ui/toast') vi.mocked(updateDatasetSetting).mockRejectedValueOnce(new Error('Network error')) const { result } = renderHook(() => useFormState()) @@ -562,10 +559,7 @@ describe('useFormState', () => { await result.current.handleSave() }) - expect(Toast.default.notify).toHaveBeenCalledWith({ - type: 'error', - message: expect.any(String), - }) + expect(toast.error).toHaveBeenCalledWith(expect.any(String)) }) it('should include partial_member_list when permission is partialMembers', async () => { diff --git a/web/app/components/datasets/settings/form/hooks/use-form-state.ts b/web/app/components/datasets/settings/form/hooks/use-form-state.ts index 614995d43a..d00534f7f4 100644 --- a/web/app/components/datasets/settings/form/hooks/use-form-state.ts +++ b/web/app/components/datasets/settings/form/hooks/use-form-state.ts @@ -6,7 +6,7 @@ import type { IconInfo, SummaryIndexSetting as SummaryIndexSettingType } from '@ import type { RetrievalConfig } from '@/types/app' import { useCallback, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' -import Toast from '@/app/components/base/toast' +import { toast } from '@/app/components/base/ui/toast' import { isReRankModelSelected } from '@/app/components/datasets/common/check-rerank-model' import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import { useModelList } from '@/app/components/header/account-setting/model-provider-page/hooks' @@ -123,12 +123,12 @@ export const useFormState = () => { return if (!name?.trim()) { - Toast.notify({ type: 'error', message: t('form.nameError', { ns: 'datasetSettings' }) }) + toast.error(t('form.nameError', { ns: 'datasetSettings' })) return } if (!isReRankModelSelected({ rerankModelList, retrievalConfig, indexMethod })) { - Toast.notify({ type: 'error', message: t('datasetConfig.rerankModelRequired', { ns: 'appDebug' }) }) + toast.error(t('datasetConfig.rerankModelRequired', { ns: 'appDebug' })) return } @@ -176,7 +176,7 @@ export const useFormState = () => { } await updateDatasetSetting({ datasetId: currentDataset!.id, body }) - Toast.notify({ type: 'success', message: t('actionMsg.modifiedSuccessfully', { ns: 'common' }) }) + toast.success(t('actionMsg.modifiedSuccessfully', { ns: 'common' })) if (mutateDatasets) { await mutateDatasets() @@ -184,7 +184,7 @@ export const useFormState = () => { } } catch { - Toast.notify({ type: 'error', message: t('actionMsg.modifiedUnsuccessfully', { ns: 'common' }) }) + toast.error(t('actionMsg.modifiedUnsuccessfully', { ns: 'common' })) } finally { setLoading(false) diff --git a/web/eslint-suppressions.json b/web/eslint-suppressions.json index 36c77e051c..d0aa842e11 100644 --- a/web/eslint-suppressions.json +++ b/web/eslint-suppressions.json @@ -959,7 +959,7 @@ }, "app/components/app/configuration/dataset-config/params-config/index.tsx": { "no-restricted-imports": { - "count": 2 + "count": 1 }, "react/set-state-in-effect": { "count": 1 @@ -3166,11 +3166,6 @@ "count": 2 } }, - "app/components/datasets/common/document-status-with-action/auto-disabled-document.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, "app/components/datasets/common/image-list/more.tsx": { "tailwindcss/enforce-consistent-class-order": { "count": 1 @@ -3190,9 +3185,6 @@ } }, "app/components/datasets/common/image-uploader/hooks/use-upload.ts": { - "no-restricted-imports": { - "count": 1 - }, "ts/no-explicit-any": { "count": 3 } @@ -3222,7 +3214,7 @@ }, "app/components/datasets/common/retrieval-param-config/index.tsx": { "no-restricted-imports": { - "count": 2 + "count": 1 } }, "app/components/datasets/create-from-pipeline/create-options/create-from-dsl-modal/dsl-confirm-modal.tsx": { @@ -3573,11 +3565,6 @@ "count": 1 } }, - "app/components/datasets/documents/components/document-list/hooks/use-document-actions.ts": { - "no-restricted-imports": { - "count": 1 - } - }, "app/components/datasets/documents/components/documents-header.tsx": { "no-restricted-imports": { "count": 1 @@ -3590,7 +3577,7 @@ }, "app/components/datasets/documents/components/rename-modal.tsx": { "no-restricted-imports": { - "count": 2 + "count": 1 } }, "app/components/datasets/documents/create-from-pipeline/actions/index.tsx": { @@ -4258,9 +4245,6 @@ } }, "app/components/datasets/list/dataset-card/hooks/use-dataset-card-state.ts": { - "no-restricted-imports": { - "count": 1 - }, "react/set-state-in-effect": { "count": 1 } @@ -4430,11 +4414,6 @@ "count": 7 } }, - "app/components/datasets/settings/form/hooks/use-form-state.ts": { - "no-restricted-imports": { - "count": 1 - } - }, "app/components/datasets/settings/index-method/index.tsx": { "no-restricted-imports": { "count": 1 diff --git a/web/i18n/en-US/dataset.json b/web/i18n/en-US/dataset.json index 72d0a7b909..c6b15fbe9b 100644 --- a/web/i18n/en-US/dataset.json +++ b/web/i18n/en-US/dataset.json @@ -176,6 +176,7 @@ "serviceApi.enabled": "Enabled", "serviceApi.title": "Service API", "unavailable": "Unavailable", + "unknownError": "Unknown error", "updated": "Updated", "weightedScore.customized": "Customized", "weightedScore.description": "By adjusting the weights assigned, this rerank strategy determines whether to prioritize semantic or keyword matching.",