Skip to content

Commit

Permalink
Merge pull request #149 from edx/dsheraz/PROD-2477
Browse files Browse the repository at this point in the history
feat: add feature based enrollment components
  • Loading branch information
DawoudSheraz authored Sep 23, 2021
2 parents 2ee8d54 + cc6c7b7 commit 52cbb6f
Show file tree
Hide file tree
Showing 13 changed files with 639 additions and 5 deletions.
67 changes: 67 additions & 0 deletions src/FeatureBasedEnrollments/FeatureBasedEnrollment.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import React, {
useEffect,
useContext,
useState,
} from 'react';
import { camelCaseObject } from '@edx/frontend-platform';
import { Row, Col } from '@edx/paragon';
import PropTypes from 'prop-types';

import UserMessagesContext from '../userMessages/UserMessagesContext';
import AlertList from '../userMessages/AlertList';
import getFeatureBasedEnrollmentDetails from './data/api';
import PageLoading from '../components/common/PageLoading';
import FeatureBasedEnrollmentCard from './FeatureBasedEnrollmentCard';

export default function FeatureBasedEnrollment({ courseId }) {
const [fbeData, setFBEData] = useState(undefined);
const { add, clear } = useContext(UserMessagesContext);

useEffect(() => {
clear('featureBasedEnrollment');
setFBEData(undefined);
getFeatureBasedEnrollmentDetails(courseId).then((response) => {
const camelCaseResponse = camelCaseObject(response);
if (camelCaseResponse.errors) {
camelCaseResponse.errors.forEach(error => add(error));
} else {
setFBEData(camelCaseResponse);
}
});
}, [courseId]);

return (
<section className="mb-3">
<AlertList topic="featureBasedEnrollment" className="mb-3" />
<h3 id="fbe-title-header" className="my-4">Feature Based Enrollment Configuration</h3>

{/* eslint-disable-next-line no-nested-ternary */}
{fbeData
? (
(Object.keys(fbeData).length > 0
? (
<>
<h4 className="my-4">Course Title: {fbeData.courseName}</h4>
<Row>
<Col>
<FeatureBasedEnrollmentCard title="Content Type Gating" fbeData={fbeData.gatingConfig} />
</Col>
<Col>
<FeatureBasedEnrollmentCard title="Duration Config" fbeData={fbeData.durationConfig} />
</Col>
</Row>
</>
)
: (<p className="my-4">No Feature Based Enrollment Configurations were found.</p>)
)
)
: (
<PageLoading srMessage="Loading" />
)}
</section>
);
}

FeatureBasedEnrollment.propTypes = {
courseId: PropTypes.string.isRequired,
};
85 changes: 85 additions & 0 deletions src/FeatureBasedEnrollments/FeatureBasedEnrollment.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { mount } from 'enzyme';
import React from 'react';
import { waitForComponentToPaint } from '../setupTest';
import FeatureBasedEnrollment from './FeatureBasedEnrollment';
import UserMessagesProvider from '../userMessages/UserMessagesProvider';
import { fbeEnabledResponse } from './data/test/featureBasedEnrollment';

import * as api from './data/api';

const FeatureBasedEnrollmentWrapper = (props) => (
<UserMessagesProvider>
<FeatureBasedEnrollment {...props} />
</UserMessagesProvider>
);

describe('Feature Based Enrollment', () => {
const props = {
courseId: 'course-v1:testX+test123+2030',
};

let wrapper;

beforeEach(async () => {
// api file has only one default export, so that will be spied-on
jest.spyOn(api, 'default').mockImplementationOnce(() => Promise.resolve(fbeEnabledResponse));
wrapper = mount(<FeatureBasedEnrollmentWrapper {...props} />);
await waitForComponentToPaint(wrapper);
});

afterEach(() => {
wrapper.unmount();
});

it('default props', () => {
const courseId = wrapper.prop('courseId');
expect(courseId).toEqual(props.courseId);
});

it('Successful fetch for FBE data', async () => {
const cardList = wrapper.find('Card');
const courseTitle = wrapper.find('h4');

expect(cardList).toHaveLength(2);
expect(wrapper.find('h3#fbe-title-header').text()).toEqual('Feature Based Enrollment Configuration');
expect(courseTitle.text()).toEqual('Course Title: test course');
});

it('No FBE Data', async () => {
jest.spyOn(api, 'default').mockImplementationOnce(() => Promise.resolve({}));
wrapper = mount(<FeatureBasedEnrollmentWrapper {...props} />);
await waitForComponentToPaint(wrapper);

const cardList = wrapper.find('Card');
const noRecordMessage = wrapper.find('p');

expect(cardList).toHaveLength(0);
expect(wrapper.find('h3#fbe-title-header').text()).toEqual('Feature Based Enrollment Configuration');
expect(noRecordMessage.text()).toEqual('No Feature Based Enrollment Configurations were found.');
});

it('Page Loading component render', async () => {
wrapper = mount(<FeatureBasedEnrollmentWrapper {...props} />);
expect(wrapper.find('PageLoading').html()).toEqual(expect.stringContaining('Loading'));
});

it('Error fetching FBE data', async () => {
const fbeErrors = {
errors: [
{
code: null,
dismissible: true,
text: 'Error fetching FBE Data',
type: 'error',
topic: 'featureBasedEnrollment',
},
],
};
jest.spyOn(api, 'default').mockImplementationOnce(() => Promise.resolve(fbeErrors));
wrapper = mount(<FeatureBasedEnrollmentWrapper {...props} />);
await waitForComponentToPaint(wrapper);

const alert = wrapper.find('.alert');
expect(alert.text()).toEqual('Error fetching FBE Data');
});
});
44 changes: 44 additions & 0 deletions src/FeatureBasedEnrollments/FeatureBasedEnrollmentCard.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
Card, Badge,
} from '@edx/paragon';
import { formatDate } from '../utils';

