refactor(web): use generated dataset card queries (#39203)

This commit is contained in:
yyh 2026-07-17 22:50:40 +08:00 committed by GitHub
parent 5c6372d2f7
commit a7ef41a8f5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 92 additions and 349 deletions

View File

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

View File

@ -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<ModalState>({
@ -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 {

View File

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