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 dd8f00930ee..e6414b4b5b1 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 @@ -1,341 +1,84 @@ +import type { ReactNode } from 'react' import type { DataSet } from '@/models/datasets' -import { act, renderHook, waitFor } from '@testing-library/react' -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { IndexingType } from '@/app/components/datasets/create/step-two' -import { ChunkingMode, DatasetPermission, DataSourceType } from '@/models/datasets' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { act, renderHook } from '@testing-library/react' +import { createElement } from 'react' +import { describe, expect, it, vi } from 'vitest' import { useDatasetCardState } from '../use-dataset-card-state' -const { mockToastSuccess, mockToastError } = vi.hoisted(() => ({ - mockToastSuccess: vi.fn(), - mockToastError: vi.fn(), +const mocks = vi.hoisted(() => ({ + request: vi.fn(), })) -vi.mock('@langgenius/dify-ui/toast', () => ({ - toast: { - success: mockToastSuccess, - error: mockToastError, - }, -})) - -const mockCheckUsage = vi.fn() -const mockDeleteDataset = vi.fn() -const mockExportPipeline = vi.fn() -const mockPush = vi.fn() - vi.mock('@/next/navigation', () => ({ - useRouter: () => ({ - push: mockPush, - }), + useRouter: () => ({ push: vi.fn() }), })) -vi.mock('@/service/use-dataset-card', () => ({ - useCheckDatasetUsage: () => ({ mutateAsync: mockCheckUsage }), - useDeleteDataset: () => ({ mutateAsync: mockDeleteDataset }), +vi.mock('@/service/base', () => ({ + request: (...args: unknown[]) => mocks.request(...args), + sseGeneratorPost: vi.fn(), })) vi.mock('@/service/use-pipeline', () => ({ - useExportPipelineDSL: () => ({ mutateAsync: mockExportPipeline }), + useExportPipelineDSL: () => ({ mutateAsync: vi.fn() }), })) +const dataset = { + id: 'dataset-1', + name: 'Test Dataset', +} as DataSet + +function renderDatasetCardState() { + const onSuccess = vi.fn() + const queryClient = new QueryClient({ + defaultOptions: { + mutations: { retry: false }, + queries: { + retry: false, + staleTime: 5 * 60 * 1000, + }, + }, + }) + const wrapper = ({ children }: { children: ReactNode }) => + createElement(QueryClientProvider, { client: queryClient }, children) + const rendered = renderHook(useDatasetCardState, { + initialProps: { dataset, onSuccess }, + wrapper, + }) + + return { ...rendered, onSuccess } +} + describe('useDatasetCardState', () => { - const createMockDataset = (overrides: Partial = {}): DataSet => - ({ - id: 'dataset-1', - name: 'Test Dataset', - description: 'Test description', - provider: 'vendor', - permission: DatasetPermission.allTeamMembers, - data_source_type: DataSourceType.FILE, - indexing_technique: IndexingType.QUALIFIED, - embedding_available: true, - app_count: 5, - document_count: 10, - word_count: 1000, - created_at: 1609459200, - updated_at: 1609545600, - tags: [{ id: 'tag-1', name: 'Tag 1', type: 'knowledge', binding_count: '' }], - embedding_model: 'text-embedding-ada-002', - embedding_model_provider: 'openai', - created_by: 'user-1', - doc_form: ChunkingMode.text, - pipeline_id: 'pipeline-1', - ...overrides, - }) as DataSet - - beforeEach(() => { - vi.clearAllMocks() - mockCheckUsage.mockResolvedValue({ is_using: false }) - mockDeleteDataset.mockResolvedValue({}) - mockExportPipeline.mockResolvedValue({ data: 'yaml content' }) - }) - - afterEach(() => { - vi.restoreAllMocks() - }) - - describe('Initial State', () => { - it('should have initial modal state closed', () => { - const dataset = createMockDataset() - const { result } = renderHook(() => useDatasetCardState({ dataset, onSuccess: vi.fn() })) - - expect(result.current.modalState.showRenameModal).toBe(false) - expect(result.current.modalState.showConfirmDelete).toBe(false) - expect(result.current.modalState.confirmMessage).toBe('') - }) - - it('should not be exporting initially', () => { - const dataset = createMockDataset() - const { result } = renderHook(() => useDatasetCardState({ dataset, onSuccess: vi.fn() })) - - expect(result.current.exporting).toBe(false) - }) - }) - - describe('Modal Handlers', () => { - it('should open rename modal when openRenameModal is called', () => { - const dataset = createMockDataset() - const { result } = renderHook(() => useDatasetCardState({ dataset, onSuccess: vi.fn() })) - - act(() => { - result.current.openRenameModal() - }) - - expect(result.current.modalState.showRenameModal).toBe(true) - }) - - it('should close rename modal when closeRenameModal is called', () => { - const dataset = createMockDataset() - const { result } = renderHook(() => useDatasetCardState({ dataset, onSuccess: vi.fn() })) - - act(() => { - result.current.openRenameModal() - }) - - act(() => { - result.current.closeRenameModal() - }) - - expect(result.current.modalState.showRenameModal).toBe(false) - }) - - it('should close confirm delete modal when closeConfirmDelete is called', async () => { - const dataset = createMockDataset() - const { result } = renderHook(() => useDatasetCardState({ dataset, onSuccess: vi.fn() })) - - // First trigger show confirm delete - act(() => { - result.current.detectIsUsedByApp() - }) - - await waitFor(() => { - expect(result.current.modalState.showConfirmDelete).toBe(true) - }) - - act(() => { - result.current.closeConfirmDelete() - }) - - expect(result.current.modalState.showConfirmDelete).toBe(false) - }) - }) - - describe('detectIsUsedByApp', () => { - it('should check usage and show confirm modal with not-in-use message', async () => { - mockCheckUsage.mockResolvedValue({ is_using: false }) - const dataset = createMockDataset() - const { result } = renderHook(() => useDatasetCardState({ dataset, onSuccess: vi.fn() })) - - await act(async () => { - await result.current.detectIsUsedByApp() - }) - - expect(mockCheckUsage).toHaveBeenCalledWith('dataset-1') - expect(result.current.modalState.showConfirmDelete).toBe(true) - expect(result.current.modalState.confirmMessage).toContain('deleteDatasetConfirmContent') - }) - - it('should show in-use message when dataset is used by app', async () => { - mockCheckUsage.mockResolvedValue({ is_using: true }) - const dataset = createMockDataset() - const { result } = renderHook(() => useDatasetCardState({ dataset, onSuccess: vi.fn() })) - - await act(async () => { - await result.current.detectIsUsedByApp() - }) - - expect(result.current.modalState.confirmMessage).toContain('datasetUsedByApp') - }) - }) - - describe('onConfirmDelete', () => { - it('should delete dataset and call onSuccess', async () => { - const onSuccess = vi.fn() - const dataset = createMockDataset() - const { result } = renderHook(() => useDatasetCardState({ dataset, onSuccess })) - - await act(async () => { - await result.current.onConfirmDelete() - }) - - expect(mockDeleteDataset).toHaveBeenCalledWith('dataset-1') - expect(onSuccess).toHaveBeenCalled() - }) - - it('should close confirm modal after delete', async () => { - const dataset = createMockDataset() - const { result } = renderHook(() => useDatasetCardState({ dataset, onSuccess: vi.fn() })) - - // First open confirm modal - await act(async () => { - await result.current.detectIsUsedByApp() - }) - - await act(async () => { - await result.current.onConfirmDelete() - }) - - expect(result.current.modalState.showConfirmDelete).toBe(false) - }) - }) - - describe('handleExportPipeline', () => { - it('should not export if pipeline_id is missing', async () => { - const dataset = createMockDataset({ pipeline_id: undefined }) - const { result } = renderHook(() => useDatasetCardState({ dataset, onSuccess: vi.fn() })) - - await act(async () => { - await result.current.handleExportPipeline() - }) - - expect(mockExportPipeline).not.toHaveBeenCalled() - }) - - it('should export pipeline with correct parameters', async () => { - const dataset = createMockDataset({ pipeline_id: 'pipeline-1', name: 'Test Pipeline' }) - const { result } = renderHook(() => useDatasetCardState({ dataset, onSuccess: vi.fn() })) - - await act(async () => { - await result.current.handleExportPipeline(true) - }) - - expect(mockExportPipeline).toHaveBeenCalledWith({ - pipelineId: 'pipeline-1', - include: true, - }) - }) - }) - - describe('Edge Cases', () => { - it('should handle undefined onSuccess', async () => { - const dataset = createMockDataset() - const { result } = renderHook(() => useDatasetCardState({ dataset })) - - // Should not throw when onSuccess is undefined - await act(async () => { - await result.current.onConfirmDelete() - }) - - expect(mockDeleteDataset).toHaveBeenCalled() - }) - }) - - describe('Error Handling', () => { - it('should show error toast when export pipeline fails', async () => { - const { toast } = await import('@langgenius/dify-ui/toast') - mockExportPipeline.mockRejectedValue(new Error('Export failed')) - - const dataset = createMockDataset({ pipeline_id: 'pipeline-1' }) - const { result } = renderHook(() => useDatasetCardState({ dataset, onSuccess: vi.fn() })) - - await act(async () => { - await result.current.handleExportPipeline() - }) - - expect(toast.error).toHaveBeenCalledWith(expect.any(String)) - }) - - it('should handle Response error in detectIsUsedByApp', async () => { - const { toast } = await import('@langgenius/dify-ui/toast') - const mockResponse = new Response(JSON.stringify({ message: 'API Error' }), { - status: 400, - }) - mockCheckUsage.mockRejectedValue(mockResponse) - - const dataset = createMockDataset() - const { result } = renderHook(() => useDatasetCardState({ dataset, onSuccess: vi.fn() })) - - await act(async () => { - await result.current.detectIsUsedByApp() - }) - - expect(toast.error).toHaveBeenCalledWith(expect.stringContaining('API Error')) - }) - - it('should handle generic Error in detectIsUsedByApp', async () => { - const { toast } = await import('@langgenius/dify-ui/toast') - mockCheckUsage.mockRejectedValue(new Error('Network error')) - - const dataset = createMockDataset() - const { result } = renderHook(() => useDatasetCardState({ dataset, onSuccess: vi.fn() })) - - await act(async () => { - await result.current.detectIsUsedByApp() - }) - - expect(toast.error).toHaveBeenCalledWith('Network error') - }) - - it('should handle error without message in detectIsUsedByApp', async () => { - const { toast } = await import('@langgenius/dify-ui/toast') - mockCheckUsage.mockRejectedValue({}) - - const dataset = createMockDataset() - const { result } = renderHook(() => useDatasetCardState({ dataset, onSuccess: vi.fn() })) - - await act(async () => { - await result.current.detectIsUsedByApp() - }) - - expect(toast.error).toHaveBeenCalledWith('dataset.unknownError') - }) - - it('should handle exporting state correctly', async () => { - const dataset = createMockDataset({ pipeline_id: 'pipeline-1' }) - const { result } = renderHook(() => useDatasetCardState({ dataset, onSuccess: vi.fn() })) - - // Exporting should initially be false - expect(result.current.exporting).toBe(false) - - // Export should work when not exporting - await act(async () => { - await result.current.handleExportPipeline() - }) - - expect(mockExportPipeline).toHaveBeenCalled() - }) - - it('should reset exporting state after export completes', async () => { - const dataset = createMockDataset({ pipeline_id: 'pipeline-1' }) - const { result } = renderHook(() => useDatasetCardState({ dataset, onSuccess: vi.fn() })) - - await act(async () => { - await result.current.handleExportPipeline() - }) - - expect(result.current.exporting).toBe(false) - }) - - it('should reset exporting state even when export fails', async () => { - mockExportPipeline.mockRejectedValue(new Error('Export failed')) - - const dataset = createMockDataset({ pipeline_id: 'pipeline-1' }) - const { result } = renderHook(() => useDatasetCardState({ dataset, onSuccess: vi.fn() })) - - await act(async () => { - await result.current.handleExportPipeline() - }) - - expect(result.current.exporting).toBe(false) - }) + it('uses the latest usage state before deleting the dataset', async () => { + mocks.request + .mockResolvedValueOnce(Response.json({ is_using: false })) + .mockResolvedValueOnce(Response.json({ is_using: true })) + .mockResolvedValueOnce(new Response(null, { status: 204 })) + const { onSuccess, result } = renderDatasetCardState() + + await act(result.current.detectIsUsedByApp) + expect(result.current.modalState.confirmMessage).toContain('deleteDatasetConfirmContent') + + act(result.current.closeConfirmDelete) + await act(result.current.detectIsUsedByApp) + + expect(result.current.modalState.confirmMessage).toContain('datasetUsedByApp') + expect(result.current.modalState.showConfirmDelete).toBe(true) + expect(mocks.request).toHaveBeenCalledTimes(2) + expect( + mocks.request.mock.calls.every(([url]) => + String(url).endsWith('/datasets/dataset-1/use-check'), + ), + ).toBe(true) + + await act(result.current.onConfirmDelete) + + expect(mocks.request).toHaveBeenCalledTimes(3) + const deleteRequest = mocks.request.mock.calls[2]?.[2]?.request as Request + expect(deleteRequest.method).toBe('DELETE') + expect(deleteRequest.url).toContain('/datasets/dataset-1') + expect(result.current.modalState.showConfirmDelete).toBe(false) + expect(onSuccess).toHaveBeenCalledOnce() }) }) 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 0f500fbbafd..86de9f9a77e 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 @@ -1,9 +1,10 @@ import type { DataSet } from '@/models/datasets' import { toast } from '@langgenius/dify-ui/toast' +import { useMutation, useQueryClient } from '@tanstack/react-query' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' import { useRouter } from '@/next/navigation' -import { useCheckDatasetUsage, useDeleteDataset } from '@/service/use-dataset-card' +import { consoleQuery } from '@/service/client' import { useExportPipelineDSL } from '@/service/use-pipeline' import { downloadBlob } from '@/utils/download' @@ -22,6 +23,7 @@ type UseDatasetCardStateOptions = { export const useDatasetCardState = ({ dataset, onSuccess }: UseDatasetCardStateOptions) => { const { t } = useTranslation() const { push } = useRouter() + const queryClient = useQueryClient() // Modal state const [modalState, setModalState] = useState({ @@ -56,8 +58,9 @@ export const useDatasetCardState = ({ dataset, onSuccess }: UseDatasetCardStateO }, []) // API mutations - const { mutateAsync: checkUsage } = useCheckDatasetUsage() - const { mutateAsync: deleteDatasetMutation } = useDeleteDataset() + const { mutateAsync: deleteDatasetMutation } = useMutation( + consoleQuery.datasets.byDatasetId.delete.mutationOptions(), + ) const { mutateAsync: exportPipelineConfig } = useExportPipelineDSL() // Export pipeline handler @@ -86,7 +89,18 @@ export const useDatasetCardState = ({ dataset, onSuccess }: UseDatasetCardStateO // Delete flow handlers const detectIsUsedByApp = useCallback(async () => { try { - const { is_using: isUsedByApp } = await checkUsage(dataset.id) + const { is_using: isUsedByApp } = await queryClient.fetchQuery( + consoleQuery.datasets.byDatasetId.useCheck.get.queryOptions({ + input: { + params: { + dataset_id: dataset.id, + }, + }, + staleTime: 0, + retry: false, + context: { silent: true }, + }), + ) const message = isUsedByApp ? t(($) => $.datasetUsedByApp, { ns: 'dataset' })! : t(($) => $.deleteDatasetConfirmContent, { ns: 'dataset' })! @@ -103,11 +117,15 @@ export const useDatasetCardState = ({ dataset, onSuccess }: UseDatasetCardStateO toast.error((e as Error)?.message || t(($) => $.unknownError, { ns: 'dataset' })) } } - }, [dataset.id, checkUsage, t]) + }, [dataset.id, queryClient, t]) const onConfirmDelete = useCallback(async () => { try { - await deleteDatasetMutation(dataset.id) + await deleteDatasetMutation({ + params: { + dataset_id: dataset.id, + }, + }) toast.success(t(($) => $.datasetDeleted, { ns: 'dataset' })) onSuccess?.() } finally { diff --git a/web/service/use-dataset-card.ts b/web/service/use-dataset-card.ts deleted file mode 100644 index 05365479dc4..00000000000 --- a/web/service/use-dataset-card.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { useMutation } from '@tanstack/react-query' -import { checkIsUsedInApp, deleteDataset } from './datasets' - -const NAME_SPACE = 'dataset-card' - -export const useCheckDatasetUsage = () => { - return useMutation({ - mutationKey: [NAME_SPACE, 'check-usage'], - mutationFn: (datasetId: string) => checkIsUsedInApp(datasetId), - }) -} - -export const useDeleteDataset = () => { - return useMutation({ - mutationKey: [NAME_SPACE, 'delete'], - mutationFn: (datasetId: string) => deleteDataset(datasetId), - }) -}