export default function FeatureBasedEnrollmentCard({ title, fbeData }) {
return (
<Card className="px-3 mb-1">
<Card.Body className="p-0">
<Card.Title as="h3" className="btn-header mt-4">
{title} { fbeData.enabled ? <Badge variant="success">Enabled</Badge> : <Badge variant="danger">Disabled</Badge> }
</Card.Title>

<table className="fbe-table">
<tbody>

<tr>
<th>Enabled As Of</th>
<td>{fbeData.enabledAsOf !== 'N/A' ? formatDate(fbeData.enabledAsOf) : fbeData.enabledAsOf}</td>
</tr>

<tr>
<th>Reason</th>
<td>{fbeData.reason}</td>
</tr>

</tbody>

</table>
</Card.Body>
</Card>
);
}

FeatureBasedEnrollmentCard.propTypes = {
title: PropTypes.string.isRequired,
fbeData: PropTypes.shape({
enabled: PropTypes.bool.isRequired,
reason: PropTypes.string.isRequired,
enabledAsOf: PropTypes.string.isRequired,
}).isRequired,
};
85 changes: 85 additions & 0 deletions src/FeatureBasedEnrollments/FeatureBasedEnrollmentCard.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { mount } from 'enzyme';
import React from 'react';
import FeatureBasedEnrollmentCard from './FeatureBasedEnrollmentCard';
import {
fbeDurationConfigEnabled,
fbeDurationConfigDisabled,
fbeGatingConfigEnabled,
fbeGatingConfigDisabled,
} from './data/test/featureBasedEnrollment';

describe('Feature Based Enrollment Card Component', () => {
let wrapper;

afterEach(() => {
wrapper.unmount();
});

describe('Gating config', () => {
const title = 'Gating Config';

it('Gating config enabled', () => {
wrapper = mount(<FeatureBasedEnrollmentCard title={title} fbeData={fbeGatingConfigEnabled} />);
const header = wrapper.find('h3.card-title');
const dataTable = wrapper.find('table.fbe-table tr');
const dateRow = dataTable.at(0);
const reasonRow = dataTable.at(1);

expect(header.text()).toEqual('Gating Config Enabled');
expect(dateRow.find('th').at(0).text()).toEqual('Enabled As Of');
expect(dateRow.find('td').at(0).text()).toEqual('Jan 1, 2020 12:00 AM');

expect(reasonRow.find('th').at(0).text()).toEqual('Reason');
expect(reasonRow.find('td').at(0).text()).toEqual('Site');
});

it('Gating config disabled', () => {
wrapper = mount(<FeatureBasedEnrollmentCard title={title} fbeData={fbeGatingConfigDisabled} />);
const header = wrapper.find('h3.card-title');
const dataTable = wrapper.find('table.fbe-table tr');
const dateRow = dataTable.at(0);
const reasonRow = dataTable.at(1);

expect(header.text()).toEqual('Gating Config Disabled');
expect(dateRow.find('th').at(0).text()).toEqual('Enabled As Of');
expect(dateRow.find('td').at(0).text()).toEqual('N/A');

expect(reasonRow.find('th').at(0).text()).toEqual('Reason');
expect(reasonRow.find('td').at(0).text()).toEqual('');
});
});

describe('Duration config', () => {
const title = 'Duration Config';

it('Duration config enabled', () => {
wrapper = mount(<FeatureBasedEnrollmentCard title={title} fbeData={fbeDurationConfigEnabled} />);
const header = wrapper.find('h3.card-title');
const dataTable = wrapper.find('table.fbe-table tr');
const dateRow = dataTable.at(0);
const reasonRow = dataTable.at(1);

expect(header.text()).toEqual('Duration Config Enabled');
expect(dateRow.find('th').at(0).text()).toEqual('Enabled As Of');
expect(dateRow.find('td').at(0).text()).toEqual('Feb 1, 2020 12:00 AM');

expect(reasonRow.find('th').at(0).text()).toEqual('Reason');
expect(reasonRow.find('td').at(0).text()).toEqual('Site Config');
});

it('Duration config disabled', () => {
wrapper = mount(<FeatureBasedEnrollmentCard title={title} fbeData={fbeDurationConfigDisabled} />);
const header = wrapper.find('h3.card-title');
const dataTable = wrapper.find('table.fbe-table tr');
const dateRow = dataTable.at(0);
const reasonRow = dataTable.at(1);

expect(header.text()).toEqual('Duration Config Disabled');
expect(dateRow.find('th').at(0).text()).toEqual('Enabled As Of');
expect(dateRow.find('td').at(0).text()).toEqual('N/A');

expect(reasonRow.find('th').at(0).text()).toEqual('Reason');
expect(reasonRow.find('td').at(0).text()).toEqual('');
});
});
});
111 changes: 111 additions & 0 deletions src/FeatureBasedEnrollments/FeatureBasedEnrollmentIndexPage.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { history } from '@edx/frontend-platform';
import React, {
useRef,
useEffect,
useCallback,
useContext,
useState,
} from 'react';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
import { Input, Button } from '@edx/paragon';

