Skip to content

Commit 002143f

Browse files
committed
feat: add collections query to search results
1 parent dcf05cd commit 002143f

File tree

4 files changed

+92
-8
lines changed

4 files changed

+92
-8
lines changed
Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,36 @@
1-
import React from 'react';
1+
import React, { useEffect, useMemo } from 'react';
22
import { FormattedMessage } from '@edx/frontend-platform/i18n';
33

44
import messages from './messages';
5+
import { useSearchContext } from '../search-manager';
56

6-
const LibraryCollections = () => (
7-
<div className="d-flex my-6 justify-content-center">
8-
<FormattedMessage
9-
{...messages.collectionsTempPlaceholder}
10-
/>
7+
type LibraryCollectionsProps = {
8+
libraryId: string,
9+
variant: 'full' | 'preview',
10+
};
11+
12+
/**
13+
* Library Collections to show collections grid
14+
*
15+
* Use style to:
16+
* - 'full': Show all collections with Infinite scroll pagination.
17+
* - 'preview': Show first 4 collections without pagination.
18+
*/
19+
const LibraryCollections = ({ libraryId, variant }: LibraryCollectionsProps) => {
20+
const {
21+
collectionHits,
22+
totalCollectionHits,
23+
isFetchingNextPage,
24+
hasNextPage,
25+
fetchNextPage,
26+
isFiltered,
27+
setExtraFilter,
28+
} = useSearchContext();
29+
30+
// __AUTO_GENERATED_PRINT_VAR_START__
31+
console.log("LibraryCollections collectionHits: ", collectionHits); // __AUTO_GENERATED_PRINT_VAR_END__
32+
return <div className="d-flex my-6 justify-content-center">
1133
</div>
12-
);
34+
};
1335

1436
export default LibraryCollections;

src/search-manager/SearchManager.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import React from 'react';
99
import { useSearchParams } from 'react-router-dom';
1010
import { MeiliSearch, type Filter } from 'meilisearch';
1111

12-
import { ContentHit, SearchSortOption, forceArray } from './data/api';
12+
import { CollectionHit, ContentHit, SearchSortOption, forceArray } from './data/api';
1313
import { useContentSearchConnection, useContentSearchResults } from './data/apiHooks';
1414

1515
export interface SearchContextData {
@@ -40,6 +40,8 @@ export interface SearchContextData {
4040
fetchNextPage: () => void;
4141
closeSearchModal: () => void;
4242
hasError: boolean;
43+
collectionHits: CollectionHit[];
44+
totalCollectionHits: number;
4345
}
4446

4547
const SearchContext = React.createContext<SearchContextData | undefined>(undefined);

src/search-manager/data/api.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,31 @@ export interface ContentHit {
121121
lastPublished: number | null;
122122
}
123123

124+
/**
125+
* Information about a single collection returned in the search results
126+
* Defined in edx-platform/openedx/core/djangoapps/content/search/documents.py
127+
*/
128+
export interface CollectionHit {
129+
id: string;
130+
type: 'collection';
131+
displayName: string;
132+
description: string;
133+
/** The course or library ID */
134+
contextKey: string;
135+
org: string;
136+
/**
137+
* Breadcrumbs:
138+
* - First one is the name of the course/library itself.
139+
* - After that is the name and usage key of any parent Section/Subsection/Unit/etc.
140+
*/
141+
breadcrumbs: Array<{ displayName: string }>;
142+
// tags: ContentHitTags;
143+
/** Same fields with <mark>...</mark> highlights */
144+
created: number;
145+
modified: number;
146+
accessId: number;
147+
}
148+
124149
/**
125150
* Convert search hits to camelCase
126151
* @param hit A search result directly from Meilisearch
@@ -185,13 +210,16 @@ export async function fetchSearchResults({
185210
...problemTypesFilterFormatted,
186211
].flat()];
187212

213+
const collectionsFilter = 'type = "collection"'
214+
188215
// First query is always to get the hits, with all the filters applied.
189216
queries.push({
190217
indexUid: indexName,
191218
q: searchKeywords,
192219
filter: [
193220
// top-level entries in the array are AND conditions and must all match
194221
// Inner arrays are OR conditions, where only one needs to match.
222+
`NOT ${collectionsFilter}`, // exclude collections
195223
...typeFilters,
196224
...extraFilterFormatted,
197225
...tagsFilterFormatted,
@@ -219,13 +247,37 @@ export async function fetchSearchResults({
219247
limit: 0, // We don't need any "hits" for this - just the facetDistribution
220248
});
221249

250+
// Third query is to get the hits for collections, with all the filters applied.
251+
queries.push({
252+
indexUid: indexName,
253+
q: searchKeywords,
254+
filter: [
255+
// top-level entries in the array are AND conditions and must all match
256+
// Inner arrays are OR conditions, where only one needs to match.
257+
collectionsFilter, // include only collections
258+
...typeFilters,
259+
...extraFilterFormatted,
260+
...tagsFilterFormatted,
261+
],
262+
attributesToHighlight: ['display_name', 'content'],
263+
highlightPreTag: HIGHLIGHT_PRE_TAG,
264+
highlightPostTag: HIGHLIGHT_POST_TAG,
265+
attributesToCrop: ['content'],
266+
cropLength: 20,
267+
sort,
268+
offset,
269+
limit,
270+
});
271+
222272
const { results } = await client.multiSearch(({ queries }));
223273
return {
224274
hits: results[0].hits.map(formatSearchHit),
225275
totalHits: results[0].totalHits ?? results[0].estimatedTotalHits ?? results[0].hits.length,
226276
blockTypes: results[1].facetDistribution?.block_type ?? {},
227277
problemTypes: results[1].facetDistribution?.['content.problem_types'] ?? {},
228278
nextOffset: results[0].hits.length === limit ? offset + limit : undefined,
279+
collectionHits: results[2].hits.map(formatSearchHit),
280+
totalCollectionHits: results[2].totalHits ?? results[2].estimatedTotalHits ?? results[2].hits.length,
229281
};
230282
}
231283

src/search-manager/data/apiHooks.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,14 @@ export const useContentSearchResults = ({
103103
[pages],
104104
);
105105

106+
const collectionHits = React.useMemo(
107+
() => pages?.reduce((allHits, page) => [...allHits, ...page.collectionHits], []) ?? [],
108+
[pages],
109+
);
110+
106111
return {
107112
hits,
113+
collectionHits,
108114
// The distribution of block type filter options
109115
blockTypes: pages?.[0]?.blockTypes ?? {},
110116
problemTypes: pages?.[0]?.problemTypes ?? {},
@@ -119,6 +125,8 @@ export const useContentSearchResults = ({
119125
hasNextPage: query.hasNextPage,
120126
// The last page has the most accurate count of total hits
121127
totalHits: pages?.[pages.length - 1]?.totalHits ?? 0,
128+
// The last page has the most accurate count of total hits
129+
totalCollectionHits: pages?.[pages.length - 1]?.totalCollectionHits ?? 0,
122130
};
123131
};
124132

0 commit comments

Comments
 (0)