diff --git a/src/library-authoring/LibraryAuthoringPage.test.tsx b/src/library-authoring/LibraryAuthoringPage.test.tsx index f6d55792a..836051327 100644 --- a/src/library-authoring/LibraryAuthoringPage.test.tsx +++ b/src/library-authoring/LibraryAuthoringPage.test.tsx @@ -42,40 +42,12 @@ const returnEmptyResult = (_url, req) => { // We have to replace the query (search keywords) in the mock results with the actual query, // because otherwise we may have an inconsistent state that causes more queries and unexpected results. mockEmptyResult.results[0].query = query; - mockEmptyResult.results[2].query = query; // And fake the required '_formatted' fields; it contains the highlighting ... around matched words // eslint-disable-next-line no-underscore-dangle, no-param-reassign mockEmptyResult.results[0]?.hits.forEach((hit) => { hit._formatted = { ...hit }; }); - // eslint-disable-next-line no-underscore-dangle, no-param-reassign - mockEmptyResult.results[2]?.hits.forEach((hit) => { hit._formatted = { ...hit }; }); return mockEmptyResult; }; -/** - * Returns 2 components from the search query. - * This lets us test that the StudioHome "View All" button is hidden when a - * low number of search results are shown (<=4 by default). -*/ -const returnLowNumberResults = (_url, req) => { - const requestData = JSON.parse(req.body?.toString() ?? ''); - const query = requestData?.queries[0]?.q ?? ''; - const newMockResult = { ...mockResult }; - // We have to replace the query (search keywords) in the mock results with the actual query, - // because otherwise we may have an inconsistent state that causes more queries and unexpected results. - newMockResult.results[0].query = query; - // Limit number of results to just 2 - newMockResult.results[0].hits = mockResult.results[0]?.hits.slice(0, 2); - newMockResult.results[2].hits = mockResult.results[2]?.hits.slice(0, 2); - newMockResult.results[0].estimatedTotalHits = 2; - newMockResult.results[2].estimatedTotalHits = 2; - // And fake the required '_formatted' fields; it contains the highlighting ... around matched words - // eslint-disable-next-line no-underscore-dangle, no-param-reassign - newMockResult.results[0]?.hits.forEach((hit) => { hit._formatted = { ...hit }; }); - // eslint-disable-next-line no-underscore-dangle, no-param-reassign - newMockResult.results[2]?.hits.forEach((hit) => { hit._formatted = { ...hit }; }); - return newMockResult; -}; - const path = '/library/:libraryId/*'; const libraryTitle = mockContentLibrary.libraryData.title; @@ -136,35 +108,25 @@ describe('', () => { expect(screen.queryByText('You have not added any content to this library yet.')).not.toBeInTheDocument(); - // "Recently Modified" header + sort shown - expect(screen.getAllByText('Recently Modified').length).toEqual(2); - expect(screen.getByText('Collections (6)')).toBeInTheDocument(); - expect(screen.getByText('Components (10)')).toBeInTheDocument(); + expect(screen.getAllByText('Recently Modified').length).toEqual(1); expect((await screen.findAllByText('Introduction to Testing'))[0]).toBeInTheDocument(); // Navigate to the components tab fireEvent.click(screen.getByRole('tab', { name: 'Components' })); // "Recently Modified" default sort shown expect(screen.getAllByText('Recently Modified').length).toEqual(1); - expect(screen.queryByText('Collections (6)')).not.toBeInTheDocument(); - expect(screen.queryByText('Components (10)')).not.toBeInTheDocument(); // Navigate to the collections tab fireEvent.click(screen.getByRole('tab', { name: 'Collections' })); // "Recently Modified" default sort shown expect(screen.getAllByText('Recently Modified').length).toEqual(1); - expect(screen.queryByText('Collections (6)')).not.toBeInTheDocument(); - expect(screen.queryByText('Components (10)')).not.toBeInTheDocument(); expect(screen.queryByText('There are 10 components in this library')).not.toBeInTheDocument(); expect((await screen.findAllByText('Collection 1'))[0]).toBeInTheDocument(); // Go back to Home tab // This step is necessary to avoid the url change leak to other tests - fireEvent.click(screen.getByRole('tab', { name: 'Home' })); - // "Recently Modified" header + sort shown - expect(screen.getAllByText('Recently Modified').length).toEqual(2); - expect(screen.getByText('Collections (6)')).toBeInTheDocument(); - expect(screen.getByText('Components (10)')).toBeInTheDocument(); + fireEvent.click(screen.getByRole('tab', { name: 'All Content' })); + expect(screen.getAllByText('Recently Modified').length).toEqual(1); }); it('shows a library without components and collections', async () => { @@ -188,7 +150,7 @@ describe('', () => { fireEvent.click(cancelButton); expect(collectionModalHeading).not.toBeInTheDocument(); - fireEvent.click(screen.getByRole('tab', { name: 'Home' })); + fireEvent.click(screen.getByRole('tab', { name: 'All Content' })); expect(screen.getByText('You have not added any content to this library yet.')).toBeInTheDocument(); const addComponentButton = screen.getByRole('button', { name: /add component/i }); @@ -246,7 +208,7 @@ describe('', () => { // Go back to Home tab // This step is necessary to avoid the url change leak to other tests - fireEvent.click(screen.getByRole('tab', { name: 'Home' })); + fireEvent.click(screen.getByRole('tab', { name: 'All Content' })); }); it('should open and close new content sidebar', async () => { @@ -328,68 +290,6 @@ describe('', () => { expect(manageAccess).not.toBeInTheDocument(); }); - it('show the "View All" button when viewing library with many components', async () => { - await renderLibraryPage(); - - expect(screen.getByText('Content library')).toBeInTheDocument(); - expect((await screen.findAllByText(libraryTitle))[0]).toBeInTheDocument(); - - // "Recently Modified" header + sort shown - await waitFor(() => { expect(screen.getAllByText('Recently Modified').length).toEqual(2); }); - expect(screen.getByText('Collections (6)')).toBeInTheDocument(); - expect(screen.getByText('Components (10)')).toBeInTheDocument(); - expect(screen.getAllByText('Introduction to Testing')[0]).toBeInTheDocument(); - expect(screen.queryByText('You have not added any content to this library yet.')).not.toBeInTheDocument(); - - // There should be two "View All" button, since the Components and Collections count - // are above the preview limit (4) - expect(screen.getAllByText('View All').length).toEqual(2); - - // Clicking on first "View All" button should navigate to the Collections tab - fireEvent.click(screen.getAllByText('View All')[0]); - // "Recently Modified" default sort shown - expect(screen.getAllByText('Recently Modified').length).toEqual(1); - expect(screen.queryByText('Collections (6)')).not.toBeInTheDocument(); - expect(screen.queryByText('Components (10)')).not.toBeInTheDocument(); - expect(screen.getByText('Collection 1')).toBeInTheDocument(); - - fireEvent.click(screen.getByRole('tab', { name: 'Home' })); - // Clicking on second "View All" button should navigate to the Components tab - fireEvent.click(screen.getAllByText('View All')[1]); - // "Recently Modified" default sort shown - expect(screen.getAllByText('Recently Modified').length).toEqual(1); - expect(screen.queryByText('Collections (6)')).not.toBeInTheDocument(); - expect(screen.queryByText('Components (10)')).not.toBeInTheDocument(); - expect(screen.getAllByText('Introduction to Testing')[0]).toBeInTheDocument(); - - // Go back to Home tab - // This step is necessary to avoid the url change leak to other tests - fireEvent.click(screen.getByRole('tab', { name: 'Home' })); - // "Recently Modified" header + sort shown - expect(screen.getAllByText('Recently Modified').length).toEqual(2); - expect(screen.getByText('Collections (6)')).toBeInTheDocument(); - expect(screen.getByText('Components (10)')).toBeInTheDocument(); - }); - - it('should not show the "View All" button when viewing library with low number of components', async () => { - fetchMock.post(searchEndpoint, returnLowNumberResults, { overwriteRoutes: true }); - await renderLibraryPage(); - - expect(screen.getByText('Content library')).toBeInTheDocument(); - expect((await screen.findAllByText(libraryTitle))[0]).toBeInTheDocument(); - - // "Recently Modified" header + sort shown - await waitFor(() => { expect(screen.getAllByText('Recently Modified').length).toEqual(2); }); - expect(screen.getByText('Collections (2)')).toBeInTheDocument(); - expect(screen.getByText('Components (2)')).toBeInTheDocument(); - expect(screen.getAllByText('Introduction to Testing')[0]).toBeInTheDocument(); - expect(screen.queryByText('You have not added any content to this library yet.')).not.toBeInTheDocument(); - - // There should not be any "View All" button on page since Components count - // is less than the preview limit (4) - expect(screen.queryByText('View All')).not.toBeInTheDocument(); - }); - it('sorts library components', async () => { await renderLibraryPage(); @@ -444,7 +344,7 @@ describe('', () => { // Re-selecting the previous sort option resets sort to default "Recently Modified" await testSortOption('Recently Published', 'modified:desc', true); - expect(screen.getAllByText('Recently Modified').length).toEqual(3); + expect(screen.getAllByText('Recently Modified').length).toEqual(2); // Enter a keyword into the search box const searchBox = screen.getByRole('searchbox'); @@ -467,7 +367,6 @@ describe('', () => { expect(mockResult0.display_name).toStrictEqual(displayName); await renderLibraryPage(); - // Click on the first component. It should appear twice, in both "Recently Modified" and "Components" fireEvent.click((await screen.findAllByText(displayName))[0]); const sidebar = screen.getByTestId('library-sidebar'); @@ -579,7 +478,7 @@ describe('', () => { } // Validate click on Problem type - const problemMenu = screen.getByText('Problem'); + const problemMenu = screen.getAllByText('Problem')[0]; expect(problemMenu).toBeInTheDocument(); fireEvent.click(problemMenu); await waitFor(() => { @@ -647,7 +546,8 @@ describe('', () => { expect(screen.getByText(/add content/i)).toBeInTheDocument(); // Open New collection Modal - const newCollectionButton = screen.getAllByRole('button', { name: /collection/i })[4]; + const sidebar = screen.getByTestId('library-sidebar'); + const newCollectionButton = within(sidebar).getAllByRole('button', { name: /collection/i })[0]; fireEvent.click(newCollectionButton); const collectionModalHeading = await screen.findByRole('heading', { name: /new collection/i }); expect(collectionModalHeading).toBeInTheDocument(); @@ -691,7 +591,8 @@ describe('', () => { expect(screen.getByText(/add content/i)).toBeInTheDocument(); // Open New collection Modal - const newCollectionButton = screen.getAllByRole('button', { name: /collection/i })[4]; + const sidebar = screen.getByTestId('library-sidebar'); + const newCollectionButton = within(sidebar).getAllByRole('button', { name: /collection/i })[0]; fireEvent.click(newCollectionButton); const collectionModalHeading = await screen.findByRole('heading', { name: /new collection/i }); expect(collectionModalHeading).toBeInTheDocument(); @@ -724,7 +625,8 @@ describe('', () => { expect(screen.getByText(/add content/i)).toBeInTheDocument(); // Open New collection Modal - const newCollectionButton = screen.getAllByRole('button', { name: /collection/i })[4]; + const sidebar = screen.getByTestId('library-sidebar'); + const newCollectionButton = within(sidebar).getAllByRole('button', { name: /collection/i })[0]; fireEvent.click(newCollectionButton); const collectionModalHeading = await screen.findByRole('heading', { name: /new collection/i }); expect(collectionModalHeading).toBeInTheDocument(); @@ -739,22 +641,6 @@ describe('', () => { fireEvent.click(createButton); }); - it('shows both components and collections in recently modified section', async () => { - await renderLibraryPage(); - - expect(await screen.findByText('Content library')).toBeInTheDocument(); - expect((await screen.findAllByText(libraryTitle))[0]).toBeInTheDocument(); - - // "Recently Modified" header + sort shown - expect(screen.getAllByText('Recently Modified').length).toEqual(2); - const recentModifiedContainer = (await screen.findAllByText('Recently Modified'))[1].parentElement?.parentElement?.parentElement; - expect(recentModifiedContainer).toBeTruthy(); - - const container = within(recentModifiedContainer!); - expect(container.queryAllByText('Text').length).toBeGreaterThan(0); - expect(container.queryAllByText('Collection').length).toBeGreaterThan(0); - }); - it('shows a single block when usageKey query param is set', async () => { render(, { path, diff --git a/src/library-authoring/LibraryAuthoringPage.tsx b/src/library-authoring/LibraryAuthoringPage.tsx index ebb0b0308..202581e50 100644 --- a/src/library-authoring/LibraryAuthoringPage.tsx +++ b/src/library-authoring/LibraryAuthoringPage.tsx @@ -35,35 +35,11 @@ import { SearchKeywordsField, SearchSortWidget, } from '../search-manager'; -import LibraryComponents from './components/LibraryComponents'; -import LibraryCollections from './collections/LibraryCollections'; -import LibraryHome from './LibraryHome'; +import LibraryContent, { ContentType } from './LibraryContent'; import { LibrarySidebar } from './library-sidebar'; import { SidebarBodyComponentId, useLibraryContext } from './common/context'; import messages from './messages'; -enum TabList { - home = '', - components = 'components', - collections = 'collections', -} - -interface TabContentProps { - eventKey: string; - handleTabChange: (key: string) => void; -} - -const TabContent = ({ eventKey, handleTabChange }: TabContentProps) => { - switch (eventKey) { - case TabList.components: - return ; - case TabList.collections: - return ; - default: - return ; - } -}; - const HeaderActions = () => { const intl = useIntl(); const { @@ -162,15 +138,15 @@ const LibraryAuthoringPage = ({ returnToLibrarySelection }: LibraryAuthoringPage openInfoSidebar, } = useLibraryContext(); - const [activeKey, setActiveKey] = useState(''); + const [activeKey, setActiveKey] = useState(ContentType.home); useEffect(() => { const currentPath = location.pathname.split('/').pop(); if (componentPickerMode || currentPath === libraryId || currentPath === '') { - setActiveKey(TabList.home); - } else if (currentPath && currentPath in TabList) { - setActiveKey(TabList[currentPath]); + setActiveKey(ContentType.home); + } else if (currentPath && currentPath in ContentType) { + setActiveKey(ContentType[currentPath]); } }, [location.pathname]); @@ -203,7 +179,7 @@ const LibraryAuthoringPage = ({ returnToLibrarySelection }: LibraryAuthoringPage return ; } - const handleTabChange = (key: string) => { + const handleTabChange = (key: ContentType) => { setActiveKey(key); if (!componentPickerMode) { navigate({ @@ -235,6 +211,14 @@ const LibraryAuthoringPage = ({ returnToLibrarySelection }: LibraryAuthoringPage extraFilter.push('last_published IS NOT NULL'); } + const activeTypeFilters = { + components: 'NOT type = "collection"', + collections: 'type = "collection"', + }; + if (activeKey !== ContentType.home) { + extraFilter.push(activeTypeFilters[activeKey]); + } + return (
@@ -275,11 +259,11 @@ const LibraryAuthoringPage = ({ returnToLibrarySelection }: LibraryAuthoringPage onSelect={handleTabChange} className="my-3" > - - - + + + - + {!componentPickerMode && } diff --git a/src/library-authoring/collections/LibraryCollections.test.tsx b/src/library-authoring/LibraryContent.test.tsx similarity index 59% rename from src/library-authoring/collections/LibraryCollections.test.tsx rename to src/library-authoring/LibraryContent.test.tsx index 43d7324cc..d7b49320e 100644 --- a/src/library-authoring/collections/LibraryCollections.test.tsx +++ b/src/library-authoring/LibraryContent.test.tsx @@ -1,15 +1,17 @@ import fetchMock from 'fetch-mock-jest'; import { + fireEvent, render, screen, initializeMocks, -} from '../../testUtils'; -import { getContentSearchConfigUrl } from '../../search-manager/data/api'; -import { mockContentLibrary } from '../data/api.mocks'; -import mockEmptyResult from '../../search-modal/__mocks__/empty-search-result.json'; -import { LibraryProvider } from '../common/context'; -import LibraryCollections from './LibraryCollections'; +} from '../testUtils'; +import { getContentSearchConfigUrl } from '../search-manager/data/api'; +import { mockContentLibrary } from './data/api.mocks'; +import mockEmptyResult from '../search-modal/__mocks__/empty-search-result.json'; +import { LibraryProvider } from './common/context'; +import LibraryContent from './LibraryContent'; +import { libraryComponentsMock } from './__mocks__'; const searchEndpoint = 'http://mock.meilisearch.local/multi-search'; @@ -18,8 +20,8 @@ const mockFetchNextPage = jest.fn(); const mockUseSearchContext = jest.fn(); const data = { - totalHits: 1, - hits: [], + totalContentAndCollectionHits: 0, + contentAndCollectionHits: [], isFetchingNextPage: false, hasNextPage: false, fetchNextPage: mockFetchNextPage, @@ -40,8 +42,8 @@ const returnEmptyResult = (_url: string, req) => { return mockEmptyResult; }; -jest.mock('../../search-manager', () => ({ - ...jest.requireActual('../../search-manager'), +jest.mock('../search-manager', () => ({ + ...jest.requireActual('../search-manager'), useSearchContext: () => mockUseSearchContext(), })); @@ -58,7 +60,7 @@ const withLibraryId = (libraryId: string) => ({ ), }); -describe('', () => { +describe('', () => { beforeEach(() => { const { axiosMock } = initializeMocks(); @@ -83,7 +85,31 @@ describe('', () => { isLoading: true, }); - render(, withLibraryId(mockContentLibrary.libraryId)); + render(, withLibraryId(mockContentLibrary.libraryId)); expect(screen.getByText('Loading...')).toBeInTheDocument(); }); + + it('should render an empty state when there are no results', async () => { + mockUseSearchContext.mockReturnValue({ + ...data, + totalHits: 0, + }); + render(, withLibraryId(mockContentLibrary.libraryId)); + expect(screen.getByText('You have not added any content to this library yet.')).toBeInTheDocument(); + }); + + it('should load more results when the user scrolls to the bottom', async () => { + mockUseSearchContext.mockReturnValue({ + ...data, + hits: libraryComponentsMock, + hasNextPage: true, + }); + render(, withLibraryId(mockContentLibrary.libraryId)); + + Object.defineProperty(window, 'innerHeight', { value: 800 }); + Object.defineProperty(document.body, 'scrollHeight', { value: 1600 }); + + fireEvent.scroll(window, { target: { scrollY: 1000 } }); + expect(mockFetchNextPage).toHaveBeenCalled(); + }); }); diff --git a/src/library-authoring/LibraryContent.tsx b/src/library-authoring/LibraryContent.tsx new file mode 100644 index 000000000..15049e25b --- /dev/null +++ b/src/library-authoring/LibraryContent.tsx @@ -0,0 +1,92 @@ +import { useEffect } from 'react'; +import { LoadingSpinner } from '../generic/Loading'; +import { useSearchContext } from '../search-manager'; +import { NoComponents, NoSearchResults } from './EmptyStates'; +import { useLibraryContext } from './common/context'; +import CollectionCard from './components/CollectionCard'; +import ComponentCard from './components/ComponentCard'; +import { useLoadOnScroll } from '../hooks'; +import messages from './collections/messages'; + +export enum ContentType { + home = '', + components = 'components', + collections = 'collections', +} + +/** + * Library Content to show content grid + * + * Use content to: + * - 'collections': Suggest to create a collection on empty state. +* - Anything else to suggest to add content on empty state. + */ + +type LibraryContentProps = { + contentType?: ContentType; +}; + +const LibraryContent = ({ contentType = ContentType.home }: LibraryContentProps) => { + const { + hits, + totalHits, + isFetchingNextPage, + hasNextPage, + fetchNextPage, + isLoading, + isFiltered, + usageKey, + } = useSearchContext(); + const { openAddContentSidebar, openComponentInfoSidebar, openCreateCollectionModal } = useLibraryContext(); + + useEffect(() => { + if (usageKey) { + openComponentInfoSidebar(usageKey); + } + }, [usageKey]); + + useLoadOnScroll( + hasNextPage, + isFetchingNextPage, + fetchNextPage, + true, + ); + + if (isLoading) { + return ; + } + if (totalHits === 0) { + if (contentType === ContentType.collections) { + return isFiltered + ? + : ( + + ); + } + return isFiltered ? : ; + } + + return ( +
+ {hits.map((contentHit) => ( + contentHit.type === 'collection' ? ( + + ) : ( + + ) + ))} +
+ ); +}; + +export default LibraryContent; diff --git a/src/library-authoring/LibraryHome.tsx b/src/library-authoring/LibraryHome.tsx deleted file mode 100644 index 170c12d3d..000000000 --- a/src/library-authoring/LibraryHome.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { Stack } from '@openedx/paragon'; -import { useIntl } from '@edx/frontend-platform/i18n'; - -import { LoadingSpinner } from '../generic/Loading'; -import { useSearchContext } from '../search-manager'; -import { NoComponents, NoSearchResults } from './EmptyStates'; -import LibraryCollections from './collections/LibraryCollections'; -import { LibraryComponents } from './components'; -import LibrarySection from './components/LibrarySection'; -import LibraryRecentlyModified from './LibraryRecentlyModified'; -import messages from './messages'; -import { useLibraryContext } from './common/context'; - -type LibraryHomeProps = { - tabList: { home: string, components: string, collections: string }, - handleTabChange: (key: string) => void, -}; - -const LibraryHome = ({ tabList, handleTabChange } : LibraryHomeProps) => { - const intl = useIntl(); - const { - totalHits: componentCount, - totalCollectionHits: collectionCount, - isLoading, - isFiltered, - } = useSearchContext(); - const { openAddContentSidebar } = useLibraryContext(); - - const renderEmptyState = () => { - if (isLoading) { - return ; - } - if (componentCount === 0 && collectionCount === 0) { - return isFiltered ? : ; - } - return null; - }; - - return ( - - - { - renderEmptyState() - || ( - <> - handleTabChange(tabList.collections)} - > - - - handleTabChange(tabList.components)} - > - - - - ) - } - - ); -}; - -export default LibraryHome; diff --git a/src/library-authoring/LibraryRecentlyModified.tsx b/src/library-authoring/LibraryRecentlyModified.tsx deleted file mode 100644 index 6d67e5a55..000000000 --- a/src/library-authoring/LibraryRecentlyModified.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { useIntl } from '@edx/frontend-platform/i18n'; -import { orderBy } from 'lodash'; - -import { SearchContextProvider, useSearchContext } from '../search-manager'; -import { type CollectionHit, type ContentHit, SearchSortOption } from '../search-manager/data/api'; -import LibrarySection, { LIBRARY_SECTION_PREVIEW_LIMIT } from './components/LibrarySection'; -import messages from './messages'; -import ComponentCard from './components/ComponentCard'; -import CollectionCard from './components/CollectionCard'; -import { useLibraryContext } from './common/context'; - -const RecentlyModified: React.FC> = () => { - const intl = useIntl(); - const { - hits, - collectionHits, - totalHits, - totalCollectionHits, - } = useSearchContext(); - - const componentCount = totalHits + totalCollectionHits; - // Since we only display a fixed number of items in preview, - // only these number of items are use in sort step below - const componentList = hits.slice(0, LIBRARY_SECTION_PREVIEW_LIMIT); - const collectionList = collectionHits.slice(0, LIBRARY_SECTION_PREVIEW_LIMIT); - // Sort them by `modified` field in reverse and display them - const recentItems = orderBy([ - ...componentList, - ...collectionList, - ], ['modified'], ['desc']).slice(0, LIBRARY_SECTION_PREVIEW_LIMIT); - - return componentCount > 0 - ? ( - -
- {recentItems.map((contentHit) => ( - contentHit.type === 'collection' ? ( - - ) : ( - - ) - ))} -
-
- ) - : null; -}; - -const LibraryRecentlyModified: React.FC> = () => { - const { libraryId, showOnlyPublished } = useLibraryContext(); - - const extraFilter = [`context_key = "${libraryId}"`]; - if (showOnlyPublished) { - extraFilter.push('last_published IS NOT NULL'); - } - - return ( - - - - ); -}; - -export default LibraryRecentlyModified; diff --git a/src/library-authoring/__mocks__/collection-search.json b/src/library-authoring/__mocks__/collection-search.json index dba7008aa..81e8afcb5 100644 --- a/src/library-authoring/__mocks__/collection-search.json +++ b/src/library-authoring/__mocks__/collection-search.json @@ -180,34 +180,7 @@ "display_name": "Blank Problem", "description": "Problem" } - } - ], - "query": "", - "processingTimeMs": 1, - "limit": 20, - "offset": 0, - "estimatedTotalHits": 5 - }, - { - "indexUid": "studio_content", - "hits": [], - "query": "", - "processingTimeMs": 0, - "limit": 0, - "offset": 0, - "estimatedTotalHits": 5, - "facetDistribution": { - "block_type": { - "html": 4, - "problem": 1 }, - "content.problem_types": {} - }, - "facetStats": {} - }, - { - "indexUid": "studio_content", - "hits": [ { "display_name": "My first collection", "block_id": "my-first-collection", @@ -246,12 +219,30 @@ "access_id": 16, "num_children": 1 } + ], "query": "", - "processingTimeMs": 0, - "limit": 1, + "processingTimeMs": 1, + "limit": 20, "offset": 0, - "estimatedTotalHits": 1 + "estimatedTotalHits": 5 + }, + { + "indexUid": "studio_content", + "hits": [], + "query": "", + "processingTimeMs": 0, + "limit": 0, + "offset": 0, + "estimatedTotalHits": 5, + "facetDistribution": { + "block_type": { + "html": 4, + "problem": 1 + }, + "content.problem_types": {} + }, + "facetStats": {} } ] } diff --git a/src/library-authoring/__mocks__/library-search.json b/src/library-authoring/__mocks__/library-search.json index aebfcb81e..d4456c655 100644 --- a/src/library-authoring/__mocks__/library-search.json +++ b/src/library-authoring/__mocks__/library-search.json @@ -32,6 +32,199 @@ "description": "Testing" } }, + { + "display_name": "Collection 1", + "block_id": "col1", + "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque et mi ac nisi accumsan imperdiet vitae at odio. Vivamus tempor nec lorem eget lacinia. Vivamus efficitur lacus non dapibus porta. Nulla venenatis luctus nisi id posuere. Sed sollicitudin magna a sem ultrices accumsan. Praesent volutpat tortor vitae luctus rutrum. Integer.", + "id": 1, + "type": "collection", + "breadcrumbs": [ + { + "display_name": "CS problems 2" + } + ], + "created": 1725534795.628254, + "modified": 1725878053.420395, + "context_key": "lib:OpenedX:CSPROB2", + "org": "OpenedX", + "access_id": 16, + "_formatted": { + "display_name": "Collection 1", + "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque et mi ac nisi accumsan imperdiet…", + "id": "1", + "type": "collection", + "breadcrumbs": [ + { + "display_name": "CS problems 2" + } + ], + "created": "1725534795.628254", + "modified": "1725534795.628266", + "context_key": "lib:OpenedX:CSPROB2", + "org": "OpenedX", + "access_id": "16" + } + }, + { + "display_name": "Collection 2", + "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque et mi ac nisi accumsan imperdiet vitae at odio. Vivamus tempor nec lorem eget lacinia. Vivamus efficitur lacus non dapibus porta. Nulla venenatis luctus nisi id posuere. Sed sollicitudin magna a sem ultrices accumsan. Praesent volutpat tortor vitae luctus rutrum. Integer. Descrition 58", + "id": 2, + "type": "collection", + "breadcrumbs": [ + { + "display_name": "CS problems 2" + } + ], + "created": 1725534795.619101, + "modified": 1725534795.619113, + "context_key": "lib:OpenedX:CSPROB2", + "org": "OpenedX", + "access_id": 16, + "_formatted": { + "display_name": "Collection 2", + "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque et mi ac nisi accumsan imperdiet…", + "id": "2", + "type": "collection", + "breadcrumbs": [ + { + "display_name": "CS problems 2" + } + ], + "created": "1725534795.619101", + "modified": "1725534795.619113", + "context_key": "lib:OpenedX:CSPROB2", + "org": "OpenedX", + "access_id": "16" + } + }, + { + "display_name": "Collection 3", + "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque et mi ac nisi accumsan imperdiet vitae at odio. Vivamus tempor nec lorem eget lacinia. Vivamus efficitur lacus non dapibus porta. Nulla venenatis luctus nisi id posuere. Sed sollicitudin magna a sem ultrices accumsan. Praesent volutpat tortor vitae luctus rutrum. Integer. Descrition 57", + "id": 3, + "type": "collection", + "breadcrumbs": [ + { + "display_name": "CS problems 2" + } + ], + "created": 1725534795.609781, + "modified": 1725534795.609794, + "context_key": "lib:OpenedX:CSPROB2", + "org": "OpenedX", + "access_id": 16, + "_formatted": { + "display_name": "Collection 3", + "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque et mi ac nisi accumsan imperdiet…", + "id": "3", + "type": "collection", + "breadcrumbs": [ + { + "display_name": "CS problems 2" + } + ], + "created": "1725534795.609781", + "modified": "1725534795.609794", + "context_key": "lib:OpenedX:CSPROB2", + "org": "OpenedX", + "access_id": "16" + } + }, + { + "display_name": "Collection 4", + "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque et mi ac nisi accumsan imperdiet vitae at odio. Vivamus tempor nec lorem eget lacinia. Vivamus efficitur lacus non dapibus porta. Nulla venenatis luctus nisi id posuere. Sed sollicitudin magna a sem ultrices accumsan. Praesent volutpat tortor vitae luctus rutrum. Integer. Descrition 56", + "id": 4, + "type": "collection", + "breadcrumbs": [ + { + "display_name": "CS problems 2" + } + ], + "created": 1725534795.596287, + "modified": 1725534795.5963, + "context_key": "lib:OpenedX:CSPROB2", + "org": "OpenedX", + "access_id": 16, + "_formatted": { + "display_name": "Collection 4", + "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque et mi ac nisi accumsan imperdiet…", + "id": "4", + "type": "collection", + "breadcrumbs": [ + { + "display_name": "CS problems 2" + } + ], + "created": "1725534795.596287", + "modified": "1725534795.5963", + "context_key": "lib:OpenedX:CSPROB2", + "org": "OpenedX", + "access_id": "16" + } + }, + { + "display_name": "Collection 5", + "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque et mi ac nisi accumsan imperdiet vitae at odio. Vivamus tempor nec lorem eget lacinia. Vivamus efficitur lacus non dapibus porta. Nulla venenatis luctus nisi id posuere. Sed sollicitudin magna a sem ultrices accumsan. Praesent volutpat tortor vitae luctus rutrum. Integer. Descrition 55", + "id": 5, + "type": "collection", + "breadcrumbs": [ + { + "display_name": "CS problems 2" + } + ], + "created": 1725534795.583068, + "modified": 1725534795.583082, + "context_key": "lib:OpenedX:CSPROB2", + "org": "OpenedX", + "access_id": 16, + "_formatted": { + "display_name": "Collection 5", + "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque et mi ac nisi accumsan imperdiet…", + "id": "5", + "type": "collection", + "breadcrumbs": [ + { + "display_name": "CS problems 2" + } + ], + "created": "1725534795.583068", + "modified": "1725534795.583082", + "context_key": "lib:OpenedX:CSPROB2", + "org": "OpenedX", + "access_id": "16" + } + }, + { + "display_name": "Collection 6", + "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque et mi ac nisi accumsan imperdiet vitae at odio. Vivamus tempor nec lorem eget lacinia. Vivamus efficitur lacus non dapibus porta. Nulla venenatis luctus nisi id posuere. Sed sollicitudin magna a sem ultrices accumsan. Praesent volutpat tortor vitae luctus rutrum. Integer. Descrition 54", + "id": 6, + "type": "collection", + "breadcrumbs": [ + { + "display_name": "CS problems 2" + } + ], + "created": 1725534795.573794, + "modified": 1725534795.573808, + "context_key": "lib:OpenedX:CSPROB2", + "org": "OpenedX", + "access_id": 16, + "_formatted": { + "display_name": "Collection 6", + "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque et mi ac nisi accumsan imperdiet…", + "id": "6", + "type": "collection", + "breadcrumbs": [ + { + "display_name": "CS problems 2" + } + ], + "created": "1725534795.573794", + "modified": "1725534795.573808", + "context_key": "lib:OpenedX:CSPROB2", + "org": "OpenedX", + "access_id": "16" + } + }, { "id": "lbaximtesthtml73a22298-bcd9-4f4c-ae34-0bc2b0612480-46b4a7f2", "display_name": "Second Text Component", @@ -318,209 +511,6 @@ } }, "facetStats": {} - }, - { - "indexUid": "studio", - "hits": [ - { - "display_name": "Collection 1", - "block_id": "col1", - "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque et mi ac nisi accumsan imperdiet vitae at odio. Vivamus tempor nec lorem eget lacinia. Vivamus efficitur lacus non dapibus porta. Nulla venenatis luctus nisi id posuere. Sed sollicitudin magna a sem ultrices accumsan. Praesent volutpat tortor vitae luctus rutrum. Integer.", - "id": 1, - "type": "collection", - "breadcrumbs": [ - { - "display_name": "CS problems 2" - } - ], - "created": 1725534795.628254, - "modified": 1725878053.420395, - "context_key": "lib:OpenedX:CSPROB2", - "org": "OpenedX", - "access_id": 16, - "_formatted": { - "display_name": "Collection 1", - "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque et mi ac nisi accumsan imperdiet…", - "id": "1", - "type": "collection", - "breadcrumbs": [ - { - "display_name": "CS problems 2" - } - ], - "created": "1725534795.628254", - "modified": "1725534795.628266", - "context_key": "lib:OpenedX:CSPROB2", - "org": "OpenedX", - "access_id": "16" - } - }, - { - "display_name": "Collection 2", - "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque et mi ac nisi accumsan imperdiet vitae at odio. Vivamus tempor nec lorem eget lacinia. Vivamus efficitur lacus non dapibus porta. Nulla venenatis luctus nisi id posuere. Sed sollicitudin magna a sem ultrices accumsan. Praesent volutpat tortor vitae luctus rutrum. Integer. Descrition 58", - "id": 2, - "type": "collection", - "breadcrumbs": [ - { - "display_name": "CS problems 2" - } - ], - "created": 1725534795.619101, - "modified": 1725534795.619113, - "context_key": "lib:OpenedX:CSPROB2", - "org": "OpenedX", - "access_id": 16, - "_formatted": { - "display_name": "Collection 2", - "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque et mi ac nisi accumsan imperdiet…", - "id": "2", - "type": "collection", - "breadcrumbs": [ - { - "display_name": "CS problems 2" - } - ], - "created": "1725534795.619101", - "modified": "1725534795.619113", - "context_key": "lib:OpenedX:CSPROB2", - "org": "OpenedX", - "access_id": "16" - } - }, - { - "display_name": "Collection 3", - "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque et mi ac nisi accumsan imperdiet vitae at odio. Vivamus tempor nec lorem eget lacinia. Vivamus efficitur lacus non dapibus porta. Nulla venenatis luctus nisi id posuere. Sed sollicitudin magna a sem ultrices accumsan. Praesent volutpat tortor vitae luctus rutrum. Integer. Descrition 57", - "id": 3, - "type": "collection", - "breadcrumbs": [ - { - "display_name": "CS problems 2" - } - ], - "created": 1725534795.609781, - "modified": 1725534795.609794, - "context_key": "lib:OpenedX:CSPROB2", - "org": "OpenedX", - "access_id": 16, - "_formatted": { - "display_name": "Collection 3", - "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque et mi ac nisi accumsan imperdiet…", - "id": "3", - "type": "collection", - "breadcrumbs": [ - { - "display_name": "CS problems 2" - } - ], - "created": "1725534795.609781", - "modified": "1725534795.609794", - "context_key": "lib:OpenedX:CSPROB2", - "org": "OpenedX", - "access_id": "16" - } - }, - { - "display_name": "Collection 4", - "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque et mi ac nisi accumsan imperdiet vitae at odio. Vivamus tempor nec lorem eget lacinia. Vivamus efficitur lacus non dapibus porta. Nulla venenatis luctus nisi id posuere. Sed sollicitudin magna a sem ultrices accumsan. Praesent volutpat tortor vitae luctus rutrum. Integer. Descrition 56", - "id": 4, - "type": "collection", - "breadcrumbs": [ - { - "display_name": "CS problems 2" - } - ], - "created": 1725534795.596287, - "modified": 1725534795.5963, - "context_key": "lib:OpenedX:CSPROB2", - "org": "OpenedX", - "access_id": 16, - "_formatted": { - "display_name": "Collection 4", - "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque et mi ac nisi accumsan imperdiet…", - "id": "4", - "type": "collection", - "breadcrumbs": [ - { - "display_name": "CS problems 2" - } - ], - "created": "1725534795.596287", - "modified": "1725534795.5963", - "context_key": "lib:OpenedX:CSPROB2", - "org": "OpenedX", - "access_id": "16" - } - }, - { - "display_name": "Collection 5", - "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque et mi ac nisi accumsan imperdiet vitae at odio. Vivamus tempor nec lorem eget lacinia. Vivamus efficitur lacus non dapibus porta. Nulla venenatis luctus nisi id posuere. Sed sollicitudin magna a sem ultrices accumsan. Praesent volutpat tortor vitae luctus rutrum. Integer. Descrition 55", - "id": 5, - "type": "collection", - "breadcrumbs": [ - { - "display_name": "CS problems 2" - } - ], - "created": 1725534795.583068, - "modified": 1725534795.583082, - "context_key": "lib:OpenedX:CSPROB2", - "org": "OpenedX", - "access_id": 16, - "_formatted": { - "display_name": "Collection 5", - "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque et mi ac nisi accumsan imperdiet…", - "id": "5", - "type": "collection", - "breadcrumbs": [ - { - "display_name": "CS problems 2" - } - ], - "created": "1725534795.583068", - "modified": "1725534795.583082", - "context_key": "lib:OpenedX:CSPROB2", - "org": "OpenedX", - "access_id": "16" - } - }, - { - "display_name": "Collection 6", - "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque et mi ac nisi accumsan imperdiet vitae at odio. Vivamus tempor nec lorem eget lacinia. Vivamus efficitur lacus non dapibus porta. Nulla venenatis luctus nisi id posuere. Sed sollicitudin magna a sem ultrices accumsan. Praesent volutpat tortor vitae luctus rutrum. Integer. Descrition 54", - "id": 6, - "type": "collection", - "breadcrumbs": [ - { - "display_name": "CS problems 2" - } - ], - "created": 1725534795.573794, - "modified": 1725534795.573808, - "context_key": "lib:OpenedX:CSPROB2", - "org": "OpenedX", - "access_id": 16, - "_formatted": { - "display_name": "Collection 6", - "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque et mi ac nisi accumsan imperdiet…", - "id": "6", - "type": "collection", - "breadcrumbs": [ - { - "display_name": "CS problems 2" - } - ], - "created": "1725534795.573794", - "modified": "1725534795.573808", - "context_key": "lib:OpenedX:CSPROB2", - "org": "OpenedX", - "access_id": "16" - } - } - ], - "query": "learn", - "processingTimeMs": 1, - "limit": 6, - "offset": 0, - "estimatedTotalHits": 6 } ] } diff --git a/src/library-authoring/collections/LibraryCollectionComponents.tsx b/src/library-authoring/collections/LibraryCollectionComponents.tsx index 27ffd5639..87dacd3ab 100644 --- a/src/library-authoring/collections/LibraryCollectionComponents.tsx +++ b/src/library-authoring/collections/LibraryCollectionComponents.tsx @@ -1,9 +1,9 @@ import { Stack } from '@openedx/paragon'; import { NoComponents, NoSearchResults } from '../EmptyStates'; import { useSearchContext } from '../../search-manager'; -import { LibraryComponents } from '../components'; import messages from './messages'; import { useLibraryContext } from '../common/context'; +import LibraryContent, { ContentType } from '../LibraryContent'; const LibraryCollectionComponents = () => { const { totalHits: componentCount, isFiltered } = useSearchContext(); @@ -24,7 +24,7 @@ const LibraryCollectionComponents = () => { return (

Content ({componentCount})

- +
); }; diff --git a/src/library-authoring/collections/LibraryCollectionPage.test.tsx b/src/library-authoring/collections/LibraryCollectionPage.test.tsx index 7c7d1afc6..1d15837a8 100644 --- a/src/library-authoring/collections/LibraryCollectionPage.test.tsx +++ b/src/library-authoring/collections/LibraryCollectionPage.test.tsx @@ -37,7 +37,7 @@ const searchEndpoint = 'http://mock.meilisearch.local/multi-search'; const path = '/library/:libraryId/*'; const libraryTitle = mockContentLibrary.libraryData.title; const mockCollection = { - collectionId: mockResult.results[2].hits[0].block_id, + collectionId: mockResult.results[0].hits[5].block_id, collectionNeverLoads: mockGetCollectionMetadata.collectionIdLoading, collectionNoComponents: 'collection-no-components', collectionEmpty: mockGetCollectionMetadata.collectionIdError, @@ -62,23 +62,21 @@ describe('', () => { // because otherwise Instantsearch will update the UI and change the query, // leading to unexpected results in the test cases. mockResultCopy.results[0].query = query; - mockResultCopy.results[2].query = query; // And fake the required '_formatted' fields; it contains the highlighting ... around matched words // eslint-disable-next-line no-underscore-dangle, no-param-reassign mockResultCopy.results[0]?.hits.forEach((hit) => { hit._formatted = { ...hit }; }); - const collectionQueryId = requestData?.queries[0]?.filter?.[3]?.split('collections.key = "')[1].split('"')[0]; + const collectionQueryId = requestData?.queries[0]?.filter?.[2]?.split('collections.key = "')[1].split('"')[0]; switch (collectionQueryId) { case mockCollection.collectionNeverLoads: return new Promise(() => {}); case mockCollection.collectionEmpty: - mockResultCopy.results[2].hits = []; - mockResultCopy.results[2].estimatedTotalHits = 0; + mockResultCopy.results[0].hits = []; + mockResultCopy.results[0].totalHits = 0; break; case mockCollection.collectionNoComponents: mockResultCopy.results[0].hits = []; - mockResultCopy.results[0].estimatedTotalHits = 0; + mockResultCopy.results[0].totalHits = 0; mockResultCopy.results[1].facetDistribution.block_type = {}; - mockResultCopy.results[2].hits[0].num_children = 0; break; default: break; @@ -181,7 +179,7 @@ describe('', () => { // should not be impacted by the search await waitFor(() => { expect(fetchMock).toHaveFetchedTimes(2, searchEndpoint, 'post'); }); - expect(screen.queryByText('No matching components found in this collections.')).toBeInTheDocument(); + expect(screen.queryByText('No matching components found in this collection.')).toBeInTheDocument(); }); it('should open and close new content sidebar', async () => { diff --git a/src/library-authoring/collections/LibraryCollectionPage.tsx b/src/library-authoring/collections/LibraryCollectionPage.tsx index 00b2967f7..bd08309fa 100644 --- a/src/library-authoring/collections/LibraryCollectionPage.tsx +++ b/src/library-authoring/collections/LibraryCollectionPage.tsx @@ -196,7 +196,6 @@ const LibraryCollectionPage = () => { { - const { - collectionHits, - totalCollectionHits, - isFetchingNextPage, - hasNextPage, - fetchNextPage, - isLoading, - isFiltered, - } = useSearchContext(); - - const { openCreateCollectionModal } = useLibraryContext(); - - const collectionList = variant === 'preview' ? collectionHits.slice(0, LIBRARY_SECTION_PREVIEW_LIMIT) : collectionHits; - - useLoadOnScroll( - hasNextPage, - isFetchingNextPage, - fetchNextPage, - variant === 'full', - ); - - if (isLoading) { - return ; - } - - if (totalCollectionHits === 0) { - return isFiltered - ? - : ( - - ); - } - - return ( -
- {collectionList.map((collectionHit) => ( - - ))} -
- ); -}; - -export default LibraryCollections; diff --git a/src/library-authoring/collections/messages.ts b/src/library-authoring/collections/messages.ts index 29fe9e280..1dfaecd4a 100644 --- a/src/library-authoring/collections/messages.ts +++ b/src/library-authoring/collections/messages.ts @@ -63,7 +63,7 @@ const messages = defineMessages({ }, noSearchResultsInCollection: { id: 'course-authoring.library-authoring.collections-pag.no-search-results.text', - defaultMessage: 'No matching components found in this collections.', + defaultMessage: 'No matching components found in this collection.', description: 'Message displayed when no matching components are found in collection', }, newContentButton: { diff --git a/src/library-authoring/component-info/ComponentInfo.tsx b/src/library-authoring/component-info/ComponentInfo.tsx index 36832fc32..43ccbd3c4 100644 --- a/src/library-authoring/component-info/ComponentInfo.tsx +++ b/src/library-authoring/component-info/ComponentInfo.tsx @@ -12,7 +12,7 @@ import { } from '@openedx/paragon/icons'; import { SidebarAdditionalActions, useLibraryContext } from '../common/context'; -import { ComponentMenu } from '../components'; +import ComponentMenu from '../components'; import { canEditComponent } from '../components/ComponentEditorModal'; import ComponentDetails from './ComponentDetails'; import ComponentManagement from './ComponentManagement'; diff --git a/src/library-authoring/component-info/ManageCollections.test.tsx b/src/library-authoring/component-info/ManageCollections.test.tsx index 1bd4b0f16..abcd643dc 100644 --- a/src/library-authoring/component-info/ManageCollections.test.tsx +++ b/src/library-authoring/component-info/ManageCollections.test.tsx @@ -39,14 +39,14 @@ describe('', () => { fetchMock.mockReset(); fetchMock.post(searchEndpoint, (_url, req) => { const requestData = JSON.parse(req.body?.toString() ?? ''); - const query = requestData?.queries[2]?.q ?? ''; + const query = requestData?.queries[0]?.q ?? ''; // We have to replace the query (search keywords) in the mock results with the actual query, // because otherwise Instantsearch will update the UI and change the query, // leading to unexpected results in the test cases. - mockCollectionsResults.results[2].query = query; + mockCollectionsResults.results[0].query = query; // And fake the required '_formatted' fields; it contains the highlighting ... around matched words // eslint-disable-next-line no-underscore-dangle, no-param-reassign - mockCollectionsResults.results[2]?.hits.forEach((hit) => { hit._formatted = { ...hit }; }); + mockCollectionsResults.results[0]?.hits.forEach((hit) => { hit._formatted = { ...hit }; }); return mockCollectionsResults; }); }); diff --git a/src/library-authoring/component-info/ManageCollections.tsx b/src/library-authoring/component-info/ManageCollections.tsx index 10b1643cb..099e66631 100644 --- a/src/library-authoring/component-info/ManageCollections.tsx +++ b/src/library-authoring/component-info/ManageCollections.tsx @@ -29,7 +29,7 @@ interface CollectionsDrawerProps extends ManageCollectionsProps { const CollectionsSelectableBox = ({ usageKey, collections, onClose }: CollectionsDrawerProps) => { const type = 'checkbox'; const intl = useIntl(); - const { collectionHits } = useSearchContext(); + const { hits } = useSearchContext(); const { showToast } = useContext(ToastContext); const collectionKeys = collections.map((collection) => collection.key); const [selectedCollections, { @@ -67,7 +67,7 @@ const CollectionsSelectableBox = ({ usageKey, collections, onClose }: Collection columns={1} ariaLabelledby={intl.formatMessage(messages.manageCollectionsSelectionLabel)} > - {collectionHits.map((collectionHit) => ( + {hits.map((collectionHit) => ( ', () => { onChange.mockClear(); - // Select another component (the second "Select" button is the same component as the first, - // but in the "Components" section instead of the "Recently Changed" section) - fireEvent.click(screen.queryAllByRole('button', { name: 'Select' })[2]); + // Select another component + fireEvent.click(screen.queryAllByRole('button', { name: 'Select' })[1]); await waitFor(() => expect(onChange).toHaveBeenCalledWith([ { usageKey: 'lb:Axim:TEST:html:571fe018-f3ce-45c9-8f53-5dafcb422fdd', diff --git a/src/library-authoring/components/LibraryComponents.test.tsx b/src/library-authoring/components/LibraryComponents.test.tsx deleted file mode 100644 index 0e85cd7fe..000000000 --- a/src/library-authoring/components/LibraryComponents.test.tsx +++ /dev/null @@ -1,176 +0,0 @@ -import fetchMock from 'fetch-mock-jest'; - -import { - fireEvent, - render, - screen, - initializeMocks, -} from '../../testUtils'; -import { getContentSearchConfigUrl } from '../../search-manager/data/api'; -import { mockContentLibrary } from '../data/api.mocks'; -import mockEmptyResult from '../../search-modal/__mocks__/empty-search-result.json'; -import { LibraryProvider } from '../common/context'; -import { libraryComponentsMock } from '../__mocks__'; -import LibraryComponents from './LibraryComponents'; - -const searchEndpoint = 'http://mock.meilisearch.local/multi-search'; - -mockContentLibrary.applyMock(); -const mockFetchNextPage = jest.fn(); -const mockUseSearchContext = jest.fn(); - -const data = { - totalHits: 1, - hits: [], - isFetchingNextPage: false, - hasNextPage: false, - fetchNextPage: mockFetchNextPage, - searchKeywords: '', - isFiltered: false, - isLoading: false, -}; - -const returnEmptyResult = (_url: string, req) => { - const requestData = JSON.parse(req.body?.toString() ?? ''); - const query = requestData?.queries[0]?.q ?? ''; - // We have to replace the query (search keywords) in the mock results with the actual query, - // because otherwise we may have an inconsistent state that causes more queries and unexpected results. - mockEmptyResult.results[0].query = query; - // And fake the required '_formatted' fields; it contains the highlighting ... around matched words - // eslint-disable-next-line no-underscore-dangle, no-param-reassign - mockEmptyResult.results[0]?.hits.forEach((hit: any) => { hit._formatted = { ...hit }; }); - return mockEmptyResult; -}; - -jest.mock('../../search-manager', () => ({ - ...jest.requireActual('../../search-manager'), - useSearchContext: () => mockUseSearchContext(), -})); - -const clipboardBroadcastChannelMock = { - postMessage: jest.fn(), - close: jest.fn(), -}; - -(global as any).BroadcastChannel = jest.fn(() => clipboardBroadcastChannelMock); - -const withLibraryId = (libraryId: string) => ({ - extraWrapper: ({ children }: { children: React.ReactNode }) => ( - {children} - ), -}); - -describe('', () => { - beforeEach(() => { - const { axiosMock } = initializeMocks(); - - fetchMock.post(searchEndpoint, returnEmptyResult, { overwriteRoutes: true }); - - // The API method to get the Meilisearch connection details uses Axios: - axiosMock.onGet(getContentSearchConfigUrl()).reply(200, { - url: 'http://mock.meilisearch.local', - index_name: 'studio', - api_key: 'test-key', - }); - }); - - afterEach(() => { - fetchMock.reset(); - }); - - it('should render empty state', async () => { - mockUseSearchContext.mockReturnValue({ - ...data, - totalHits: 0, - }); - - render(, withLibraryId(mockContentLibrary.libraryId)); - expect(await screen.findByText(/you have not added any content to this library yet\./i)); - expect(await screen.findByRole('button', { name: /add component/i })).toBeInTheDocument(); - }); - - it('should render empty state without add content button', async () => { - mockUseSearchContext.mockReturnValue({ - ...data, - totalHits: 0, - }); - - render(, withLibraryId(mockContentLibrary.libraryIdReadOnly)); - expect(await screen.findByText(/you have not added any content to this library yet\./i)); - expect(screen.queryByRole('button', { name: /add component/i })).not.toBeInTheDocument(); - }); - - it('should render a spinner while loading', async () => { - mockUseSearchContext.mockReturnValue({ - ...data, - isLoading: true, - }); - - render(, withLibraryId(mockContentLibrary.libraryId)); - expect(screen.getByText('Loading...')).toBeInTheDocument(); - }); - - it('should render components in full variant', async () => { - mockUseSearchContext.mockReturnValue({ - ...data, - hits: libraryComponentsMock, - }); - render(, withLibraryId(mockContentLibrary.libraryId)); - - expect(await screen.findByText('This is a text: ID=1')).toBeInTheDocument(); - expect(screen.getByText('This is a text: ID=2')).toBeInTheDocument(); - expect(screen.getByText('Video Component 3')).toBeInTheDocument(); - expect(screen.getByText('Video Component 4')).toBeInTheDocument(); - expect(screen.getByText('This is a problem: ID=5')).toBeInTheDocument(); - expect(screen.getByText('This is a problem: ID=6')).toBeInTheDocument(); - }); - - it('should render components in preview variant', async () => { - mockUseSearchContext.mockReturnValue({ - ...data, - hits: libraryComponentsMock, - }); - render(, withLibraryId(mockContentLibrary.libraryId)); - - expect(await screen.findByText('This is a text: ID=1')).toBeInTheDocument(); - expect(screen.getByText('This is a text: ID=2')).toBeInTheDocument(); - expect(screen.getByText('Video Component 3')).toBeInTheDocument(); - expect(screen.getByText('Video Component 4')).toBeInTheDocument(); - expect(screen.queryByText('This is a problem: ID=5')).not.toBeInTheDocument(); - expect(screen.queryByText('This is a problem: ID=6')).not.toBeInTheDocument(); - }); - - it('should call `fetchNextPage` on scroll to bottom in full variant', async () => { - mockUseSearchContext.mockReturnValue({ - ...data, - hits: libraryComponentsMock, - hasNextPage: true, - }); - - render(, withLibraryId(mockContentLibrary.libraryId)); - - Object.defineProperty(window, 'innerHeight', { value: 800 }); - Object.defineProperty(document.body, 'scrollHeight', { value: 1600 }); - - fireEvent.scroll(window, { target: { scrollY: 1000 } }); - - expect(mockFetchNextPage).toHaveBeenCalled(); - }); - - it('should not call `fetchNextPage` on scroll to bottom in preview variant', async () => { - mockUseSearchContext.mockReturnValue({ - ...data, - hits: libraryComponentsMock, - hasNextPage: true, - }); - - render(, withLibraryId(mockContentLibrary.libraryId)); - - Object.defineProperty(window, 'innerHeight', { value: 800 }); - Object.defineProperty(document.body, 'scrollHeight', { value: 1600 }); - - fireEvent.scroll(window, { target: { scrollY: 1000 } }); - - expect(mockFetchNextPage).not.toHaveBeenCalled(); - }); -}); diff --git a/src/library-authoring/components/LibraryComponents.tsx b/src/library-authoring/components/LibraryComponents.tsx deleted file mode 100644 index 772dd7631..000000000 --- a/src/library-authoring/components/LibraryComponents.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { useEffect } from 'react'; - -import { LoadingSpinner } from '../../generic/Loading'; -import { useLoadOnScroll } from '../../hooks'; -import { useSearchContext } from '../../search-manager'; -import { NoComponents, NoSearchResults } from '../EmptyStates'; -import ComponentCard from './ComponentCard'; -import { LIBRARY_SECTION_PREVIEW_LIMIT } from './LibrarySection'; -import { useLibraryContext } from '../common/context'; - -type LibraryComponentsProps = { - variant: 'full' | 'preview', -}; - -/** - * Library Components to show components grid - * - * Use style to: - * - 'full': Show all components with Infinite scroll pagination. - * - 'preview': Show first 4 components without pagination. - */ -const LibraryComponents = ({ variant }: LibraryComponentsProps) => { - const { - hits, - totalHits: componentCount, - isFetchingNextPage, - hasNextPage, - fetchNextPage, - isLoading, - isFiltered, - usageKey, - } = useSearchContext(); - const { openAddContentSidebar, openComponentInfoSidebar } = useLibraryContext(); - - useEffect(() => { - if (usageKey) { - openComponentInfoSidebar(usageKey); - } - }, [usageKey]); - - const componentList = variant === 'preview' ? hits.slice(0, LIBRARY_SECTION_PREVIEW_LIMIT) : hits; - - useLoadOnScroll( - hasNextPage, - isFetchingNextPage, - fetchNextPage, - variant === 'full', - ); - - if (isLoading) { - return ; - } - - if (componentCount === 0) { - return isFiltered ? : ; - } - - return ( -
- { componentList.map((contentHit) => ( - - )) } -
- ); -}; - -export default LibraryComponents; diff --git a/src/library-authoring/components/LibrarySection.tsx b/src/library-authoring/components/LibrarySection.tsx deleted file mode 100644 index 14b6f8c59..000000000 --- a/src/library-authoring/components/LibrarySection.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import React from 'react'; -import { Card, ActionRow, Button } from '@openedx/paragon'; - -export const LIBRARY_SECTION_PREVIEW_LIMIT = 4; - -const LibrarySection: React.FC<{ - title: string, - viewAllAction?: () => void, - contentCount: number, - previewLimit?: number, - children: React.ReactNode, -}> = ({ - title, - viewAllAction, - contentCount, - previewLimit = LIBRARY_SECTION_PREVIEW_LIMIT, - children, -}) => ( - - previewLimit - && ( - - - - ) - } - /> - - {children} - - -); - -export default LibrarySection; diff --git a/src/library-authoring/components/index.ts b/src/library-authoring/components/index.ts index 3a928498c..119d3de91 100644 --- a/src/library-authoring/components/index.ts +++ b/src/library-authoring/components/index.ts @@ -1,2 +1 @@ -export { default as LibraryComponents } from './LibraryComponents'; -export { ComponentMenu } from './ComponentCard'; +export { ComponentMenu as default } from './ComponentCard'; diff --git a/src/library-authoring/messages.ts b/src/library-authoring/messages.ts index f0ac2cc1d..73ffe2066 100644 --- a/src/library-authoring/messages.ts +++ b/src/library-authoring/messages.ts @@ -33,7 +33,7 @@ const messages = defineMessages({ }, homeTab: { id: 'course-authoring.library-authoring.home-tab', - defaultMessage: 'Home', + defaultMessage: 'All Content', description: 'Tab label for the home tab', }, componentsTab: { diff --git a/src/search-manager/SearchManager.ts b/src/search-manager/SearchManager.ts index 297ce53b0..3776aaa30 100644 --- a/src/search-manager/SearchManager.ts +++ b/src/search-manager/SearchManager.ts @@ -10,7 +10,7 @@ import { MeiliSearch, type Filter } from 'meilisearch'; import { union } from 'lodash'; import { - CollectionHit, ContentHit, SearchSortOption, forceArray, OverrideQueries, + CollectionHit, ContentHit, SearchSortOption, forceArray, } from './data/api'; import { useContentSearchConnection, useContentSearchResults } from './data/apiHooks'; @@ -34,7 +34,7 @@ export interface SearchContextData { searchSortOrder: SearchSortOption; setSearchSortOrder: React.Dispatch>; defaultSearchSortOrder: SearchSortOption; - hits: ContentHit[]; + hits: (ContentHit | CollectionHit)[]; totalHits: number; isLoading: boolean; hasNextPage: boolean | undefined; @@ -42,8 +42,6 @@ export interface SearchContextData { fetchNextPage: () => void; closeSearchModal: () => void; hasError: boolean; - collectionHits: CollectionHit[]; - totalCollectionHits: number; usageKey: string; } @@ -93,10 +91,10 @@ export const SearchContextProvider: React.FC<{ overrideSearchSortOrder?: SearchSortOption children: React.ReactNode, closeSearchModal?: () => void, - overrideQueries?: OverrideQueries, + skipBlockTypeFetch?: boolean, skipUrlUpdate?: boolean, }> = ({ - overrideSearchSortOrder, overrideQueries, skipUrlUpdate, ...props + overrideSearchSortOrder, skipBlockTypeFetch, skipUrlUpdate, ...props }) => { const [searchKeywords, setSearchKeywords] = React.useState(''); const [blockTypesFilter, setBlockTypesFilter] = React.useState([]); @@ -165,7 +163,7 @@ export const SearchContextProvider: React.FC<{ problemTypesFilter, tagsFilter, sort, - overrideQueries, + skipBlockTypeFetch, }); return React.createElement(SearchContext.Provider, { diff --git a/src/search-manager/data/api.ts b/src/search-manager/data/api.ts index 08bb0fd63..b9bf192ae 100644 --- a/src/search-manager/data/api.ts +++ b/src/search-manager/data/api.ts @@ -1,7 +1,7 @@ import { camelCaseObject, getConfig } from '@edx/frontend-platform'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import type { - Filter, MeiliSearch, MultiSearchQuery, SearchParams, + Filter, MeiliSearch, MultiSearchQuery, } from 'meilisearch'; export const getContentSearchConfigUrl = () => new URL( @@ -126,6 +126,7 @@ export interface ContentHit extends BaseContentHit { * - First one is the name of the course/library itself. * - After that is the name and usage key of any parent Section/Subsection/Unit/etc. */ + type: 'course_block' | 'library_block'; breadcrumbs: [{ displayName: string }, ...Array<{ displayName: string, usageKey: string }>]; description?: string; content?: ContentDetails; @@ -149,6 +150,7 @@ export interface ContentPublishedData { * Defined in edx-platform/openedx/core/djangoapps/content/search/documents.py */ export interface CollectionHit extends BaseContentHit { + type: 'collection'; description: string; numChildren?: number; } @@ -169,29 +171,6 @@ export function formatSearchHit(hit: Record): ContentHit | Collecti return camelCaseObject(newHit); } -export interface OverrideQueries { - components?: SearchParams, - blockTypes?: SearchParams, - collections?: SearchParams, -} - -function applyOverrideQueries( - queries: MultiSearchQuery[], - overrideQueries?: OverrideQueries, -): MultiSearchQuery[] { - const newQueries = [...queries]; - if (overrideQueries?.components) { - newQueries[0] = { ...overrideQueries.components, indexUid: queries[0].indexUid }; - } - if (overrideQueries?.blockTypes) { - newQueries[1] = { ...overrideQueries.blockTypes, indexUid: queries[1].indexUid }; - } - if (overrideQueries?.collections) { - newQueries[2] = { ...overrideQueries.collections, indexUid: queries[2].indexUid }; - } - return newQueries; -} - interface FetchSearchParams { client: MeiliSearch, indexName: string, @@ -204,7 +183,7 @@ interface FetchSearchParams { sort?: SearchSortOption[], /** How many results to skip, e.g. if limit=20 then passing offset=20 gets the second page. */ offset?: number, - overrideQueries?: OverrideQueries, + skipBlockTypeFetch?: boolean, } export async function fetchSearchResults({ @@ -216,18 +195,16 @@ export async function fetchSearchResults({ tagsFilter, extraFilter, sort, - overrideQueries, offset = 0, + skipBlockTypeFetch = false, }: FetchSearchParams): Promise<{ - hits: ContentHit[], + hits: (ContentHit | CollectionHit)[], nextOffset: number | undefined, totalHits: number, blockTypes: Record, problemTypes: Record, - collectionHits: CollectionHit[], - totalCollectionHits: number, }> { - let queries: MultiSearchQuery[] = []; + const queries: MultiSearchQuery[] = []; // Convert 'extraFilter' into an array const extraFilterFormatted = forceArray(extraFilter); @@ -246,8 +223,6 @@ export async function fetchSearchResults({ ...problemTypesFilterFormatted, ].flat()]; - const collectionsFilter = 'type = "collection"'; - // First query is always to get the hits, with all the filters applied. queries.push({ indexUid: indexName, @@ -255,7 +230,6 @@ export async function fetchSearchResults({ filter: [ // top-level entries in the array are AND conditions and must all match // Inner arrays are OR conditions, where only one needs to match. - `NOT ${collectionsFilter}`, // exclude collections ...typeFilters, ...extraFilterFormatted, ...tagsFilterFormatted, @@ -270,52 +244,27 @@ export async function fetchSearchResults({ }); // The second query is to get the possible values for the "block types" filter - queries.push({ - indexUid: indexName, - q: searchKeywords, - facets: ['block_type', 'content.problem_types'], - filter: [ - ...extraFilterFormatted, - // We exclude the block type filter here so we get all the other available options for it. - ...tagsFilterFormatted, - ], - limit: 0, // We don't need any "hits" for this - just the facetDistribution - }); - - // Third query is to get the hits for collections, with all the filters applied. - queries.push({ - indexUid: indexName, - q: searchKeywords, - filter: [ - // top-level entries in the array are AND conditions and must all match - // Inner arrays are OR conditions, where only one needs to match. - collectionsFilter, // include only collections - ...extraFilterFormatted, - // We exclude the block type filter as collections are only of 1 type i.e. collection. - ...tagsFilterFormatted, - ], - attributesToHighlight: ['display_name', 'description'], - highlightPreTag: HIGHLIGHT_PRE_TAG, - highlightPostTag: HIGHLIGHT_POST_TAG, - attributesToCrop: ['description'], - sort, - offset, - limit, - }); - - queries = applyOverrideQueries(queries, overrideQueries); + if (!skipBlockTypeFetch) { + queries.push({ + indexUid: indexName, + facets: ['block_type', 'content.problem_types'], + filter: [ + ...extraFilterFormatted, + // We exclude the block type filter here so we get all the other available options for it. + ...tagsFilterFormatted, + ], + limit: 0, // We don't need any "hits" for this - just the facetDistribution + }); + } const { results } = await client.multiSearch(({ queries })); - const componentHitLength = results[0].hits.length; - const collectionHitLength = results[2].hits.length; + const hitLength = results[0].hits.length; return { hits: results[0].hits.map(formatSearchHit) as ContentHit[], - totalHits: results[0].totalHits ?? results[0].estimatedTotalHits ?? componentHitLength, - blockTypes: results[1].facetDistribution?.block_type ?? {}, - problemTypes: results[1].facetDistribution?.['content.problem_types'] ?? {}, - nextOffset: componentHitLength === limit || collectionHitLength === limit ? offset + limit : undefined, - collectionHits: results[2].hits.map(formatSearchHit) as CollectionHit[], - totalCollectionHits: results[2].totalHits ?? results[2].estimatedTotalHits ?? collectionHitLength, + totalHits: results[0].totalHits ?? results[0].estimatedTotalHits ?? hitLength, + blockTypes: results[1]?.facetDistribution?.block_type ?? {}, + problemTypes: results[1]?.facetDistribution?.['content.problem_types'] ?? {}, + nextOffset: hitLength === limit ? offset + limit : undefined, }; } @@ -553,19 +502,3 @@ export async function fetchTagsThatMatchKeyword({ return { matches: Array.from(matches).map((tagPath) => ({ tagPath })), mayBeMissingResults: hits.length === limit }; } - -/** - * Fetch single document by its id - */ -/* istanbul ignore next */ -export async function fetchDocumentById({ client, indexName, id } : { - /** The Meilisearch client instance */ - client: MeiliSearch; - /** Which index to search */ - indexName: string; - /** document id */ - id: string | number; -}): Promise { - const doc = await client.index(indexName).getDocument(id); - return formatSearchHit(doc); -} diff --git a/src/search-manager/data/apiHooks.ts b/src/search-manager/data/apiHooks.ts index cd63bbb34..923749b20 100644 --- a/src/search-manager/data/apiHooks.ts +++ b/src/search-manager/data/apiHooks.ts @@ -9,9 +9,7 @@ import { fetchSearchResults, fetchTagsThatMatchKeyword, getContentSearchConfig, - fetchDocumentById, fetchBlockTypes, - OverrideQueries, } from './api'; /** @@ -57,7 +55,7 @@ export const useContentSearchResults = ({ problemTypesFilter = [], tagsFilter = [], sort = [], - overrideQueries, + skipBlockTypeFetch = false, }: { /** The Meilisearch API client */ client?: MeiliSearch; @@ -75,8 +73,8 @@ export const useContentSearchResults = ({ tagsFilter?: string[]; /** Sort search results using these options */ sort?: SearchSortOption[]; - /** Set true to fetch collections along with components */ - overrideQueries?: OverrideQueries, + /** If true, don't fetch the block types from the server */ + skipBlockTypeFetch?: boolean; }) => { const query = useInfiniteQuery({ enabled: client !== undefined && indexName !== undefined, @@ -92,9 +90,9 @@ export const useContentSearchResults = ({ problemTypesFilter, tagsFilter, sort, - overrideQueries, ], queryFn: ({ pageParam = 0 }) => { + // istanbul ignore if: this should never happen if (client === undefined || indexName === undefined) { throw new Error('Required data unexpectedly undefined. Check "enable" condition of useQuery.'); } @@ -110,7 +108,7 @@ export const useContentSearchResults = ({ // For infinite pagination of results, we can retrieve additional pages if requested. // Note that if there are 20 results per page, the "second page" has offset=20, not 2. offset: pageParam, - overrideQueries, + skipBlockTypeFetch, }); }, getNextPageParam: (lastPage) => lastPage.nextOffset, @@ -125,14 +123,8 @@ export const useContentSearchResults = ({ [pages], ); - const collectionHits = React.useMemo( - () => pages?.reduce((allHits, page) => [...allHits, ...page.collectionHits], []) ?? [], - [pages], - ); - return { hits, - collectionHits, // The distribution of block type filter options blockTypes: pages?.[0]?.blockTypes ?? {}, problemTypes: pages?.[0]?.problemTypes ?? {}, @@ -147,7 +139,6 @@ export const useContentSearchResults = ({ hasNextPage: query.hasNextPage, // The last page has the most accurate count of total hits totalHits: pages?.[pages.length - 1]?.totalHits ?? 0, - totalCollectionHits: pages?.[pages.length - 1]?.totalCollectionHits ?? 0, }; }; @@ -186,6 +177,7 @@ export const useTagFilterOptions = (args: { ], queryFn: () => { const { client, indexName } = args; + // istanbul ignore if: this should never happen if (client === undefined || indexName === undefined) { throw new Error('Required data unexpectedly undefined. Check "enable" condition of useQuery.'); } @@ -210,6 +202,7 @@ export const useTagFilterOptions = (args: { ], queryFn: () => { const { client, indexName } = args; + // istanbul ignore if: this should never happen if (client === undefined || indexName === undefined) { throw new Error('Required data unexpectedly undefined. Check "enable" condition of useQuery.'); } @@ -259,27 +252,3 @@ export const useGetBlockTypes = (extraFilters: Filter) => { queryFn: () => fetchBlockTypes(client!, indexName!, extraFilters), }); }; - -/* istanbul ignore next */ -export const useGetSingleDocument = ({ client, indexName, id }: { - client?: MeiliSearch; - indexName?: string; - id: string | number; -}) => ( - useQuery({ - enabled: client !== undefined && indexName !== undefined, - queryKey: [ - 'content_search', - client?.config.apiKey, - client?.config.host, - indexName, - id, - ], - queryFn: () => { - if (client === undefined || indexName === undefined) { - throw new Error('Required data unexpectedly undefined. Check "enable" condition of useQuery.'); - } - return fetchDocumentById({ client, indexName, id }); - }, - }) -); diff --git a/src/search-modal/SearchResults.tsx b/src/search-modal/SearchResults.tsx index 7c741e9ce..2989621b0 100644 --- a/src/search-modal/SearchResults.tsx +++ b/src/search-modal/SearchResults.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { StatefulButton } from '@openedx/paragon'; import { useIntl } from '@edx/frontend-platform/i18n'; -import { useSearchContext } from '../search-manager'; +import { ContentHit, useSearchContext } from '../search-manager'; import SearchResult from './SearchResult'; import messages from './messages'; @@ -28,7 +28,9 @@ const SearchResults: React.FC> = () => { return ( <> - {hits.map((hit) => )} + {hits.filter((hit): hit is ContentHit => hit.type !== 'collection').map( + (hit) => , + )} {hasNextPage ? ( ', () => { // because otherwise Instantsearch will update the UI and change the query, // leading to unexpected results in the test cases. mockResult.results[0].query = query; - mockResult.results[2].query = query; // And fake the required '_formatted' fields; it contains the highlighting ... around matched words // eslint-disable-next-line no-underscore-dangle, no-param-reassign mockResult.results[0]?.hits.forEach((hit) => { hit._formatted = { ...hit }; }); - // eslint-disable-next-line no-underscore-dangle, no-param-reassign - mockResult.results[2]?.hits.forEach((hit) => { hit._formatted = { ...hit }; }); return mockResult; }); fetchMock.post(tagsKeywordSearchEndpoint, mockTagsKeywordSearchResult); @@ -173,8 +170,8 @@ describe('', () => { expect(fetchMock).toHaveLastFetched((_url, req) => { const requestData = JSON.parse(req.body?.toString() ?? ''); const requestedFilter = requestData?.queries[0].filter; - return requestedFilter?.[2] === 'type = "course_block"' - && requestedFilter?.[3] === 'context_key = "course-v1:org+test+123"'; + return requestedFilter?.[1] === 'type = "course_block"' + && requestedFilter?.[2] === 'context_key = "course-v1:org+test+123"'; }); // Now we should see the results: expect(queryByText('Enter a keyword')).toBeNull(); @@ -362,8 +359,8 @@ describe('', () => { const requestData = JSON.parse(req.body?.toString() ?? ''); const requestedFilter = requestData?.queries[0].filter; // the filter is: - // ['NOT type == "collection"', '', 'type = "course_block"', 'context_key = "course-v1:org+test+123"'] - return (requestedFilter?.length === 4); + // ['', 'type = "course_block"', 'context_key = "course-v1:org+test+123"'] + return (requestedFilter?.length === 3); }); // Now we should see the results: expect(getByText('6 results found')).toBeInTheDocument(); @@ -389,7 +386,6 @@ describe('', () => { const requestData = JSON.parse(req.body?.toString() ?? ''); const requestedFilter = requestData?.queries[0].filter; return JSON.stringify(requestedFilter) === JSON.stringify([ - 'NOT type = "collection"', [ 'block_type = problem', 'content.problem_types = choiceresponse', @@ -423,7 +419,6 @@ describe('', () => { const requestData = JSON.parse(req.body?.toString() ?? ''); const requestedFilter = requestData?.queries?.[0]?.filter; return JSON.stringify(requestedFilter) === JSON.stringify([ - 'NOT type = "collection"', [], 'type = "course_block"', 'context_key = "course-v1:org+test+123"', @@ -459,7 +454,6 @@ describe('', () => { const requestData = JSON.parse(req.body?.toString() ?? ''); const requestedFilter = requestData?.queries?.[0]?.filter; return JSON.stringify(requestedFilter) === JSON.stringify([ - 'NOT type = "collection"', [], 'type = "course_block"', 'context_key = "course-v1:org+test+123"', diff --git a/src/search-modal/__mocks__/empty-search-result.json b/src/search-modal/__mocks__/empty-search-result.json index 52c41bb57..a0ba5d6db 100644 --- a/src/search-modal/__mocks__/empty-search-result.json +++ b/src/search-modal/__mocks__/empty-search-result.json @@ -22,15 +22,6 @@ "block_type": {} }, "facetStats": {} - }, - { - "indexUid": "studio", - "hits": [], - "query": "noresult", - "processingTimeMs": 0, - "limit": 20, - "offset": 0, - "estimatedTotalHits": 0 } ] }