From d9be05dda75d4cd4334c73a5048a5040c2a620bd Mon Sep 17 00:00:00 2001 From: Daniel Valenzuela Date: Sun, 3 Nov 2024 02:09:36 -0300 Subject: [PATCH] feat: unify library component and collections listing --- .../LibraryAuthoringPage.test.tsx | 140 +------ .../LibraryAuthoringPage.tsx | 28 +- ...yHome.test.tsx => LibraryContent.test.tsx} | 30 +- .../{LibraryHome.tsx => LibraryContent.tsx} | 38 +- .../__mocks__/collection-search.json | 49 +-- .../__mocks__/library-search.json | 396 +++++++++--------- .../LibraryCollectionComponents.tsx | 4 +- .../LibraryCollectionPage.test.tsx | 14 +- .../collections/LibraryCollections.test.tsx | 89 ---- .../collections/LibraryCollections.tsx | 64 --- src/library-authoring/collections/messages.ts | 2 +- src/library-authoring/common/context.tsx | 1 - .../component-info/ComponentInfo.tsx | 2 +- .../component-info/ManageCollections.test.tsx | 6 +- .../component-info/ManageCollections.tsx | 6 +- .../component-picker/ComponentPicker.test.tsx | 5 +- .../components/LibraryComponents.test.tsx | 161 ------- .../components/LibraryComponents.tsx | 64 --- src/library-authoring/components/index.ts | 3 +- src/library-authoring/messages.ts | 10 - src/search-manager/data/api.ts | 3 - src/search-modal/SearchResults.tsx | 4 +- src/search-modal/SearchUI.test.tsx | 14 +- .../__mocks__/empty-search-result.json | 9 - 24 files changed, 311 insertions(+), 831 deletions(-) rename src/library-authoring/{LibraryHome.test.tsx => LibraryContent.test.tsx} (80%) rename src/library-authoring/{LibraryHome.tsx => LibraryContent.tsx} (55%) delete mode 100644 src/library-authoring/collections/LibraryCollections.test.tsx delete mode 100644 src/library-authoring/collections/LibraryCollections.tsx delete mode 100644 src/library-authoring/components/LibraryComponents.test.tsx delete mode 100644 src/library-authoring/components/LibraryComponents.tsx diff --git a/src/library-authoring/LibraryAuthoringPage.test.tsx b/src/library-authoring/LibraryAuthoringPage.test.tsx index 84e50c70fc..1f1f1dddeb 100644 --- a/src/library-authoring/LibraryAuthoringPage.test.tsx +++ b/src/library-authoring/LibraryAuthoringPage.test.tsx @@ -40,40 +40,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; @@ -133,35 +105,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 () => { @@ -185,7 +147,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 }); @@ -243,7 +205,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 () => { @@ -325,68 +287,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(); @@ -441,7 +341,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'); @@ -464,7 +364,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'); @@ -576,7 +475,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(() => { @@ -644,7 +543,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(); @@ -688,7 +588,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(); @@ -721,7 +622,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(); @@ -736,22 +638,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 e618d801a2..d4889955ca 100644 --- a/src/library-authoring/LibraryAuthoringPage.tsx +++ b/src/library-authoring/LibraryAuthoringPage.tsx @@ -33,9 +33,7 @@ import { SearchKeywordsField, SearchSortWidget, } from '../search-manager'; -import LibraryComponents from './components/LibraryComponents'; -import LibraryCollections from './collections/LibraryCollections'; -import LibraryHome from './LibraryHome'; +import LibraryContent from './LibraryContent'; import { LibrarySidebar } from './library-sidebar'; import { SidebarBodyComponentId, useLibraryContext } from './common/context'; import messages from './messages'; @@ -46,21 +44,6 @@ enum TabList { collections = 'collections', } -interface TabContentProps { - eventKey: string; -} - -const TabContent = ({ eventKey }: TabContentProps) => { - switch (eventKey) { - case TabList.components: - return ; - case TabList.collections: - return ; - default: - return ; - } -}; - const HeaderActions = () => { const intl = useIntl(); const { @@ -218,9 +201,12 @@ const LibraryAuthoringPage = ({ returnToLibrarySelection }: LibraryAuthoringPage extraFilter.push('last_published IS NOT NULL'); } + const activeTypeFilters = { + components: 'NOT type = "collection"', + collections: 'type = "collection"', + }; if (activeKey !== TabList.home) { - const activeType = activeKey === 'components' ? 'component' : 'collection'; - extraFilter.push(`type = "${activeType}"`); + extraFilter.push(activeTypeFilters[activeKey]); } return ( @@ -267,7 +253,7 @@ const LibraryAuthoringPage = ({ returnToLibrarySelection }: LibraryAuthoringPage - + {!componentPickerMode && } diff --git a/src/library-authoring/LibraryHome.test.tsx b/src/library-authoring/LibraryContent.test.tsx similarity index 80% rename from src/library-authoring/LibraryHome.test.tsx rename to src/library-authoring/LibraryContent.test.tsx index 7c74b0c4e8..67536a134e 100644 --- a/src/library-authoring/LibraryHome.test.tsx +++ b/src/library-authoring/LibraryContent.test.tsx @@ -1,6 +1,7 @@ import fetchMock from 'fetch-mock-jest'; import { + fireEvent, render, screen, initializeMocks, @@ -9,7 +10,8 @@ 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 LibraryHome from './LibraryHome'; +import LibraryContent from './LibraryContent'; +import { libraryComponentsMock } from './__mocks__'; const searchEndpoint = 'http://mock.meilisearch.local/multi-search'; @@ -83,27 +85,31 @@ describe('', () => { isLoading: true, }); - render(, withLibraryId(mockContentLibrary.libraryId)); + render(, withLibraryId(mockContentLibrary.libraryId)); expect(screen.getByText('Loading...')).toBeInTheDocument(); }); - it('should render a load more button when there are more pages', async () => { + it('should render an empty state when there are no results', async () => { mockUseSearchContext.mockReturnValue({ ...data, - totalContentAndCollectionHits: 1, - hasNextPage: true, + totalHits: 0, }); - - render(, withLibraryId(mockContentLibrary.libraryId)); - expect(screen.getByText('Show more results')).toBeInTheDocument(); + render(, withLibraryId(mockContentLibrary.libraryId)); + expect(screen.getByText('You have not added any content to this library yet.')).toBeInTheDocument(); }); - it('should render an empty state when there are no results', async () => { + it('should load more results when the user scrolls to the bottom', async () => { mockUseSearchContext.mockReturnValue({ ...data, - totalHits: 0, + hits: libraryComponentsMock, + hasNextPage: true, }); - render(, withLibraryId(mockContentLibrary.libraryId)); - expect(screen.getByText('You have not added any content to this library yet.')).toBeInTheDocument(); + 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/LibraryHome.tsx b/src/library-authoring/LibraryContent.tsx similarity index 55% rename from src/library-authoring/LibraryHome.tsx rename to src/library-authoring/LibraryContent.tsx index 8b51148112..6060d070c6 100644 --- a/src/library-authoring/LibraryHome.tsx +++ b/src/library-authoring/LibraryContent.tsx @@ -1,3 +1,4 @@ +import { useEffect } from 'react'; import { LoadingSpinner } from '../generic/Loading'; import { CollectionHit, ContentHit, useSearchContext } from '../search-manager'; import { NoComponents, NoSearchResults } from './EmptyStates'; @@ -5,8 +6,21 @@ import { useLibraryContext } from './common/context'; import CollectionCard from './components/CollectionCard'; import ComponentCard from './components/ComponentCard'; import { useLoadOnScroll } from '../hooks'; +import messages from './collections/messages'; -const LibraryHome = () => { +/** + * 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 = { + content: string; +}; + +const LibraryContent = ({ content }: LibraryContentProps) => { const { hits, totalHits, @@ -15,8 +29,15 @@ const LibraryHome = () => { fetchNextPage, isLoading, isFiltered, + usageKey, } = useSearchContext(); - const { openAddContentSidebar } = useLibraryContext(); + const { openAddContentSidebar, openComponentInfoSidebar, openCreateCollectionModal } = useLibraryContext(); + + useEffect(() => { + if (usageKey) { + openComponentInfoSidebar(usageKey); + } + }, [usageKey]); useLoadOnScroll( hasNextPage, @@ -29,6 +50,17 @@ const LibraryHome = () => { return ; } if (totalHits === 0) { + if (content === 'collections') { + return isFiltered + ? + : ( + + ); + } return isFiltered ? : ; } @@ -51,4 +83,4 @@ const LibraryHome = () => { ); }; -export default LibraryHome; +export default LibraryContent; diff --git a/src/library-authoring/__mocks__/collection-search.json b/src/library-authoring/__mocks__/collection-search.json index dba7008aaf..81e8afcb58 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": 1, + "limit": 20, + "offset": 0, + "estimatedTotalHits": 5 + }, + { + "indexUid": "studio_content", + "hits": [], + "query": "", "processingTimeMs": 0, - "limit": 1, + "limit": 0, "offset": 0, - "estimatedTotalHits": 1 + "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 aebfcb81ed..d4456c6558 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 6bfc73e678..d6da79d700 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 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 7c7d1afc6f..1d15837a81 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/LibraryCollections.test.tsx b/src/library-authoring/collections/LibraryCollections.test.tsx deleted file mode 100644 index e3213c763d..0000000000 --- a/src/library-authoring/collections/LibraryCollections.test.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import fetchMock from 'fetch-mock-jest'; - -import { - 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'; - -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(); - mockFetchNextPage.mockReset(); - }); - - it('should render a spinner while loading', async () => { - mockUseSearchContext.mockReturnValue({ - ...data, - isLoading: true, - }); - - render(, withLibraryId(mockContentLibrary.libraryId)); - expect(screen.getByText('Loading...')).toBeInTheDocument(); - }); -}); diff --git a/src/library-authoring/collections/LibraryCollections.tsx b/src/library-authoring/collections/LibraryCollections.tsx deleted file mode 100644 index f673d9f4da..0000000000 --- a/src/library-authoring/collections/LibraryCollections.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { LoadingSpinner } from '../../generic/Loading'; -import { useLoadOnScroll } from '../../hooks'; -import { CollectionHit, useSearchContext } from '../../search-manager'; -import { NoComponents, NoSearchResults } from '../EmptyStates'; -import CollectionCard from '../components/CollectionCard'; -import messages from './messages'; -import { useLibraryContext } from '../common/context'; - -/** - * Library Collections to show collections grid - * - * Use style to: - * - 'full': Show all collections with Infinite scroll pagination. - * - 'preview': Show first 4 collections without pagination. - */ -const LibraryCollections = () => { - const { - hits, - totalHits: totalCollectionHits, - isFetchingNextPage, - hasNextPage, - fetchNextPage, - isLoading, - isFiltered, - } = useSearchContext(); - - const { openCreateCollectionModal } = useLibraryContext(); - - useLoadOnScroll( - hasNextPage, - isFetchingNextPage, - fetchNextPage, - true, - ); - - if (isLoading) { - return ; - } - - if (totalCollectionHits === 0) { - return isFiltered - ? - : ( - - ); - } - - return ( -
- {hits.map((collectionHit) => ( - - ))} -
- ); -}; - -export default LibraryCollections; diff --git a/src/library-authoring/collections/messages.ts b/src/library-authoring/collections/messages.ts index 29fe9e280d..1dfaecd4a1 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/common/context.tsx b/src/library-authoring/common/context.tsx index fe4d281ce9..be229065f5 100644 --- a/src/library-authoring/common/context.tsx +++ b/src/library-authoring/common/context.tsx @@ -17,7 +17,6 @@ export interface SelectedComponent { export type ComponentSelectedEvent = (selectedComponent: SelectedComponent) => void; export type ComponentSelectionChangedEvent = (selectedComponents: SelectedComponent[]) => void; -export const LIBRARY_SECTION_PREVIEW_LIMIT = 4; type NoComponentPickerType = { componentPickerMode?: undefined; diff --git a/src/library-authoring/component-info/ComponentInfo.tsx b/src/library-authoring/component-info/ComponentInfo.tsx index 36832fc329..43ccbd3c4a 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 1bd4b0f164..abcd643dc3 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 10b1643cb0..bf4d46b13e 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) => ( diff --git a/src/library-authoring/component-picker/ComponentPicker.test.tsx b/src/library-authoring/component-picker/ComponentPicker.test.tsx index 4c06a9846e..fc42184251 100644 --- a/src/library-authoring/component-picker/ComponentPicker.test.tsx +++ b/src/library-authoring/component-picker/ComponentPicker.test.tsx @@ -201,9 +201,8 @@ describe('', () => { 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 07f8b060f7..0000000000 --- a/src/library-authoring/components/LibraryComponents.test.tsx +++ /dev/null @@ -1,161 +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 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 3d2ceedc08..0000000000 --- a/src/library-authoring/components/LibraryComponents.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { useEffect } from 'react'; - -import { LoadingSpinner } from '../../generic/Loading'; -import { useLoadOnScroll } from '../../hooks'; -import { ContentHit, useSearchContext } from '../../search-manager'; -import { NoComponents, NoSearchResults } from '../EmptyStates'; -import ComponentCard from './ComponentCard'; -import { useLibraryContext } from '../common/context'; - - -/** - * 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 = () => { - const { - hits, - totalHits: componentCount, - isFetchingNextPage, - hasNextPage, - fetchNextPage, - isLoading, - isFiltered, - usageKey, - } = useSearchContext(); - const { openAddContentSidebar, openComponentInfoSidebar } = useLibraryContext(); - - useEffect(() => { - if (usageKey) { - openComponentInfoSidebar(usageKey); - } - }, [usageKey]); - - useLoadOnScroll( - hasNextPage, - isFetchingNextPage, - fetchNextPage, - true, - ); - - if (isLoading) { - return ; - } - - if (componentCount === 0) { - return isFiltered ? : ; - } - - return ( -
- { hits.map((contentHit) => ( - - )) } -
- ); -}; - -export default LibraryComponents; diff --git a/src/library-authoring/components/index.ts b/src/library-authoring/components/index.ts index 3a928498c7..119d3de913 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 b2b82cbc47..4ad392cb52 100644 --- a/src/library-authoring/messages.ts +++ b/src/library-authoring/messages.ts @@ -111,16 +111,6 @@ const messages = defineMessages({ defaultMessage: 'Change Library', description: 'Breadcrumbs link to return to library selection', }, - showMoreContent: { - id: 'course-authoring.library-authoring.showMoreResults', - defaultMessage: 'Show more results', - description: 'Show more results - a button to add to the list of results by loading more from the server', - }, - loadingMoreContent: { - id: 'course-authoring.library-authoring.loadingMoreResults', - defaultMessage: 'Loading more results', - description: 'Loading more results - the button displays this message while more results are loading', - }, }); export default messages; diff --git a/src/search-manager/data/api.ts b/src/search-manager/data/api.ts index 57e6d0f18c..8d1679ec48 100644 --- a/src/search-manager/data/api.ts +++ b/src/search-manager/data/api.ts @@ -186,9 +186,6 @@ function applyOverrideQueries( if (overrideQueries?.blockTypes) { newQueries[1] = { ...overrideQueries.blockTypes, indexUid: queries[1].indexUid }; } - if (overrideQueries?.collections) { - newQueries[2] = { ...overrideQueries.collections, indexUid: queries[2].indexUid }; - } return newQueries; } diff --git a/src/search-modal/SearchResults.tsx b/src/search-modal/SearchResults.tsx index 7c741e9ce2..a95f3d61a0 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,7 @@ const SearchResults: React.FC> = () => { return ( <> - {hits.map((hit) => )} + {hits.filter(hit => 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 52c41bb57a..a0ba5d6db9 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 } ] }