Skip to content

Commit

Permalink
feat: display search result cards in catalog tab (#1059)
Browse files Browse the repository at this point in the history
* feat: display search result cards in catalog tab

* fix: failing test in BudgetDetailPage

* fix: replace word register with enroll

* fix: implemented reviewer comments

* fix: lint error

* fix: lint error

* feat: added policy's catalog uuid to search filter

* fix: failing test

* fix: refactored based on reviewer feedback

* fix: lint error

* fix: refactored code to include new api field and updated test

* fix: removing unused prop in test

* fix: refactored

* fix: search filters

* chore: refactored
  • Loading branch information
katrinan029 authored Oct 27, 2023
1 parent c0a7605 commit 017f349
Show file tree
Hide file tree
Showing 11 changed files with 214 additions and 158 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,20 @@ import { Row, Col } from '@edx/paragon';

import { SearchData, SEARCH_FACET_FILTERS } from '@edx/frontend-enterprise-catalog-search';
import CatalogSearch from './search/CatalogSearch';
import { LANGUAGE_REFINEMENT, LEARNING_TYPE_REFINEMENT } from './data';
import {
LANGUAGE_REFINEMENT,
LEARNING_TYPE_REFINEMENT,
useBudgetId,
useSubsidyAccessPolicy,
} from './data';
import { configuration } from '../../config';

const BudgetDetailCatalogTabContents = () => {
const { subsidyAccessPolicyId } = useBudgetId();
const {
data: subsidyAccessPolicy,
} = useSubsidyAccessPolicy(subsidyAccessPolicyId);

const language = {
attribute: LANGUAGE_REFINEMENT,
title: 'Language',
Expand Down Expand Up @@ -38,7 +48,7 @@ const BudgetDetailCatalogTabContents = () => {
indexName={configuration.ALGOLIA.INDEX_NAME}
searchClient={searchClient}
>
<CatalogSearch />
<CatalogSearch catalogUuid={subsidyAccessPolicy?.catalogUuid} />
</InstantSearch>
</SearchData>
</Col>
Expand Down
152 changes: 97 additions & 55 deletions src/components/learner-credit-management/cards/CourseCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,95 +3,137 @@
import React from 'react';
import PropTypes from 'prop-types';

import { camelCaseObject } from '@edx/frontend-platform';
import cardFallbackImg from '@edx/brand/paragon/images/card-imagecap-fallback.png';
import {
Badge, Button, Card, Hyperlink,
Badge,
Button,
Card,
Stack,
Hyperlink,
useMediaQuery,
breakpoints,
} from '@edx/paragon';
import { EXEC_COURSE_TYPE } from '../data/constants';
import { formatDate } from '../data/utils';
import { injectIntl } from '@edx/frontend-platform/i18n';
import { camelCaseObject } from '@edx/frontend-platform';
import cardFallbackImg from '@edx/brand/paragon/images/card-imagecap-fallback.png';

import { EXEC_ED_COURSE_TYPE } from '../data';
import { formatPrice, formatDate, getEnrollmentDeadline } from '../data/utils';
import CARD_TEXT from '../constants';

const CourseCard = ({
onClick, original,
original,
}) => {
const {
title,
availability,
cardImageUrl,
courseType,
normalizedMetadata,
partners,
title,
} = camelCaseObject(original);

let priceText;
const isSmall = useMediaQuery({ maxWidth: breakpoints.small.maxWidth });
const isExtraSmall = useMediaQuery({ maxWidth: breakpoints.extraSmall.maxWidth });

const {
BADGE,
BUTTON_ACTION,
PRICE,
ENROLLMENT,
} = CARD_TEXT;

const price = normalizedMetadata?.contentPrice ? formatPrice(normalizedMetadata.contentPrice, { minimumFractionDigits: 0 }) : 'N/A';

const imageSrc = cardImageUrl || cardFallbackImg;

let logoSrc;
let logoAlt;
if (partners.length === 1) {
logoSrc = partners[0]?.logoImageUrl;
logoAlt = `${partners[0]?.name}'s logo`;
}

const altText = `${title} course image`;

const formattedAvailability = availability?.length ? availability.join(', ') : null;

const enrollmentDeadline = getEnrollmentDeadline(normalizedMetadata?.enrollByDate);

let courseEnrollmentInfo;
let execEdEnrollmentInfo;
if (normalizedMetadata?.enrollByDate) {
courseEnrollmentInfo = `${formattedAvailability}${ENROLLMENT.text} ${enrollmentDeadline}`;
execEdEnrollmentInfo = `Starts ${formatDate(normalizedMetadata.startDate)}
${ENROLLMENT.text} ${enrollmentDeadline}`;
} else {
courseEnrollmentInfo = formattedAvailability;
execEdEnrollmentInfo = formattedAvailability;
}

const isExecEd = courseType === EXEC_ED_COURSE_TYPE;

return (
<Card
className="course-card"
onClick={() => onClick(original)}
orientation="horizontal"
tabIndex="0"
orientation={isSmall ? 'vertical' : 'horizontal'}
>
<Card.ImageCap
src={cardImageUrl || cardFallbackImg}
fallbackSrc={cardFallbackImg}
logoSrc={partners[0]?.logo_image_url}
src={imageSrc}
srcAlt={altText}
logoAlt={partners[0]?.name}
logoSrc={logoSrc}
logoAlt={logoAlt}
/>
<div className="card-container">
<div className="section-1">
<p className="mb-1 lead font-weight-bold">{title}</p>
<p>{partners[0]?.name}</p>
{courseType === EXEC_COURSE_TYPE && (
<Badge variant="light" className="mb-4">
Executive Education
</Badge>
)}
{courseType !== EXEC_COURSE_TYPE && (
<p className="spacer" />
<Card.Body>
<Card.Header
title={title}
className="mb-0 mt-0"
subtitle={partners[0]?.name}
actions={(
<Stack gap={1} className="text-right">
<p className="h4 mt-2.5 mb-0">{price}</p>
<span className="micro">{PRICE.subText}</span>
</Stack>
)}
<p className={`small ${courseType !== EXEC_COURSE_TYPE ? 'mt-5 mb-0' : ''}`}>
Starts {formatDate(normalizedMetadata?.start_date)}
Learner must register by {formatDate(normalizedMetadata?.enroll_by_date)}
</p>
</div>
<Card.Section className="section-2">
<p className="lead font-weight-bold mb-0">{priceText}</p>
<p className="micro mb-5.5">Per learner price</p>
<Card.Footer orientation="horizontal" className="footer">
<Button as={Hyperlink} destination="https://edx.org" target="_blank">View course</Button>

<Button>Assign</Button>
</Card.Footer>
/>
<Card.Section>
<Badge variant="light">
{isExecEd ? BADGE.execEd : BADGE.course}
</Badge>
</Card.Section>
</div>
<Card.Footer
orientation={isExtraSmall ? 'horizontal' : 'vertical'}
textElement={isExecEd ? execEdEnrollmentInfo : courseEnrollmentInfo}
>
<Button
// TODO: Implementation to follow in ENT-7594
as={Hyperlink}
destination="https://enterprise.stage.edx.org"
target="_blank"
variant="outline-primary"
>
{BUTTON_ACTION.viewCourse}
</Button>
<Button>{BUTTON_ACTION.assign}</Button>
</Card.Footer>
</Card.Body>
</Card>
);
};

CourseCard.defaultProps = {
onClick: () => {},
};

CourseCard.propTypes = {
onClick: PropTypes.func,
original: PropTypes.shape({
title: PropTypes.string,
availability: PropTypes.arrayOf(PropTypes.string),
cardImageUrl: PropTypes.string,
courseType: PropTypes.string,
normalizedMetadata: PropTypes.shape(),
originalImageUrl: PropTypes.string,
partners: PropTypes.arrayOf(
PropTypes.shape({
logoImageUrl: PropTypes.string,
name: PropTypes.string,
logo_image_url: PropTypes.string,
}),
),
normalizedMetadata: PropTypes.shape({
startDate: PropTypes.string,
endDate: PropTypes.string,
enrollByDate: PropTypes.string,
}),
courseType: PropTypes.string,
title: PropTypes.string,
}).isRequired,
};

export default CourseCard;
export default injectIntl(CourseCard);
71 changes: 37 additions & 34 deletions src/components/learner-credit-management/cards/CourseCard.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,47 +4,46 @@ import '@testing-library/jest-dom/extend-expect';

import { IntlProvider } from '@edx/frontend-platform/i18n';
import CourseCard from './CourseCard';
import { CONTENT_TYPE_COURSE, EXEC_ED_TITLE } from '../data/constants';

jest.mock('@edx/frontend-platform', () => ({
...jest.requireActual('@edx/frontend-platform'),
}));

const TEST_CATALOG = ['ayylmao'];

const originalData = {
title: 'Course Title',
availability: ['Upcoming'],
card_image_url: undefined,
partners: [{ logo_image_url: '', name: 'Course Provider' }],
first_enrollable_paid_seat_price: 100,
course_type: 'course',
normalized_metadata: {
enroll_by_date: '2016-02-18T04:00:00Z',
start_date: '2016-04-18T04:00:00Z',
content_price: 100,
},
original_image_url: '',
enterprise_catalog_query_titles: TEST_CATALOG,
advertised_course_run: { pacing_type: 'self_paced' },
partners: [{ logo_image_url: '', name: 'Course Provider' }],
title: 'Course Title',
};

const defaultProps = {
original: originalData,
learningType: CONTENT_TYPE_COURSE,
};

const execEdData = {
title: 'Exec Ed Course Title',
availability: ['Upcoming'],
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' },
course_type: 'executive-education-2u',
entitlements: [{ price: '999.00' }],
normalized_metadata: {
enroll_by_date: '2016-02-18T04:00:00Z',
start_date: '2016-04-18T04:00:00Z',
content_price: 999,
},
original_image_url: '',
partners: [{ logo_image_url: '', name: 'Course Provider' }],
title: 'Exec Ed Title',
};

const execEdProps = {
original: execEdData,
learningType: EXEC_ED_TITLE,
};

describe('Course card works as expected', () => {
test('card renders as expected', () => {
test('course card renders', () => {
render(
<IntlProvider locale="en">
<CourseCard {...defaultProps} />
Expand All @@ -54,21 +53,14 @@ describe('Course card works as expected', () => {
expect(
screen.queryByText(defaultProps.original.partners[0].name),
).toBeInTheDocument();
expect(screen.queryByText('Course Title')).toBeInTheDocument();
expect(screen.queryByText('$100')).toBeInTheDocument();
expect(screen.queryByText('Per learner price')).toBeInTheDocument();
expect(screen.queryByText('Upcoming • Learner must enroll by Feb 18, 2016')).toBeInTheDocument();
expect(screen.queryByText('Course')).toBeInTheDocument();
expect(screen.queryByText('View course')).toBeInTheDocument();
expect(screen.queryByText('Assign')).toBeInTheDocument();
});
test('exec ed card renders as expected', () => {
render(
<IntlProvider locale="en">
<CourseCard {...execEdProps} />
</IntlProvider>,
);
expect(screen.queryByText(execEdProps.original.title)).toBeInTheDocument();
expect(
screen.queryByText(execEdProps.original.partners[0].name),
).toBeInTheDocument();
expect(screen.queryByText('Exec Ed Course Title')).toBeInTheDocument();
});

test('test card renders default image', async () => {
render(
<IntlProvider locale="en">
Expand All @@ -79,4 +71,15 @@ describe('Course card works as expected', () => {
fireEvent.error(screen.getByAltText(imageAltText));
await expect(screen.getByAltText(imageAltText).src).not.toBeUndefined;
});

test('exec ed card renders', async () => {
render(
<IntlProvider locale="en">
<CourseCard {...execEdProps} />
</IntlProvider>,
);
expect(screen.queryByText('$999')).toBeInTheDocument();
expect(screen.queryByText('Starts Apr 18, 2016 • Learner must enroll by Feb 18, 2016')).toBeInTheDocument();
expect(screen.queryByText('Executive Education')).toBeInTheDocument();
});
});
18 changes: 18 additions & 0 deletions src/components/learner-credit-management/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
const CARD_TEXT = {
BADGE: {
course: 'Course',
execEd: 'Executive Education',
},
BUTTON_ACTION: {
viewCourse: 'View course',
assign: 'Assign',
},
ENROLLMENT: {
text: 'Learner must enroll by',
},
PRICE: {
subText: 'Per learner price',
},
};

export default CARD_TEXT;
8 changes: 7 additions & 1 deletion src/components/learner-credit-management/data/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,18 @@ export const LANGUAGE_REFINEMENT = 'language';
// Learning types
export const CONTENT_TYPE_COURSE = 'course';
export const EXEC_ED_TITLE = 'Executive Education';
export const EXEC_COURSE_TYPE = 'executive-education-2u';
export const EXEC_ED_COURSE_TYPE = 'executive-education-2u';

// Learner must enroll within 90 days of assignment
export const ASSIGNMENT_ENROLLMENT_DEADLINE = 90;

// Number of items to display per page in Budget Detail assignment/spend tables
export const PAGE_SIZE = 25;
export const DEFAULT_PAGE = 0; // `DataTable` uses zero-index array

// Number of items to display per page in Budget Catalog tab
export const SEARCH_RESULT_PAGE_SIZE = 15;

// Query Key factory for the learner credit management module, intended to be used with `@tanstack/react-query`.
// Inspired by https://tkdodo.eu/blog/effective-react-query-keys#use-query-key-factories.
export const learnerCreditManagementQueryKeys = {
Expand Down
11 changes: 11 additions & 0 deletions src/components/learner-credit-management/data/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { camelCaseObject } from '@edx/frontend-platform';
import {
LOW_REMAINING_BALANCE_PERCENT_THRESHOLD,
NO_BALANCE_REMAINING_DOLLAR_THRESHOLD,
ASSIGNMENT_ENROLLMENT_DEADLINE,
} from './constants';
import { BUDGET_STATUSES } from '../../EnterpriseApp/data/constants';
import EnterpriseAccessApiService from '../../../data/services/EnterpriseAccessApiService';
Expand Down Expand Up @@ -199,6 +200,16 @@ export function formatDate(date) {
return dayjs(date).format('MMM D, YYYY');
}

// Exec ed and open courses cards should display either the enrollment deadline
// or 90 days from the present date on user pageload, whichever is sooner.
export function getEnrollmentDeadline(enrollByDate) {
const courseEnrollByDate = dayjs(enrollByDate);
const assignmentEnrollmentDeadline = dayjs().add(ASSIGNMENT_ENROLLMENT_DEADLINE, 'days');

return courseEnrollByDate <= assignmentEnrollmentDeadline
? formatDate(courseEnrollByDate)
: formatDate(assignmentEnrollmentDeadline);
}
/**
* Retrieves content assignments for the given budget's assignment configuration UUID (retrieved from the associated
* subsidy access policy).
Expand Down
Loading

0 comments on commit 017f349

Please sign in to comment.