@@ -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
}
]
}