diff --git a/web/app/components/datasets/list/__tests__/index.spec.tsx b/web/app/components/datasets/list/__tests__/index.spec.tsx index 898b1458b2..579180085c 100644 --- a/web/app/components/datasets/list/__tests__/index.spec.tsx +++ b/web/app/components/datasets/list/__tests__/index.spec.tsx @@ -59,7 +59,7 @@ vi.mock('@/hooks/use-knowledge', () => ({ vi.mock('@/service/knowledge/use-dataset', () => ({ useDatasetList: vi.fn(() => ({ - data: { pages: [{ data: [] }] }, + data: { pages: [{ data: [], total: 1 }] }, fetchNextPage: vi.fn(), hasNextPage: false, isFetching: false, @@ -135,10 +135,18 @@ vi.mock('@/app/components/datasets/create/website/base/checkbox-with-label', () })) describe('List', () => { - beforeEach(() => { + beforeEach(async () => { vi.clearAllMocks() mockBrandingEnabled = false mockSearchParams = new URLSearchParams() + const { useDatasetList } = await import('@/service/knowledge/use-dataset') + vi.mocked(useDatasetList).mockReturnValue({ + data: { pages: [{ data: [], total: 1 }] }, + fetchNextPage: vi.fn(), + hasNextPage: false, + isFetching: false, + isFetchingNextPage: false, + } as unknown as ReturnType) }) describe('Rendering', () => { @@ -190,19 +198,34 @@ describe('List', () => { }) describe('Props', () => { - it('should pass includeAll prop to Datasets', () => { + it('should query datasets with includeAll disabled initially', async () => { + const { useDatasetList } = await import('@/service/knowledge/use-dataset') + render() - expect(screen.getByTestId('include-all')).toHaveTextContent('false') + + expect(useDatasetList).toHaveBeenCalledWith(expect.objectContaining({ + include_all: false, + })) }) - it('should pass empty keywords initially', () => { + it('should query datasets with empty keywords initially', async () => { + const { useDatasetList } = await import('@/service/knowledge/use-dataset') + render() - expect(screen.getByTestId('keywords')).toHaveTextContent('') + + expect(useDatasetList).toHaveBeenCalledWith(expect.objectContaining({ + keyword: '', + })) }) - it('should pass empty tags initially', () => { + it('should query datasets with empty tags initially', async () => { + const { useDatasetList } = await import('@/service/knowledge/use-dataset') + render() - expect(screen.getByTestId('tags')).toHaveTextContent('') + + expect(useDatasetList).toHaveBeenCalledWith(expect.objectContaining({ + tag_ids: [], + })) }) }) @@ -256,6 +279,57 @@ describe('List', () => { // Should render without errors even with empty data expect(screen.getByTestId('datasets-component')).toBeInTheDocument() }) + + it('should render first empty state when there are no datasets and no active filters', async () => { + const { useDatasetList } = await import('@/service/knowledge/use-dataset') + vi.mocked(useDatasetList).mockReturnValue({ + data: { pages: [{ data: [], total: 0 }] }, + fetchNextPage: vi.fn(), + hasNextPage: false, + isFetching: false, + isFetchingNextPage: false, + } as unknown as ReturnType) + + render() + + expect(screen.getByText('dataset.firstEmpty.title')).toBeInTheDocument() + expect(screen.queryByTestId('datasets-component')).not.toBeInTheDocument() + expect(screen.queryByTestId('dataset-footer')).not.toBeInTheDocument() + }) + + it('should not render first empty state before the first dataset page resolves', async () => { + const { useDatasetList } = await import('@/service/knowledge/use-dataset') + vi.mocked(useDatasetList).mockReturnValue({ + data: { pages: [] }, + fetchNextPage: vi.fn(), + hasNextPage: false, + isFetching: false, + isFetchingNextPage: false, + } as unknown as ReturnType) + + render() + + expect(screen.queryByText('dataset.firstEmpty.title')).not.toBeInTheDocument() + expect(screen.getByTestId('datasets-component')).toBeInTheDocument() + }) + + it('should keep the regular list for empty filtered results', async () => { + const { useDatasetList } = await import('@/service/knowledge/use-dataset') + vi.mocked(useDatasetList).mockImplementation(params => ({ + data: { pages: [{ data: [], total: params.include_all ? 0 : 1 }] }, + fetchNextPage: vi.fn(), + hasNextPage: false, + isFetching: false, + isFetchingNextPage: false, + } as unknown as ReturnType)) + + render() + + fireEvent.click(screen.getByTestId('include-all-checkbox')) + + expect(screen.getByTestId('datasets-component')).toBeInTheDocument() + expect(screen.queryByText('dataset.firstEmpty.title')).not.toBeInTheDocument() + }) }) describe('Branch Coverage', () => { diff --git a/web/app/components/datasets/list/index.tsx b/web/app/components/datasets/list/index.tsx index fa7afd2d06..56e4c99386 100644 --- a/web/app/components/datasets/list/index.tsx +++ b/web/app/components/datasets/list/index.tsx @@ -5,7 +5,7 @@ import { useSuspenseQuery } from '@tanstack/react-query' import { useBoolean, useDebounceFn } from 'ahooks' // Libraries -import { useState } from 'react' +import { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import Input from '@/app/components/base/input' import CheckboxWithLabel from '@/app/components/datasets/create/website/base/checkbox-with-label' @@ -15,7 +15,7 @@ import { TagFilter } from '@/features/tag-management/components/tag-filter' import { TagManagementModal } from '@/features/tag-management/components/tag-management-modal' import useDocumentTitle from '@/hooks/use-document-title' import { useSearchParams } from '@/next/navigation' -import { useDatasetApiBaseUrl, useInvalidDatasetList } from '@/service/knowledge/use-dataset' +import { useDatasetApiBaseUrl, useDatasetList, useInvalidDatasetList } from '@/service/knowledge/use-dataset' import { systemFeaturesQueryOptions } from '@/service/system-features' // Components import ExternalAPIPanel from '../external-api/external-api-panel' @@ -55,8 +55,21 @@ const List = () => { } const isCurrentWorkspaceManager = useAppContextSelector(state => state.isCurrentWorkspaceManager) + const isCurrentWorkspaceEditor = useAppContextSelector(state => state.isCurrentWorkspaceEditor) const { data: apiBaseInfo } = useDatasetApiBaseUrl() - const showEmptyDataList = searchParams.get('emptyDataList') === 'true' + const emptyDataList = searchParams.get('emptyDataList') === 'true' + const datasetListQuery = useDatasetList({ + initialPage: 1, + tag_ids: tagIDs, + limit: 30, + include_all: includeAll, + keyword: searchKeywords, + }) + const pages = useMemo(() => emptyDataList ? [{ data: [], total: 0 }] : datasetListQuery.data?.pages ?? [], [datasetListQuery.data?.pages, emptyDataList]) + const hasResolvedFirstPage = pages.length > 0 + const hasAnyDataset = (pages[0]?.total ?? 0) > 0 + const hasActiveFilters = tagIDs.length > 0 || keywords.trim().length > 0 || searchKeywords.trim().length > 0 || includeAll + const showEmptyDataList = !hasAnyDataset && isCurrentWorkspaceEditor && (emptyDataList || (hasResolvedFirstPage && !hasActiveFilters)) return (