<>
- {(!contentType || contentType.length === 2)
- && (noCourseResults === noProgramResults || !noCourseResults) && (
- <>
-
- >
- )}
- {(!contentType || contentType.length === 2)
- && noCourseResults
- && !noProgramResults && (
- <>
-
- >
- )}
- {specifiedContentType === CONTENT_TYPE_PROGRAM && (
-
- )}
- {specifiedContentType === CONTENT_TYPE_COURSE && (
-
- )}
+ {contentToRender([...contentWithResults, ...contentWithoutResults])}
>
diff --git a/src/components/catalogs/CatalogSearch.test.jsx b/src/components/catalogs/CatalogSearch.test.jsx
index ea8b6751..69b9e4a4 100644
--- a/src/components/catalogs/CatalogSearch.test.jsx
+++ b/src/components/catalogs/CatalogSearch.test.jsx
@@ -18,10 +18,13 @@ jest.mock('react-instantsearch-dom', () => ({
const DEFAULT_SEARCH_CONTEXT_VALUE = { refinements: {} };
const COURSE_SEARCH_CONTEXT_VALUE = {
- refinements: { content_type: ['course'] },
+ refinements: { learning_type: ['course'] },
};
const PROGRAM_SEARCH_CONTEXT_VALUE = {
- refinements: { content_type: ['program'] },
+ refinements: { learning_type: ['program'] },
+};
+const EXEC_ED_COURSE_SEARCH_CONTEXT_VALUE = {
+ refinements: { learning_type: ['executive-education-2u'] },
};
// eslint-disable-next-line react/prop-types
@@ -64,4 +67,12 @@ describe('Catalog Search component', () => {
);
expect(screen.getByText('SEARCH')).toBeInTheDocument();
});
+ it('properly renders component with exec ed course content type context', () => {
+ renderWithRouter(
+
,
+ );
+ expect(screen.getByText('SEARCH')).toBeInTheDocument();
+ });
});
diff --git a/src/components/courseCard/CourseCard.jsx b/src/components/courseCard/CourseCard.jsx
index b0c09017..f2f9d0a0 100644
--- a/src/components/courseCard/CourseCard.jsx
+++ b/src/components/courseCard/CourseCard.jsx
@@ -6,19 +6,32 @@ import PropTypes from 'prop-types';
import { Badge, Card } from '@edx/paragon';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import messages from './CourseCard.messages';
+import { CONTENT_TYPE_COURSE } from '../../constants';
import defaultCardHeader from '../../static/default-card-header-light.png';
-function CourseCard({ intl, onClick, original }) {
+function CourseCard({
+ intl, onClick, original, learningType,
+}) {
const {
title,
card_image_url,
partners,
first_enrollable_paid_seat_price,
enterprise_catalog_query_titles,
- availability,
+ entitlements,
+ advertised_course_run,
} = original;
- const rowPrice = first_enrollable_paid_seat_price;
- const priceText = rowPrice != null ? `$${rowPrice.toString()}` : 'N/A';
+ let rowPrice;
+ let priceText;
+
+ if (learningType === CONTENT_TYPE_COURSE) {
+ rowPrice = first_enrollable_paid_seat_price;
+ priceText = rowPrice != null ? `$${rowPrice.toString()}` : 'N/A';
+ } else {
+ [rowPrice] = entitlements || [null];
+ priceText = rowPrice != null ? `$${rowPrice.price?.toString()}` : 'N/A';
+ }
+
const imageSrc = card_image_url || defaultCardHeader;
const altText = `${title} course image`;
@@ -39,7 +52,7 @@ function CourseCard({ intl, onClick, original }) {
- {priceText} • {availability[0]}
+ {priceText} • {advertised_course_run ? advertised_course_run.pacing_type?.replace('_', ' ') : 'NA'}
{enterprise_catalog_query_titles.includes(
@@ -79,9 +92,12 @@ CourseCard.defaultProps = {
CourseCard.propTypes = {
intl: intlShape.isRequired,
onClick: PropTypes.func,
+ learningType: PropTypes.string.isRequired,
original: PropTypes.shape({
title: PropTypes.string,
card_image_url: PropTypes.string,
+ entitlements: PropTypes.arrayOf(PropTypes.shape()),
+ advertised_course_run: PropTypes.shape(),
partners: PropTypes.arrayOf(
PropTypes.shape({
name: PropTypes.string,
@@ -91,7 +107,6 @@ CourseCard.propTypes = {
first_enrollable_paid_seat_price: PropTypes.number,
enterprise_catalog_query_titles: PropTypes.arrayOf(PropTypes.string),
original_image_url: PropTypes.string,
- availability: PropTypes.arrayOf(PropTypes.string),
}).isRequired,
};
diff --git a/src/components/courseCard/CourseCard.test.jsx b/src/components/courseCard/CourseCard.test.jsx
index 1db127d5..4a382d0d 100644
--- a/src/components/courseCard/CourseCard.test.jsx
+++ b/src/components/courseCard/CourseCard.test.jsx
@@ -4,6 +4,7 @@ import '@testing-library/jest-dom/extend-expect';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import CourseCard from './CourseCard';
+import { CONTENT_TYPE_COURSE, EXECUTIVE_EDUCATION_2U_COURSE_TYPE } from '../../constants';
jest.mock('@edx/frontend-platform', () => ({
...jest.requireActual('@edx/frontend-platform'),
@@ -18,11 +19,28 @@ const originalData = {
first_enrollable_paid_seat_price: 100,
original_image_url: '',
enterprise_catalog_query_titles: TEST_CATALOG,
- availability: ['Available Now'],
+ advertised_course_run: { pacing_type: 'self_paced' },
};
const defaultProps = {
original: originalData,
+ learningType: CONTENT_TYPE_COURSE,
+};
+
+const execEdData = {
+ title: 'Course Title',
+ card_image_url: undefined,
+ partners: [{ logo_image_url: '', name: 'Course Provider' }],
+ first_enrollable_paid_seat_price: 100,
+ original_image_url: '',
+ enterprise_catalog_query_titles: TEST_CATALOG,
+ advertised_course_run: { pacing_type: 'instructor_paced' },
+ entitlements: [{ price: '999' }],
+};
+
+const execEdProps = {
+ original: execEdData,
+ learningType: EXECUTIVE_EDUCATION_2U_COURSE_TYPE,
};
describe('Course card works as expected', () => {
@@ -39,7 +57,7 @@ describe('Course card works as expected', () => {
expect(
screen.queryByText(defaultProps.original.partners[0].name),
).toBeInTheDocument();
- expect(screen.queryByText('$100 • Available Now')).toBeInTheDocument();
+ expect(screen.queryByText('$100 • self paced')).toBeInTheDocument();
expect(screen.queryByText('Business')).toBeInTheDocument();
});
test('test card renders default image', async () => {
@@ -52,4 +70,17 @@ describe('Course card works as expected', () => {
fireEvent.error(screen.getByAltText(imageAltText));
await expect(screen.getByAltText(imageAltText).src).not.toBeUndefined;
});
+ test('exec ed card renders correct price from entitlement', async () => {
+ process.env.EDX_FOR_BUSINESS_TITLE = 'ayylmao';
+ process.env.EDX_FOR_ONLINE_EDU_TITLE = 'foo';
+ process.env.EDX_ENTERPRISE_ALACARTE_TITLE = 'baz';
+ render(
+
+
+ ,
+ );
+ expect(screen.queryByText(execEdProps.original.title)).toBeInTheDocument();
+ expect(screen.queryByText('$999 • instructor paced')).toBeInTheDocument();
+ expect(screen.queryByText('Business')).toBeInTheDocument();
+ });
});
diff --git a/src/config/constants.js b/src/config/constants.js
index 527d3000..5d43734a 100644
--- a/src/config/constants.js
+++ b/src/config/constants.js
@@ -1,2 +1,3 @@
export const FEATURE_ENABLE_PROGRAMS = 'ENABLE_PROGRAMS';
export const FEATURE_PROGRAM_TYPE_FACET = 'ENABLE_PROGRAM_TYPE_FACET';
+export const FEATURE_EXEC_ED_INCLUSION = 'ENABLE_EXEC_ED_INCLUSION';
diff --git a/src/config/index.js b/src/config/index.js
index dceea83d..b13aebc9 100644
--- a/src/config/index.js
+++ b/src/config/index.js
@@ -1,6 +1,7 @@
import qs from 'query-string';
import {
FEATURE_ENABLE_PROGRAMS,
+ FEATURE_EXEC_ED_INCLUSION,
FEATURE_PROGRAM_TYPE_FACET,
} from './constants';
@@ -16,6 +17,9 @@ const features = {
PROGRAM_TYPE_FACET:
process.env.FEATURE_PROGRAM_TYPE_FACET === 'true'
|| hasFeatureFlagEnabled(FEATURE_PROGRAM_TYPE_FACET),
+ EXEC_ED_INCLUSION:
+ process.env.EXEC_ED_INCLUSION === 'true'
+ || hasFeatureFlagEnabled(FEATURE_EXEC_ED_INCLUSION),
};
export default features;
diff --git a/src/constants.js b/src/constants.js
index 4088c5a7..32f1a88b 100644
--- a/src/constants.js
+++ b/src/constants.js
@@ -17,7 +17,10 @@ export const AVAILABILITY_REFINEMENT_DEFAULTS = [
'Starting Soon',
'Upcoming',
];
+
export const CONTENT_TYPE_REFINEMENT = 'content_type';
+export const COURSE_TYPE_REFINEMENT = 'course_type';
+export const LEARNING_TYPE_REFINEMENT = 'learning_type';
export const HIDE_CARDS_REFINEMENT = 'hide_cards';
export const HIDE_PRICE_REFINEMENT = 'hide_price';
export const NUM_RESULTS_PER_PAGE = 40;
@@ -27,6 +30,7 @@ export const NUM_RESULTS_PROGRAM = 4;
export const NUM_RESULTS_COURSE = 8;
export const COURSE_TITLE = 'Courses';
export const PROGRAM_TITLE = 'Programs';
+export const EXEC_ED_TITLE = 'Executive Education';
export const NO_RESULTS_DECK_ITEM_COUNT = 4;
export const NO_RESULTS_PAGE_ITEM_COUNT = 1;
export const NO_RESULTS_PAGE_SIZE = 4;
@@ -35,7 +39,6 @@ const VERIFIED_AUDIT_COURSE_TYPE = 'verified-audit';
const PROFESSIONAL_COURSE_TYPE = 'professional';
const CREDIT_VERIFIED_AUDIT_COURSE_TYPE = 'credit-verified-audit';
export const EXECUTIVE_EDUCATION_2U_COURSE_TYPE = 'executive-education-2u';
-export const TWOU_COURSE_TYPES = [EXECUTIVE_EDUCATION_2U_COURSE_TYPE];
export const EDX_COURSES_COURSE_TYPES = [
AUDIT_COURSE_TYPE,
VERIFIED_AUDIT_COURSE_TYPE,
@@ -43,6 +46,10 @@ export const EDX_COURSES_COURSE_TYPES = [
CREDIT_VERIFIED_AUDIT_COURSE_TYPE,
];
+export const EDX_COURSE_TITLE_DESC = 'Self paced online learning from world-class academic institutions and corporate partners.';
+export const TWOU_EXEC_ED_TITLE_DESC = 'Immersive, instructor led online short courses designed to develop interpersonal, analytical, and critical thinking skills.';
+export const PROGRAM_TITLE_DESC = 'Multi-course bundled learning for skills mastery and to earn credentials such as Professional Certificates, MicroBachelors™, MicroMasters®, and Master’s Degrees.';
+
const OVERRIDE_FACET_FILTERS = [];
if (features.PROGRAM_TYPE_FACET) {
const PROGRAM_TYPE_FACET_OVERRIDE = {
diff --git a/src/utils/algoliaUtils.js b/src/utils/algoliaUtils.js
index 423e5979..6bc0f64c 100644
--- a/src/utils/algoliaUtils.js
+++ b/src/utils/algoliaUtils.js
@@ -42,7 +42,46 @@ function mapAlgoliaObjectToCourse(algoliaCourseObject, intl, messages) {
}
/**
- * Converts and algolia course object, into a course representation usable in this UI
+ * Converts an algolia exec ed course object, into a course representation usable in this UI
+ */
+function mapAlgoliaObjectToExecEd(algoliaCourseObject, intl, messages) {
+ const {
+ title: courseTitle,
+ partners,
+ entitlements: coursePrice, // todo
+ enterprise_catalog_query_titles: courseAssociatedCatalogs,
+ full_description: courseDescription,
+ original_image_url: bannerImageUrl,
+ marketing_url: marketingUrl,
+ advertised_course_run: courseRun,
+ upcoming_course_runs: upcomingRuns,
+ skill_names: skillNames,
+ } = algoliaCourseObject;
+ const { start: startDate, end: endDate } = courseRun;
+ const priceText = coursePrice != null
+ ? `$${coursePrice[0].price.toString()}`
+ : intl.formatMessage(
+ messages['catalogSearchResult.table.priceNotAvailable'],
+ );
+ return {
+ contentType: CONTENT_TYPE_COURSE,
+ courseTitle,
+ courseProvider: partners[0].name,
+ coursePrice: priceText,
+ courseAssociatedCatalogs,
+ courseDescription,
+ partnerLogoImageUrl: partners[0].logo_image_url,
+ bannerImageUrl,
+ marketingUrl,
+ startDate,
+ endDate,
+ upcomingRuns,
+ skillNames,
+ };
+}
+
+/**
+ * Converts an algolia course object, into a course representation usable in this UI
*/
function mapAlgoliaObjectToProgram(algoliaProgramObject) {
const {
@@ -75,4 +114,6 @@ function mapAlgoliaObjectToProgram(algoliaProgramObject) {
};
}
-export { mapAlgoliaObjectToProgram, mapAlgoliaObjectToCourse, extractUuid };
+export {
+ mapAlgoliaObjectToProgram, mapAlgoliaObjectToExecEd, mapAlgoliaObjectToCourse, extractUuid,
+};