Skip to content

Commit

Permalink
fix: hide video banner if no videos are available (#1254)
Browse files Browse the repository at this point in the history
  • Loading branch information
omar-sarfraz authored Feb 7, 2025
1 parent 81c544f commit 039e151
Show file tree
Hide file tree
Showing 12 changed files with 109 additions and 16 deletions.
3 changes: 2 additions & 1 deletion src/components/microlearning/VideoBanner.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { FormattedMessage } from '@edx/frontend-platform/i18n';
import BetaBadge from './BetaBadge';
import { useEnterpriseCustomer } from '../app/data';
import './styles/VideoDetailPage.scss';
import { SEARCH_INDEX_IDS } from '../../constants';

const VideoBanner = () => {
const { data: enterpriseCustomer } = useEnterpriseCustomer();
Expand Down Expand Up @@ -55,7 +56,7 @@ const VideoBanner = () => {
<Card.Footer className="col-3 justify-content-end">
<Button
as={Link}
to="#videos-section"
to={`#${SEARCH_INDEX_IDS.VIDEOS}`}
variant="outline-primary"
onClick={sendPushEvent}
>
Expand Down
20 changes: 17 additions & 3 deletions src/components/search/Search.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { useContext, useEffect } from 'react';
import {
useCallback, useContext, useEffect, useState,
} from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { Helmet } from 'react-helmet';
import { Configure, InstantSearch } from 'react-instantsearch-dom';
Expand Down Expand Up @@ -79,12 +81,22 @@ const Search = () => {
closePathwayModal,
} = useSearchPathwayModal();

const [shouldShowVideosBanner, setShouldShowVideosBanner] = useState(false);

const enableVideos = (
canOnlyViewHighlightSets === false
&& features.FEATURE_ENABLE_VIDEO_CATALOG
&& hasValidLicenseOrSubRequest
);

const showVideosBanner = useCallback(() => {
setShouldShowVideosBanner(true);
}, []);

const hideVideosBanner = useCallback(() => {
setShouldShowVideosBanner(false);
}, []);

const PAGE_TITLE = intl.formatMessage({
id: 'enterprise.search.page.title',
defaultMessage: 'Search Courses and Programs - {enterpriseName}',
Expand Down Expand Up @@ -153,13 +165,15 @@ const Search = () => {
{/* No content type refinement */}
{(contentType === undefined || contentType.length === 0) && (
<Stack className="my-5" gap={5}>
{enableVideos && <VideoBanner />}
{shouldShowVideosBanner && <VideoBanner />}
{!hasRefinements && <ContentHighlights />}
{canOnlyViewHighlightSets === false && enterpriseCustomer.enableAcademies && <SearchAcademy />}
{features.ENABLE_PATHWAYS && (canOnlyViewHighlightSets === false) && <SearchPathway filter={filters} />}
{features.ENABLE_PROGRAMS && (canOnlyViewHighlightSets === false) && <SearchProgram filter={filters} />}
{canOnlyViewHighlightSets === false && <SearchCourse filter={filters} />}
{enableVideos && <SearchVideo filter={filters} />}
{enableVideos && (
<SearchVideo filter={filters} showVideosBanner={showVideosBanner} hideVideosBanner={hideVideosBanner} />
)}
</Stack>
)}
{/* render a single contentType if the refinement exist and is either a course, program or learnerpathway */}
Expand Down
4 changes: 3 additions & 1 deletion src/components/search/SearchCourse.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Configure, Index } from 'react-instantsearch-dom';
import { getConfig } from '@edx/frontend-platform/config';
import { useIntl } from '@edx/frontend-platform/i18n';
import { NUM_RESULTS_COURSE, CONTENT_TYPE_COURSE, COURSE_TITLE } from './constants';
import { SEARCH_INDEX_IDS } from '../../constants';
import SearchResults from './SearchResults';
import SearchCourseCard from './SearchCourseCard';

Expand All @@ -11,7 +12,7 @@ const SearchCourse = ({ filter }) => {
const config = getConfig();
const intl = useIntl();
return (
<Index indexName={config.ALGOLIA_INDEX_NAME} indexId="search-courses">
<Index indexName={config.ALGOLIA_INDEX_NAME} indexId={SEARCH_INDEX_IDS.COURSE}>
<Configure
hitsPerPage={NUM_RESULTS_COURSE}
filters={defaultFilter}
Expand All @@ -27,6 +28,7 @@ const SearchCourse = ({ filter }) => {
description: 'Translated title for the enterprise search page course section.',
})
}
componentId={SEARCH_INDEX_IDS.COURSE}
/>
</Index>
);
Expand Down
4 changes: 3 additions & 1 deletion src/components/search/SearchPathway.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { getConfig } from '@edx/frontend-platform/config';
import { useIntl } from '@edx/frontend-platform/i18n';

import { NUM_RESULTS_PATHWAY, CONTENT_TYPE_PATHWAY, PATHWAY_TITLE } from './constants';
import { SEARCH_INDEX_IDS } from '../../constants';
import SearchResults from './SearchResults';
import SearchPathwayCard from '../pathway/SearchPathwayCard';

Expand All @@ -12,7 +13,7 @@ const SearchPathway = ({ filter }) => {
const config = getConfig();
const intl = useIntl();
return (
<Index indexName={config.ALGOLIA_INDEX_NAME} indexId="search-pathways">
<Index indexName={config.ALGOLIA_INDEX_NAME} indexId={SEARCH_INDEX_IDS.PATHWAYS}>
<Configure
hitsPerPage={NUM_RESULTS_PATHWAY}
filters={defaultFilter}
Expand All @@ -29,6 +30,7 @@ const SearchPathway = ({ filter }) => {
})
}
isPathwaySearchResults
componentId={SEARCH_INDEX_IDS.PATHWAYS}
/>
</Index>
);
Expand Down
4 changes: 3 additions & 1 deletion src/components/search/SearchProgram.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { getConfig } from '@edx/frontend-platform/config';
import { useIntl } from '@edx/frontend-platform/i18n';

import { NUM_RESULTS_PROGRAM, CONTENT_TYPE_PROGRAM, PROGRAM_TITLE } from './constants';
import { SEARCH_INDEX_IDS } from '../../constants';
import SearchResults from './SearchResults';
import SearchProgramCard from './SearchProgramCard';

Expand All @@ -12,7 +13,7 @@ const SearchProgram = ({ filter }) => {
const config = getConfig();
const intl = useIntl();
return (
<Index indexName={config.ALGOLIA_INDEX_NAME} indexId="search-programs">
<Index indexName={config.ALGOLIA_INDEX_NAME} indexId={SEARCH_INDEX_IDS.PROGRAMS}>
<Configure
hitsPerPage={NUM_RESULTS_PROGRAM}
filters={defaultFilter}
Expand All @@ -28,6 +29,7 @@ const SearchProgram = ({ filter }) => {
description: 'Translated title for the enterprise search page program section.',
})
}
componentId={SEARCH_INDEX_IDS.PROGRAMS}
/>
</Index>
);
Expand Down
23 changes: 21 additions & 2 deletions src/components/search/SearchResults.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useContext, useMemo } from 'react';
import { useContext, useEffect, useMemo } from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { connectStateResults } from 'react-instantsearch-dom';
Expand Down Expand Up @@ -34,6 +34,8 @@ const SearchResults = ({
translatedTitle,
isPathwaySearchResults,
showBetaBadge,
componentId,
handlers,
}) => {
const { refinements, dispatch } = useContext(SearchContext);
const nbHits = useNbHitsFromSearchResults(searchResults);
Expand Down Expand Up @@ -102,7 +104,7 @@ const SearchResults = ({
// Theses changes are temporary and will be removed once the BetaBadge component is removed from
// the SearchResults component
return (
<div className="d-flex align-items-center" id={showBetaBadge ? 'videos-section' : 'some-other-section'}>
<div className="d-flex align-items-center" id={componentId}>
{translatedTitle || title} ({nbHits} {resultsLabel}) {showBetaBadge && <BetaBadge />}
{query && <>{' '}for &quot;{query}&quot;</>}
</div>
Expand All @@ -112,6 +114,14 @@ const SearchResults = ({
[nbHits, query, title],
);

useEffect(() => {
if (nbHits > 0) {
handlers.searchResults?.();
} else {
handlers.noSearchResults?.();
}
}, [nbHits, handlers]);

const SkeletonCard = getSkeletonCardFromTitle(title);

const mappedHitsCards = useMemo(
Expand Down Expand Up @@ -202,6 +212,11 @@ SearchResults.propTypes = {
translatedTitle: PropTypes.string,
isPathwaySearchResults: PropTypes.bool,
showBetaBadge: PropTypes.bool,
componentId: PropTypes.string.isRequired,
handlers: PropTypes.shape({
searchResults: PropTypes.func,
noSearchResults: PropTypes.func,
}),
};

SearchResults.defaultProps = {
Expand All @@ -213,6 +228,10 @@ SearchResults.defaultProps = {
translatedTitle: undefined,
isPathwaySearchResults: false,
showBetaBadge: false,
handlers: {
searchResults: () => {},
noSearchResults: () => {},
},
};

export default connectStateResults(SearchResults);
12 changes: 10 additions & 2 deletions src/components/search/SearchVideo.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,17 @@ import { Configure, Index } from 'react-instantsearch-dom';
import { getConfig } from '@edx/frontend-platform/config';
import { useIntl } from '@edx/frontend-platform/i18n';
import { VIDEO_TITLE, NUM_RESULTS_VIDEO, CONTENT_TYPE_VIDEO } from './constants';
import { SEARCH_INDEX_IDS } from '../../constants';
import SearchResults from './SearchResults';
import SearchVideoCard from './SearchVideoCard';

const SearchVideo = ({ filter }) => {
const SearchVideo = ({ filter, showVideosBanner, hideVideosBanner }) => {
const defaultFilter = `content_type:${CONTENT_TYPE_VIDEO} AND ${filter}`;
const config = getConfig();
const intl = useIntl();

return (
<Index indexName={config.ALGOLIA_REPLICA_INDEX_NAME} indexId="search-videos">
<Index indexName={config.ALGOLIA_REPLICA_INDEX_NAME} indexId={SEARCH_INDEX_IDS.VIDEOS}>
<Configure
hitsPerPage={NUM_RESULTS_VIDEO}
filters={defaultFilter}
Expand All @@ -28,6 +29,11 @@ const SearchVideo = ({ filter }) => {
description: 'Translated title for the enterprise search page videos section.',
})
}
componentId={SEARCH_INDEX_IDS.VIDEOS}
handlers={{
searchResults: showVideosBanner,
noSearchResults: hideVideosBanner,
}}
showBetaBadge
/>
</Index>
Expand All @@ -36,6 +42,8 @@ const SearchVideo = ({ filter }) => {

SearchVideo.propTypes = {
filter: PropTypes.string.isRequired,
showVideosBanner: PropTypes.func.isRequired,
hideVideosBanner: PropTypes.func.isRequired,
};

export default SearchVideo;
14 changes: 13 additions & 1 deletion src/components/search/tests/Search.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { IntlProvider } from '@edx/frontend-platform/i18n';
import { SearchContext } from '@edx/frontend-enterprise-catalog-search';
import { AppContext } from '@edx/frontend-platform/react';
import { QueryClientProvider } from '@tanstack/react-query';
import '../../skills-quiz/__mocks__/react-instantsearch-dom';
import * as MockReactInstantSearch from '../../skills-quiz/__mocks__/react-instantsearch-dom';
import { queryClient, renderWithRouter } from '../../../utils/tests';
import '@testing-library/jest-dom';
import Search from '../Search';
Expand Down Expand Up @@ -78,6 +78,7 @@ describe('<Search />', () => {
useDefaultSearchFilters.mockReturnValue(mockFilter);
useHasValidLicenseOrSubscriptionRequestsEnabled.mockReturnValue(true);
useAlgoliaSearch.mockReturnValue([{ search: jest.fn(), appId: 'test-app-id' }, { indexName: 'mock-index-name' }]);
MockReactInstantSearch.configure.nbHits = 2;
});
test('renders the video beta banner component', () => {
features.FEATURE_ENABLE_VIDEO_CATALOG = true;
Expand All @@ -88,4 +89,15 @@ describe('<Search />', () => {
);
expect(screen.getByText('Videos Now Available with Your Subscription')).toBeInTheDocument();
});
test('renders correctly when no search results are found', () => {
MockReactInstantSearch.configure.nbHits = 0;

renderWithRouter(
<SearchWrapper>
<Search />
</SearchWrapper>,
);

expect(screen.queryByText('Videos Now Available with Your Subscription')).toBeNull();
});
});
22 changes: 22 additions & 0 deletions src/components/search/tests/SearchResults.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -430,4 +430,26 @@ describe('<SearchResults />', () => {
expect(screen.queryByText(new RegExp(noResultsMessage.messageTitle, 'i'))).toBeNull();
expect(screen.queryByText(new RegExp(noResultsMessage.messageContent, 'i'))).toBeNull();
});

test('calls noSearchResults handler when no results are found', async () => {
const noSearchResultsMock = jest.fn();
renderWithRouter(
<SearchResultsWithContext {...propsForNoVideoResults} handlers={{ noSearchResults: noSearchResultsMock }} />,
);

await waitFor(() => {
expect(noSearchResultsMock).toHaveBeenCalled();
});
});

test('calls searchResults handler when results are found', async () => {
const searchResultsMock = jest.fn();
renderWithRouter(
<SearchResultsWithContext {...propsForVideoResults} handlers={{ searchResults: searchResultsMock }} />,
);

await waitFor(() => {
expect(searchResultsMock).toHaveBeenCalled();
});
});
});
12 changes: 8 additions & 4 deletions src/components/skills-quiz/__mocks__/react-instantsearch-dom.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,17 @@ const fakeHits = [
},
];

MockReactInstantSearch.configure = {
nbHits: 2,
};

MockReactInstantSearch.connectStateResults = Component => (props) => (
<Component
searchResults={{
hits: fakeHits,
hits: MockReactInstantSearch.configure.nbHits === 0 ? [] : fakeHits,
hitsPerPage: 25,
nbHits: 2,
nbPages: 1,
nbHits: MockReactInstantSearch.configure.nbHits,
nbPages: MockReactInstantSearch.configure.nbHits === 0 ? 0 : 1,
page: 1,
}}
isSearchStalled={false}
Expand All @@ -48,7 +52,7 @@ MockReactInstantSearch.connectStateResults = Component => (props) => (

MockReactInstantSearch.connectPagination = Component => (props) => (
<Component
nbPages={1}
nbPages={MockReactInstantSearch.configure.nbHits === 0 ? 0 : 1}
currentRefinement={1}
maxPagesDisplayed={5}
{...props}
Expand Down
1 change: 1 addition & 0 deletions src/constants/index.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './course';
export * from './subsidies';
export * from './search';
6 changes: 6 additions & 0 deletions src/constants/search.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const SEARCH_INDEX_IDS = {
COURSE: 'search-courses',
PATHWAYS: 'search-pathways',
PROGRAMS: 'search-programs',
VIDEOS: 'search-videos',
};

0 comments on commit 039e151

Please sign in to comment.