From dcacedd9108277e7fbea7ed2af943fb70bdbc741 Mon Sep 17 00:00:00 2001 From: Navin Karkera Date: Wed, 4 Sep 2024 19:06:47 +0530 Subject: [PATCH 01/16] feat: add collections query to search results --- src/library-authoring/LibraryCollections.tsx | 36 +++++++++++--- src/search-manager/SearchManager.ts | 4 +- src/search-manager/data/api.ts | 52 ++++++++++++++++++++ src/search-manager/data/apiHooks.ts | 8 +++ 4 files changed, 92 insertions(+), 8 deletions(-) diff --git a/src/library-authoring/LibraryCollections.tsx b/src/library-authoring/LibraryCollections.tsx index 2f1eb8951f..8d625bdb9f 100644 --- a/src/library-authoring/LibraryCollections.tsx +++ b/src/library-authoring/LibraryCollections.tsx @@ -1,14 +1,36 @@ -import React from 'react'; +import React, { useEffect, useMemo } from 'react'; import { FormattedMessage } from '@edx/frontend-platform/i18n'; import messages from './messages'; +import { useSearchContext } from '../search-manager'; -const LibraryCollections = () => ( -
- +type LibraryCollectionsProps = { + libraryId: string, + variant: 'full' | 'preview', +}; + +/** + * 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 = ({ libraryId, variant }: LibraryCollectionsProps) => { + const { + collectionHits, + totalCollectionHits, + isFetchingNextPage, + hasNextPage, + fetchNextPage, + isFiltered, + setExtraFilter, + } = useSearchContext(); + + // __AUTO_GENERATED_PRINT_VAR_START__ + console.log("LibraryCollections collectionHits: ", collectionHits); // __AUTO_GENERATED_PRINT_VAR_END__ + return
-); +}; export default LibraryCollections; diff --git a/src/search-manager/SearchManager.ts b/src/search-manager/SearchManager.ts index d980a851d5..2062b90f16 100644 --- a/src/search-manager/SearchManager.ts +++ b/src/search-manager/SearchManager.ts @@ -8,7 +8,7 @@ import React from 'react'; import { useSearchParams } from 'react-router-dom'; import { MeiliSearch, type Filter } from 'meilisearch'; -import { ContentHit, SearchSortOption, forceArray } from './data/api'; +import { CollectionHit, ContentHit, SearchSortOption, forceArray } from './data/api'; import { useContentSearchConnection, useContentSearchResults } from './data/apiHooks'; export interface SearchContextData { @@ -39,6 +39,8 @@ export interface SearchContextData { fetchNextPage: () => void; closeSearchModal: () => void; hasError: boolean; + collectionHits: CollectionHit[]; + totalCollectionHits: number; } const SearchContext = React.createContext(undefined); diff --git a/src/search-manager/data/api.ts b/src/search-manager/data/api.ts index d220787929..459c75678e 100644 --- a/src/search-manager/data/api.ts +++ b/src/search-manager/data/api.ts @@ -121,6 +121,31 @@ export interface ContentHit { lastPublished: number | null; } +/** + * Information about a single collection returned in the search results + * Defined in edx-platform/openedx/core/djangoapps/content/search/documents.py + */ +export interface CollectionHit { + id: string; + type: 'collection'; + displayName: string; + description: string; + /** The course or library ID */ + contextKey: string; + org: string; + /** + * Breadcrumbs: + * - 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. + */ + breadcrumbs: Array<{ displayName: string }>; + // tags: ContentHitTags; + /** Same fields with ... highlights */ + created: number; + modified: number; + accessId: number; +} + /** * Convert search hits to camelCase * @param hit A search result directly from Meilisearch @@ -185,6 +210,8 @@ 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, @@ -192,6 +219,7 @@ 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, @@ -219,6 +247,28 @@ export async function fetchSearchResults({ 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 + ...typeFilters, + ...extraFilterFormatted, + ...tagsFilterFormatted, + ], + attributesToHighlight: ['display_name', 'content'], + highlightPreTag: HIGHLIGHT_PRE_TAG, + highlightPostTag: HIGHLIGHT_POST_TAG, + attributesToCrop: ['content'], + cropLength: 20, + sort, + offset, + limit, + }); + const { results } = await client.multiSearch(({ queries })); return { hits: results[0].hits.map(formatSearchHit), @@ -226,6 +276,8 @@ export async function fetchSearchResults({ blockTypes: results[1].facetDistribution?.block_type ?? {}, problemTypes: results[1].facetDistribution?.['content.problem_types'] ?? {}, nextOffset: results[0].hits.length === limit ? offset + limit : undefined, + collectionHits: results[2].hits.map(formatSearchHit), + totalCollectionHits: results[2].totalHits ?? results[2].estimatedTotalHits ?? results[2].hits.length, }; } diff --git a/src/search-manager/data/apiHooks.ts b/src/search-manager/data/apiHooks.ts index f5af9a159a..a9603a221b 100644 --- a/src/search-manager/data/apiHooks.ts +++ b/src/search-manager/data/apiHooks.ts @@ -103,8 +103,14 @@ 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 ?? {}, @@ -119,6 +125,8 @@ export const useContentSearchResults = ({ hasNextPage: query.hasNextPage, // The last page has the most accurate count of total hits totalHits: pages?.[pages.length - 1]?.totalHits ?? 0, + // The last page has the most accurate count of total hits + totalCollectionHits: pages?.[pages.length - 1]?.totalCollectionHits ?? 0, }; }; From 29dff81fe375eb1056515f48b5f49c34423cc7d4 Mon Sep 17 00:00:00 2001 From: Navin Karkera Date: Wed, 4 Sep 2024 20:42:47 +0530 Subject: [PATCH 02/16] feat: collections tab with basic cards --- .../LibraryAuthoringPage.tsx | 2 +- src/library-authoring/LibraryCollections.tsx | 54 +++++++++++++++++-- src/library-authoring/LibraryHome.tsx | 9 ++-- src/search-manager/data/apiHooks.ts | 1 - 4 files changed, 55 insertions(+), 11 deletions(-) diff --git a/src/library-authoring/LibraryAuthoringPage.tsx b/src/library-authoring/LibraryAuthoringPage.tsx index 7ec5e63a8a..af4e5b6926 100644 --- a/src/library-authoring/LibraryAuthoringPage.tsx +++ b/src/library-authoring/LibraryAuthoringPage.tsx @@ -206,7 +206,7 @@ const LibraryAuthoringPage = () => { /> } + element={} /> setExtraFilter, } = useSearchContext(); - // __AUTO_GENERATED_PRINT_VAR_START__ - console.log("LibraryCollections collectionHits: ", collectionHits); // __AUTO_GENERATED_PRINT_VAR_END__ - return
-
+ const collectionList = variant === 'preview' ? collectionHits.slice(0, LIBRARY_SECTION_PREVIEW_LIMIT) : collectionHits; + + useEffect(() => { + if (variant === 'full') { + const onscroll = () => { + // Verify the position of the scroll to implementa a infinite scroll. + // Used `loadLimit` to fetch next page before reach the end of the screen. + const loadLimit = 300; + const scrolledTo = window.scrollY + window.innerHeight; + const scrollDiff = document.body.scrollHeight - scrolledTo; + const isNearToBottom = scrollDiff <= loadLimit; + if (isNearToBottom && hasNextPage && !isFetchingNextPage) { + fetchNextPage(); + } + }; + window.addEventListener('scroll', onscroll); + return () => { + window.removeEventListener('scroll', onscroll); + }; + } + return () => {}; + }, [hasNextPage, isFetchingNextPage, fetchNextPage]); + + if (totalCollectionHits === 0) { + return isFiltered ? : ; + } + + return ( + + { collectionList.map((contentHit) => ( + + )) } + + ); }; export default LibraryCollections; diff --git a/src/library-authoring/LibraryHome.tsx b/src/library-authoring/LibraryHome.tsx index 4500cf1a91..b32c9ce98c 100644 --- a/src/library-authoring/LibraryHome.tsx +++ b/src/library-authoring/LibraryHome.tsx @@ -20,13 +20,12 @@ const LibraryHome = ({ libraryId, tabList, handleTabChange } : LibraryHomeProps) const intl = useIntl(); const { totalHits: componentCount, + totalCollectionHits: collectionCount, isFiltered, } = useSearchContext(); - const collectionCount = 0; - const renderEmptyState = () => { - if (componentCount === 0) { + if (componentCount === 0 && collectionCount === 0) { return isFiltered ? : ; } return null; @@ -42,9 +41,9 @@ const LibraryHome = ({ libraryId, tabList, handleTabChange } : LibraryHomeProps) handleTabChange(tabList.collections)} > - + Date: Thu, 5 Sep 2024 17:45:02 +0530 Subject: [PATCH 03/16] feat: add collection card also fix inifinite scroll for collections --- src/generic/block-type-utils/constants.ts | 1 + src/library-authoring/LibraryCollections.tsx | 11 ++- .../components/CollectionCard.tsx | 86 +++++++++++++++++++ src/library-authoring/components/messages.ts | 10 +++ src/search-manager/data/api.ts | 15 ++-- 5 files changed, 111 insertions(+), 12 deletions(-) create mode 100644 src/library-authoring/components/CollectionCard.tsx diff --git a/src/generic/block-type-utils/constants.ts b/src/generic/block-type-utils/constants.ts index 9b6cee0993..792ab33ace 100644 --- a/src/generic/block-type-utils/constants.ts +++ b/src/generic/block-type-utils/constants.ts @@ -51,6 +51,7 @@ export const STRUCTURAL_TYPE_ICONS: Record = { vertical: UNIT_TYPE_ICONS_MAP.vertical, sequential: Folder, chapter: Folder, + collection: Folder, }; export const COMPONENT_TYPE_STYLE_COLOR_MAP = { diff --git a/src/library-authoring/LibraryCollections.tsx b/src/library-authoring/LibraryCollections.tsx index b8bb73b0ce..1ed4a3bf5c 100644 --- a/src/library-authoring/LibraryCollections.tsx +++ b/src/library-authoring/LibraryCollections.tsx @@ -5,7 +5,7 @@ import { CardGrid } from '@openedx/paragon'; import messages from './messages'; import { useSearchContext } from '../search-manager'; import { NoComponents, NoSearchResults } from './EmptyStates'; -import ComponentCard from './components/ComponentCard'; +import CollectionCard from './components/CollectionCard'; import { LIBRARY_SECTION_PREVIEW_LIMIT } from './components/LibrarySection'; type LibraryCollectionsProps = { @@ -68,11 +68,10 @@ const LibraryCollections = ({ libraryId, variant }: LibraryCollectionsProps) => }} hasEqualColumnHeights > - { collectionList.map((contentHit) => ( - ( + )) } diff --git a/src/library-authoring/components/CollectionCard.tsx b/src/library-authoring/components/CollectionCard.tsx new file mode 100644 index 0000000000..f414199009 --- /dev/null +++ b/src/library-authoring/components/CollectionCard.tsx @@ -0,0 +1,86 @@ +import React, { useContext, useMemo, useState } from 'react'; +import { useIntl } from '@edx/frontend-platform/i18n'; +import { + ActionRow, + Card, + Container, + Icon, + IconButton, + Dropdown, + Stack, +} from '@openedx/paragon'; +import { MoreVert } from '@openedx/paragon/icons'; + +import { getItemIcon, getComponentStyleColor } from '../../generic/block-type-utils'; +import { updateClipboard } from '../../generic/data/api'; +import TagCount from '../../generic/tag-count'; +import { ToastContext } from '../../generic/toast-context'; +import { type CollectionHit, Highlight } from '../../search-manager'; +import { LibraryContext } from '../common/context'; +import messages from './messages'; +import { STUDIO_CLIPBOARD_CHANNEL } from '../../constants'; + +type CollectionCardProps = { + collectionHit: CollectionHit, +}; + +const CollectionCard = ({ collectionHit } : CollectionCardProps) => { + const intl = useIntl(); + + const { + type, + formatted, + tags, + } = collectionHit; + const { displayName = '', description = '' } = formatted; + + const tagCount = useMemo(() => { + if (!tags) { + return 0; + } + return (tags.level0?.length || 0) + (tags.level1?.length || 0) + + (tags.level2?.length || 0) + (tags.level3?.length || 0); + }, [tags]); + + const componentIcon = getItemIcon(type); + + return ( + + + + } + actions={( + + + + )} + /> + + + + + + {intl.formatMessage(messages.collectionType)} + + + +
+ +
+ +
+
+
+
+ ); +}; + +export default CollectionCard; diff --git a/src/library-authoring/components/messages.ts b/src/library-authoring/components/messages.ts index 50c085b16f..344a35b8aa 100644 --- a/src/library-authoring/components/messages.ts +++ b/src/library-authoring/components/messages.ts @@ -6,6 +6,16 @@ const messages = defineMessages({ defaultMessage: 'Component actions menu', description: 'Alt/title text for the component card menu button.', }, + collectionCardMenuAlt: { + id: 'course-authoring.library-authoring.collection.menu', + defaultMessage: 'Collection actions menu', + description: 'Alt/title text for the collection card menu button.', + }, + collectionType: { + id: 'course-authoring.library-authoring.collection.type', + defaultMessage: 'Collection', + description: 'Collection type text', + }, menuEdit: { id: 'course-authoring.library-authoring.component.menu.edit', defaultMessage: 'Edit', diff --git a/src/search-manager/data/api.ts b/src/search-manager/data/api.ts index 459c75678e..056cf95be5 100644 --- a/src/search-manager/data/api.ts +++ b/src/search-manager/data/api.ts @@ -144,18 +144,21 @@ export interface CollectionHit { created: number; modified: number; accessId: number; + /** Same fields with ... highlights */ + formatted: { displayName: string, description: string }; } /** * Convert search hits to camelCase * @param hit A search result directly from Meilisearch */ -function formatSearchHit(hit: Record): ContentHit { +function formatSearchHit(hit: Record): ContentHit | CollectionHit { // eslint-disable-next-line @typescript-eslint/naming-convention const { _formatted, ...newHit } = hit; newHit.formatted = { displayName: _formatted.display_name, content: _formatted.content ?? {}, + description: _formatted.description, }; return camelCaseObject(newHit); } @@ -255,15 +258,15 @@ export async function fetchSearchResults({ // 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 - ...typeFilters, ...extraFilterFormatted, + // We exclude the block type filter as collections are only of 1 type i.e. collection. ...tagsFilterFormatted, ], - attributesToHighlight: ['display_name', 'content'], + attributesToHighlight: ['display_name', 'description'], highlightPreTag: HIGHLIGHT_PRE_TAG, highlightPostTag: HIGHLIGHT_POST_TAG, - attributesToCrop: ['content'], - cropLength: 20, + attributesToCrop: ['description'], + cropLength: 15, sort, offset, limit, @@ -275,7 +278,7 @@ export async function fetchSearchResults({ totalHits: results[0].totalHits ?? results[0].estimatedTotalHits ?? results[0].hits.length, blockTypes: results[1].facetDistribution?.block_type ?? {}, problemTypes: results[1].facetDistribution?.['content.problem_types'] ?? {}, - nextOffset: results[0].hits.length === limit ? offset + limit : undefined, + nextOffset: results[0].hits.length === limit || results[2].hits.length === limit ? offset + limit : undefined, collectionHits: results[2].hits.map(formatSearchHit), totalCollectionHits: results[2].totalHits ?? results[2].estimatedTotalHits ?? results[2].hits.length, }; From 470ca53d5e655a1f2cf54a034c7265bc5c5e565c Mon Sep 17 00:00:00 2001 From: Navin Karkera Date: Thu, 5 Sep 2024 17:53:15 +0530 Subject: [PATCH 04/16] chore: fix lint issues --- src/library-authoring/LibraryAuthoringPage.tsx | 2 +- src/library-authoring/LibraryCollections.tsx | 8 ++------ src/library-authoring/LibraryHome.tsx | 2 +- src/library-authoring/components/CollectionCard.tsx | 7 +------ src/search-manager/SearchManager.ts | 4 +++- src/search-manager/data/api.ts | 6 +++--- 6 files changed, 11 insertions(+), 18 deletions(-) diff --git a/src/library-authoring/LibraryAuthoringPage.tsx b/src/library-authoring/LibraryAuthoringPage.tsx index af4e5b6926..c74943a755 100644 --- a/src/library-authoring/LibraryAuthoringPage.tsx +++ b/src/library-authoring/LibraryAuthoringPage.tsx @@ -206,7 +206,7 @@ const LibraryAuthoringPage = () => { /> } + element={} /> { +const LibraryCollections = ({ variant }: LibraryCollectionsProps) => { const { collectionHits, totalCollectionHits, @@ -28,7 +25,6 @@ const LibraryCollections = ({ libraryId, variant }: LibraryCollectionsProps) => hasNextPage, fetchNextPage, isFiltered, - setExtraFilter, } = useSearchContext(); const collectionList = variant === 'preview' ? collectionHits.slice(0, LIBRARY_SECTION_PREVIEW_LIMIT) : collectionHits; diff --git a/src/library-authoring/LibraryHome.tsx b/src/library-authoring/LibraryHome.tsx index b32c9ce98c..73d0e44ad5 100644 --- a/src/library-authoring/LibraryHome.tsx +++ b/src/library-authoring/LibraryHome.tsx @@ -43,7 +43,7 @@ const LibraryHome = ({ libraryId, tabList, handleTabChange } : LibraryHomeProps) contentCount={collectionCount} viewAllAction={() => handleTabChange(tabList.collections)} > - +
Date: Thu, 5 Sep 2024 18:16:50 +0530 Subject: [PATCH 05/16] feat: collection empty states --- src/library-authoring/EmptyStates.tsx | 23 +++++++++++++++----- src/library-authoring/LibraryCollections.tsx | 2 +- src/library-authoring/messages.ts | 15 +++++++++++++ 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/src/library-authoring/EmptyStates.tsx b/src/library-authoring/EmptyStates.tsx index e8e03cb4a6..433b0542d0 100644 --- a/src/library-authoring/EmptyStates.tsx +++ b/src/library-authoring/EmptyStates.tsx @@ -1,3 +1,4 @@ +/* eslint-disable react/require-default-props */ import React, { useContext } from 'react'; import { useParams } from 'react-router'; import { FormattedMessage } from '@edx/frontend-platform/i18n'; @@ -10,7 +11,11 @@ import messages from './messages'; import { LibraryContext } from './common/context'; import { useContentLibrary } from './data/apiHooks'; -export const NoComponents = () => { +type NoSearchResultsProps = { + searchType?: 'collection' | 'component', +}; + +export const NoComponents = ({ searchType = 'component' }: NoSearchResultsProps) => { const { openAddContentSidebar } = useContext(LibraryContext); const { libraryId } = useParams(); const { data: libraryData } = useContentLibrary(libraryId); @@ -18,19 +23,25 @@ export const NoComponents = () => { return ( - + {searchType === 'collection' + ? + : } {canEditLibrary && ( )} ); }; -export const NoSearchResults = () => ( - - +export const NoSearchResults = ({ searchType = 'component' }: NoSearchResultsProps) => ( + + {searchType === 'collection' + ? + : } ); diff --git a/src/library-authoring/LibraryCollections.tsx b/src/library-authoring/LibraryCollections.tsx index 9505459c50..f35214ffec 100644 --- a/src/library-authoring/LibraryCollections.tsx +++ b/src/library-authoring/LibraryCollections.tsx @@ -51,7 +51,7 @@ const LibraryCollections = ({ variant }: LibraryCollectionsProps) => { }, [hasNextPage, isFetchingNextPage, fetchNextPage]); if (totalCollectionHits === 0) { - return isFiltered ? : ; + return isFiltered ? : ; } return ( diff --git a/src/library-authoring/messages.ts b/src/library-authoring/messages.ts index 0f9bdfbd5b..d086d6b565 100644 --- a/src/library-authoring/messages.ts +++ b/src/library-authoring/messages.ts @@ -21,16 +21,31 @@ const messages = defineMessages({ defaultMessage: 'No matching components found in this library.', description: 'Message displayed when no search results are found', }, + noSearchResultsCollections: { + id: 'course-authoring.library-authoring.no-search-results-collections', + defaultMessage: 'No matching collections found in this library.', + description: 'Message displayed when no matching collections are found', + }, noComponents: { id: 'course-authoring.library-authoring.no-components', defaultMessage: 'You have not added any content to this library yet.', description: 'Message displayed when the library is empty', }, + noCollections: { + id: 'course-authoring.library-authoring.no-collections', + defaultMessage: 'You have not added any collection to this library yet.', + description: 'Message displayed when the library has no collections', + }, addComponent: { id: 'course-authoring.library-authoring.add-component', defaultMessage: 'Add component', description: 'Button text to add a new component', }, + addCollection: { + id: 'course-authoring.library-authoring.add-collection', + defaultMessage: 'Add collection', + description: 'Button text to add a new collection', + }, homeTab: { id: 'course-authoring.library-authoring.home-tab', defaultMessage: 'Home', From bef73714fde250d91d0e9ea3974029e93c4ede73 Mon Sep 17 00:00:00 2001 From: Navin Karkera Date: Thu, 5 Sep 2024 19:58:57 +0530 Subject: [PATCH 06/16] test: add test for collections card --- .../LibraryAuthoringPage.test.tsx | 56 +++-- .../components/CollectionCard.test.tsx | 74 +++++++ src/search-manager/data/api.ts | 8 +- src/search-manager/index.ts | 2 +- .../__mocks__/empty-search-result.json | 9 + src/search-modal/__mocks__/search-result.json | 202 ++++++++++++++++++ 6 files changed, 333 insertions(+), 18 deletions(-) create mode 100644 src/library-authoring/components/CollectionCard.test.tsx diff --git a/src/library-authoring/LibraryAuthoringPage.test.tsx b/src/library-authoring/LibraryAuthoringPage.test.tsx index 6a5cac0912..e5b6c1a14e 100644 --- a/src/library-authoring/LibraryAuthoringPage.test.tsx +++ b/src/library-authoring/LibraryAuthoringPage.test.tsx @@ -28,9 +28,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; }; @@ -48,10 +51,14 @@ const returnLowNumberResults = (_url, req) => { 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; }; @@ -129,7 +136,7 @@ describe('', () => { // "Recently Modified" header + sort shown expect(screen.getAllByText('Recently Modified').length).toEqual(2); - expect(screen.getByText('Collections (0)')).toBeInTheDocument(); + expect(screen.getByText('Collections (6)')).toBeInTheDocument(); expect(screen.getByText('Components (10)')).toBeInTheDocument(); expect((await screen.findAllByText('Introduction to Testing'))[0]).toBeInTheDocument(); @@ -137,34 +144,38 @@ describe('', () => { fireEvent.click(screen.getByRole('tab', { name: 'Components' })); // "Recently Modified" default sort shown expect(screen.getAllByText('Recently Modified').length).toEqual(1); - expect(screen.queryByText('Collections (0)')).not.toBeInTheDocument(); + 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 (0)')).not.toBeInTheDocument(); + 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(screen.getByText('Coming soon!')).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 (0)')).toBeInTheDocument(); + expect(screen.getByText('Collections (6)')).toBeInTheDocument(); expect(screen.getByText('Components (10)')).toBeInTheDocument(); }); - it('shows a library without components', async () => { + it('shows a library without components and collections', async () => { fetchMock.post(searchEndpoint, returnEmptyResult, { overwriteRoutes: true }); await renderLibraryPage(); expect(await screen.findByText('Content library')).toBeInTheDocument(); expect((await screen.findAllByText(libraryTitle))[0]).toBeInTheDocument(); + fireEvent.click(screen.getByRole('tab', { name: 'Collections' })); + expect(screen.getByText('You have not added any collection to this library yet.')).toBeInTheDocument(); + + fireEvent.click(screen.getByRole('tab', { name: 'Home' })); expect(screen.getByText('You have not added any content to this library yet.')).toBeInTheDocument(); }); @@ -211,6 +222,14 @@ describe('', () => { // Navigate to the components tab fireEvent.click(screen.getByRole('tab', { name: 'Components' })); expect(screen.getByText('No matching components found in this library.')).toBeInTheDocument(); + + // Navigate to the collections tab + fireEvent.click(screen.getByRole('tab', { name: 'Collections' })); + expect(screen.getByText('No matching collections found in this library.')).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' })); }); it('should open and close new content sidebar', async () => { @@ -282,20 +301,29 @@ describe('', () => { // "Recently Modified" header + sort shown await waitFor(() => { expect(screen.getAllByText('Recently Modified').length).toEqual(2); }); - expect(screen.getByText('Collections (0)')).toBeInTheDocument(); + 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 only be one "View All" button, since the Components count + // There should be two "View All" button, since the Components and Collections count // are above the preview limit (4) - expect(screen.getByText('View All')).toBeInTheDocument(); + 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(); - // Clicking on "View All" button should navigate to the Components tab - fireEvent.click(screen.getByText('View All')); + 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 (0)')).not.toBeInTheDocument(); + expect(screen.queryByText('Collections (6)')).not.toBeInTheDocument(); expect(screen.queryByText('Components (10)')).not.toBeInTheDocument(); expect(screen.getAllByText('Introduction to Testing')[0]).toBeInTheDocument(); @@ -304,7 +332,7 @@ describe('', () => { fireEvent.click(screen.getByRole('tab', { name: 'Home' })); // "Recently Modified" header + sort shown expect(screen.getAllByText('Recently Modified').length).toEqual(2); - expect(screen.getByText('Collections (0)')).toBeInTheDocument(); + expect(screen.getByText('Collections (6)')).toBeInTheDocument(); expect(screen.getByText('Components (10)')).toBeInTheDocument(); }); @@ -317,7 +345,7 @@ describe('', () => { // "Recently Modified" header + sort shown await waitFor(() => { expect(screen.getAllByText('Recently Modified').length).toEqual(2); }); - expect(screen.getByText('Collections (0)')).toBeInTheDocument(); + 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(); diff --git a/src/library-authoring/components/CollectionCard.test.tsx b/src/library-authoring/components/CollectionCard.test.tsx new file mode 100644 index 0000000000..7276dfada4 --- /dev/null +++ b/src/library-authoring/components/CollectionCard.test.tsx @@ -0,0 +1,74 @@ +import React from 'react'; +import { AppProvider } from '@edx/frontend-platform/react'; +import { initializeMockApp } from '@edx/frontend-platform'; +import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; +import { IntlProvider } from '@edx/frontend-platform/i18n'; +import { render } from '@testing-library/react'; +import MockAdapter from 'axios-mock-adapter'; +import type { Store } from 'redux'; + +import { ToastProvider } from '../../generic/toast-context'; +import { type CollectionHit } from '../../search-manager'; +import initializeStore from '../../store'; +import CollectionCard from './CollectionCard'; + +let store: Store; +let axiosMock: MockAdapter; + +const CollectionHitSample: CollectionHit = { + id: '1', + type: 'collection', + contextKey: 'lb:org1:Demo_Course', + org: 'org1', + breadcrumbs: [{ displayName: 'Demo Lib' }], + displayName: 'Collection Display Name', + description: 'Collection description', + formatted: { + displayName: 'Collection Display Formated Name', + description: 'Collection description', + }, + created: 1722434322294, + modified: 1722434322294, + accessId: 1, + tags: {}, +}; + +const RootWrapper = () => ( + + + + + + + +); + +describe('', () => { + beforeEach(() => { + initializeMockApp({ + authenticatedUser: { + userId: 3, + username: 'abc123', + administrator: true, + roles: [], + }, + }); + store = initializeStore(); + + axiosMock = new MockAdapter(getAuthenticatedHttpClient()); + }); + + afterEach(() => { + jest.clearAllMocks(); + axiosMock.restore(); + }); + + it('should render the card with title and description', () => { + const { getByText } = render(); + + expect(getByText('Collection Display Formated Name')).toBeInTheDocument(); + expect(getByText('Collection description')).toBeInTheDocument(); + }); +}); diff --git a/src/search-manager/data/api.ts b/src/search-manager/data/api.ts index ab0bab8393..cdbc3811a7 100644 --- a/src/search-manager/data/api.ts +++ b/src/search-manager/data/api.ts @@ -139,7 +139,7 @@ export interface CollectionHit { * - After that is the name and usage key of any parent Section/Subsection/Unit/etc. */ breadcrumbs: Array<{ displayName: string }>; - // tags: ContentHitTags; + tags: ContentHitTags; /** Same fields with ... highlights */ created: number; modified: number; @@ -193,6 +193,8 @@ export async function fetchSearchResults({ totalHits: number, blockTypes: Record, problemTypes: Record, + collectionHits: CollectionHit[], + totalCollectionHits: number, }> { const queries: MultiSearchQuery[] = []; @@ -274,12 +276,12 @@ export async function fetchSearchResults({ const { results } = await client.multiSearch(({ queries })); return { - hits: results[0].hits.map(formatSearchHit), + hits: results[0].hits.map(formatSearchHit) as ContentHit[], totalHits: results[0].totalHits ?? results[0].estimatedTotalHits ?? results[0].hits.length, blockTypes: results[1].facetDistribution?.block_type ?? {}, problemTypes: results[1].facetDistribution?.['content.problem_types'] ?? {}, nextOffset: results[0].hits.length === limit || results[2].hits.length === limit ? offset + limit : undefined, - collectionHits: results[2].hits.map(formatSearchHit), + collectionHits: results[2].hits.map(formatSearchHit) as CollectionHit[], totalCollectionHits: results[2].totalHits ?? results[2].estimatedTotalHits ?? results[2].hits.length, }; } diff --git a/src/search-manager/index.ts b/src/search-manager/index.ts index 267a333ecb..878bb14902 100644 --- a/src/search-manager/index.ts +++ b/src/search-manager/index.ts @@ -8,4 +8,4 @@ export { default as SearchSortWidget } from './SearchSortWidget'; export { default as Stats } from './Stats'; export { HIGHLIGHT_PRE_TAG, HIGHLIGHT_POST_TAG } from './data/api'; -export type { ContentHit } from './data/api'; +export type { CollectionHit, ContentHit } from './data/api'; diff --git a/src/search-modal/__mocks__/empty-search-result.json b/src/search-modal/__mocks__/empty-search-result.json index a0ba5d6db9..52c41bb57a 100644 --- a/src/search-modal/__mocks__/empty-search-result.json +++ b/src/search-modal/__mocks__/empty-search-result.json @@ -22,6 +22,15 @@ "block_type": {} }, "facetStats": {} + }, + { + "indexUid": "studio", + "hits": [], + "query": "noresult", + "processingTimeMs": 0, + "limit": 20, + "offset": 0, + "estimatedTotalHits": 0 } ] } diff --git a/src/search-modal/__mocks__/search-result.json b/src/search-modal/__mocks__/search-result.json index 6800f83065..6646b9ec87 100644 --- a/src/search-modal/__mocks__/search-result.json +++ b/src/search-modal/__mocks__/search-result.json @@ -365,6 +365,208 @@ } }, "facetStats": {} + }, + { + "indexUid": "studio", + "hits": [ + { + "display_name": "Collection 1", + "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": 1725534795.628266, + "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 } ] } From bca38f70e51382c17e9822b6838ac10be8e90aa3 Mon Sep 17 00:00:00 2001 From: Navin Karkera Date: Fri, 6 Sep 2024 13:07:33 +0530 Subject: [PATCH 07/16] refactor: move load on scroll logic to custom hook --- src/library-authoring/LibraryCollections.tsx | 29 +++++------------ .../components/LibraryComponents.tsx | 30 +++++------------- src/search-manager/SearchManager.ts | 31 +++++++++++++++++++ src/search-manager/index.ts | 2 +- 4 files changed, 47 insertions(+), 45 deletions(-) diff --git a/src/library-authoring/LibraryCollections.tsx b/src/library-authoring/LibraryCollections.tsx index f35214ffec..e037277066 100644 --- a/src/library-authoring/LibraryCollections.tsx +++ b/src/library-authoring/LibraryCollections.tsx @@ -1,7 +1,6 @@ -import React, { useEffect } from 'react'; import { CardGrid } from '@openedx/paragon'; -import { useSearchContext } from '../search-manager'; +import { useLoadOnScroll, useSearchContext } from '../search-manager'; import { NoComponents, NoSearchResults } from './EmptyStates'; import CollectionCard from './components/CollectionCard'; import { LIBRARY_SECTION_PREVIEW_LIMIT } from './components/LibrarySection'; @@ -29,26 +28,12 @@ const LibraryCollections = ({ variant }: LibraryCollectionsProps) => { const collectionList = variant === 'preview' ? collectionHits.slice(0, LIBRARY_SECTION_PREVIEW_LIMIT) : collectionHits; - useEffect(() => { - if (variant === 'full') { - const onscroll = () => { - // Verify the position of the scroll to implementa a infinite scroll. - // Used `loadLimit` to fetch next page before reach the end of the screen. - const loadLimit = 300; - const scrolledTo = window.scrollY + window.innerHeight; - const scrollDiff = document.body.scrollHeight - scrolledTo; - const isNearToBottom = scrollDiff <= loadLimit; - if (isNearToBottom && hasNextPage && !isFetchingNextPage) { - fetchNextPage(); - } - }; - window.addEventListener('scroll', onscroll); - return () => { - window.removeEventListener('scroll', onscroll); - }; - } - return () => {}; - }, [hasNextPage, isFetchingNextPage, fetchNextPage]); + useLoadOnScroll( + hasNextPage, + isFetchingNextPage, + fetchNextPage, + variant === 'full', + ); if (totalCollectionHits === 0) { return isFiltered ? : ; diff --git a/src/library-authoring/components/LibraryComponents.tsx b/src/library-authoring/components/LibraryComponents.tsx index 4065826428..2aa3014b7e 100644 --- a/src/library-authoring/components/LibraryComponents.tsx +++ b/src/library-authoring/components/LibraryComponents.tsx @@ -1,7 +1,7 @@ -import React, { useEffect, useMemo } from 'react'; +import React, { useMemo } from 'react'; import { CardGrid } from '@openedx/paragon'; -import { useSearchContext } from '../../search-manager'; +import { useLoadOnScroll, useSearchContext } from '../../search-manager'; import { NoComponents, NoSearchResults } from '../EmptyStates'; import { useLibraryBlockTypes } from '../data/apiHooks'; import ComponentCard from './ComponentCard'; @@ -43,26 +43,12 @@ const LibraryComponents = ({ libraryId, variant }: LibraryComponentsProps) => { return result; }, [blockTypesData]); - useEffect(() => { - if (variant === 'full') { - const onscroll = () => { - // Verify the position of the scroll to implementa a infinite scroll. - // Used `loadLimit` to fetch next page before reach the end of the screen. - const loadLimit = 300; - const scrolledTo = window.scrollY + window.innerHeight; - const scrollDiff = document.body.scrollHeight - scrolledTo; - const isNearToBottom = scrollDiff <= loadLimit; - if (isNearToBottom && hasNextPage && !isFetchingNextPage) { - fetchNextPage(); - } - }; - window.addEventListener('scroll', onscroll); - return () => { - window.removeEventListener('scroll', onscroll); - }; - } - return () => {}; - }, [hasNextPage, isFetchingNextPage, fetchNextPage]); + useLoadOnScroll( + hasNextPage, + isFetchingNextPage, + fetchNextPage, + variant === 'full', + ); if (componentCount === 0) { return isFiltered ? : ; diff --git a/src/search-manager/SearchManager.ts b/src/search-manager/SearchManager.ts index d1e925a91a..7b6992f3c3 100644 --- a/src/search-manager/SearchManager.ts +++ b/src/search-manager/SearchManager.ts @@ -184,3 +184,34 @@ export const useSearchContext = () => { } return ctx; }; + +/** + * Hook which loads next page of items on scroll + */ +export const useLoadOnScroll = ( + hasNextPage: boolean | undefined, + isFetchingNextPage: boolean, + fetchNextPage: () => void, + enabled: boolean, +) => { + React.useEffect(() => { + if (enabled) { + const onscroll = () => { + // Verify the position of the scroll to implementa a infinite scroll. + // Used `loadLimit` to fetch next page before reach the end of the screen. + const loadLimit = 300; + const scrolledTo = window.scrollY + window.innerHeight; + const scrollDiff = document.body.scrollHeight - scrolledTo; + const isNearToBottom = scrollDiff <= loadLimit; + if (isNearToBottom && hasNextPage && !isFetchingNextPage) { + fetchNextPage(); + } + }; + window.addEventListener('scroll', onscroll); + return () => { + window.removeEventListener('scroll', onscroll); + }; + } + return () => {}; + }, [hasNextPage, isFetchingNextPage, fetchNextPage]); +}; diff --git a/src/search-manager/index.ts b/src/search-manager/index.ts index 878bb14902..b06f9e193c 100644 --- a/src/search-manager/index.ts +++ b/src/search-manager/index.ts @@ -1,4 +1,4 @@ -export { SearchContextProvider, useSearchContext } from './SearchManager'; +export { SearchContextProvider, useLoadOnScroll, useSearchContext } from './SearchManager'; export { default as ClearFiltersButton } from './ClearFiltersButton'; export { default as FilterByBlockType } from './FilterByBlockType'; export { default as FilterByTags } from './FilterByTags'; From 9d1d3f908cd0f1ba9f8ddb5abc001c96c828b299 Mon Sep 17 00:00:00 2001 From: Navin Karkera Date: Fri, 6 Sep 2024 18:33:34 +0530 Subject: [PATCH 08/16] test: fix failing tests --- src/search-modal/SearchUI.test.tsx | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/search-modal/SearchUI.test.tsx b/src/search-modal/SearchUI.test.tsx index 1574b68917..acb3b5efe3 100644 --- a/src/search-modal/SearchUI.test.tsx +++ b/src/search-modal/SearchUI.test.tsx @@ -100,9 +100,12 @@ describe('', () => { // 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); @@ -174,8 +177,8 @@ describe('', () => { expect(fetchMock).toHaveLastFetched((_url, req) => { const requestData = JSON.parse(req.body?.toString() ?? ''); const requestedFilter = requestData?.queries[0].filter; - return requestedFilter?.[1] === 'type = "course_block"' - && requestedFilter?.[2] === 'context_key = "course-v1:org+test+123"'; + return requestedFilter?.[2] === 'type = "course_block"' + && requestedFilter?.[3] === 'context_key = "course-v1:org+test+123"'; }); // Now we should see the results: expect(queryByText('Enter a keyword')).toBeNull(); @@ -398,8 +401,9 @@ describe('', () => { expect(fetchMock).toHaveLastFetched((_url, req) => { const requestData = JSON.parse(req.body?.toString() ?? ''); const requestedFilter = requestData?.queries[0].filter; - // the filter is: ['type = "course_block"', 'context_key = "course-v1:org+test+123"'] - return (requestedFilter?.length === 3); + // the filter is: + // ['NOT type == "collection"', '', 'type = "course_block"', 'context_key = "course-v1:org+test+123"'] + return (requestedFilter?.length === 4); }); // Now we should see the results: expect(getByText('6 results found')).toBeInTheDocument(); @@ -425,6 +429,7 @@ 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', @@ -458,6 +463,7 @@ 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"', @@ -493,6 +499,7 @@ 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"', From c3b8df57faece6e7a0723795761de6661de21987 Mon Sep 17 00:00:00 2001 From: Navin Karkera Date: Mon, 9 Sep 2024 11:55:20 +0530 Subject: [PATCH 09/16] refactor: improve tests Rebase on top of https://github.com/openedx/frontend-app-course-authoring/pull/1263 and fix updated tests --- src/library-authoring/EmptyStates.tsx | 1 - .../__mocks__/library-search.json | 746 +++++++++++------- src/search-manager/data/api.ts | 51 +- src/search-modal/__mocks__/search-result.json | 199 +---- 4 files changed, 503 insertions(+), 494 deletions(-) diff --git a/src/library-authoring/EmptyStates.tsx b/src/library-authoring/EmptyStates.tsx index 433b0542d0..7fa0d51900 100644 --- a/src/library-authoring/EmptyStates.tsx +++ b/src/library-authoring/EmptyStates.tsx @@ -1,4 +1,3 @@ -/* eslint-disable react/require-default-props */ import React, { useContext } from 'react'; import { useParams } from 'react-router'; import { FormattedMessage } from '@edx/frontend-platform/i18n'; diff --git a/src/library-authoring/__mocks__/library-search.json b/src/library-authoring/__mocks__/library-search.json index c800f368ad..94bf46a181 100644 --- a/src/library-authoring/__mocks__/library-search.json +++ b/src/library-authoring/__mocks__/library-search.json @@ -1,273 +1,485 @@ { - "comment": "This mock is captured from a real search result and roughly edited to match the mocks in src/library-authoring/data/api.mocks.ts", - "note": "The _formatted fields have been removed from this result and should be re-added programatically when mocking.", - "results": [ + "comment": "This mock is captured from a real search result and roughly edited to match the mocks in src/library-authoring/data/api.mocks.ts", + "note": "The _formatted fields have been removed from this result and should be re-added programatically when mocking.", + "results": [ + { + "indexUid": "studio_content", + "hits": [ { - "indexUid": "studio_content", - "hits": [ - { - "id": "lbaximtesthtml571fe018-f3ce-45c9-8f53-5dafcb422fdd-273ebd90", - "display_name": "Introduction to Testing", - "block_id": "571fe018-f3ce-45c9-8f53-5dafcb422fdd", - "content": { - "html_content": "This is a text component which uses HTML." - }, - "tags": {}, - "type": "library_block", - "breadcrumbs": [ - { - "display_name": "Test Library" - } - ], - "created": 1721857069.042984, - "modified": 1725398676.078056, - "last_published": 1725035862.450613, - "usage_key": "lb:Axim:TEST:html:571fe018-f3ce-45c9-8f53-5dafcb422fdd", - "block_type": "html", - "context_key": "lib:Axim:TEST", - "org": "Axim", - "access_id": 15 - }, - { - "id": "lbaximtesthtml73a22298-bcd9-4f4c-ae34-0bc2b0612480-46b4a7f2", - "display_name": "Second Text Component", - "block_id": "73a22298-bcd9-4f4c-ae34-0bc2b0612480", - "content": { - "html_content": "Preview of the second text component here" - }, - "type": "library_block", - "breadcrumbs": [ - { - "display_name": "Test Library" - } - ], - "created": 1724879593.066427, - "modified": 1725034981.663482, - "last_published": 1725035862.450613, - "usage_key": "lb:Axim:TEST:html:73a22298-bcd9-4f4c-ae34-0bc2b0612480", - "block_type": "html", - "context_key": "lib:Axim:TEST", - "org": "Axim", - "access_id": 15 - }, - { - "id": "lbaximtesthtmlbe5b5db9-26ba-4fac-86af-654538c70b5e-73dbaa95", - "display_name": "Third Text component", - "block_id": "be5b5db9-26ba-4fac-86af-654538c70b5e", - "content": { - "html_content": "This is a text component that I've edited within the library. " - }, - "tags": {}, - "type": "library_block", - "breadcrumbs": [ - { - "display_name": "Test Library" - } - ], - "created": 1721857034.455737, - "modified": 1722551300.377488, - "last_published": 1724879092.002222, - "usage_key": "lb:Axim:TEST:html:be5b5db9-26ba-4fac-86af-654538c70b5e", - "block_type": "html", - "context_key": "lib:Axim:TEST", - "org": "Axim", - "access_id": 15 - }, - { - "id": "lbaximtesthtmle59e8c73-4056-4894-bca4-062781fb3f68-46a404b2", - "display_name": "Text 4", - "block_id": "e59e8c73-4056-4894-bca4-062781fb3f68", - "content": { - "html_content": "" - }, - "tags": {}, - "type": "library_block", - "breadcrumbs": [ - { - "display_name": "Test Library" - } - ], - "created": 1720774228.49832, - "modified": 1720774228.49832, - "last_published": 1724879092.002222, - "usage_key": "lb:Axim:TEST:html:e59e8c73-4056-4894-bca4-062781fb3f68", - "block_type": "html", - "context_key": "lib:Axim:TEST", - "org": "Axim", - "access_id": 15 - }, - { - "id": "lbaximtestproblemf16116c9-516e-4bb9-b99e-103599f62417-f2798115", - "display_name": "Blank Problem", - "block_id": "f16116c9-516e-4bb9-b99e-103599f62417", - "content": { - "problem_types": [], - "capa_content": " " - }, - "type": "library_block", - "breadcrumbs": [ - { - "display_name": "Test Library" - } - ], - "created": 1724725821.973896, - "modified": 1724725821.973896, - "last_published": 1724879092.002222, - "usage_key": "lb:Axim:TEST:problem:f16116c9-516e-4bb9-b99e-103599f62417", - "block_type": "problem", - "context_key": "lib:Axim:TEST", - "org": "Axim", - "access_id": 15 - }, - { - "id": "lbaximtestproblem2ace6b9b-6620-413c-a66f-19c797527f34-3a7973b7", - "display_name": "Multiple Choice Problem", - "block_id": "2ace6b9b-6620-413c-a66f-19c797527f34", - "content": { - "problem_types": ["multiplechoiceresponse"], - "capa_content": "What is the gradient of an inverted hyperspace manifold?cos (x) ey ln(z) i + sin(x)ey ln(z)j + sin(x) ey(1/z)k " - }, - "tags": {}, - "type": "library_block", - "breadcrumbs": [ - { - "display_name": "Test Library" - } - ], - "created": 1720774232.76135, - "modified": 1720774232.76135, - "last_published": 1724879092.002222, - "usage_key": "lb:Axim:TEST:problem:2ace6b9b-6620-413c-a66f-19c797527f34", - "block_type": "problem", - "context_key": "lib:Axim:TEST", - "org": "Axim", - "access_id": 15 - }, - { - "id": "lbaximtestproblem7d7e98ba-3ac9-4aa8-8946-159129b39a28-3a7973b7", - "display_name": "Single Choice Problem", - "block_id": "7d7e98ba-3ac9-4aa8-8946-159129b39a28", - "content": { - "problem_types": ["choiceresponse"], - "capa_content": "Blah blah?" - }, - "tags": {}, - "type": "library_block", - "breadcrumbs": [ - { - "display_name": "Test Library" - } - ], - "created": 1720774232.76135, - "modified": 1720774232.76135, - "last_published": 1724879092.002222, - "usage_key": "lb:Axim:TEST:problem:7d7e98ba-3ac9-4aa8-8946-159129b39a28", - "block_type": "problem", - "context_key": "lib:Axim:TEST", - "org": "Axim", - "access_id": 15 - }, - { - "id": "lbaximtestproblem4e1a72f9-ac93-42aa-a61c-ab5f9698c398-3a7973b7", - "display_name": "Numerical Response Problem", - "block_id": "4e1a72f9-ac93-42aa-a61c-ab5f9698c398", - "content": { - "problem_types": ["numericalresponse"], - "capa_content": "What is 1 + 1?" - }, - "tags": {}, - "type": "library_block", - "breadcrumbs": [ - { - "display_name": "Test Library" - } - ], - "created": 1720774232.76135, - "modified": 1720774232.76135, - "last_published": 1724879092.002222, - "usage_key": "lb:Axim:TEST:problem:4e1a72f9-ac93-42aa-a61c-ab5f9698c398", - "block_type": "problem", - "context_key": "lib:Axim:TEST", - "org": "Axim", - "access_id": 15 - }, - { - "id": "lbaximtestproblemad483625-ade2-4712-88d8-c9743abbd291-3a7973b7", - "display_name": "Option Response Problem", - "block_id": "ad483625-ade2-4712-88d8-c9743abbd291", - "content": { - "problem_types": ["optionresponse"], - "capa_content": "What is foobar?" - }, - "tags": {}, - "type": "library_block", - "breadcrumbs": [ - { - "display_name": "Test Library" - } - ], - "created": 1720774232.76135, - "modified": 1720774232.76135, - "last_published": 1724879092.002222, - "usage_key": "lb:Axim:TEST:problem:ad483625-ade2-4712-88d8-c9743abbd291", - "block_type": "problem", - "context_key": "lib:Axim:TEST", - "org": "Axim", - "access_id": 15 - }, - { - "id": "lbaximtestproblemb4c859cb-de70-421a-917b-e6e01ce44bd8-3a7973b7", - "display_name": "String Response Problem", - "block_id": "b4c859cb-de70-421a-917b-e6e01ce44bd8", - "content": { - "problem_types": ["stringresponse"], - "capa_content": "What is your name?" - }, - "tags": {}, - "type": "library_block", - "breadcrumbs": [ - { - "display_name": "Test Library" - } - ], - "created": 1720774232.76135, - "modified": 1720774232.76135, - "last_published": 1724879092.002222, - "usage_key": "lb:Axim:TEST:problem:b4c859cb-de70-421a-917b-e6e01ce44bd8", - "block_type": "problem", - "context_key": "lib:Axim:TEST", - "org": "Axim", - "access_id": 15 - } + "id": "lbaximtesthtml571fe018-f3ce-45c9-8f53-5dafcb422fdd-273ebd90", + "display_name": "Introduction to Testing", + "block_id": "571fe018-f3ce-45c9-8f53-5dafcb422fdd", + "content": { + "html_content": "This is a text component which uses HTML." + }, + "tags": {}, + "type": "library_block", + "breadcrumbs": [ + { + "display_name": "Test Library" + } + ], + "created": 1721857069.042984, + "modified": 1725398676.078056, + "last_published": 1725035862.450613, + "usage_key": "lb:Axim:TEST:html:571fe018-f3ce-45c9-8f53-5dafcb422fdd", + "block_type": "html", + "context_key": "lib:Axim:TEST", + "org": "Axim", + "access_id": 15 + }, + { + "id": "lbaximtesthtml73a22298-bcd9-4f4c-ae34-0bc2b0612480-46b4a7f2", + "display_name": "Second Text Component", + "block_id": "73a22298-bcd9-4f4c-ae34-0bc2b0612480", + "content": { + "html_content": "Preview of the second text component here" + }, + "type": "library_block", + "breadcrumbs": [ + { + "display_name": "Test Library" + } + ], + "created": 1724879593.066427, + "modified": 1725034981.663482, + "last_published": 1725035862.450613, + "usage_key": "lb:Axim:TEST:html:73a22298-bcd9-4f4c-ae34-0bc2b0612480", + "block_type": "html", + "context_key": "lib:Axim:TEST", + "org": "Axim", + "access_id": 15 + }, + { + "id": "lbaximtesthtmlbe5b5db9-26ba-4fac-86af-654538c70b5e-73dbaa95", + "display_name": "Third Text component", + "block_id": "be5b5db9-26ba-4fac-86af-654538c70b5e", + "content": { + "html_content": "This is a text component that I've edited within the library. " + }, + "tags": {}, + "type": "library_block", + "breadcrumbs": [ + { + "display_name": "Test Library" + } + ], + "created": 1721857034.455737, + "modified": 1722551300.377488, + "last_published": 1724879092.002222, + "usage_key": "lb:Axim:TEST:html:be5b5db9-26ba-4fac-86af-654538c70b5e", + "block_type": "html", + "context_key": "lib:Axim:TEST", + "org": "Axim", + "access_id": 15 + }, + { + "id": "lbaximtesthtmle59e8c73-4056-4894-bca4-062781fb3f68-46a404b2", + "display_name": "Text 4", + "block_id": "e59e8c73-4056-4894-bca4-062781fb3f68", + "content": { + "html_content": "" + }, + "tags": {}, + "type": "library_block", + "breadcrumbs": [ + { + "display_name": "Test Library" + } + ], + "created": 1720774228.49832, + "modified": 1720774228.49832, + "last_published": 1724879092.002222, + "usage_key": "lb:Axim:TEST:html:e59e8c73-4056-4894-bca4-062781fb3f68", + "block_type": "html", + "context_key": "lib:Axim:TEST", + "org": "Axim", + "access_id": 15 + }, + { + "id": "lbaximtestproblemf16116c9-516e-4bb9-b99e-103599f62417-f2798115", + "display_name": "Blank Problem", + "block_id": "f16116c9-516e-4bb9-b99e-103599f62417", + "content": { + "problem_types": [], + "capa_content": " " + }, + "type": "library_block", + "breadcrumbs": [ + { + "display_name": "Test Library" + } + ], + "created": 1724725821.973896, + "modified": 1724725821.973896, + "last_published": 1724879092.002222, + "usage_key": "lb:Axim:TEST:problem:f16116c9-516e-4bb9-b99e-103599f62417", + "block_type": "problem", + "context_key": "lib:Axim:TEST", + "org": "Axim", + "access_id": 15 + }, + { + "id": "lbaximtestproblem2ace6b9b-6620-413c-a66f-19c797527f34-3a7973b7", + "display_name": "Multiple Choice Problem", + "block_id": "2ace6b9b-6620-413c-a66f-19c797527f34", + "content": { + "problem_types": [ + "multiplechoiceresponse" + ], + "capa_content": "What is the gradient of an inverted hyperspace manifold?cos (x) ey ln(z) i + sin(x)ey ln(z)j + sin(x) ey(1/z)k " + }, + "tags": {}, + "type": "library_block", + "breadcrumbs": [ + { + "display_name": "Test Library" + } + ], + "created": 1720774232.76135, + "modified": 1720774232.76135, + "last_published": 1724879092.002222, + "usage_key": "lb:Axim:TEST:problem:2ace6b9b-6620-413c-a66f-19c797527f34", + "block_type": "problem", + "context_key": "lib:Axim:TEST", + "org": "Axim", + "access_id": 15 + }, + { + "id": "lbaximtestproblem7d7e98ba-3ac9-4aa8-8946-159129b39a28-3a7973b7", + "display_name": "Single Choice Problem", + "block_id": "7d7e98ba-3ac9-4aa8-8946-159129b39a28", + "content": { + "problem_types": [ + "choiceresponse" + ], + "capa_content": "Blah blah?" + }, + "tags": {}, + "type": "library_block", + "breadcrumbs": [ + { + "display_name": "Test Library" + } + ], + "created": 1720774232.76135, + "modified": 1720774232.76135, + "last_published": 1724879092.002222, + "usage_key": "lb:Axim:TEST:problem:7d7e98ba-3ac9-4aa8-8946-159129b39a28", + "block_type": "problem", + "context_key": "lib:Axim:TEST", + "org": "Axim", + "access_id": 15 + }, + { + "id": "lbaximtestproblem4e1a72f9-ac93-42aa-a61c-ab5f9698c398-3a7973b7", + "display_name": "Numerical Response Problem", + "block_id": "4e1a72f9-ac93-42aa-a61c-ab5f9698c398", + "content": { + "problem_types": [ + "numericalresponse" + ], + "capa_content": "What is 1 + 1?" + }, + "tags": {}, + "type": "library_block", + "breadcrumbs": [ + { + "display_name": "Test Library" + } + ], + "created": 1720774232.76135, + "modified": 1720774232.76135, + "last_published": 1724879092.002222, + "usage_key": "lb:Axim:TEST:problem:4e1a72f9-ac93-42aa-a61c-ab5f9698c398", + "block_type": "problem", + "context_key": "lib:Axim:TEST", + "org": "Axim", + "access_id": 15 + }, + { + "id": "lbaximtestproblemad483625-ade2-4712-88d8-c9743abbd291-3a7973b7", + "display_name": "Option Response Problem", + "block_id": "ad483625-ade2-4712-88d8-c9743abbd291", + "content": { + "problem_types": [ + "optionresponse" + ], + "capa_content": "What is foobar?" + }, + "tags": {}, + "type": "library_block", + "breadcrumbs": [ + { + "display_name": "Test Library" + } + ], + "created": 1720774232.76135, + "modified": 1720774232.76135, + "last_published": 1724879092.002222, + "usage_key": "lb:Axim:TEST:problem:ad483625-ade2-4712-88d8-c9743abbd291", + "block_type": "problem", + "context_key": "lib:Axim:TEST", + "org": "Axim", + "access_id": 15 + }, + { + "id": "lbaximtestproblemb4c859cb-de70-421a-917b-e6e01ce44bd8-3a7973b7", + "display_name": "String Response Problem", + "block_id": "b4c859cb-de70-421a-917b-e6e01ce44bd8", + "content": { + "problem_types": [ + "stringresponse" + ], + "capa_content": "What is your name?" + }, + "tags": {}, + "type": "library_block", + "breadcrumbs": [ + { + "display_name": "Test Library" + } + ], + "created": 1720774232.76135, + "modified": 1720774232.76135, + "last_published": 1724879092.002222, + "usage_key": "lb:Axim:TEST:problem:b4c859cb-de70-421a-917b-e6e01ce44bd8", + "block_type": "problem", + "context_key": "lib:Axim:TEST", + "org": "Axim", + "access_id": 15 + } + ], + "query": "", + "processingTimeMs": 1, + "limit": 20, + "offset": 0, + "estimatedTotalHits": 10 + }, + { + "indexUid": "studio_content", + "hits": [], + "query": "", + "processingTimeMs": 0, + "limit": 0, + "offset": 0, + "estimatedTotalHits": 10, + "facetDistribution": { + "block_type": { + "html": 4, + "problem": 6 + }, + "content.problem_types": { + "multiplechoiceresponse": 1, + "choiceresponse": 1, + "numericalresponse": 1, + "optionresponse": 1, + "stringresponse": 1 + } + }, + "facetStats": {} + }, + { + "indexUid": "studio", + "hits": [ + { + "display_name": "Collection 1", + "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": 1725534795.628266, + "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" + } ], - "query": "", - "processingTimeMs": 1, - "limit": 20, - "offset": 0, - "estimatedTotalHits": 10 + "created": "1725534795.619101", + "modified": "1725534795.619113", + "context_key": "lib:OpenedX:CSPROB2", + "org": "OpenedX", + "access_id": "16" + } }, { - "indexUid": "studio_content", - "hits": [], - "query": "", - "processingTimeMs": 0, - "limit": 0, - "offset": 0, - "estimatedTotalHits": 10, - "facetDistribution": { - "block_type": { - "html": 4, - "problem": 6 - }, - "content.problem_types": { - "multiplechoiceresponse": 1, - "choiceresponse": 1, - "numericalresponse": 1, - "optionresponse": 1, - "stringresponse": 1 - } - }, - "facetStats": {} + "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" + } } - ] -} \ No newline at end of file + ], + "query": "learn", + "processingTimeMs": 1, + "limit": 6, + "offset": 0, + "estimatedTotalHits": 6 + } + ] +} diff --git a/src/search-manager/data/api.ts b/src/search-manager/data/api.ts index cdbc3811a7..1bf33769b2 100644 --- a/src/search-manager/data/api.ts +++ b/src/search-manager/data/api.ts @@ -95,29 +95,37 @@ interface ContentHitTags { * Information about a single XBlock returned in the search results * Defined in edx-platform/openedx/core/djangoapps/content/search/documents.py */ -export interface ContentHit { +interface BaseContentHit { id: string; - usageKey: string; - type: 'course_block' | 'library_block'; - blockId: string; + type: 'course_block' | 'library_block' | 'collection'; displayName: string; - /** The block_type part of the usage key. What type of XBlock this is. */ - blockType: string; /** The course or library ID */ contextKey: string; org: string; + breadcrumbs: Array<{ displayName: string }>; + tags: ContentHitTags; + /** Same fields with ... highlights */ + formatted: { displayName: string, content?: ContentDetails, description?: string }; + created: number; + modified: number; +} + +/** + * Information about a single XBlock returned in the search results + * Defined in edx-platform/openedx/core/djangoapps/content/search/documents.py + */ +export interface ContentHit extends BaseContentHit { + usageKey: string; + blockId: string; + /** The block_type part of the usage key. What type of XBlock this is. */ + blockType: string; /** * Breadcrumbs: * - 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. */ breadcrumbs: [{ displayName: string }, ...Array<{ displayName: string, usageKey: string }>]; - tags: ContentHitTags; content?: ContentDetails; - /** Same fields with ... highlights */ - formatted: { displayName: string, content?: ContentDetails }; - created: number; - modified: number; lastPublished: number | null; } @@ -125,27 +133,10 @@ export interface ContentHit { * Information about a single collection returned in the search results * Defined in edx-platform/openedx/core/djangoapps/content/search/documents.py */ -export interface CollectionHit { - id: string; - type: 'collection'; - displayName: string; +export interface CollectionHit extends BaseContentHit { description: string; - /** The course or library ID */ - contextKey: string; - org: string; - /** - * Breadcrumbs: - * - 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. - */ - breadcrumbs: Array<{ displayName: string }>; - tags: ContentHitTags; - /** Same fields with ... highlights */ - created: number; - modified: number; accessId: number; - /** Same fields with ... highlights */ - formatted: { displayName: string, description: string }; + componentCount?: number; } /** diff --git a/src/search-modal/__mocks__/search-result.json b/src/search-modal/__mocks__/search-result.json index 6646b9ec87..85308e6d30 100644 --- a/src/search-modal/__mocks__/search-result.json +++ b/src/search-modal/__mocks__/search-result.json @@ -368,205 +368,12 @@ }, { "indexUid": "studio", - "hits": [ - { - "display_name": "Collection 1", - "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": 1725534795.628266, - "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" - } - } - ], + "hits": [], "query": "learn", "processingTimeMs": 1, - "limit": 6, + "limit": 20, "offset": 0, - "estimatedTotalHits": 6 + "estimatedTotalHits": 0 } ] } From 6b7ec88045ca6d9673cadd82576963e28b775df0 Mon Sep 17 00:00:00 2001 From: Navin Karkera Date: Mon, 9 Sep 2024 13:06:10 +0530 Subject: [PATCH 10/16] refactor: create base card component Extract common parts from component and collections card --- .../components/BaseComponentCard.tsx | 81 +++++++++++++++++++ .../components/CollectionCard.tsx | 72 +++++------------ .../components/ComponentCard.tsx | 70 ++++------------ src/search-manager/data/api.ts | 2 +- src/search-manager/index.ts | 2 +- 5 files changed, 119 insertions(+), 108 deletions(-) create mode 100644 src/library-authoring/components/BaseComponentCard.tsx diff --git a/src/library-authoring/components/BaseComponentCard.tsx b/src/library-authoring/components/BaseComponentCard.tsx new file mode 100644 index 0000000000..fa7d6138bf --- /dev/null +++ b/src/library-authoring/components/BaseComponentCard.tsx @@ -0,0 +1,81 @@ +import React, { useMemo } from 'react'; +import { + Card, + Container, + Icon, + Stack, +} from '@openedx/paragon'; +import { ReactNodeLike } from 'prop-types'; + +import { getItemIcon, getComponentStyleColor } from '../../generic/block-type-utils'; +import TagCount from '../../generic/tag-count'; +import { ContentHitTags, Highlight } from '../../search-manager'; + +type BaseComponentCardProps = { + type: string, + displayName: string, + description: string, + tags: ContentHitTags, + actions: ReactNodeLike, + blockTypeDisplayName: string, + openInfoSidebar: () => void +}; + +const BaseComponentCard = ({ + type, + displayName, + description, + tags, + actions, + blockTypeDisplayName, + openInfoSidebar, +} : BaseComponentCardProps) => { + const tagCount = useMemo(() => { + if (!tags) { + return 0; + } + return (tags.level0?.length || 0) + (tags.level1?.length || 0) + + (tags.level2?.length || 0) + (tags.level3?.length || 0); + }, [tags]); + + const componentIcon = getItemIcon(type); + + return ( + + { + if (['Enter', ' '].includes(e.key)) { + openInfoSidebar(); + } + }} + > + + } + actions={actions} + /> + + + + + + {blockTypeDisplayName} + + + +
+ +
+ +
+
+
+
+ ); +}; + +export default BaseComponentCard; diff --git a/src/library-authoring/components/CollectionCard.tsx b/src/library-authoring/components/CollectionCard.tsx index 774cd10b5b..477a264e4a 100644 --- a/src/library-authoring/components/CollectionCard.tsx +++ b/src/library-authoring/components/CollectionCard.tsx @@ -1,19 +1,14 @@ -import React, { useMemo } from 'react'; import { useIntl } from '@edx/frontend-platform/i18n'; import { ActionRow, - Card, - Container, Icon, IconButton, - Stack, } from '@openedx/paragon'; import { MoreVert } from '@openedx/paragon/icons'; -import { getItemIcon, getComponentStyleColor } from '../../generic/block-type-utils'; -import TagCount from '../../generic/tag-count'; -import { type CollectionHit, Highlight } from '../../search-manager'; +import { type CollectionHit } from '../../search-manager'; import messages from './messages'; +import BaseComponentCard from './BaseComponentCard'; type CollectionCardProps = { collectionHit: CollectionHit, @@ -29,52 +24,25 @@ const CollectionCard = ({ collectionHit } : CollectionCardProps) => { } = collectionHit; const { displayName = '', description = '' } = formatted; - const tagCount = useMemo(() => { - if (!tags) { - return 0; - } - return (tags.level0?.length || 0) + (tags.level1?.length || 0) - + (tags.level2?.length || 0) + (tags.level3?.length || 0); - }, [tags]); - - const componentIcon = getItemIcon(type); - return ( - - - - } - actions={( - - - - )} - /> - - - - - - {intl.formatMessage(messages.collectionType)} - - - -
- -
- -
-
-
-
+ + + + )} + blockTypeDisplayName={intl.formatMessage(messages.collectionType)} + openInfoSidebar={() => {}} + /> ); }; diff --git a/src/library-authoring/components/ComponentCard.tsx b/src/library-authoring/components/ComponentCard.tsx index f460bc3ba4..b2033ca373 100644 --- a/src/library-authoring/components/ComponentCard.tsx +++ b/src/library-authoring/components/ComponentCard.tsx @@ -1,24 +1,20 @@ -import React, { useContext, useMemo, useState } from 'react'; +import React, { useContext, useState } from 'react'; import { useIntl } from '@edx/frontend-platform/i18n'; import { ActionRow, - Card, - Container, Icon, IconButton, Dropdown, - Stack, } from '@openedx/paragon'; import { MoreVert } from '@openedx/paragon/icons'; -import { getItemIcon, getComponentStyleColor } from '../../generic/block-type-utils'; import { updateClipboard } from '../../generic/data/api'; -import TagCount from '../../generic/tag-count'; import { ToastContext } from '../../generic/toast-context'; -import { type ContentHit, Highlight } from '../../search-manager'; +import { type ContentHit } from '../../search-manager'; import { LibraryContext } from '../common/context'; import messages from './messages'; import { STUDIO_CLIPBOARD_CHANNEL } from '../../constants'; +import BaseComponentCard from './BaseComponentCard'; type ComponentCardProps = { contentHit: ContentHit, @@ -77,55 +73,21 @@ const ComponentCard = ({ contentHit, blockTypeDisplayName } : ComponentCardProps } = contentHit; const description = formatted?.content?.htmlContent ?? ''; const displayName = formatted?.displayName ?? ''; - const tagCount = useMemo(() => { - if (!tags) { - return 0; - } - return (tags.level0?.length || 0) + (tags.level1?.length || 0) - + (tags.level2?.length || 0) + (tags.level3?.length || 0); - }, [tags]); - - const componentIcon = getItemIcon(blockType); return ( - - openComponentInfoSidebar(usageKey)} - onKeyDown={(e: React.KeyboardEvent) => { - if (['Enter', ' '].includes(e.key)) { - openComponentInfoSidebar(usageKey); - } - }} - > - - } - actions={( - - - - )} - /> - - - - - - {blockTypeDisplayName} - - - -
- -
- -
-
-
-
+ + + + )} + blockTypeDisplayName={blockTypeDisplayName} + openInfoSidebar={() => openComponentInfoSidebar(usageKey)} + /> ); }; diff --git a/src/search-manager/data/api.ts b/src/search-manager/data/api.ts index 1bf33769b2..25851cbac7 100644 --- a/src/search-manager/data/api.ts +++ b/src/search-manager/data/api.ts @@ -83,7 +83,7 @@ function formatTagsFilter(tagsFilter?: string[]): string[] { /** * The tags that are associated with a search result, at various levels of the tag hierarchy. */ -interface ContentHitTags { +export interface ContentHitTags { taxonomy?: string[]; level0?: string[]; level1?: string[]; diff --git a/src/search-manager/index.ts b/src/search-manager/index.ts index b06f9e193c..0bd178131f 100644 --- a/src/search-manager/index.ts +++ b/src/search-manager/index.ts @@ -8,4 +8,4 @@ export { default as SearchSortWidget } from './SearchSortWidget'; export { default as Stats } from './Stats'; export { HIGHLIGHT_PRE_TAG, HIGHLIGHT_POST_TAG } from './data/api'; -export type { CollectionHit, ContentHit } from './data/api'; +export type { CollectionHit, ContentHit, ContentHitTags } from './data/api'; From 48a2d81b22919d22e920bd28e50ef42d44d6bbde Mon Sep 17 00:00:00 2001 From: Navin Karkera Date: Mon, 9 Sep 2024 16:31:32 +0530 Subject: [PATCH 11/16] feat: display both collections and components in recent section --- .../LibraryAuthoringPage.test.tsx | 20 +++++- .../LibraryRecentlyModified.tsx | 66 +++++++++++++++++-- .../__mocks__/library-search.json | 4 +- 3 files changed, 80 insertions(+), 10 deletions(-) diff --git a/src/library-authoring/LibraryAuthoringPage.test.tsx b/src/library-authoring/LibraryAuthoringPage.test.tsx index e5b6c1a14e..62e02d8ea8 100644 --- a/src/library-authoring/LibraryAuthoringPage.test.tsx +++ b/src/library-authoring/LibraryAuthoringPage.test.tsx @@ -433,8 +433,8 @@ describe('', () => { await renderLibraryPage(); // Click on the first component - waitFor(() => expect(screen.queryByText(displayName)).toBeInTheDocument()); - fireEvent.click(screen.getAllByText(displayName)[0]); + expect((await screen.findAllByText(displayName))[0]).toBeInTheDocument(); + fireEvent.click((await screen.findAllByText(displayName))[0]); const sidebar = screen.getByTestId('library-sidebar'); @@ -546,4 +546,20 @@ describe('', () => { expect(screen.getByText(/no matching components/i)).toBeInTheDocument(); }); + + it('shows both components and collections in recently modified section', async () => { + const doc = await renderLibraryPage(); + + expect(await doc.findByText('Content library')).toBeInTheDocument(); + expect((await doc.findAllByText(libraryTitle))[0]).toBeInTheDocument(); + + // "Recently Modified" header + sort shown + expect(doc.getAllByText('Recently Modified').length).toEqual(2); + const recentModifiedContainer = (await doc.findAllByText('Recently Modified'))[1].parentElement?.parentElement?.parentElement; + if (recentModifiedContainer) { + const container = within(recentModifiedContainer); + expect(container.queryAllByText('Text').length).toBeGreaterThan(0); + expect(container.queryAllByText('Collection').length).toBeGreaterThan(0); + } + }); }); diff --git a/src/library-authoring/LibraryRecentlyModified.tsx b/src/library-authoring/LibraryRecentlyModified.tsx index 7708f47ac4..57828871ef 100644 --- a/src/library-authoring/LibraryRecentlyModified.tsx +++ b/src/library-authoring/LibraryRecentlyModified.tsx @@ -1,15 +1,46 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import { useIntl } from '@edx/frontend-platform/i18n'; +import { orderBy } from 'lodash'; +import { CardGrid } from '@openedx/paragon'; import { SearchContextProvider, useSearchContext } from '../search-manager'; -import { SearchSortOption } from '../search-manager/data/api'; -import LibraryComponents from './components/LibraryComponents'; -import LibrarySection from './components/LibrarySection'; +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 { useLibraryBlockTypes } from './data/apiHooks'; +import CollectionCard from './components/CollectionCard'; const RecentlyModified: React.FC<{ libraryId: string }> = ({ libraryId }) => { const intl = useIntl(); - const { totalHits: componentCount } = useSearchContext(); + 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); + + const { data: blockTypesData } = useLibraryBlockTypes(libraryId); + const blockTypes = useMemo(() => { + const result = {}; + if (blockTypesData) { + blockTypesData.forEach(blockType => { + result[blockType.blockType] = blockType; + }); + } + return result; + }, [blockTypesData]); return componentCount > 0 ? ( @@ -17,7 +48,30 @@ const RecentlyModified: React.FC<{ libraryId: string }> = ({ libraryId }) => { title={intl.formatMessage(messages.recentlyModifiedTitle)} contentCount={componentCount} > - + + {recentItems.map((contentHit) => ( + contentHit.type === 'collection' ? ( + + ) : ( + + ) + ))} +
) : null; diff --git a/src/library-authoring/__mocks__/library-search.json b/src/library-authoring/__mocks__/library-search.json index 94bf46a181..f84d16c611 100644 --- a/src/library-authoring/__mocks__/library-search.json +++ b/src/library-authoring/__mocks__/library-search.json @@ -20,7 +20,7 @@ } ], "created": 1721857069.042984, - "modified": 1725398676.078056, + "modified": 1725878053.420395, "last_published": 1725035862.450613, "usage_key": "lb:Axim:TEST:html:571fe018-f3ce-45c9-8f53-5dafcb422fdd", "block_type": "html", @@ -293,7 +293,7 @@ } ], "created": 1725534795.628254, - "modified": 1725534795.628266, + "modified": 1725878053.420395, "context_key": "lib:OpenedX:CSPROB2", "org": "OpenedX", "access_id": 16, From 86fa53e2f45703f56266b7373d231e3ab75bf447 Mon Sep 17 00:00:00 2001 From: Navin Karkera Date: Mon, 9 Sep 2024 21:16:35 +0530 Subject: [PATCH 12/16] refactor: address review comments --- src/hooks.js | 37 ---------- src/hooks.ts | 68 +++++++++++++++++++ src/library-authoring/LibraryCollections.tsx | 3 +- .../components/BaseComponentCard.tsx | 3 +- .../components/CollectionCard.test.tsx | 8 +-- .../components/LibraryComponents.tsx | 3 +- src/search-manager/SearchManager.ts | 31 --------- src/search-manager/index.ts | 2 +- 8 files changed, 78 insertions(+), 77 deletions(-) delete mode 100644 src/hooks.js create mode 100644 src/hooks.ts diff --git a/src/hooks.js b/src/hooks.js deleted file mode 100644 index 73597e3ef6..0000000000 --- a/src/hooks.js +++ /dev/null @@ -1,37 +0,0 @@ -import { useEffect, useState } from 'react'; -import { history } from '@edx/frontend-platform'; - -export const useScrollToHashElement = ({ isLoading }) => { - const [elementWithHash, setElementWithHash] = useState(null); - - useEffect(() => { - const currentHash = window.location.hash.substring(1); - - if (currentHash) { - const element = document.getElementById(currentHash); - if (element) { - element.scrollIntoView(); - history.replace({ hash: '' }); - } - setElementWithHash(currentHash); - } - }, [isLoading]); - - return { elementWithHash }; -}; - -export const useEscapeClick = ({ onEscape, dependency }) => { - useEffect(() => { - const handleEscapeClick = (event) => { - if (event.key === 'Escape') { - onEscape(); - } - }; - - window.addEventListener('keydown', handleEscapeClick); - - return () => { - window.removeEventListener('keydown', handleEscapeClick); - }; - }, [dependency]); -}; diff --git a/src/hooks.ts b/src/hooks.ts new file mode 100644 index 0000000000..1840e1069c --- /dev/null +++ b/src/hooks.ts @@ -0,0 +1,68 @@ +import { useEffect, useState } from 'react'; +import { history } from '@edx/frontend-platform'; + +export const useScrollToHashElement = ({ isLoading }: { isLoading: boolean }) => { + const [elementWithHash, setElementWithHash] = useState(null); + + useEffect(() => { + const currentHash = window.location.hash.substring(1); + + if (currentHash) { + const element = document.getElementById(currentHash); + if (element) { + element.scrollIntoView(); + history.replace({ hash: '' }); + } + setElementWithHash(currentHash); + } + }, [isLoading]); + + return { elementWithHash }; +}; + +export const useEscapeClick = ({ onEscape, dependency }: { onEscape: () => void, dependency: any }) => { + useEffect(() => { + const handleEscapeClick = (event: KeyboardEvent) => { + if (event.key === 'Escape') { + onEscape(); + } + }; + + window.addEventListener('keydown', handleEscapeClick); + + return () => { + window.removeEventListener('keydown', handleEscapeClick); + }; + }, [dependency]); +}; + +/** + * Hook which loads next page of items on scroll + */ +export const useLoadOnScroll = ( + hasNextPage: boolean | undefined, + isFetchingNextPage: boolean, + fetchNextPage: () => void, + enabled: boolean, +) => { + useEffect(() => { + if (enabled) { + const onscroll = () => { + // Verify the position of the scroll to implementa a infinite scroll. + // Used `loadLimit` to fetch next page before reach the end of the screen. + const loadLimit = 300; + const scrolledTo = window.scrollY + window.innerHeight; + const scrollDiff = document.body.scrollHeight - scrolledTo; + const isNearToBottom = scrollDiff <= loadLimit; + if (isNearToBottom && hasNextPage && !isFetchingNextPage) { + fetchNextPage(); + } + }; + window.addEventListener('scroll', onscroll); + return () => { + window.removeEventListener('scroll', onscroll); + }; + } + return () => { }; + }, [hasNextPage, isFetchingNextPage, fetchNextPage]); +}; diff --git a/src/library-authoring/LibraryCollections.tsx b/src/library-authoring/LibraryCollections.tsx index e037277066..24fc112c1b 100644 --- a/src/library-authoring/LibraryCollections.tsx +++ b/src/library-authoring/LibraryCollections.tsx @@ -1,6 +1,7 @@ import { CardGrid } from '@openedx/paragon'; -import { useLoadOnScroll, useSearchContext } from '../search-manager'; +import { useLoadOnScroll } from '../hooks'; +import { useSearchContext } from '../search-manager'; import { NoComponents, NoSearchResults } from './EmptyStates'; import CollectionCard from './components/CollectionCard'; import { LIBRARY_SECTION_PREVIEW_LIMIT } from './components/LibrarySection'; diff --git a/src/library-authoring/components/BaseComponentCard.tsx b/src/library-authoring/components/BaseComponentCard.tsx index fa7d6138bf..1cd77ee7fb 100644 --- a/src/library-authoring/components/BaseComponentCard.tsx +++ b/src/library-authoring/components/BaseComponentCard.tsx @@ -5,7 +5,6 @@ import { Icon, Stack, } from '@openedx/paragon'; -import { ReactNodeLike } from 'prop-types'; import { getItemIcon, getComponentStyleColor } from '../../generic/block-type-utils'; import TagCount from '../../generic/tag-count'; @@ -16,7 +15,7 @@ type BaseComponentCardProps = { displayName: string, description: string, tags: ContentHitTags, - actions: ReactNodeLike, + actions: React.ReactNode, blockTypeDisplayName: string, openInfoSidebar: () => void }; diff --git a/src/library-authoring/components/CollectionCard.test.tsx b/src/library-authoring/components/CollectionCard.test.tsx index 7276dfada4..98ec9c6fd7 100644 --- a/src/library-authoring/components/CollectionCard.test.tsx +++ b/src/library-authoring/components/CollectionCard.test.tsx @@ -3,7 +3,7 @@ import { AppProvider } from '@edx/frontend-platform/react'; import { initializeMockApp } from '@edx/frontend-platform'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { IntlProvider } from '@edx/frontend-platform/i18n'; -import { render } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; import MockAdapter from 'axios-mock-adapter'; import type { Store } from 'redux'; @@ -66,9 +66,9 @@ describe('', () => { }); it('should render the card with title and description', () => { - const { getByText } = render(); + render(); - expect(getByText('Collection Display Formated Name')).toBeInTheDocument(); - expect(getByText('Collection description')).toBeInTheDocument(); + expect(screen.getByText('Collection Display Formated Name')).toBeInTheDocument(); + expect(screen.getByText('Collection description')).toBeInTheDocument(); }); }); diff --git a/src/library-authoring/components/LibraryComponents.tsx b/src/library-authoring/components/LibraryComponents.tsx index 2aa3014b7e..4b6eb6c647 100644 --- a/src/library-authoring/components/LibraryComponents.tsx +++ b/src/library-authoring/components/LibraryComponents.tsx @@ -1,7 +1,8 @@ import React, { useMemo } from 'react'; import { CardGrid } from '@openedx/paragon'; -import { useLoadOnScroll, useSearchContext } from '../../search-manager'; +import { useLoadOnScroll } from '../../hooks'; +import { useSearchContext } from '../../search-manager'; import { NoComponents, NoSearchResults } from '../EmptyStates'; import { useLibraryBlockTypes } from '../data/apiHooks'; import ComponentCard from './ComponentCard'; diff --git a/src/search-manager/SearchManager.ts b/src/search-manager/SearchManager.ts index 7b6992f3c3..d1e925a91a 100644 --- a/src/search-manager/SearchManager.ts +++ b/src/search-manager/SearchManager.ts @@ -184,34 +184,3 @@ export const useSearchContext = () => { } return ctx; }; - -/** - * Hook which loads next page of items on scroll - */ -export const useLoadOnScroll = ( - hasNextPage: boolean | undefined, - isFetchingNextPage: boolean, - fetchNextPage: () => void, - enabled: boolean, -) => { - React.useEffect(() => { - if (enabled) { - const onscroll = () => { - // Verify the position of the scroll to implementa a infinite scroll. - // Used `loadLimit` to fetch next page before reach the end of the screen. - const loadLimit = 300; - const scrolledTo = window.scrollY + window.innerHeight; - const scrollDiff = document.body.scrollHeight - scrolledTo; - const isNearToBottom = scrollDiff <= loadLimit; - if (isNearToBottom && hasNextPage && !isFetchingNextPage) { - fetchNextPage(); - } - }; - window.addEventListener('scroll', onscroll); - return () => { - window.removeEventListener('scroll', onscroll); - }; - } - return () => {}; - }, [hasNextPage, isFetchingNextPage, fetchNextPage]); -}; diff --git a/src/search-manager/index.ts b/src/search-manager/index.ts index 0bd178131f..0f716a9c3e 100644 --- a/src/search-manager/index.ts +++ b/src/search-manager/index.ts @@ -1,4 +1,4 @@ -export { SearchContextProvider, useLoadOnScroll, useSearchContext } from './SearchManager'; +export { SearchContextProvider, useSearchContext } from './SearchManager'; export { default as ClearFiltersButton } from './ClearFiltersButton'; export { default as FilterByBlockType } from './FilterByBlockType'; export { default as FilterByTags } from './FilterByTags'; From 9452c6940b3ca0c2b9dd23bb9da6ea76615e1bfa Mon Sep 17 00:00:00 2001 From: Navin Karkera Date: Tue, 10 Sep 2024 10:41:52 +0530 Subject: [PATCH 13/16] refactor: remove accessId field from collection --- src/library-authoring/components/CollectionCard.test.tsx | 1 - src/search-manager/data/api.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/src/library-authoring/components/CollectionCard.test.tsx b/src/library-authoring/components/CollectionCard.test.tsx index 98ec9c6fd7..db3f752f64 100644 --- a/src/library-authoring/components/CollectionCard.test.tsx +++ b/src/library-authoring/components/CollectionCard.test.tsx @@ -29,7 +29,6 @@ const CollectionHitSample: CollectionHit = { }, created: 1722434322294, modified: 1722434322294, - accessId: 1, tags: {}, }; diff --git a/src/search-manager/data/api.ts b/src/search-manager/data/api.ts index 25851cbac7..8c4cf183d3 100644 --- a/src/search-manager/data/api.ts +++ b/src/search-manager/data/api.ts @@ -135,7 +135,6 @@ export interface ContentHit extends BaseContentHit { */ export interface CollectionHit extends BaseContentHit { description: string; - accessId: number; componentCount?: number; } From 3e32a046c1745e835eaaf570087681a244b74733 Mon Sep 17 00:00:00 2001 From: Navin Karkera Date: Tue, 10 Sep 2024 10:51:46 +0530 Subject: [PATCH 14/16] test: fix test --- src/library-authoring/LibraryAuthoringPage.test.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/library-authoring/LibraryAuthoringPage.test.tsx b/src/library-authoring/LibraryAuthoringPage.test.tsx index 62e02d8ea8..8703256b7c 100644 --- a/src/library-authoring/LibraryAuthoringPage.test.tsx +++ b/src/library-authoring/LibraryAuthoringPage.test.tsx @@ -548,14 +548,14 @@ describe('', () => { }); it('shows both components and collections in recently modified section', async () => { - const doc = await renderLibraryPage(); + await renderLibraryPage(); - expect(await doc.findByText('Content library')).toBeInTheDocument(); - expect((await doc.findAllByText(libraryTitle))[0]).toBeInTheDocument(); + expect(await screen.findByText('Content library')).toBeInTheDocument(); + expect((await screen.findAllByText(libraryTitle))[0]).toBeInTheDocument(); // "Recently Modified" header + sort shown - expect(doc.getAllByText('Recently Modified').length).toEqual(2); - const recentModifiedContainer = (await doc.findAllByText('Recently Modified'))[1].parentElement?.parentElement?.parentElement; + expect(screen.getAllByText('Recently Modified').length).toEqual(2); + const recentModifiedContainer = (await screen.findAllByText('Recently Modified'))[1].parentElement?.parentElement?.parentElement; if (recentModifiedContainer) { const container = within(recentModifiedContainer); expect(container.queryAllByText('Text').length).toBeGreaterThan(0); From d4889ebc636bcab507e8dbfc8ca8a4649e497f24 Mon Sep 17 00:00:00 2001 From: Navin Karkera Date: Thu, 12 Sep 2024 15:24:38 +0530 Subject: [PATCH 15/16] refactor: apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Chris Chávez --- src/hooks.ts | 2 +- src/library-authoring/LibraryAuthoringPage.test.tsx | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/hooks.ts b/src/hooks.ts index 1840e1069c..87b94a4f4d 100644 --- a/src/hooks.ts +++ b/src/hooks.ts @@ -48,7 +48,7 @@ export const useLoadOnScroll = ( useEffect(() => { if (enabled) { const onscroll = () => { - // Verify the position of the scroll to implementa a infinite scroll. + // Verify the position of the scroll to implement an infinite scroll. // Used `loadLimit` to fetch next page before reach the end of the screen. const loadLimit = 300; const scrolledTo = window.scrollY + window.innerHeight; diff --git a/src/library-authoring/LibraryAuthoringPage.test.tsx b/src/library-authoring/LibraryAuthoringPage.test.tsx index 8703256b7c..13d2232968 100644 --- a/src/library-authoring/LibraryAuthoringPage.test.tsx +++ b/src/library-authoring/LibraryAuthoringPage.test.tsx @@ -556,10 +556,10 @@ describe('', () => { // "Recently Modified" header + sort shown expect(screen.getAllByText('Recently Modified').length).toEqual(2); const recentModifiedContainer = (await screen.findAllByText('Recently Modified'))[1].parentElement?.parentElement?.parentElement; - if (recentModifiedContainer) { - const container = within(recentModifiedContainer); - expect(container.queryAllByText('Text').length).toBeGreaterThan(0); - expect(container.queryAllByText('Collection').length).toBeGreaterThan(0); - } + expect(recentModifiedContainer).toBeTruthy(); + + const container = within(recentModifiedContainer); + expect(container.queryAllByText('Text').length).toBeGreaterThan(0); + expect(container.queryAllByText('Collection').length).toBeGreaterThan(0); }); }); From fab32c7bd7bc87f3f5d4df0a9ab011cb30053e4a Mon Sep 17 00:00:00 2001 From: Navin Karkera Date: Thu, 12 Sep 2024 15:40:40 +0530 Subject: [PATCH 16/16] test: reuse testutils --- .../LibraryAuthoringPage.test.tsx | 2 +- .../components/CollectionCard.test.tsx | 41 ++----------------- 2 files changed, 4 insertions(+), 39 deletions(-) diff --git a/src/library-authoring/LibraryAuthoringPage.test.tsx b/src/library-authoring/LibraryAuthoringPage.test.tsx index 13d2232968..e3c7c2d093 100644 --- a/src/library-authoring/LibraryAuthoringPage.test.tsx +++ b/src/library-authoring/LibraryAuthoringPage.test.tsx @@ -558,7 +558,7 @@ describe('', () => { const recentModifiedContainer = (await screen.findAllByText('Recently Modified'))[1].parentElement?.parentElement?.parentElement; expect(recentModifiedContainer).toBeTruthy(); - const container = within(recentModifiedContainer); + const container = within(recentModifiedContainer!); expect(container.queryAllByText('Text').length).toBeGreaterThan(0); expect(container.queryAllByText('Collection').length).toBeGreaterThan(0); }); diff --git a/src/library-authoring/components/CollectionCard.test.tsx b/src/library-authoring/components/CollectionCard.test.tsx index db3f752f64..cc353312e6 100644 --- a/src/library-authoring/components/CollectionCard.test.tsx +++ b/src/library-authoring/components/CollectionCard.test.tsx @@ -1,20 +1,8 @@ -import React from 'react'; -import { AppProvider } from '@edx/frontend-platform/react'; -import { initializeMockApp } from '@edx/frontend-platform'; -import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; -import { IntlProvider } from '@edx/frontend-platform/i18n'; -import { render, screen } from '@testing-library/react'; -import MockAdapter from 'axios-mock-adapter'; -import type { Store } from 'redux'; +import { initializeMocks, render, screen } from '../../testUtils'; -import { ToastProvider } from '../../generic/toast-context'; import { type CollectionHit } from '../../search-manager'; -import initializeStore from '../../store'; import CollectionCard from './CollectionCard'; -let store: Store; -let axiosMock: MockAdapter; - const CollectionHitSample: CollectionHit = { id: '1', type: 'collection', @@ -32,40 +20,17 @@ const CollectionHitSample: CollectionHit = { tags: {}, }; -const RootWrapper = () => ( - - - - - - - -); - describe('', () => { beforeEach(() => { - initializeMockApp({ - authenticatedUser: { - userId: 3, - username: 'abc123', - administrator: true, - roles: [], - }, - }); - store = initializeStore(); - - axiosMock = new MockAdapter(getAuthenticatedHttpClient()); + initializeMocks(); }); afterEach(() => { jest.clearAllMocks(); - axiosMock.restore(); }); it('should render the card with title and description', () => { - render(); + render(); expect(screen.getByText('Collection Display Formated Name')).toBeInTheDocument(); expect(screen.getByText('Collection description')).toBeInTheDocument();