diff --git a/packages/catalog-search/src/SearchBox.jsx b/packages/catalog-search/src/SearchBox.jsx index 5d76226c..07a73268 100644 --- a/packages/catalog-search/src/SearchBox.jsx +++ b/packages/catalog-search/src/SearchBox.jsx @@ -50,6 +50,7 @@ export const SearchBoxBase = ({ const [autocompleteHits, setAutocompleteHits] = useState([]); const [showSuggestions, setShowSuggestions] = useState(false); const [searchQuery, setSearchQuery] = useState(''); + const [preQueryHits, setPreQueryHits] = useState([]); /** * Handles when a search is submitted by adding the user's search @@ -103,15 +104,23 @@ export const SearchBoxBase = ({ attributesToRetrieve: ALGOLIA_ATTRIBUTES_TO_RETRIEVE, }); if (nbHits > 0) { + setPreQueryHits([]); setAutocompleteHits(hits); setShowSuggestions(true); } else { // If there are no results of the suggested search, hide the empty suggestion component setShowSuggestions(false); } - // Hide the results as soon as the user removes the entire query string, instead of waiting a second + // Display the prequery results when user clicks on search box but has not began typing } else { - setShowSuggestions(false); + const { hits } = await index.search(query, { + filters, + attributesToHighlight: ['title'], + attributesToRetrieve: ALGOLIA_ATTRIBUTES_TO_RETRIEVE, + }); + setAutocompleteHits([]); + setPreQueryHits(hits); + setShowSuggestions(true); } }; // Since the debounced method is called in a useEffect hook, use `useCallback` to account for repeated invoking of the @@ -155,6 +164,9 @@ export const SearchBoxBase = ({ value={defaultRefinement} onSubmit={handleSubmit} onClear={handleClear} + onFocus={(query) => { + setSearchQuery(query); + }} onChange={(query) => { setSearchQuery(query); }} @@ -172,6 +184,7 @@ export const SearchBoxBase = ({ { showSuggestions && ( handleSubmit(searchQuery)} handleSuggestionClickSubmit={hit => handleSuggestionSubmit(hit)} diff --git a/packages/catalog-search/src/SearchSuggestionItem.jsx b/packages/catalog-search/src/SearchSuggestionItem.jsx index 5b19ca1a..7d0546ce 100644 --- a/packages/catalog-search/src/SearchSuggestionItem.jsx +++ b/packages/catalog-search/src/SearchSuggestionItem.jsx @@ -1,9 +1,10 @@ import React from 'react'; +import { Image } from '@edx/paragon'; import { Link } from 'react-router-dom'; import PropTypes from 'prop-types'; const SearchSuggestionItem = ({ - url, suggestionItemHandler, hit, disableSuggestionRedirect, + url, suggestionItemHandler, hit, disableSuggestionRedirect, isPreQuery, }) => { const authoringOrganization = hit.key && hit.key.split('+')[0]; // If the disable redirect bool is provided, prevent the redirect from happening and instead call the provided submit @@ -14,28 +15,44 @@ const SearchSuggestionItem = ({ suggestionItemHandler(hit); } }; - return ( - -
- { /* eslint-disable-next-line react/no-danger, no-underscore-dangle */ } -
- { - authoringOrganization && ( -
- {authoringOrganization} +
+ {isPreQuery ? ( + +
+ +
+ {/* eslint-disable-next-line react/no-danger, no-underscore-dangle */} +
+
+ {hit.partners[0]?.name} | {hit.learning_type} +
- ) - } -
- { - hit.program_type && ( -

- {hit.program_type} -

- ) - } - +
+ + ) : ( + +
+ { /* eslint-disable-next-line react/no-danger, no-underscore-dangle */} +
+ { + authoringOrganization && ( +
+ {authoringOrganization} +
+ ) + } +
+ { + hit.program_type && ( +

+ {hit.program_type} +

+ ) + } + + )} +
); }; @@ -47,8 +64,16 @@ SearchSuggestionItem.propTypes = { title: PropTypes.string, program_type: PropTypes.string, _highlightResult: PropTypes.shape({ title: PropTypes.shape({ value: PropTypes.string }) }), + card_image_url: PropTypes.string, + partners: PropTypes.arrayOf( + PropTypes.shape({ + name: PropTypes.string, + }), + ), + learning_type: PropTypes.string, }).isRequired, disableSuggestionRedirect: PropTypes.bool.isRequired, + isPreQuery: PropTypes.bool.isRequired, }; SearchSuggestionItem.defaultProps = { diff --git a/packages/catalog-search/src/SearchSuggestions.jsx b/packages/catalog-search/src/SearchSuggestions.jsx index af8f743d..1e665eac 100644 --- a/packages/catalog-search/src/SearchSuggestions.jsx +++ b/packages/catalog-search/src/SearchSuggestions.jsx @@ -4,10 +4,12 @@ import PropTypes from 'prop-types'; import { MAX_NUM_SUGGESTIONS, LEARNING_TYPE_COURSE, LEARNING_TYPE_PROGRAM, LEARNING_TYPE_EXECUTIVE_EDUCATION, COURSE_TYPE_EXECUTIVE_EDUCATION, + MAX_NUM_PRE_QUERY_SUGGESTIONS, } from './data/constants'; import SearchSuggestionItem from './SearchSuggestionItem'; const SearchSuggestions = ({ + preQueryHits, autoCompleteHits, enterpriseSlug, handleSubmit, @@ -24,9 +26,16 @@ const SearchSuggestions = ({ }; const getLinkToProgram = (program) => `/${enterpriseSlug}/program/${program.aggregation_key.split(':').pop()}`; + const preQuerySuggestions = []; const courses = []; const programs = []; const execEdCourses = []; + + if (preQueryHits) { + preQueryHits.forEach((hit) => { + preQuerySuggestions.push(hit); + }); + } autoCompleteHits.forEach((hit) => { const { learning_type: learningType } = hit; if (learningType === LEARNING_TYPE_COURSE) { courses.push(hit); } @@ -35,6 +44,36 @@ const SearchSuggestions = ({ }); return (
+ {preQuerySuggestions.length > 0 && ( +
+
+ Top-rated courses +
+ { + preQuerySuggestions.slice(0, MAX_NUM_PRE_QUERY_SUGGESTIONS) + .map((hit) => { + const getUrl = (course) => { + const { learning_type: learningType } = course; + if (learningType === LEARNING_TYPE_COURSE || learningType === LEARNING_TYPE_EXECUTIVE_EDUCATION) { + return getLinkToCourse(course); + } + return getLinkToProgram(course); + }; + + return ( + 0} + disableSuggestionRedirect={disableSuggestionRedirect} + suggestionItemHandler={handleSuggestionClickSubmit} + /> + ); + }) + } +
+ )} {courses.length > 0 && (
@@ -47,6 +86,7 @@ const SearchSuggestions = ({ key={hit.title} url={getLinkToCourse(hit)} hit={hit} + isPreQuery={preQuerySuggestions.length > 0} disableSuggestionRedirect={disableSuggestionRedirect} suggestionItemHandler={handleSuggestionClickSubmit} /> @@ -107,6 +147,7 @@ SearchSuggestions.propTypes = { handleSubmit: PropTypes.func, handleSuggestionClickSubmit: PropTypes.func, disableSuggestionRedirect: PropTypes.bool, + preQueryHits: PropTypes.arrayOf(PropTypes.shape()).isRequired, }; SearchSuggestions.defaultProps = { diff --git a/packages/catalog-search/src/data/constants.js b/packages/catalog-search/src/data/constants.js index e000b0fc..bf11b32b 100644 --- a/packages/catalog-search/src/data/constants.js +++ b/packages/catalog-search/src/data/constants.js @@ -87,6 +87,7 @@ export const NUM_CURRENT_REFINEMENTS_TO_DISPLAY = 3; export const NUM_RESULTS_PER_PAGE = 24; export const MAX_NUM_SUGGESTIONS = 3; +export const MAX_NUM_PRE_QUERY_SUGGESTIONS = 5; export const NO_OPTIONS_FOUND = 'No options found.'; diff --git a/packages/catalog-search/src/styles/_SearchSuggestions.scss b/packages/catalog-search/src/styles/_SearchSuggestions.scss index 7d589775..b75aadfc 100644 --- a/packages/catalog-search/src/styles/_SearchSuggestions.scss +++ b/packages/catalog-search/src/styles/_SearchSuggestions.scss @@ -1,4 +1,4 @@ -.suggestions{ +.suggestions { position: absolute; background: white; margin-top: auto; @@ -13,53 +13,84 @@ margin: 5px 10px 5px 10px; padding: 8px; color: #000; + font-size: 75%; white-space: pre-wrap; - &:hover{ - color:white; - background-color:#000000 ; - text-decoration: none; - } - &:hover span { - color:white; + &:hover { + color: white; + background-color: #000000; + text-decoration: none; } - em { - font-weight: bold; - font-style: normal; - &:hover{ + + &:hover span { color: white; } - &:not(:first-child) { - margin-left: 4px; // add space between _highlightResult.title.value hits + + em { + font-weight: bold; + font-style: normal; + + &:hover { + color: white; + } + + &:not(:first-child) { + margin-left: 4px; // add space between _highlightResult.title.value hits + } } -} - div{ + + div { display: flex; } - .authoring-org-badge{ + + .authoring-org-badge { line-height: 1.5; } - .program-type{ + .program-type { font-size: .9rem; margin-bottom: 0px; } } +.prequery-item { + margin: 5px 10px 5px 10px; + color: #000; + white-space: pre-wrap; + + &:hover { + color: white; + background-color: #000000; + text-decoration: none; + } + + &:hover span { + color: white; + } +} + +.prequery-image { + width: 40px; + height: 40px; + object-fit: cover; + border-radius: 0 !important; +} + .suggestion-heading { - color: #707070!important + color: #707070 !important } .view-all-btn { - color:#000; + color: #000; border-radius: 0px; - &:hover{ - color:#000; + + &:hover { + color: #000; } } .suggestions-section { color: #707070; font-size: .9rem; -} +} \ No newline at end of file diff --git a/packages/catalog-search/src/tests/SearchSuggestions.test.jsx b/packages/catalog-search/src/tests/SearchSuggestions.test.jsx index 1d322c1c..747a0edc 100644 --- a/packages/catalog-search/src/tests/SearchSuggestions.test.jsx +++ b/packages/catalog-search/src/tests/SearchSuggestions.test.jsx @@ -13,6 +13,9 @@ const fakeSuggestionsData = { key: 'edX+courseX', title: 'test-course', _highlightResult: { title: { value: 'test-course' } }, + partners: [{ + name: 'edx-partner', + }], }, { learning_type: 'program', @@ -22,6 +25,9 @@ const fakeSuggestionsData = { aggregation_key: '123:456', authoring_organizations: [{ key: 'harvard' }], program_type: 'xSeries', + partners: [{ + name: 'harvard-partner', + }], }, ], }; @@ -74,7 +80,17 @@ describe('', () => { expect(screen.getByText('xSeries')).not.toBeNull(); expect(screen.getByText('View all results')).not.toBeNull(); }); - + test('renders only prequery suggestions', () => { + renderWithRouter(); + expect(screen.getByText('Top-rated courses')).not.toBeNull(); + expect(screen.queryByText('Courses')).toBeNull(); + expect(screen.queryByText('Programs')).toBeNull(); + }); test('renders no errors when no authoring orgs found for programs data', () => { renderWithRouter(