import UserMessagesContext from '../userMessages/UserMessagesContext';
import AlertList from '../userMessages/AlertList';
import { isValidCourseID } from '../utils';
import FeatureBasedEnrollment from './FeatureBasedEnrollment';

export default function FeatureBasedEnrollmentIndexPage({ location }) {
const params = new Map(
location.search
.slice(1) // removes '?' mark from start
.split('&')
.map(queryParams => queryParams.split('=')),
);

const searchRef = useRef();
const { add, clear } = useContext(UserMessagesContext);
const [searchValue, setSearchValue] = useState(params.get('course_id') || undefined);

if (params.has('course_id')) {
const courseId = params.get('course_id');
params.set('course_id', decodeURIComponent(courseId));
}

function pushHistoryIfChanged(nextUrl) {
if (nextUrl !== location.pathname + location.search) {
history.push(nextUrl);
}
}

function validateInput(inputValue) {
if (!isValidCourseID(inputValue)) {
add({
code: null,
dismissible: false,
text: `Supplied course ID "${inputValue}" is either invalid or incorrect.`,
type: 'error',
topic: 'featureBasedEnrollmentGeneral',
});
history.replace('/feature_based_enrollments');
return false;
}
return true;
}

const handleSearchInput = useCallback((inputValue) => {
clear('featureBasedEnrollmentGeneral');
if (inputValue !== undefined && inputValue !== '') {
if (!validateInput(inputValue)) {
return;
}
setSearchValue(inputValue);
pushHistoryIfChanged(`/feature_based_enrollments/?course_id=${inputValue}`);
} else if (inputValue === '') {
history.replace('/feature_based_enrollments');
}
});

const submit = useCallback((event) => {
const inputValue = searchRef.current.value;
setSearchValue(undefined);
event.preventDefault();
handleSearchInput(inputValue);
return false;
});

// Run this once when the user re-loads the page with course_id query param present in url
useEffect(() => {
if (params.get('course_id')) {
handleSearchInput(params.get('course_id'));
}
}, []);

return (
<main className="ml-5 mr-5 mt-3 mb-5">

<section className="mb-3">
<Link to="/">&lt; Back to Tools</Link>
</section>

<AlertList topic="featureBasedEnrollmentGeneral" className="mb-3" />

<section className="mb-3">
<form className="form-inline">
<label htmlFor="courseId">Course ID</label>
<Input ref={searchRef} className="flex-grow-1 mr-1" name="courseId" type="text" defaultValue={searchValue} />
<Button type="submit" onClick={submit} variant="primary">Search</Button>
</form>
</section>

{searchValue && <FeatureBasedEnrollment courseId={searchValue} />}
</main>
);
}

FeatureBasedEnrollmentIndexPage.propTypes = {
location: PropTypes.shape({
pathname: PropTypes.string,
search: PropTypes.string,
}).isRequired,
};
Loading

0 comments on commit 52cbb6f

Please sign in to comment.