refactor: GradesView component modernization
This commit is contained in:
@@ -1,37 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { Button, Icon } from '@edx/paragon';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import thunkActions from 'data/thunkActions';
|
||||
|
||||
import messages from './FilterMenuToggle.messages';
|
||||
|
||||
/**
|
||||
* Controls for filtering the GradebookTable. Contains the "Edit Filters" button for opening the filter drawer
|
||||
* as well as the search box for searching by username/email.
|
||||
*/
|
||||
export const FilterMenuToggle = ({ toggleFilterDrawer }) => (
|
||||
<Button
|
||||
id="edit-filters-btn"
|
||||
className="btn-primary align-self-start"
|
||||
onClick={toggleFilterDrawer}
|
||||
>
|
||||
<Icon className="fa fa-filter" /> <FormattedMessage {...messages.editFilters} />
|
||||
</Button>
|
||||
);
|
||||
|
||||
FilterMenuToggle.propTypes = {
|
||||
// From Redux
|
||||
toggleFilterDrawer: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export const mapStateToProps = () => ({});
|
||||
|
||||
export const mapDispatchToProps = {
|
||||
toggleFilterDrawer: thunkActions.app.filterMenu.toggle,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(FilterMenuToggle);
|
||||
@@ -1,11 +0,0 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
editFilters: {
|
||||
id: 'gradebook.GradesView.editFilterLabel',
|
||||
defaultMessage: 'Edit Filters',
|
||||
description: 'A labeled button in the Grades tab that opens/closes the Filters tab, allowing the grades to be filtered',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -1,42 +0,0 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import thunkActions from 'data/thunkActions';
|
||||
|
||||
import { FilterMenuToggle, mapDispatchToProps, mapStateToProps } from './FilterMenuToggle';
|
||||
|
||||
jest.mock('@edx/paragon', () => ({
|
||||
Button: () => 'Button',
|
||||
Icon: () => 'Icon',
|
||||
}));
|
||||
jest.mock('data/thunkActions', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
app: {
|
||||
filterMenu: { toggle: jest.fn() },
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('FilterMenuToggle component', () => {
|
||||
describe('snapshots', () => {
|
||||
test('basic snapshot', () => {
|
||||
const toggleFilterDrawer = jest.fn().mockName('this.props.toggleFilterDrawer');
|
||||
expect(shallow((
|
||||
<FilterMenuToggle {...{ toggleFilterDrawer }} />
|
||||
))).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
describe('mapStateToProps', () => {
|
||||
test('does not connect any selectors', () => {
|
||||
expect(mapStateToProps({ test: 'state' })).toEqual({});
|
||||
});
|
||||
});
|
||||
describe('mapDispatchToProps', () => {
|
||||
test('toggleFilterDrawer from thunkActions.app.filterMenu.toggle', () => {
|
||||
expect(mapDispatchToProps.toggleFilterDrawer).toEqual(
|
||||
thunkActions.app.filterMenu.toggle,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,44 +0,0 @@
|
||||
/* eslint-disable react/sort-comp, react/button-has-type, import/no-named-as-default */
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
|
||||
/**
|
||||
* <FilteredUsersLabel />
|
||||
* Simple label component displaying the filtered and total users shown
|
||||
*/
|
||||
export const FilteredUsersLabel = ({
|
||||
filteredUsersCount,
|
||||
totalUsersCount,
|
||||
}) => {
|
||||
if (!totalUsersCount) {
|
||||
return null;
|
||||
}
|
||||
const bold = (val) => (<span className="font-weight-bold">{val}</span>);
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="gradebook.GradesTab.usersVisibilityLabel'"
|
||||
defaultMessage="Showing {filteredUsers} of {totalUsers} total learners"
|
||||
description="Users visibility label"
|
||||
values={{
|
||||
filteredUsers: bold(filteredUsersCount),
|
||||
totalUsers: bold(totalUsersCount),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
FilteredUsersLabel.propTypes = {
|
||||
filteredUsersCount: PropTypes.number.isRequired,
|
||||
totalUsersCount: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
export const mapStateToProps = (state) => ({
|
||||
totalUsersCount: selectors.grades.totalUsersCount(state),
|
||||
filteredUsersCount: selectors.grades.filteredUsersCount(state),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(FilteredUsersLabel);
|
||||
@@ -1,46 +0,0 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
import { FilteredUsersLabel, mapStateToProps } from './FilteredUsersLabel';
|
||||
|
||||
jest.mock('@edx/paragon', () => ({
|
||||
Icon: () => 'Icon',
|
||||
}));
|
||||
jest.mock('data/selectors', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
grades: {
|
||||
filteredUsersCount: state => ({ filteredUsersCount: state }),
|
||||
totalUsersCount: state => ({ totalUsersCount: state }),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('FilteredUsersLabel', () => {
|
||||
describe('component', () => {
|
||||
const props = {
|
||||
filteredUsersCount: 23,
|
||||
totalUsersCount: 140,
|
||||
};
|
||||
it('does not render if totalUsersCount is falsey', () => {
|
||||
expect(shallow(<FilteredUsersLabel {...props} totalUsersCount={0} />)).toEqual({});
|
||||
});
|
||||
test('snapshot - displays label with number of filtered users out of total', () => {
|
||||
expect(shallow(<FilteredUsersLabel {...props} />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
describe('mapStateToProps', () => {
|
||||
const testState = { a: 'nice', day: 'for', some: 'rain' };
|
||||
let mapped;
|
||||
beforeEach(() => {
|
||||
mapped = mapStateToProps(testState);
|
||||
});
|
||||
test('filteredUsersCount from grades.filteredUsersCount', () => {
|
||||
expect(mapped.filteredUsersCount).toEqual(selectors.grades.filteredUsersCount(testState));
|
||||
});
|
||||
test('totalUsersCount from grades.totalUsersCount', () => {
|
||||
expect(mapped.totalUsersCount).toEqual(selectors.grades.totalUsersCount(testState));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -29,7 +29,7 @@ testFormData.append('csv', testFile);
|
||||
const ref = {
|
||||
current: { click: jest.fn(), files: [testFile], value: 'test-value' },
|
||||
};
|
||||
describe('useAssignmentFilterData hook', () => {
|
||||
describe('useImportButtonData hook', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
React.useRef.mockReturnValue(ref);
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
/* eslint-disable react/sort-comp, react/button-has-type */
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { Toast } from '@edx/paragon';
|
||||
import {
|
||||
injectIntl,
|
||||
intlShape,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
import actions from 'data/actions';
|
||||
import { views } from 'data/constants/app';
|
||||
import messages from './ImportSuccessToast.messages';
|
||||
|
||||
/**
|
||||
* <ImportSuccessToast />
|
||||
* Toast component triggered by successful grade upload.
|
||||
* Provides a link to view the Bulk Management History tab.
|
||||
*/
|
||||
export class ImportSuccessToast extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onClose = this.onClose.bind(this);
|
||||
this.handleShowHistoryView = this.handleShowHistoryView.bind(this);
|
||||
}
|
||||
|
||||
onClose() {
|
||||
this.props.setShow(false);
|
||||
}
|
||||
|
||||
handleShowHistoryView() {
|
||||
this.props.setAppView(views.bulkManagementHistory);
|
||||
this.onClose();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Toast
|
||||
action={{
|
||||
label: this.props.intl.formatMessage(messages.showHistoryViewBtn),
|
||||
onClick: this.handleShowHistoryView,
|
||||
}}
|
||||
onClose={this.onClose}
|
||||
show={this.props.show}
|
||||
>
|
||||
{this.props.intl.formatMessage(messages.description)}
|
||||
</Toast>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ImportSuccessToast.propTypes = {
|
||||
// injected
|
||||
intl: intlShape.isRequired,
|
||||
// redux
|
||||
show: PropTypes.bool.isRequired,
|
||||
setAppView: PropTypes.func.isRequired,
|
||||
setShow: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export const mapStateToProps = (state) => ({
|
||||
show: selectors.app.showImportSuccessToast(state),
|
||||
});
|
||||
|
||||
export const mapDispatchToProps = {
|
||||
setAppView: actions.app.setView,
|
||||
setShow: actions.app.setShowImportSuccessToast,
|
||||
};
|
||||
|
||||
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(ImportSuccessToast));
|
||||
@@ -1,16 +0,0 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
description: {
|
||||
id: 'gradebook.GradesView.ImportSuccessToast.description',
|
||||
defaultMessage: 'Import Successful! Grades will be updated momentarily.',
|
||||
description: 'A message congratulating a successful Import of grades',
|
||||
},
|
||||
showHistoryViewBtn: {
|
||||
id: 'gradebook.GradesView.ImportSuccessToast.showHistoryViewBtn',
|
||||
defaultMessage: 'View Activity Log',
|
||||
description: 'The text on a button that loads a view of the Bulk Management Activity Log',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -1,110 +0,0 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
import actions from 'data/actions';
|
||||
import { views } from 'data/constants/app';
|
||||
|
||||
import {
|
||||
ImportSuccessToast,
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
} from './ImportSuccessToast';
|
||||
import messages from './ImportSuccessToast.messages';
|
||||
|
||||
jest.mock('@edx/paragon', () => ({
|
||||
Toast: () => 'Toast',
|
||||
}));
|
||||
jest.mock('data/selectors', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
app: {
|
||||
showImportSuccessToast: (state) => ({ showImportSuccessToast: state }),
|
||||
},
|
||||
},
|
||||
}));
|
||||
jest.mock('data/actions', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
app: {
|
||||
setView: jest.fn(),
|
||||
setShow: jest.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('ImportSuccessToast component', () => {
|
||||
describe('snapshots', () => {
|
||||
let el;
|
||||
let props = {
|
||||
show: true,
|
||||
};
|
||||
beforeEach(() => {
|
||||
props = {
|
||||
...props,
|
||||
intl: { formatMessage: (msg) => msg.defaultMessage },
|
||||
setAppView: jest.fn(),
|
||||
setShow: jest.fn(),
|
||||
};
|
||||
el = shallow(<ImportSuccessToast {...props} />);
|
||||
});
|
||||
test('snapshot', () => {
|
||||
el.instance().handleShowHistoryView = jest.fn().mockName('handleShowHistoryView');
|
||||
el.instance().onClose = jest.fn().mockName('onClose');
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
describe('Toast props', () => {
|
||||
let toastProps;
|
||||
beforeEach(() => {
|
||||
toastProps = el.props();
|
||||
});
|
||||
test('action has translated label and onClick from this.handleShowHistoryView', () => {
|
||||
expect(toastProps.action).toEqual({
|
||||
label: props.intl.formatMessage(messages.showHistoryViewBtn),
|
||||
onClick: el.instance().handleShowHistoryView,
|
||||
});
|
||||
});
|
||||
test('onClose from this.onClose method', () => {
|
||||
expect(toastProps.onClose).toEqual(el.instance().onClose);
|
||||
});
|
||||
test('show from show prop', () => {
|
||||
expect(toastProps.show).toEqual(props.show);
|
||||
el.setProps({ show: false });
|
||||
expect(el.props().show).toEqual(false);
|
||||
});
|
||||
});
|
||||
describe('onClose', () => {
|
||||
it('calls props.setShow(false)', () => {
|
||||
el.instance().onClose();
|
||||
expect(props.setShow).toHaveBeenCalledWith(false);
|
||||
});
|
||||
});
|
||||
describe('handleShowHistoryView', () => {
|
||||
it('calls setAppView with views.bulkManagementHistory and this.onClose', () => {
|
||||
el.instance().onClose = jest.fn();
|
||||
el.instance().handleShowHistoryView();
|
||||
expect(props.setAppView).toHaveBeenCalledWith(views.bulkManagementHistory);
|
||||
expect(el.instance().onClose).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('behavior', () => {
|
||||
});
|
||||
describe('mapStateToProps', () => {
|
||||
const testState = { somewhere: 'over', the: 'rainbow' };
|
||||
const mapped = mapStateToProps(testState);
|
||||
test('show from app showImportSuccessToast selector', () => {
|
||||
expect(mapped.show).toEqual(
|
||||
selectors.app.showImportSuccessToast(testState),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('mapDispatchToProps', () => {
|
||||
test('setAppView from actions.app.setView', () => {
|
||||
expect(mapDispatchToProps.setAppView).toEqual(actions.app.setView);
|
||||
});
|
||||
test('setShow from actions.setShowImportSuccessToast', () => {
|
||||
expect(mapDispatchToProps.setShow).toEqual(actions.app.setShowImportSuccessToast);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,72 +0,0 @@
|
||||
/* eslint-disable react/sort-comp, react/button-has-type */
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import actions from 'data/actions';
|
||||
import selectors from 'data/selectors';
|
||||
|
||||
import NetworkButton from 'components/NetworkButton';
|
||||
import messages from './InterventionsReport.messages';
|
||||
|
||||
/**
|
||||
* <InterventionsReport />
|
||||
* Provides download buttons for Bulk Management and Intervention reports, only if
|
||||
* showBulkManagement is set in redus.
|
||||
*/
|
||||
export class InterventionsReport extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleClick = this.handleClick.bind(this);
|
||||
}
|
||||
|
||||
handleClick() {
|
||||
this.props.downloadInterventionReport();
|
||||
window.location.assign(this.props.interventionExportUrl);
|
||||
}
|
||||
|
||||
render() {
|
||||
return this.props.showBulkManagement && (
|
||||
<div>
|
||||
<h4 className="mt-0">
|
||||
<FormattedMessage {...messages.title} />
|
||||
</h4>
|
||||
<div
|
||||
className="d-flex justify-content-between align-items-center"
|
||||
>
|
||||
<div className="intervention-report-description">
|
||||
<FormattedMessage {...messages.description} />
|
||||
</div>
|
||||
<NetworkButton
|
||||
label={messages.downloadBtn}
|
||||
onClick={this.handleClick}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
InterventionsReport.defaultProps = {
|
||||
showBulkManagement: false,
|
||||
};
|
||||
|
||||
InterventionsReport.propTypes = {
|
||||
// redux
|
||||
downloadInterventionReport: PropTypes.func.isRequired,
|
||||
interventionExportUrl: PropTypes.string.isRequired,
|
||||
showBulkManagement: PropTypes.bool,
|
||||
};
|
||||
|
||||
export const mapStateToProps = (state) => ({
|
||||
interventionExportUrl: selectors.root.interventionExportUrl(state),
|
||||
showBulkManagement: selectors.root.showBulkManagement(state),
|
||||
});
|
||||
|
||||
export const mapDispatchToProps = {
|
||||
downloadInterventionReport: actions.grades.downloadReport.intervention,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(InterventionsReport);
|
||||
@@ -1,21 +0,0 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: {
|
||||
id: 'gradebook.GradesView.InterventionsReport.title',
|
||||
defaultMessage: 'Interventions Report',
|
||||
description: 'The title for the Intervention report subsection',
|
||||
},
|
||||
description: {
|
||||
id: 'gradebook.GradesView.InterventionsReport.description',
|
||||
defaultMessage: 'Need to find students who may be falling behind? Download the interventions report to obtain engagement metrics such as section attempts and visits.',
|
||||
description: 'The description for the Intervention report subsection',
|
||||
},
|
||||
downloadBtn: {
|
||||
id: 'gradebook.GradesView.InterventionsReport.downloadBtn',
|
||||
defaultMessage: 'Download Interventions',
|
||||
description: 'The labeled button to download the Intervention report from the Grades View',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -1,107 +0,0 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
import actions from 'data/actions';
|
||||
|
||||
import {
|
||||
InterventionsReport,
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
} from './InterventionsReport';
|
||||
|
||||
jest.mock('@edx/paragon', () => ({
|
||||
Toast: () => 'Toast',
|
||||
}));
|
||||
jest.mock('components/NetworkButton', () => 'NetworkButton');
|
||||
jest.mock('data/selectors', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
root: {
|
||||
interventionExportUrl: (state) => ({ interventionExportUrl: state }),
|
||||
showBulkManagement: (state) => ({ showBulkManagement: state }),
|
||||
},
|
||||
},
|
||||
}));
|
||||
jest.mock('data/actions', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
grades: {
|
||||
downloadReport: { intervention: jest.fn() },
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('InterventionsReport component', () => {
|
||||
let el;
|
||||
let props = {
|
||||
interventionExportUrl: 'url.for.exporting.interventions',
|
||||
showBulkManagement: true,
|
||||
};
|
||||
let location;
|
||||
beforeAll(() => {
|
||||
location = window.location;
|
||||
});
|
||||
beforeEach(() => {
|
||||
delete window.location;
|
||||
window.location = Object.defineProperties(
|
||||
{},
|
||||
{
|
||||
...Object.getOwnPropertyDescriptors(location),
|
||||
assign: { configurable: true, value: jest.fn() },
|
||||
},
|
||||
);
|
||||
props = {
|
||||
...props,
|
||||
downloadInterventionReport: jest.fn(),
|
||||
};
|
||||
});
|
||||
afterAll(() => {
|
||||
window.location = location;
|
||||
});
|
||||
describe('snapshots', () => {
|
||||
beforeEach(() => {
|
||||
el = shallow(<InterventionsReport {...props} />);
|
||||
});
|
||||
test('snapshot', () => {
|
||||
el.instance().handleClick = jest.fn().mockName('handleClick');
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
test('returns empty if props.showBulkManagement is false', () => {
|
||||
el.setProps({ showBulkManagement: false });
|
||||
expect(el.instance().render()).toEqual(false);
|
||||
});
|
||||
});
|
||||
describe('behavior', () => {
|
||||
beforeEach(() => {
|
||||
el = shallow(<InterventionsReport {...props} />);
|
||||
});
|
||||
describe('handleClick', () => {
|
||||
it('calls props.downloadInterventionReport and navigates to props.interventionExportUrl', () => {
|
||||
el.instance().handleClick();
|
||||
expect(props.downloadInterventionReport).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('mapStateToProps', () => {
|
||||
const testState = { somewhere: 'over', the: 'rainbow' };
|
||||
const mapped = mapStateToProps(testState);
|
||||
test('interventionExportUrl from root interventionExportUrl selector', () => {
|
||||
expect(mapped.interventionExportUrl).toEqual(
|
||||
selectors.root.interventionExportUrl(testState),
|
||||
);
|
||||
});
|
||||
test('showBulkManagement from root showBulkManagement selector', () => {
|
||||
expect(mapped.showBulkManagement).toEqual(
|
||||
selectors.root.showBulkManagement(testState),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('mapDispatchToProps', () => {
|
||||
test('downloadInterventionReport from actions.grades.downloadReport.intervention', () => {
|
||||
expect(mapDispatchToProps.downloadInterventionReport).toEqual(
|
||||
actions.grades.downloadReport.intervention,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,48 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { FormControl, FormGroup, FormLabel } from '@edx/paragon';
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import actions from 'data/actions';
|
||||
import selectors from 'data/selectors';
|
||||
import messages from './ScoreViewInput.messages';
|
||||
|
||||
/**
|
||||
* <ScoreViewInput />
|
||||
* redux-connected select control for grade format (percent vs absolute)
|
||||
*/
|
||||
export const ScoreViewInput = ({ format, intl, toggleFormat }) => (
|
||||
<FormGroup controlId="ScoreView">
|
||||
<FormLabel><FormattedMessage {...messages.scoreView} />:</FormLabel>
|
||||
<FormControl
|
||||
as="select"
|
||||
value={format}
|
||||
onChange={toggleFormat}
|
||||
>
|
||||
<option value="percent">{intl.formatMessage(messages.percent)}</option>
|
||||
<option value="absolute">{intl.formatMessage(messages.absolute)}</option>
|
||||
</FormControl>
|
||||
</FormGroup>
|
||||
);
|
||||
ScoreViewInput.defaultProps = {
|
||||
format: 'percent',
|
||||
};
|
||||
ScoreViewInput.propTypes = {
|
||||
// injected
|
||||
intl: intlShape.isRequired,
|
||||
// redux
|
||||
format: PropTypes.string,
|
||||
toggleFormat: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export const mapStateToProps = (state) => ({
|
||||
format: selectors.grades.gradeFormat(state),
|
||||
});
|
||||
|
||||
export const mapDispatchToProps = {
|
||||
toggleFormat: actions.grades.toggleGradeFormat,
|
||||
};
|
||||
|
||||
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(ScoreViewInput));
|
||||
@@ -1,21 +0,0 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
scoreView: {
|
||||
id: 'gradebook.GradesView.scoreViewLabel',
|
||||
defaultMessage: 'Score View',
|
||||
description: 'The label for the dropdown list that allows a user to select the Score format',
|
||||
},
|
||||
absolute: {
|
||||
id: 'gradebook.GradesView.absoluteOption',
|
||||
defaultMessage: 'Absolute',
|
||||
description: 'A label within the Score Format dropdown list for the Absolute Grade Score option',
|
||||
},
|
||||
percent: {
|
||||
id: 'gradebook.GradesView.percentOption',
|
||||
defaultMessage: 'Percent',
|
||||
description: 'A label within the Score Format dropdown list for the Percent Grade Score option',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -1,60 +0,0 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import actions from 'data/actions';
|
||||
import selectors from 'data/selectors';
|
||||
|
||||
import {
|
||||
ScoreViewInput,
|
||||
mapDispatchToProps,
|
||||
mapStateToProps,
|
||||
} from './ScoreViewInput';
|
||||
|
||||
jest.mock('@edx/paragon', () => ({
|
||||
FormControl: () => 'FormControl',
|
||||
FormGroup: () => 'FormGroup',
|
||||
FormLabel: () => 'FormLabel',
|
||||
}));
|
||||
|
||||
jest.mock('data/actions', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
grades: { toggleGradeFormat: jest.fn() },
|
||||
},
|
||||
}));
|
||||
jest.mock('data/selectors', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
grades: { gradeFormat: (state) => ({ gradeFormat: state }) },
|
||||
},
|
||||
}));
|
||||
|
||||
describe('ScoreViewInput', () => {
|
||||
describe('component', () => {
|
||||
const props = { format: 'percent' };
|
||||
let el;
|
||||
beforeEach(() => {
|
||||
props.toggleFormat = jest.fn();
|
||||
props.intl = { formatMessage: (msg) => msg.defaultMessage };
|
||||
el = shallow(<ScoreViewInput {...props} />);
|
||||
});
|
||||
const assertions = [
|
||||
'select box with percent and absolute options',
|
||||
'onClick from props.toggleFormat',
|
||||
];
|
||||
test(`snapshot - ${assertions.join(' and ')}`, () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
describe('mapStateToProps', () => {
|
||||
test('format from grades.gradeFormat', () => {
|
||||
const testState = { some: 'state' };
|
||||
expect(mapStateToProps(testState).format).toEqual(selectors.grades.gradeFormat(testState));
|
||||
});
|
||||
});
|
||||
describe('mapDispatchToProps', () => {
|
||||
test('toggleFormat from actions.grades.toggleGradeFormat', () => {
|
||||
expect(mapDispatchToProps.toggleFormat).toEqual(actions.grades.toggleGradeFormat);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,75 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { SearchField } from '@edx/paragon';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import actions from 'data/actions';
|
||||
import selectors from 'data/selectors';
|
||||
import thunkActions from 'data/thunkActions';
|
||||
|
||||
import messages from './SearchControls.messages';
|
||||
|
||||
/**
|
||||
* Controls for filtering the GradebookTable. Contains the "Edit Filters" button for opening the filter drawer
|
||||
* as well as the search box for searching by username/email.
|
||||
*/
|
||||
export class SearchControls extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onBlur = this.onBlur.bind(this);
|
||||
this.onClear = this.onClear.bind(this);
|
||||
this.onSubmit = this.onSubmit.bind(this);
|
||||
}
|
||||
|
||||
onBlur(e) {
|
||||
this.props.setSearchValue(e.target.value);
|
||||
}
|
||||
|
||||
onClear() {
|
||||
this.props.setSearchValue('');
|
||||
this.props.fetchGrades();
|
||||
}
|
||||
|
||||
onSubmit(searchValue) {
|
||||
this.props.setSearchValue(searchValue);
|
||||
this.props.fetchGrades();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<SearchField
|
||||
onSubmit={this.onSubmit}
|
||||
inputLabel={<FormattedMessage {...messages.label} />}
|
||||
onBlur={this.onBlur}
|
||||
onClear={this.onClear}
|
||||
value={this.props.searchValue}
|
||||
/>
|
||||
<small className="form-text text-muted search-help-text">
|
||||
<FormattedMessage {...messages.hint} />
|
||||
</small>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SearchControls.propTypes = {
|
||||
// From Redux
|
||||
fetchGrades: PropTypes.func.isRequired,
|
||||
searchValue: PropTypes.string.isRequired,
|
||||
setSearchValue: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export const mapStateToProps = (state) => ({
|
||||
searchValue: selectors.app.searchValue(state),
|
||||
});
|
||||
|
||||
export const mapDispatchToProps = {
|
||||
fetchGrades: thunkActions.grades.fetchGrades,
|
||||
setSearchValue: actions.app.setSearchValue,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(SearchControls);
|
||||
@@ -1,16 +0,0 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
label: {
|
||||
id: 'gradebook.GradesView.search.label',
|
||||
defaultMessage: 'Search for a learner',
|
||||
description: 'Text prompting a user to use this functionality to search for a learner',
|
||||
},
|
||||
hint: {
|
||||
id: 'gradebook.GradesView.search.hint',
|
||||
defaultMessage: 'Search by username, email, or student key',
|
||||
description: 'A hint explaining the ways a user can search',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -1,119 +0,0 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
import actions from 'data/actions';
|
||||
import thunkActions from 'data/thunkActions';
|
||||
import {
|
||||
mapDispatchToProps,
|
||||
mapStateToProps,
|
||||
SearchControls,
|
||||
} from './SearchControls';
|
||||
|
||||
jest.mock('@edx/paragon', () => ({
|
||||
Icon: 'Icon',
|
||||
Button: 'Button',
|
||||
SearchField: 'SearchField',
|
||||
}));
|
||||
jest.mock('data/selectors', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
app: {
|
||||
searchValue: jest.fn((state) => ({ searchValue: state })),
|
||||
},
|
||||
},
|
||||
}));
|
||||
jest.mock('data/thunkActions', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
grades: {
|
||||
fetchGrades: jest.fn().mockName('thunkActions.grades.fetchGrades'),
|
||||
},
|
||||
app: {
|
||||
filterMenu: { toggle: jest.fn().mockName('thunkActions.app.filterMenu') },
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('SearchControls', () => {
|
||||
let props;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
props = {
|
||||
searchValue: 'alice',
|
||||
setSearchValue: jest.fn(),
|
||||
fetchGrades: jest.fn().mockName('fetchGrades'),
|
||||
};
|
||||
});
|
||||
|
||||
const searchControls = (overriddenProps) => {
|
||||
props = { ...props, ...overriddenProps };
|
||||
return shallow(<SearchControls {...props} />);
|
||||
};
|
||||
|
||||
describe('Component', () => {
|
||||
describe('Snapshots', () => {
|
||||
test('basic snapshot', () => {
|
||||
const wrapper = searchControls();
|
||||
wrapper.instance().onBlur = jest.fn().mockName('onBlur');
|
||||
wrapper.instance().onClear = jest.fn().mockName('onClear');
|
||||
wrapper.instance().onSubmit = jest.fn().mockName('onSubmit');
|
||||
expect(wrapper.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Behavior', () => {
|
||||
describe('onBlur', () => {
|
||||
it('saves the search value to Gradebook state but do not fetch grade', () => {
|
||||
const wrapper = searchControls();
|
||||
const event = {
|
||||
target: {
|
||||
value: 'bob',
|
||||
},
|
||||
};
|
||||
wrapper.instance().onBlur(event);
|
||||
expect(props.setSearchValue).toHaveBeenCalledWith('bob');
|
||||
expect(props.fetchGrades).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('onClear', () => {
|
||||
it('sets search value to empty string and calls fetchGrades', () => {
|
||||
const wrapper = searchControls();
|
||||
wrapper.instance().onClear();
|
||||
expect(props.setSearchValue).toHaveBeenCalledWith('');
|
||||
expect(props.fetchGrades).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('onSubmit', () => {
|
||||
it('sets search value to input and calls fetchGrades', () => {
|
||||
const wrapper = searchControls();
|
||||
|
||||
wrapper.instance().onSubmit('John');
|
||||
expect(props.setSearchValue).toHaveBeenCalledWith('John');
|
||||
expect(props.fetchGrades).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapStateToProps', () => {
|
||||
const testState = { never: 'gonna', give: 'you up' };
|
||||
test('searchValue from app.searchValue', () => {
|
||||
expect(
|
||||
mapStateToProps(testState).searchValue,
|
||||
).toEqual(selectors.app.searchValue(testState));
|
||||
});
|
||||
});
|
||||
describe('mapDispatchToProps', () => {
|
||||
test('fetchGrades from thunkActions.grades.fetchGrades', () => {
|
||||
expect(mapDispatchToProps.fetchGrades).toEqual(thunkActions.grades.fetchGrades);
|
||||
});
|
||||
|
||||
test('setSearchValue from actions.app.setSearchValue', () => {
|
||||
expect(mapDispatchToProps.setSearchValue).toEqual(actions.app.setSearchValue);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,31 +1,22 @@
|
||||
/* eslint-disable react/sort-comp, react/button-has-type, import/no-named-as-default */
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { Icon } from '@edx/paragon';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
import { selectors } from 'data/redux/hooks';
|
||||
|
||||
/**
|
||||
* <SpinnerIcon />
|
||||
* Simmple redux-connected icon component that shows a spinner overlay only if
|
||||
* redux state says it should.
|
||||
*/
|
||||
export const SpinnerIcon = ({ show }) => show && (
|
||||
<div className="spinner-overlay">
|
||||
<Icon className="fa fa-spinner fa-spin fa-5x color-black" />
|
||||
</div>
|
||||
);
|
||||
SpinnerIcon.defaultProps = {
|
||||
show: false,
|
||||
};
|
||||
SpinnerIcon.propTypes = {
|
||||
show: PropTypes.bool,
|
||||
export const SpinnerIcon = () => {
|
||||
const show = selectors.root.useShouldShowSpinner();
|
||||
return show && (
|
||||
<div className="spinner-overlay">
|
||||
<Icon className="fa fa-spinner fa-spin fa-5x color-black" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
SpinnerIcon.propTypes = {};
|
||||
|
||||
export const mapStateToProps = (state) => ({
|
||||
show: selectors.root.shouldShowSpinner(state),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(SpinnerIcon);
|
||||
export default SpinnerIcon;
|
||||
|
||||
@@ -1,32 +1,35 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
import { SpinnerIcon, mapStateToProps } from './SpinnerIcon';
|
||||
import { selectors } from 'data/redux/hooks';
|
||||
import SpinnerIcon from './SpinnerIcon';
|
||||
|
||||
jest.mock('@edx/paragon', () => ({
|
||||
Icon: () => 'Icon',
|
||||
}));
|
||||
jest.mock('data/selectors', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
root: { shouldShowSpinner: state => ({ shouldShowSpinner: state }) },
|
||||
jest.mock('data/redux/hooks', () => ({
|
||||
selectors: {
|
||||
root: { useShouldShowSpinner: jest.fn() },
|
||||
},
|
||||
}));
|
||||
|
||||
selectors.root.useShouldShowSpinner.mockReturnValue(true);
|
||||
let el;
|
||||
describe('SpinnerIcon', () => {
|
||||
describe('component', () => {
|
||||
it('snapshot - does not render if show: false', () => {
|
||||
expect(shallow(<SpinnerIcon />)).toMatchSnapshot();
|
||||
});
|
||||
test('snapshot - displays spinner overlay with spinner icon', () => {
|
||||
expect(shallow(<SpinnerIcon show />)).toMatchSnapshot();
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
el = shallow(<SpinnerIcon />);
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes redux hook', () => {
|
||||
expect(selectors.root.useShouldShowSpinner).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe('mapStateToProps', () => {
|
||||
const testState = { a: 'nice', day: 'for', some: 'sun' };
|
||||
test('show from root.shouldShowSpinner', () => {
|
||||
expect(mapStateToProps(testState).show).toEqual(selectors.root.shouldShowSpinner(testState));
|
||||
describe('component', () => {
|
||||
it('does not render if show: false', () => {
|
||||
selectors.root.useShouldShowSpinner.mockReturnValueOnce(false);
|
||||
el = shallow(<SpinnerIcon />);
|
||||
expect(el.isEmptyRender()).toEqual(true);
|
||||
});
|
||||
test('snapshot - displays spinner overlay with spinner icon', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { Alert } from '@edx/paragon';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
import actions from 'data/actions';
|
||||
import messages from './StatusAlerts.messages';
|
||||
|
||||
export class StatusAlerts extends React.Component {
|
||||
get isCourseGradeFilterAlertOpen() {
|
||||
return (
|
||||
!this.props.limitValidity.isMinValid
|
||||
|| !this.props.limitValidity.isMaxValid
|
||||
);
|
||||
}
|
||||
|
||||
get minValidityMessage() {
|
||||
return (this.props.limitValidity.isMinValid)
|
||||
? ''
|
||||
: <FormattedMessage {...messages.minGradeInvalid} />;
|
||||
}
|
||||
|
||||
get maxValidityMessage() {
|
||||
return (this.props.limitValidity.isMaxValid)
|
||||
? ''
|
||||
: <FormattedMessage {...messages.maxGradeInvalid} />;
|
||||
}
|
||||
|
||||
get courseGradeFilterAlertDialogText() {
|
||||
return (
|
||||
<>
|
||||
{this.minValidityMessage}{this.maxValidityMessage}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<Alert
|
||||
variant="success"
|
||||
onClose={this.props.handleCloseSuccessBanner}
|
||||
show={this.props.showSuccessBanner}
|
||||
>
|
||||
<FormattedMessage {...messages.editSuccessAlert} />
|
||||
</Alert>
|
||||
<Alert
|
||||
variant="danger"
|
||||
dismissible={false}
|
||||
show={this.isCourseGradeFilterAlertOpen}
|
||||
>
|
||||
{this.courseGradeFilterAlertDialogText}
|
||||
</Alert>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
StatusAlerts.defaultProps = {
|
||||
};
|
||||
|
||||
StatusAlerts.propTypes = {
|
||||
// redux
|
||||
handleCloseSuccessBanner: PropTypes.func.isRequired,
|
||||
limitValidity: PropTypes.shape({
|
||||
isMaxValid: PropTypes.bool,
|
||||
isMinValid: PropTypes.bool,
|
||||
}).isRequired,
|
||||
showSuccessBanner: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export const mapStateToProps = (state) => ({
|
||||
limitValidity: selectors.app.courseGradeFilterValidity(state),
|
||||
showSuccessBanner: selectors.grades.showSuccess(state),
|
||||
});
|
||||
|
||||
export const mapDispatchToProps = {
|
||||
handleCloseSuccessBanner: actions.grades.banner.close,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(StatusAlerts);
|
||||
@@ -1,123 +0,0 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import actions from 'data/actions';
|
||||
import selectors from 'data/selectors';
|
||||
import messages from './StatusAlerts.messages';
|
||||
import {
|
||||
StatusAlerts,
|
||||
mapDispatchToProps,
|
||||
mapStateToProps,
|
||||
} from './StatusAlerts';
|
||||
|
||||
jest.mock('@edx/paragon', () => ({
|
||||
Alert: 'Alert',
|
||||
}));
|
||||
jest.mock('data/selectors', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
app: {
|
||||
courseGradeFilterValidity: (state) => ({ courseGradeFilterValidity: state }),
|
||||
},
|
||||
grades: {
|
||||
showSuccess: (state) => ({ showSuccess: state }),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('StatusAlerts', () => {
|
||||
let props = {
|
||||
showSuccessBanner: true,
|
||||
limitValidity: {
|
||||
isMaxValid: true,
|
||||
isMinValid: true,
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
props = {
|
||||
...props,
|
||||
handleCloseSuccessBanner: jest.fn().mockName('handleCloseSuccessBanner'),
|
||||
};
|
||||
});
|
||||
|
||||
describe('snapshots', () => {
|
||||
let el;
|
||||
it('basic snapshot', () => {
|
||||
el = shallow(<StatusAlerts {...props} />);
|
||||
const courseGradeFilterAlertDialogText = 'the quiCk brown does somEthing or other';
|
||||
jest.spyOn(
|
||||
el.instance(),
|
||||
'courseGradeFilterAlertDialogText',
|
||||
'get',
|
||||
).mockReturnValue(courseGradeFilterAlertDialogText);
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('behavior', () => {
|
||||
it.each([
|
||||
[false, false],
|
||||
[false, true],
|
||||
[true, false],
|
||||
[true, true],
|
||||
])('min + max course grade validity', (isMinValid, isMaxValid) => {
|
||||
props = {
|
||||
...props,
|
||||
limitValidity: {
|
||||
isMinValid,
|
||||
isMaxValid,
|
||||
},
|
||||
};
|
||||
const el = shallow(<StatusAlerts {...props} />);
|
||||
expect(
|
||||
el.instance().isCourseGradeFilterAlertOpen,
|
||||
).toEqual(
|
||||
!isMinValid || !isMaxValid,
|
||||
);
|
||||
if (!isMaxValid) {
|
||||
if (!isMinValid) {
|
||||
expect(el.instance().courseGradeFilterAlertDialogText).toEqual(
|
||||
<>
|
||||
<FormattedMessage {...messages.minGradeInvalid} />
|
||||
<FormattedMessage {...messages.maxGradeInvalid} />
|
||||
</>,
|
||||
);
|
||||
} else {
|
||||
expect(
|
||||
el.instance().courseGradeFilterAlertDialogText,
|
||||
// eslint-disable-next-line react/jsx-curly-brace-presence
|
||||
).toEqual(<>{''}<FormattedMessage {...messages.maxGradeInvalid} /></>);
|
||||
}
|
||||
} else if (!isMinValid) {
|
||||
expect(
|
||||
el.instance().courseGradeFilterAlertDialogText,
|
||||
// eslint-disable-next-line react/jsx-curly-brace-presence
|
||||
).toEqual(<><FormattedMessage {...messages.minGradeInvalid} />{''}</>);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapStateToProps', () => {
|
||||
const testState = { A: 'pple', B: 'anana', C: 'ucumber' };
|
||||
let mapped;
|
||||
beforeEach(() => {
|
||||
mapped = mapStateToProps(testState);
|
||||
});
|
||||
test('limitValidity from app.courseGradeFitlerValidity', () => {
|
||||
expect(mapped.limitValidity).toEqual(selectors.app.courseGradeFilterValidity(testState));
|
||||
});
|
||||
test('showSuccessBanner from grades.showSuccess', () => {
|
||||
expect(mapped.showSuccessBanner).toEqual(selectors.grades.showSuccess(testState));
|
||||
});
|
||||
});
|
||||
describe('mapDispatchToProps', () => {
|
||||
test('handleCloseSuccessBanner from actions.grades.banner.close', () => {
|
||||
expect(
|
||||
mapDispatchToProps.handleCloseSuccessBanner,
|
||||
).toEqual(actions.grades.banner.close);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`StatusAlerts component render snapshot 1`] = `
|
||||
<Fragment>
|
||||
<Alert
|
||||
onClose={[MockFunction hooks.successBanner.onClose]}
|
||||
show="hooks.show-success-banner"
|
||||
variant="success"
|
||||
>
|
||||
hooks.success-banner-text
|
||||
</Alert>
|
||||
<Alert
|
||||
dismissible={false}
|
||||
show="hooks.show-grade-filter"
|
||||
variant="danger"
|
||||
>
|
||||
hooks.grade-filter-text
|
||||
</Alert>
|
||||
</Fragment>
|
||||
`;
|
||||
32
src/components/GradesView/StatusAlerts/hooks.js
Normal file
32
src/components/GradesView/StatusAlerts/hooks.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { actions, selectors } from 'data/redux/hooks';
|
||||
import messages from './messages';
|
||||
|
||||
export const useStatusAlertsData = () => {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
const limitValidity = selectors.app.useCourseGradeFilterValidity();
|
||||
const showSuccessBanner = selectors.grades.useShowSuccess();
|
||||
const handleCloseSuccessBanner = actions.grades.useCloseBanner();
|
||||
|
||||
const isCourseGradeFilterAlertOpen = !limitValidity.isMinValid || !limitValidity.isMaxValid;
|
||||
|
||||
const validityMessages = {
|
||||
min: limitValidity.isMinValid ? '' : formatMessage(messages.minGradeInvalid),
|
||||
max: limitValidity.isMaxValid ? '' : formatMessage(messages.maxGradeInvalid),
|
||||
};
|
||||
|
||||
return {
|
||||
successBanner: {
|
||||
onClose: handleCloseSuccessBanner,
|
||||
show: showSuccessBanner,
|
||||
text: formatMessage(messages.editSuccessAlert),
|
||||
},
|
||||
gradeFilter: {
|
||||
show: isCourseGradeFilterAlertOpen,
|
||||
text: `${validityMessages.min}${validityMessages.max}`,
|
||||
},
|
||||
};
|
||||
};
|
||||
export default useStatusAlertsData;
|
||||
110
src/components/GradesView/StatusAlerts/hooks.test.js
Normal file
110
src/components/GradesView/StatusAlerts/hooks.test.js
Normal file
@@ -0,0 +1,110 @@
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { formatMessage } from 'testUtils';
|
||||
import { actions, selectors } from 'data/redux/hooks';
|
||||
|
||||
import useStatusAlertsData from './hooks';
|
||||
import messages from './messages';
|
||||
|
||||
jest.mock('data/redux/hooks', () => ({
|
||||
actions: {
|
||||
grades: { useCloseBanner: jest.fn() },
|
||||
},
|
||||
selectors: {
|
||||
app: { useCourseGradeFilterValidity: jest.fn() },
|
||||
grades: { useShowSuccess: jest.fn() },
|
||||
},
|
||||
}));
|
||||
|
||||
const validity = {
|
||||
isMinValid: true,
|
||||
isMaxValid: true,
|
||||
};
|
||||
selectors.app.useCourseGradeFilterValidity.mockReturnValue(validity);
|
||||
const showSuccess = 'test-show-success';
|
||||
selectors.grades.useShowSuccess.mockReturnValue(showSuccess);
|
||||
const closeBanner = jest.fn().mockName('hooks.closeBanner');
|
||||
actions.grades.useCloseBanner.mockReturnValue(closeBanner);
|
||||
|
||||
let out;
|
||||
describe('useStatusAlertsData', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
out = useStatusAlertsData();
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes intl hook', () => {
|
||||
expect(useIntl).toHaveBeenCalled();
|
||||
});
|
||||
it('initializes redux hooks', () => {
|
||||
expect(actions.grades.useCloseBanner).toHaveBeenCalled();
|
||||
expect(selectors.app.useCourseGradeFilterValidity).toHaveBeenCalled();
|
||||
expect(selectors.grades.useShowSuccess).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe('output', () => {
|
||||
describe('successBanner', () => {
|
||||
test('onClose and show from redux', () => {
|
||||
expect(out.successBanner.onClose).toEqual(closeBanner);
|
||||
expect(out.successBanner.show).toEqual(showSuccess);
|
||||
});
|
||||
test('message', () => {
|
||||
expect(out.successBanner.text).toEqual(formatMessage(messages.editSuccessAlert));
|
||||
});
|
||||
});
|
||||
describe('gradeFilter', () => {
|
||||
describe('both filters are valid', () => {
|
||||
test('do not show', () => {
|
||||
expect(out.gradeFilter.show).toEqual(false);
|
||||
});
|
||||
});
|
||||
describe('min filter is invalid', () => {
|
||||
beforeEach(() => {
|
||||
selectors.app.useCourseGradeFilterValidity.mockReturnValue({
|
||||
isMinValid: false,
|
||||
isMaxValid: true,
|
||||
});
|
||||
out = useStatusAlertsData();
|
||||
});
|
||||
test('show grade filter banner', () => {
|
||||
expect(out.gradeFilter.show).toEqual(true);
|
||||
});
|
||||
test('filter message', () => {
|
||||
expect(out.gradeFilter.text).toEqual(formatMessage(messages.minGradeInvalid));
|
||||
});
|
||||
});
|
||||
describe('max filter is invalid', () => {
|
||||
beforeEach(() => {
|
||||
selectors.app.useCourseGradeFilterValidity.mockReturnValue({
|
||||
isMinValid: true,
|
||||
isMaxValid: false,
|
||||
});
|
||||
out = useStatusAlertsData();
|
||||
});
|
||||
test('show grade filter banner', () => {
|
||||
expect(out.gradeFilter.show).toEqual(true);
|
||||
});
|
||||
test('filter message', () => {
|
||||
expect(out.gradeFilter.text).toEqual(formatMessage(messages.maxGradeInvalid));
|
||||
});
|
||||
});
|
||||
describe('both filters are invalid', () => {
|
||||
beforeEach(() => {
|
||||
selectors.app.useCourseGradeFilterValidity.mockReturnValue({
|
||||
isMinValid: false,
|
||||
isMaxValid: false,
|
||||
});
|
||||
out = useStatusAlertsData();
|
||||
});
|
||||
test('show grade filter banner', () => {
|
||||
expect(out.gradeFilter.show).toEqual(true);
|
||||
});
|
||||
test('filter message', () => {
|
||||
expect(out.gradeFilter.text).toEqual(
|
||||
`${formatMessage(messages.minGradeInvalid)}${formatMessage(messages.maxGradeInvalid)}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
35
src/components/GradesView/StatusAlerts/index.jsx
Normal file
35
src/components/GradesView/StatusAlerts/index.jsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Alert } from '@edx/paragon';
|
||||
|
||||
import useStatusAlertsData from './hooks';
|
||||
|
||||
export const StatusAlerts = () => {
|
||||
const {
|
||||
successBanner,
|
||||
gradeFilter,
|
||||
} = useStatusAlertsData();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Alert
|
||||
variant="success"
|
||||
onClose={successBanner.onClose}
|
||||
show={successBanner.show}
|
||||
>
|
||||
{successBanner.text}
|
||||
</Alert>
|
||||
<Alert
|
||||
variant="danger"
|
||||
dismissible={false}
|
||||
show={gradeFilter.show}
|
||||
>
|
||||
{gradeFilter.text}
|
||||
</Alert>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
StatusAlerts.propTypes = {};
|
||||
|
||||
export default StatusAlerts;
|
||||
53
src/components/GradesView/StatusAlerts/index.test.jsx
Normal file
53
src/components/GradesView/StatusAlerts/index.test.jsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { Alert } from '@edx/paragon';
|
||||
|
||||
import useStatusAlertsData from './hooks';
|
||||
import StatusAlerts from '.';
|
||||
|
||||
jest.mock('./hooks', () => jest.fn());
|
||||
|
||||
const hookProps = {
|
||||
successBanner: {
|
||||
onClose: jest.fn().mockName('hooks.successBanner.onClose'),
|
||||
show: 'hooks.show-success-banner',
|
||||
text: 'hooks.success-banner-text',
|
||||
},
|
||||
gradeFilter: {
|
||||
show: 'hooks.show-grade-filter',
|
||||
text: 'hooks.grade-filter-text',
|
||||
},
|
||||
};
|
||||
useStatusAlertsData.mockReturnValue(hookProps);
|
||||
|
||||
let el;
|
||||
describe('StatusAlerts component', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
el = shallow(<StatusAlerts />);
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes component hooks', () => {
|
||||
expect(useStatusAlertsData).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe('render', () => {
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
test('success banner', () => {
|
||||
const alert = el.find(Alert).at(0);
|
||||
const props = alert.props();
|
||||
expect(props.onClose).toEqual(hookProps.successBanner.onClose);
|
||||
expect(props.show).toEqual(hookProps.successBanner.show);
|
||||
expect(alert.text()).toEqual(hookProps.successBanner.text);
|
||||
});
|
||||
test('grade filter banner', () => {
|
||||
const alert = el.find(Alert).at(1);
|
||||
const props = alert.props();
|
||||
expect(props.show).toEqual(hookProps.gradeFilter.show);
|
||||
expect(alert.text()).toEqual(hookProps.gradeFilter.text);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,44 +0,0 @@
|
||||
/* eslint-disable react/sort-comp, react/button-has-type, import/no-named-as-default */
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
|
||||
/**
|
||||
* <UsersLabel />
|
||||
* Simple label component displaying the filtered and total users shown
|
||||
*/
|
||||
export const UsersLabel = ({
|
||||
filteredUsersCount,
|
||||
totalUsersCount,
|
||||
}) => {
|
||||
if (!totalUsersCount) {
|
||||
return null;
|
||||
}
|
||||
const bold = (val) => (<span className="font-weight-bold">{val}</span>);
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="gradebook.GradesTab.usersVisibilityLabel'"
|
||||
defaultMessage="Showing {filteredUsers} of {totalUsers} total learners"
|
||||
description="Users visibility label"
|
||||
values={{
|
||||
filteredUsers: bold(filteredUsersCount),
|
||||
totalUsers: bold(totalUsersCount),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
UsersLabel.propTypes = {
|
||||
filteredUsersCount: PropTypes.number.isRequired,
|
||||
totalUsersCount: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
export const mapStateToProps = (state) => ({
|
||||
totalUsersCount: selectors.grades.totalUsersCount(state),
|
||||
filteredUsersCount: selectors.grades.filteredUsersCount(state),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(UsersLabel);
|
||||
@@ -1,46 +0,0 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
import { UsersLabel, mapStateToProps } from './UsersLabel';
|
||||
|
||||
jest.mock('@edx/paragon', () => ({
|
||||
Icon: () => 'Icon',
|
||||
}));
|
||||
jest.mock('data/selectors', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
grades: {
|
||||
filteredUsersCount: state => ({ filteredUsersCount: state }),
|
||||
totalUsersCount: state => ({ totalUsersCount: state }),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('UsersLabel', () => {
|
||||
describe('component', () => {
|
||||
const props = {
|
||||
filteredUsersCount: 23,
|
||||
totalUsersCount: 140,
|
||||
};
|
||||
it('does not render if totalUsersCount is falsey', () => {
|
||||
expect(shallow(<UsersLabel {...props} totalUsersCount={0} />)).toEqual({});
|
||||
});
|
||||
test('snapshot - displays label with number of filtered users out of total', () => {
|
||||
expect(shallow(<UsersLabel {...props} />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
describe('mapStateToProps', () => {
|
||||
const testState = { a: 'nice', day: 'for', some: 'rain' };
|
||||
let mapped;
|
||||
beforeEach(() => {
|
||||
mapped = mapStateToProps(testState);
|
||||
});
|
||||
test('filteredUsersCount from grades.filteredUsersCount', () => {
|
||||
expect(mapped.filteredUsersCount).toEqual(selectors.grades.filteredUsersCount(testState));
|
||||
});
|
||||
test('totalUsersCount from grades.totalUsersCount', () => {
|
||||
expect(mapped.totalUsersCount).toEqual(selectors.grades.totalUsersCount(testState));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,19 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`FilterMenuToggle component snapshots basic snapshot 1`] = `
|
||||
<Button
|
||||
className="btn-primary align-self-start"
|
||||
id="edit-filters-btn"
|
||||
onClick={[MockFunction this.props.toggleFilterDrawer]}
|
||||
>
|
||||
<Icon
|
||||
className="fa fa-filter"
|
||||
/>
|
||||
|
||||
<FormattedMessage
|
||||
defaultMessage="Edit Filters"
|
||||
description="A labeled button in the Grades tab that opens/closes the Filters tab, allowing the grades to be filtered"
|
||||
id="gradebook.GradesView.editFilterLabel"
|
||||
/>
|
||||
</Button>
|
||||
`;
|
||||
@@ -1,23 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`FilteredUsersLabel component snapshot - displays label with number of filtered users out of total 1`] = `
|
||||
<FormattedMessage
|
||||
defaultMessage="Showing {filteredUsers} of {totalUsers} total learners"
|
||||
description="Users visibility label"
|
||||
id="gradebook.GradesTab.usersVisibilityLabel'"
|
||||
values={
|
||||
Object {
|
||||
"filteredUsers": <span
|
||||
className="font-weight-bold"
|
||||
>
|
||||
23
|
||||
</span>,
|
||||
"totalUsers": <span
|
||||
className="font-weight-bold"
|
||||
>
|
||||
140
|
||||
</span>,
|
||||
}
|
||||
}
|
||||
/>
|
||||
`;
|
||||
@@ -1,16 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ImportSuccessToast component snapshots snapshot 1`] = `
|
||||
<Toast
|
||||
action={
|
||||
Object {
|
||||
"label": "View Activity Log",
|
||||
"onClick": [Function],
|
||||
}
|
||||
}
|
||||
onClose={[Function]}
|
||||
show={true}
|
||||
>
|
||||
Import Successful! Grades will be updated momentarily.
|
||||
</Toast>
|
||||
`;
|
||||
@@ -1,38 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`InterventionsReport component snapshots snapshot 1`] = `
|
||||
<div>
|
||||
<h4
|
||||
className="mt-0"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Interventions Report"
|
||||
description="The title for the Intervention report subsection"
|
||||
id="gradebook.GradesView.InterventionsReport.title"
|
||||
/>
|
||||
</h4>
|
||||
<div
|
||||
className="d-flex justify-content-between align-items-center"
|
||||
>
|
||||
<div
|
||||
className="intervention-report-description"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Need to find students who may be falling behind? Download the interventions report to obtain engagement metrics such as section attempts and visits."
|
||||
description="The description for the Intervention report subsection"
|
||||
id="gradebook.GradesView.InterventionsReport.description"
|
||||
/>
|
||||
</div>
|
||||
<NetworkButton
|
||||
label={
|
||||
Object {
|
||||
"defaultMessage": "Download Interventions",
|
||||
"description": "The labeled button to download the Intervention report from the Grades View",
|
||||
"id": "gradebook.GradesView.InterventionsReport.downloadBtn",
|
||||
}
|
||||
}
|
||||
onClick={[MockFunction handleClick]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -1,32 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ScoreViewInput component snapshot - select box with percent and absolute options and onClick from props.toggleFormat 1`] = `
|
||||
<FormGroup
|
||||
controlId="ScoreView"
|
||||
>
|
||||
<FormLabel>
|
||||
<FormattedMessage
|
||||
defaultMessage="Score View"
|
||||
description="The label for the dropdown list that allows a user to select the Score format"
|
||||
id="gradebook.GradesView.scoreViewLabel"
|
||||
/>
|
||||
:
|
||||
</FormLabel>
|
||||
<FormControl
|
||||
as="select"
|
||||
onChange={[MockFunction]}
|
||||
value="percent"
|
||||
>
|
||||
<option
|
||||
value="percent"
|
||||
>
|
||||
Percent
|
||||
</option>
|
||||
<option
|
||||
value="absolute"
|
||||
>
|
||||
Absolute
|
||||
</option>
|
||||
</FormControl>
|
||||
</FormGroup>
|
||||
`;
|
||||
@@ -1,28 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SearchControls Component Snapshots basic snapshot 1`] = `
|
||||
<div>
|
||||
<SearchField
|
||||
inputLabel={
|
||||
<FormattedMessage
|
||||
defaultMessage="Search for a learner"
|
||||
description="Text prompting a user to use this functionality to search for a learner"
|
||||
id="gradebook.GradesView.search.label"
|
||||
/>
|
||||
}
|
||||
onBlur={[MockFunction onBlur]}
|
||||
onClear={[MockFunction onClear]}
|
||||
onSubmit={[MockFunction onSubmit]}
|
||||
value="alice"
|
||||
/>
|
||||
<small
|
||||
className="form-text text-muted search-help-text"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Search by username, email, or student key"
|
||||
description="A hint explaining the ways a user can search"
|
||||
id="gradebook.GradesView.search.hint"
|
||||
/>
|
||||
</small>
|
||||
</div>
|
||||
`;
|
||||
@@ -9,5 +9,3 @@ exports[`SpinnerIcon component snapshot - displays spinner overlay with spinner
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`SpinnerIcon component snapshot - does not render if show: false 1`] = `""`;
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`StatusAlerts snapshots basic snapshot 1`] = `
|
||||
<React.Fragment>
|
||||
<Alert
|
||||
onClose={[MockFunction handleCloseSuccessBanner]}
|
||||
show={true}
|
||||
variant="success"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="The grade has been successfully edited. You may see a slight delay before updates appear in the Gradebook."
|
||||
description="An alert text for successfully editing a grade"
|
||||
id="gradebook.GradesView.editSuccessAlert"
|
||||
/>
|
||||
</Alert>
|
||||
<Alert
|
||||
dismissible={false}
|
||||
show={false}
|
||||
variant="danger"
|
||||
>
|
||||
the quiCk brown does somEthing or other
|
||||
</Alert>
|
||||
</React.Fragment>
|
||||
`;
|
||||
@@ -1,23 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`UsersLabel component snapshot - displays label with number of filtered users out of total 1`] = `
|
||||
<FormattedMessage
|
||||
defaultMessage="Showing {filteredUsers} of {totalUsers} total learners"
|
||||
description="Users visibility label"
|
||||
id="gradebook.GradesTab.usersVisibilityLabel'"
|
||||
values={
|
||||
Object {
|
||||
"filteredUsers": <span
|
||||
className="font-weight-bold"
|
||||
>
|
||||
23
|
||||
</span>,
|
||||
"totalUsers": <span
|
||||
className="font-weight-bold"
|
||||
>
|
||||
140
|
||||
</span>,
|
||||
}
|
||||
}
|
||||
/>
|
||||
`;
|
||||
41
src/components/GradesView/__snapshots__/index.test.jsx.snap
Normal file
41
src/components/GradesView/__snapshots__/index.test.jsx.snap
Normal file
@@ -0,0 +1,41 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`GradesView component render snapshot 1`] = `
|
||||
<Fragment>
|
||||
<SpinnerIcon />
|
||||
<InterventionsReport />
|
||||
<h3
|
||||
className="step-message-1"
|
||||
>
|
||||
filter-step-heading
|
||||
</h3>
|
||||
<div
|
||||
className="d-flex justify-content-between"
|
||||
>
|
||||
<FilterMenuToggle />
|
||||
<SearchControls />
|
||||
</div>
|
||||
<FilterBadges
|
||||
handleClose={[MockFunction hooks.handleFilterBadgeClose]}
|
||||
/>
|
||||
<StatusAlerts />
|
||||
<h3>
|
||||
gradebook-step-heading
|
||||
</h3>
|
||||
<div
|
||||
className="d-flex justify-content-between align-items-center mb-2"
|
||||
>
|
||||
<ScoreViewInput />
|
||||
<BulkManagementControls />
|
||||
</div>
|
||||
<FilteredUsersLabel />
|
||||
<GradebookTable />
|
||||
<PageButtons />
|
||||
<p>
|
||||
*
|
||||
test-masters-hint
|
||||
</p>
|
||||
<EditModal />
|
||||
<ImportSuccessToast />
|
||||
</Fragment>
|
||||
`;
|
||||
@@ -1,53 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`GradesView Component snapshots basic snapshot 1`] = `
|
||||
<React.Fragment>
|
||||
<SpinnerIcon />
|
||||
<InterventionsReport />
|
||||
<h3
|
||||
className="step-message-1"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Step 1: Filter the Grade Report"
|
||||
description="Filter controls container heading string"
|
||||
id="gradebook.GradesView.filterHeading"
|
||||
/>
|
||||
</h3>
|
||||
<div
|
||||
className="d-flex justify-content-between"
|
||||
>
|
||||
<FilterMenuToggle />
|
||||
<SearchControls />
|
||||
</div>
|
||||
<FilterBadges
|
||||
handleClose={[MockFunction this.handleFilterBadgeClose]}
|
||||
/>
|
||||
<StatusAlerts />
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
defaultMessage="Step 2: View or Modify Individual Grades"
|
||||
description="Alert text for invalid minimum course grade"
|
||||
id="gradebook.GradesView.gradebookStepHeading"
|
||||
/>
|
||||
</h3>
|
||||
<div
|
||||
className="d-flex justify-content-between align-items-center mb-2"
|
||||
>
|
||||
<ScoreViewInput />
|
||||
<BulkManagementControls />
|
||||
</div>
|
||||
<FilteredUsersLabel />
|
||||
<GradebookTable />
|
||||
<PageButtons />
|
||||
<p>
|
||||
*
|
||||
<FormattedMessage
|
||||
defaultMessage="available for learners in the Master's track only"
|
||||
description="Masters feature availability hint on Grades Tab"
|
||||
id="gradebook.GradesView.mastersHint"
|
||||
/>
|
||||
</p>
|
||||
<EditModal />
|
||||
<ImportSuccessToast />
|
||||
</React.Fragment>
|
||||
`;
|
||||
30
src/components/GradesView/hooks.js
Normal file
30
src/components/GradesView/hooks.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { actions, thunkActions } from 'data/redux/hooks';
|
||||
import messages from './messages';
|
||||
|
||||
export const useGradesViewData = ({ updateQueryParams }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const fetchGrades = thunkActions.grades.useFetchGrades();
|
||||
const resetFilters = actions.filters.useResetFilters();
|
||||
|
||||
const handleFilterBadgeClose = (filterNames) => () => {
|
||||
resetFilters(filterNames);
|
||||
updateQueryParams(filterNames.reduce(
|
||||
(obj, filterName) => ({ ...obj, [filterName]: false }),
|
||||
{},
|
||||
));
|
||||
fetchGrades();
|
||||
};
|
||||
|
||||
return {
|
||||
stepHeadings: {
|
||||
filter: formatMessage(messages.filterStepHeading),
|
||||
gradebook: formatMessage(messages.gradebookStepHeading),
|
||||
},
|
||||
handleFilterBadgeClose,
|
||||
mastersHint: formatMessage(messages.mastersHint),
|
||||
};
|
||||
};
|
||||
|
||||
export default useGradesViewData;
|
||||
62
src/components/GradesView/hooks.test.js
Normal file
62
src/components/GradesView/hooks.test.js
Normal file
@@ -0,0 +1,62 @@
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { formatMessage } from 'testUtils';
|
||||
import { actions, thunkActions } from 'data/redux/hooks';
|
||||
|
||||
import useGradesViewData from './hooks';
|
||||
import messages from './messages';
|
||||
|
||||
jest.mock('data/redux/hooks', () => ({
|
||||
actions: {
|
||||
filters: { useResetFilters: jest.fn() },
|
||||
},
|
||||
thunkActions: {
|
||||
grades: { useFetchGrades: jest.fn() },
|
||||
},
|
||||
}));
|
||||
|
||||
const fetchGrades = jest.fn();
|
||||
thunkActions.grades.useFetchGrades.mockReturnValue(fetchGrades);
|
||||
const resetFilters = jest.fn();
|
||||
actions.filters.useResetFilters.mockReturnValue(resetFilters);
|
||||
|
||||
const updateQueryParams = jest.fn();
|
||||
|
||||
let out;
|
||||
describe('useGradesViewData', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
out = useGradesViewData({ updateQueryParams });
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes intl hook', () => {
|
||||
expect(useIntl).toHaveBeenCalled();
|
||||
});
|
||||
it('initializes redux hooks', () => {
|
||||
expect(thunkActions.grades.useFetchGrades).toHaveBeenCalled();
|
||||
expect(actions.filters.useResetFilters).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe('output', () => {
|
||||
test('stepHeadings', () => {
|
||||
expect(out.stepHeadings.filter).toEqual(formatMessage(messages.filterStepHeading));
|
||||
expect(out.stepHeadings.gradebook).toEqual(formatMessage(messages.gradebookStepHeading));
|
||||
});
|
||||
test('mastersHint', () => {
|
||||
expect(out.mastersHint).toEqual(formatMessage(messages.mastersHint));
|
||||
});
|
||||
describe('handleFilterBadgeClose', () => {
|
||||
it('resets filters locally and in query params, and fetches grades', () => {
|
||||
const filters = ['some', 'filter', 'names'];
|
||||
out.handleFilterBadgeClose(filters)();
|
||||
expect(resetFilters).toHaveBeenCalledWith(filters);
|
||||
expect(updateQueryParams).toHaveBeenCalledWith({
|
||||
some: false,
|
||||
filter: false,
|
||||
names: false,
|
||||
});
|
||||
expect(fetchGrades).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,12 +1,6 @@
|
||||
/* eslint-disable react/sort-comp, react/button-has-type, import/no-named-as-default */
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import actions from 'data/actions';
|
||||
import thunkActions from 'data/thunkActions';
|
||||
|
||||
import BulkManagementControls from './BulkManagementControls';
|
||||
import EditModal from './EditModal';
|
||||
@@ -21,79 +15,55 @@ import ScoreViewInput from './ScoreViewInput';
|
||||
import SearchControls from './SearchControls';
|
||||
import SpinnerIcon from './SpinnerIcon';
|
||||
import StatusAlerts from './StatusAlerts';
|
||||
import messages from './messages';
|
||||
|
||||
export class GradesView extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleFilterBadgeClose = this.handleFilterBadgeClose.bind(this);
|
||||
}
|
||||
import useGradesViewData from './hooks';
|
||||
|
||||
handleFilterBadgeClose(filterNames) {
|
||||
return () => {
|
||||
this.props.resetFilters(filterNames);
|
||||
this.props.updateQueryParams(filterNames.reduce(
|
||||
(obj, filterName) => ({ ...obj, [filterName]: false }),
|
||||
{},
|
||||
));
|
||||
this.props.fetchGrades();
|
||||
};
|
||||
}
|
||||
export const GradesView = ({ updateQueryParams }) => {
|
||||
const {
|
||||
stepHeadings,
|
||||
handleFilterBadgeClose,
|
||||
mastersHint,
|
||||
} = useGradesViewData({ updateQueryParams });
|
||||
|
||||
render() {
|
||||
return (
|
||||
<>
|
||||
<SpinnerIcon />
|
||||
return (
|
||||
<>
|
||||
<SpinnerIcon />
|
||||
|
||||
<InterventionsReport />
|
||||
<h3 className="step-message-1">
|
||||
<FormattedMessage {...messages.filterStepHeading} />
|
||||
</h3>
|
||||
<InterventionsReport />
|
||||
<h3 className="step-message-1">
|
||||
{stepHeadings.filter}
|
||||
</h3>
|
||||
|
||||
<div className="d-flex justify-content-between">
|
||||
<FilterMenuToggle />
|
||||
<SearchControls />
|
||||
</div>
|
||||
<div className="d-flex justify-content-between">
|
||||
<FilterMenuToggle />
|
||||
<SearchControls />
|
||||
</div>
|
||||
|
||||
<FilterBadges handleClose={this.handleFilterBadgeClose} />
|
||||
<StatusAlerts />
|
||||
<FilterBadges handleClose={handleFilterBadgeClose} />
|
||||
<StatusAlerts />
|
||||
|
||||
<h3><FormattedMessage {...messages.gradebookStepHeading} /></h3>
|
||||
<h3>{stepHeadings.gradebook}</h3>
|
||||
|
||||
<div className="d-flex justify-content-between align-items-center mb-2">
|
||||
<ScoreViewInput />
|
||||
<BulkManagementControls />
|
||||
</div>
|
||||
<div className="d-flex justify-content-between align-items-center mb-2">
|
||||
<ScoreViewInput />
|
||||
<BulkManagementControls />
|
||||
</div>
|
||||
|
||||
<FilteredUsersLabel />
|
||||
<FilteredUsersLabel />
|
||||
|
||||
<GradebookTable />
|
||||
<GradebookTable />
|
||||
|
||||
<PageButtons />
|
||||
<p>* <FormattedMessage {...messages.mastersHint} /></p>
|
||||
<EditModal />
|
||||
<PageButtons />
|
||||
<p>* {mastersHint}</p>
|
||||
<EditModal />
|
||||
|
||||
<ImportSuccessToast />
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
GradesView.defaultProps = {};
|
||||
<ImportSuccessToast />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
GradesView.propTypes = {
|
||||
updateQueryParams: PropTypes.func.isRequired,
|
||||
|
||||
// redux
|
||||
fetchGrades: PropTypes.func.isRequired,
|
||||
resetFilters: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export const mapStateToProps = () => ({});
|
||||
|
||||
export const mapDispatchToProps = {
|
||||
fetchGrades: thunkActions.grades.fetchGrades,
|
||||
resetFilters: actions.filters.reset,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(GradesView);
|
||||
export default GradesView;
|
||||
|
||||
57
src/components/GradesView/index.test.jsx
Normal file
57
src/components/GradesView/index.test.jsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import FilterBadges from './FilterBadges';
|
||||
|
||||
import useGradesViewData from './hooks';
|
||||
import GradesView from '.';
|
||||
|
||||
jest.mock('./BulkManagementControls', () => 'BulkManagementControls');
|
||||
jest.mock('./EditModal', () => 'EditModal');
|
||||
jest.mock('./FilterBadges', () => 'FilterBadges');
|
||||
jest.mock('./FilteredUsersLabel', () => 'FilteredUsersLabel');
|
||||
jest.mock('./FilterMenuToggle', () => 'FilterMenuToggle');
|
||||
jest.mock('./GradebookTable', () => 'GradebookTable');
|
||||
jest.mock('./ImportSuccessToast', () => 'ImportSuccessToast');
|
||||
jest.mock('./InterventionsReport', () => 'InterventionsReport');
|
||||
jest.mock('./PageButtons', () => 'PageButtons');
|
||||
jest.mock('./ScoreViewInput', () => 'ScoreViewInput');
|
||||
jest.mock('./SearchControls', () => 'SearchControls');
|
||||
jest.mock('./SpinnerIcon', () => 'SpinnerIcon');
|
||||
jest.mock('./StatusAlerts', () => 'StatusAlerts');
|
||||
jest.mock('./hooks', () => jest.fn());
|
||||
|
||||
const hookProps = {
|
||||
stepHeadings: {
|
||||
filter: 'filter-step-heading',
|
||||
gradebook: 'gradebook-step-heading',
|
||||
},
|
||||
handleFilterBadgeClose: jest.fn().mockName('hooks.handleFilterBadgeClose'),
|
||||
mastersHint: 'test-masters-hint',
|
||||
};
|
||||
useGradesViewData.mockReturnValue(hookProps);
|
||||
|
||||
const updateQueryParams = jest.fn().mockName('props.updateQueryParams');
|
||||
|
||||
let el;
|
||||
describe('GradesView component', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
el = shallow(<GradesView updateQueryParams={updateQueryParams} />);
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes component hooks', () => {
|
||||
expect(useGradesViewData).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe('render', () => {
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
test('filterBadges load close behavior from hook', () => {
|
||||
expect(el.find(FilterBadges).props().handleClose).toEqual(
|
||||
hookProps.handleFilterBadgeClose,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,105 +0,0 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import actions from 'data/actions';
|
||||
import thunkActions from 'data/thunkActions';
|
||||
|
||||
import {
|
||||
GradesView,
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
} from '.';
|
||||
|
||||
jest.mock('data/actions', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
app: { setView: jest.fn() },
|
||||
filters: { resetFilters: jest.fn() },
|
||||
},
|
||||
}));
|
||||
jest.mock('data/thunkActions', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
grades: { fetchGrades: jest.fn() },
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('./BulkManagementControls', () => 'BulkManagementControls');
|
||||
jest.mock('./EditModal', () => 'EditModal');
|
||||
jest.mock('./FilterBadges', () => 'FilterBadges');
|
||||
jest.mock('./FilteredUsersLabel', () => 'FilteredUsersLabel');
|
||||
jest.mock('./FilterMenuToggle', () => 'FilterMenuToggle');
|
||||
jest.mock('./GradebookTable', () => 'GradebookTable');
|
||||
jest.mock('./ImportSuccessToast', () => 'ImportSuccessToast');
|
||||
jest.mock('./InterventionsReport', () => 'InterventionsReport');
|
||||
jest.mock('./PageButtons', () => 'PageButtons');
|
||||
jest.mock('./ScoreViewInput', () => 'ScoreViewInput');
|
||||
jest.mock('./SearchControls', () => 'SearchControls');
|
||||
jest.mock('./SpinnerIcon', () => 'SpinnerIcon');
|
||||
jest.mock('./StatusAlerts', () => 'StatusAlerts');
|
||||
|
||||
describe('GradesView', () => {
|
||||
let props;
|
||||
beforeEach(() => {
|
||||
props = {
|
||||
updateQueryParams: jest.fn(),
|
||||
fetchGrades: jest.fn(),
|
||||
resetFilters: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
describe('Component', () => {
|
||||
const filterNames = ['duck', 'Duck', 'Duuuuuck', 'GOOOOSE!'];
|
||||
describe('behavior', () => {
|
||||
let el;
|
||||
beforeEach(() => {
|
||||
el = shallow(<GradesView {...props} />);
|
||||
});
|
||||
describe('handleFilterBadgeClose', () => {
|
||||
beforeEach(() => {
|
||||
el.instance().handleFilterBadgeClose(filterNames)();
|
||||
});
|
||||
it('calls props.resetFilters with the filters', () => {
|
||||
expect(props.resetFilters).toHaveBeenCalledWith(filterNames);
|
||||
});
|
||||
it('calls props.updateQueryParams with a reset-filters obj', () => {
|
||||
expect(props.updateQueryParams).toHaveBeenCalledWith({
|
||||
[filterNames[0]]: false,
|
||||
[filterNames[1]]: false,
|
||||
[filterNames[2]]: false,
|
||||
[filterNames[3]]: false,
|
||||
});
|
||||
});
|
||||
it('calls fetchGrades', () => {
|
||||
expect(props.fetchGrades).toHaveBeenCalledWith();
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('snapshots', () => {
|
||||
test('basic snapshot', () => {
|
||||
const el = shallow(<GradesView {...props} />);
|
||||
el.instance().handleFilterBadgeClose = jest.fn().mockName('this.handleFilterBadgeClose');
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
test('mapStateToProps is empty', () => {
|
||||
expect(mapStateToProps({ some: 'state' })).toEqual({});
|
||||
});
|
||||
describe('mapDispatchToProps', () => {
|
||||
describe('fetchGrades', () => {
|
||||
test('from thunkActions.grades.fetchGrades', () => {
|
||||
expect(mapDispatchToProps.fetchGrades).toEqual(
|
||||
thunkActions.grades.fetchGrades,
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('resetFilters', () => {
|
||||
test('from actions.filters.reset', () => {
|
||||
expect(mapDispatchToProps.resetFilters).toEqual(
|
||||
actions.filters.reset,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user