-
- {'<< '}
-
-
-
-
-
-
-
{this.props.courseId}
- { this.props.showBulkManagement && (
-
-
-
- )}
-
- {this.props.areGradesFrozen
- && (
-
-
-
- )}
- {(this.props.canUserViewGradebook === false) && (
-
-
-
+export const GradebookHeader = () => {
+ const { formatMessage } = useIntl();
+ const {
+ areGradesFrozen,
+ canUserViewGradebook,
+ courseId,
+ handleToggleViewClick,
+ showBulkManagement,
+ toggleViewMessage,
+ } = useGradebookHeaderData();
+ const dashboardUrl = instructorDashboardUrl();
+ return (
+
+
+ {'<< '}
+ {formatMessage(messages.backToDashboard)}
+
+
{formatMessage(messages.gradebook)}
+
+
{courseId}
+ {showBulkManagement && (
+
+ {formatMessage(toggleViewMessage)}
+
)}
- );
- }
-}
-
-GradebookHeader.defaultProps = {
- // redux
- courseId: '',
- areGradesFrozen: false,
- canUserViewGradebook: false,
- showBulkManagement: false,
-};
-
-GradebookHeader.propTypes = {
- // redux
- activeView: PropTypes.string.isRequired,
- courseId: PropTypes.string,
- areGradesFrozen: PropTypes.bool,
- canUserViewGradebook: PropTypes.bool,
- setView: PropTypes.func.isRequired,
- showBulkManagement: PropTypes.bool,
-};
-
-export const mapStateToProps = (state) => ({
- activeView: selectors.app.activeView(state),
- courseId: selectors.app.courseId(state),
- areGradesFrozen: selectors.assignmentTypes.areGradesFrozen(state),
- canUserViewGradebook: selectors.roles.canUserViewGradebook(state),
- showBulkManagement: selectors.root.showBulkManagement(state),
-});
-
-export const mapDispatchToProps = {
- setView: actions.app.setView,
+ {areGradesFrozen && (
+
+ {formatMessage(messages.frozenWarning)}
+
+ )}
+ {(canUserViewGradebook === false) && (
+
+ {formatMessage(messages.unauthorizedWarning)}
+
+ )}
+
+ );
};
-export default connect(mapStateToProps, mapDispatchToProps)(GradebookHeader);
+export default GradebookHeader;
diff --git a/src/components/GradebookHeader/index.test.jsx b/src/components/GradebookHeader/index.test.jsx
new file mode 100644
index 00000000..369bdbb4
--- /dev/null
+++ b/src/components/GradebookHeader/index.test.jsx
@@ -0,0 +1,77 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import { useIntl } from '@edx/frontend-platform/i18n';
+import { Button } from '@edx/paragon';
+
+import { formatMessage } from 'testUtils';
+import { instructorDashboardUrl } from 'data/services/lms/urls';
+
+import useGradebookHeaderData from './hooks';
+import GradebookHeader from '.';
+
+jest.mock('./hooks', () => ({ __esModule: true, default: jest.fn() }));
+jest.mock('data/services/lms/urls', () => ({
+ instructorDashboardUrl: jest.fn(),
+}));
+
+instructorDashboardUrl.mockReturnValue('test-dashboard-url');
+
+const hookProps = {
+ areGradesFrozen: false,
+ canUserViewGradebook: true,
+ courseId: 'test-course-id',
+ handleToggleViewClick: jest.fn().mockName('hooks.handleToggleViewClick'),
+ showBulkManagement: false,
+ toggleViewMessage: { defaultMessage: 'toggle-view-message' },
+};
+useGradebookHeaderData.mockReturnValue(hookProps);
+
+let el;
+describe('GradebookHeader component', () => {
+ beforeAll(() => {
+ el = shallow(
);
+ });
+ describe('behavior', () => {
+ it('initializes hooks', () => {
+ expect(useGradebookHeaderData).toHaveBeenCalledWith();
+ expect(useIntl).toHaveBeenCalledWith();
+ });
+ });
+ describe('render', () => {
+ describe('default view', () => {
+ test('shapshot', () => {
+ expect(el).toMatchSnapshot();
+ });
+ });
+ describe('show bulk management', () => {
+ beforeEach(() => {
+ useGradebookHeaderData.mockReturnValueOnce({ ...hookProps, showBulkManagement: true });
+ el = shallow(
);
+ });
+ test('snapshot: show toggle view message button with handleToggleViewClick method', () => {
+ expect(el).toMatchSnapshot();
+ const { onClick, children } = el.find(Button).props();
+ expect(onClick).toEqual(hookProps.handleToggleViewClick);
+ expect(children).toEqual(formatMessage(hookProps.toggleViewMessage));
+ });
+ });
+ describe('frozen grades', () => {
+ beforeEach(() => {
+ useGradebookHeaderData.mockReturnValueOnce({ ...hookProps, areGradesFrozen: true });
+ el = shallow(
);
+ });
+ test('snapshot: show frozen warning', () => {
+ expect(el).toMatchSnapshot();
+ });
+ });
+ describe('user cannot view gradebook', () => {
+ beforeEach(() => {
+ useGradebookHeaderData.mockReturnValueOnce({ ...hookProps, canUserViewGradebook: false });
+ el = shallow(
);
+ });
+ test('snapshot: show unauthorized warning', () => {
+ expect(el).toMatchSnapshot();
+ });
+ });
+ });
+});
diff --git a/src/components/GradebookHeader/test.jsx b/src/components/GradebookHeader/test.jsx
deleted file mode 100644
index 49cebe41..00000000
--- a/src/components/GradebookHeader/test.jsx
+++ /dev/null
@@ -1,152 +0,0 @@
-import React from 'react';
-import { shallow } from 'enzyme';
-
-import { Button } from '@edx/paragon';
-import { FormattedMessage } from '@edx/frontend-platform/i18n';
-
-import actions from 'data/actions';
-import selectors from 'data/selectors';
-import { views } from 'data/constants/app';
-import messages from './messages';
-import { GradebookHeader, mapDispatchToProps, mapStateToProps } from '.';
-
-jest.mock('@edx/paragon', () => ({
- Button: () => 'Button',
-}));
-jest.mock('@edx/frontend-platform/i18n', () => ({
- defineMessages: m => m,
- FormattedMessage: () => 'FormattedMessage',
-}));
-jest.mock('data/actions', () => ({
- __esModule: true,
- default: {
- app: { setView: jest.fn() },
- },
-}));
-jest.mock('data/selectors', () => ({
- __esModule: true,
- default: {
- app: {
- activeView: jest.fn(state => ({ aciveView: state })),
- courseId: jest.fn(state => ({ courseId: state })),
- },
- assignmentTypes: { areGradesFrozen: jest.fn(state => ({ areGradesFrozen: state })) },
- roles: { canUserViewGradebook: jest.fn(state => ({ canUserViewGradebook: state })) },
- root: { showBulkManagement: jest.fn(state => ({ showBulkManagement: state })) },
- },
-}));
-
-const courseId = 'fakeID';
-describe('GradebookHeader component', () => {
- const props = {
- activeView: views.grades,
- areGradesFrozen: false,
- canUserViewGradebook: false,
- courseId,
- showBulkManagement: false,
- };
- beforeEach(() => {
- props.setView = jest.fn();
- });
- describe('snapshots', () => {
- let el;
- beforeEach(() => {
- el = shallow(
);
- el.instance().handleToggleViewClick = jest.fn().mockName('this.handleToggleViewClick');
- });
- describe('default values (grades frozen, cannot view).', () => {
- test('unauthorized warning, but no grades frozen warning', () => {
- expect(el.instance().render()).toMatchSnapshot();
- });
- });
- describe('grades frozen, cannot view', () => {
- test('unauthorized warning, and grades frozen warning.', () => {
- el.setProps({ areGradesFrozen: true });
- expect(el.instance().render()).toMatchSnapshot();
- });
- });
- describe('grades frozen, can view.', () => {
- test('grades frozen warning but no unauthorized warning', () => {
- el.setProps({ areGradesFrozen: true, canUserViewGradebook: true });
- expect(el.instance().render()).toMatchSnapshot();
- });
- });
- describe('show bulk management, active view is grades view', () => {
- test('toggle view button to activity log', () => {
- el.setProps({ showBulkManagement: true });
- expect(el.find(Button).getElement()).toEqual((
-
-
-
- ));
- expect(el.instance().render()).toMatchSnapshot();
- });
- });
- describe('show bulk management, active view is bulkManagementHistory view', () => {
- test('toggle view button to grades', () => {
- el.setProps({ showBulkManagement: true, activeView: views.bulkManagementHistory });
- expect(el.find(Button).getElement()).toEqual((
-
-
-
- ));
- expect(el.instance().render()).toMatchSnapshot();
- });
- });
- });
- describe('behavior', () => {
- let el;
- beforeEach(() => {
- el = shallow(
);
- });
- describe('handleToggleViewClick', () => {
- test('calls setView with activity view if activeView is grades', () => {
- el.instance().handleToggleViewClick();
- expect(props.setView).toHaveBeenCalledWith(views.bulkManagementHistory);
- });
- test('calls setView with grades view if activeView is bulkManagementHistory', () => {
- el.setProps({ activeView: views.bulkManagementHistory });
- el.instance().handleToggleViewClick();
- expect(props.setView).toHaveBeenCalledWith(views.grades);
- });
- });
- });
- describe('mapStateToProps', () => {
- let mapped;
- const testState = { a: 'test', example: 'state' };
- beforeEach(() => {
- mapped = mapStateToProps(testState);
- });
- test('activeView from app.activeView', () => {
- expect(mapped.activeView).toEqual(selectors.app.activeView(testState));
- });
- test('courseId from app.courseId', () => {
- expect(mapped.courseId).toEqual(selectors.app.courseId(testState));
- });
- test('areGradesFrozen from assignmentTypes selector', () => {
- expect(
- mapped.areGradesFrozen,
- ).toEqual(selectors.assignmentTypes.areGradesFrozen(testState));
- });
- test('canUserViewGradebook from roles selector', () => {
- expect(
- mapped.canUserViewGradebook,
- ).toEqual(selectors.roles.canUserViewGradebook(testState));
- });
- test('showBulkManagement from root showBulkManagement selector', () => {
- expect(mapped.showBulkManagement).toEqual(selectors.root.showBulkManagement(testState));
- });
- });
-
- describe('mapDispatchToProps', () => {
- test('setView from actions.app.setView', () => {
- expect(mapDispatchToProps.setView).toEqual(actions.app.setView);
- });
- });
-});
diff --git a/src/components/GradesView/BulkManagementControls.jsx b/src/components/GradesView/BulkManagementControls.jsx
deleted file mode 100644
index d7fcbc94..00000000
--- a/src/components/GradesView/BulkManagementControls.jsx
+++ /dev/null
@@ -1,71 +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 { views } from 'data/constants/app';
-import actions from 'data/actions';
-import selectors from 'data/selectors';
-
-import NetworkButton from 'components/NetworkButton';
-import ImportGradesButton from './ImportGradesButton';
-
-import messages from './BulkManagementControls.messages';
-
-/**
- *
- * Provides download buttons for Bulk Management and Intervention reports, only if
- * showBulkManagement is set in redus.
- */
-export class BulkManagementControls extends React.Component {
- constructor(props) {
- super(props);
- this.handleClickExportGrades = this.handleClickExportGrades.bind(this);
- this.handleViewActivityLog = this.handleViewActivityLog.bind(this);
- }
-
- handleClickExportGrades() {
- this.props.downloadBulkGradesReport();
- window.location.assign(this.props.gradeExportUrl);
- }
-
- handleViewActivityLog() {
- this.props.setView(views.bulkManagementHistory);
- }
-
- render() {
- return this.props.showBulkManagement && (
-
-
-
-
- );
- }
-}
-
-BulkManagementControls.defaultProps = {
- showBulkManagement: false,
-};
-
-BulkManagementControls.propTypes = {
- // redux
- downloadBulkGradesReport: PropTypes.func.isRequired,
- gradeExportUrl: PropTypes.string.isRequired,
- showBulkManagement: PropTypes.bool,
- setView: PropTypes.func.isRequired,
-};
-
-export const mapStateToProps = (state) => ({
- gradeExportUrl: selectors.root.gradeExportUrl(state),
- showBulkManagement: selectors.root.showBulkManagement(state),
-});
-
-export const mapDispatchToProps = {
- downloadBulkGradesReport: actions.grades.downloadReport.bulkGrades,
- setView: actions.app.setView,
-};
-
-export default connect(mapStateToProps, mapDispatchToProps)(BulkManagementControls);
diff --git a/src/components/GradesView/BulkManagementControls.test.jsx b/src/components/GradesView/BulkManagementControls.test.jsx
deleted file mode 100644
index f9fa5e24..00000000
--- a/src/components/GradesView/BulkManagementControls.test.jsx
+++ /dev/null
@@ -1,124 +0,0 @@
-import React from 'react';
-import { shallow } from 'enzyme';
-
-import actions from 'data/actions';
-import selectors from 'data/selectors';
-import { views } from 'data/constants/app';
-
-import {
- BulkManagementControls,
- mapStateToProps,
- mapDispatchToProps,
-} from './BulkManagementControls';
-
-jest.mock('./ImportGradesButton', () => 'ImportGradesButton');
-jest.mock('components/NetworkButton', () => 'NetworkButton');
-jest.mock('data/selectors', () => ({
- __esModule: true,
- default: {
- root: {
- gradeExportUrl: (state) => ({ gradeExportUrl: state }),
- interventionExportUrl: (state) => ({ interventionExportUrl: state }),
- showBulkManagement: (state) => ({ showBulkManagement: state }),
- },
- },
-}));
-jest.mock('data/actions', () => ({
- __esModule: true,
- default: {
- app: { setView: jest.fn() },
- grades: {
- downloadReport: {
- bulkGrades: jest.fn(),
- intervention: jest.fn(),
- },
- },
- },
-}));
-
-describe('BulkManagementControls', () => {
- describe('component', () => {
- let el;
- let props = {
- gradeExportUrl: 'gradesGoHere',
- interventionExportUrl: 'interventionsGoHere',
- };
- beforeEach(() => {
- props = {
- ...props,
- downloadBulkGradesReport: jest.fn(),
- downloadInterventionReport: jest.fn(),
- setView: jest.fn(),
- };
- });
- test('snapshot - empty if showBulkManagement is not truthy', () => {
- expect(shallow(
)).toEqual({});
- });
- describe('behavior', () => {
- const oldWindowLocation = window.location;
-
- beforeAll(() => {
- delete window.location;
- window.location = Object.defineProperties(
- {},
- {
- ...Object.getOwnPropertyDescriptors(oldWindowLocation),
- assign: {
- configurable: true,
- value: jest.fn(),
- },
- },
- );
- });
- beforeEach(() => {
- window.location.assign.mockReset();
- el = shallow(
);
- });
- afterAll(() => {
- // restore `window.location` to the `jsdom` `Location` object
- window.location = oldWindowLocation;
- });
- describe('handleViewActivityLog', () => {
- it('calls props.setView(views.bulkManagementHistory)', () => {
- el.instance().handleViewActivityLog();
- expect(props.setView).toHaveBeenCalledWith(views.bulkManagementHistory);
- });
- });
- describe('handleClickExportGrades', () => {
- const assertions = [
- 'calls props.downloadBulkGradesReport',
- 'sets location to props.gradeExportUrl',
- ];
- it(assertions.join(' and '), () => {
- el.instance().handleClickExportGrades();
- expect(props.downloadBulkGradesReport).toHaveBeenCalled();
- expect(window.location.assign).toHaveBeenCalledWith(props.gradeExportUrl);
- });
- });
- });
- });
-
- describe('mapStateToProps', () => {
- let mapped;
- const testState = { do: 'not', test: 'me' };
- beforeEach(() => {
- mapped = mapStateToProps(testState);
- });
- test('gradeExportUrl from root.gradeExportUrl', () => {
- expect(mapped.gradeExportUrl).toEqual(selectors.root.gradeExportUrl(testState));
- });
- test('showBulkManagement from root.showBulkManagement', () => {
- expect(mapped.showBulkManagement).toEqual(selectors.root.showBulkManagement(testState));
- });
- });
- describe('mapDispatchToProps', () => {
- test('downloadBulkGradesReport from actions.grades.downloadReport.bulkGrades', () => {
- expect(
- mapDispatchToProps.downloadBulkGradesReport,
- ).toEqual(actions.grades.downloadReport.bulkGrades);
- });
- test('setView from actions.app.setView', () => {
- expect(mapDispatchToProps.setView).toEqual(actions.app.setView);
- });
- });
-});
diff --git a/src/components/GradesView/BulkManagementControls/__snapshots__/index.test.jsx.snap b/src/components/GradesView/BulkManagementControls/__snapshots__/index.test.jsx.snap
new file mode 100644
index 00000000..a97d82f8
--- /dev/null
+++ b/src/components/GradesView/BulkManagementControls/__snapshots__/index.test.jsx.snap
@@ -0,0 +1,19 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`BulkManagementControls render snapshot - show - network and import buttons 1`] = `
+
+
+
+
+`;
diff --git a/src/components/GradesView/BulkManagementControls/hooks.js b/src/components/GradesView/BulkManagementControls/hooks.js
new file mode 100644
index 00000000..bf970224
--- /dev/null
+++ b/src/components/GradesView/BulkManagementControls/hooks.js
@@ -0,0 +1,18 @@
+import { actions, selectors } from 'data/redux/hooks';
+
+export const useBulkManagementControlsData = () => {
+ const gradeExportUrl = selectors.root.useGradeExportUrl();
+ const showBulkManagement = selectors.root.useShowBulkManagement();
+ const downloadBulkGradesReport = actions.grades.useDownloadBulkGradesReport();
+
+ const handleClickExportGrades = () => {
+ downloadBulkGradesReport();
+ window.location.assign(gradeExportUrl);
+ };
+
+ return {
+ show: showBulkManagement,
+ handleClickExportGrades,
+ };
+};
+export default useBulkManagementControlsData;
diff --git a/src/components/GradesView/BulkManagementControls/hooks.test.js b/src/components/GradesView/BulkManagementControls/hooks.test.js
new file mode 100644
index 00000000..62bca614
--- /dev/null
+++ b/src/components/GradesView/BulkManagementControls/hooks.test.js
@@ -0,0 +1,72 @@
+import { actions, selectors } from 'data/redux/hooks';
+
+import useBulkManagementControlsData from './hooks';
+
+jest.mock('data/redux/hooks', () => ({
+ actions: {
+ grades: {
+ useDownloadBulkGradesReport: jest.fn(),
+ },
+ },
+ selectors: {
+ root: {
+ useGradeExportUrl: jest.fn(),
+ useShowBulkManagement: jest.fn(),
+ },
+ },
+}));
+
+const downloadBulkGrades = jest.fn();
+actions.grades.useDownloadBulkGradesReport.mockReturnValue(downloadBulkGrades);
+const gradeExportUrl = 'test-grade-export-url';
+selectors.root.useGradeExportUrl.mockReturnValue(gradeExportUrl);
+selectors.root.useShowBulkManagement.mockReturnValue(true);
+
+let hook;
+describe('useBulkManagementControlsData', () => {
+ const oldWindowLocation = window.location;
+ beforeAll(() => {
+ delete window.location;
+ window.location = Object.defineProperties(
+ {},
+ {
+ ...Object.getOwnPropertyDescriptors(oldWindowLocation),
+ assign: { configurable: true, value: jest.fn() },
+ },
+ );
+ });
+ beforeEach(() => {
+ window.location.assign.mockReset();
+ hook = useBulkManagementControlsData();
+ });
+ afterAll(() => {
+ // restore `window.location` to the `jsdom` `Location` object
+ window.location = oldWindowLocation;
+ });
+ describe('initialization', () => {
+ it('initializes redux hooks', () => {
+ expect(selectors.root.useGradeExportUrl).toHaveBeenCalledWith();
+ expect(selectors.root.useShowBulkManagement).toHaveBeenCalledWith();
+ expect(actions.grades.useDownloadBulkGradesReport).toHaveBeenCalledWith();
+ });
+ });
+ describe('output', () => {
+ it('forwards show from showBulkManagement', () => {
+ expect(hook.show).toEqual(true);
+ selectors.root.useShowBulkManagement.mockReturnValue(false);
+ hook = useBulkManagementControlsData();
+ expect(hook.show).toEqual(false);
+ });
+ describe('handleClickExportGrades', () => {
+ beforeEach(() => {
+ hook.handleClickExportGrades();
+ });
+ it('downloads bulk grades report', () => {
+ expect(downloadBulkGrades).toHaveBeenCalledWith();
+ });
+ it('sets window location to grade export url', () => {
+ expect(window.location.assign).toHaveBeenCalledWith(gradeExportUrl);
+ });
+ });
+ });
+});
diff --git a/src/components/GradesView/BulkManagementControls/index.jsx b/src/components/GradesView/BulkManagementControls/index.jsx
new file mode 100644
index 00000000..8c8d800d
--- /dev/null
+++ b/src/components/GradesView/BulkManagementControls/index.jsx
@@ -0,0 +1,33 @@
+/* eslint-disable react/sort-comp, react/button-has-type */
+import React from 'react';
+
+import NetworkButton from 'components/NetworkButton';
+import ImportGradesButton from '../ImportGradesButton';
+
+import useBulkManagementControlsData from './hooks';
+import messages from './messages';
+
+/**
+ *
+ * Provides download buttons for Bulk Management and Intervention reports, only if
+ * showBulkManagement is set in redus.
+ */
+export const BulkManagementControls = () => {
+ const {
+ show,
+ handleClickExportGrades,
+ } = useBulkManagementControlsData();
+
+ if (!show) { return null; }
+ return (
+
+
+
+
+ );
+};
+
+export default BulkManagementControls;
diff --git a/src/components/GradesView/BulkManagementControls/index.test.jsx b/src/components/GradesView/BulkManagementControls/index.test.jsx
new file mode 100644
index 00000000..e5e3ca65
--- /dev/null
+++ b/src/components/GradesView/BulkManagementControls/index.test.jsx
@@ -0,0 +1,32 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+
+import useBulkManagementControlsData from './hooks';
+import BulkManagementControls from '.';
+
+jest.mock('../ImportGradesButton', () => 'ImportGradesButton');
+jest.mock('components/NetworkButton', () => 'NetworkButton');
+
+jest.mock('./hooks', () => jest.fn());
+
+const hookProps = {
+ show: true,
+ handleClickExportGrades: jest.fn(),
+};
+useBulkManagementControlsData.mockReturnValue(hookProps);
+
+describe('BulkManagementControls', () => {
+ describe('behavior', () => {
+ shallow(
);
+ expect(useBulkManagementControlsData).toHaveBeenCalledWith();
+ });
+ describe('render', () => {
+ test('snapshot - show - network and import buttons', () => {
+ expect(shallow(
)).toMatchSnapshot();
+ });
+ test('snapshot - empty if show is not truthy', () => {
+ useBulkManagementControlsData.mockReturnValueOnce({ ...hookProps, show: false });
+ expect(shallow(
).isEmptyRender()).toEqual(true);
+ });
+ });
+});
diff --git a/src/components/GradesView/BulkManagementControls.messages.js b/src/components/GradesView/BulkManagementControls/messages.js
similarity index 100%
rename from src/components/GradesView/BulkManagementControls.messages.js
rename to src/components/GradesView/BulkManagementControls/messages.js
diff --git a/src/components/GradesView/EditModal/ModalHeaders.jsx b/src/components/GradesView/EditModal/ModalHeaders.jsx
index df52d985..d9f4ca29 100644
--- a/src/components/GradesView/EditModal/ModalHeaders.jsx
+++ b/src/components/GradesView/EditModal/ModalHeaders.jsx
@@ -1,68 +1,53 @@
import React from 'react';
-import PropTypes from 'prop-types';
-import { connect } from 'react-redux';
-import { FormattedMessage } from '@edx/frontend-platform/i18n';
+import { useIntl } from '@edx/frontend-platform/i18n';
-import selectors from 'data/selectors';
+import { StrictDict } from 'utils';
+import { selectors } from 'data/redux/hooks';
import messages from './messages';
import HistoryHeader from './HistoryHeader';
+export const HistoryKeys = StrictDict({
+ assignment: 'assignment',
+ student: 'student',
+ originalGrade: 'original-grade',
+ currentGrade: 'current-grade',
+});
+
/**
*
* Provides a list of HistoryHeaders for the student name, assignment,
* original grade, and current override grade.
*/
-export const ModalHeaders = ({
- modalState,
- originalGrade,
- currentGrade,
-}) => (
-
- }
- value={modalState.assignmentName}
- />
- }
- value={modalState.updateUserName}
- />
- }
- value={originalGrade}
- />
- }
- value={currentGrade}
- />
-
-);
-ModalHeaders.defaultProps = {
- currentGrade: null,
- originalGrade: null,
-};
-ModalHeaders.propTypes = {
- // redux
- currentGrade: PropTypes.number,
- originalGrade: PropTypes.number,
- modalState: PropTypes.shape({
- assignmentName: PropTypes.string.isRequired,
- updateUserName: PropTypes.string,
- }).isRequired,
+export const ModalHeaders = () => {
+ const { assignmentName, updateUserName } = selectors.app.useModalData();
+ const { currentGrade, originalGrade } = selectors.grades.useGradeData();
+ const { formatMessage } = useIntl();
+ return (
+
+
+
+
+
+
+ );
};
-export const mapStateToProps = (state) => ({
- modalState: {
- assignmentName: selectors.app.modalState.assignmentName(state),
- updateUserName: selectors.app.modalState.updateUserName(state),
- },
- currentGrade: selectors.grades.gradeOverrideCurrentEarnedGradedOverride(state),
- originalGrade: selectors.grades.gradeOriginalEarnedGraded(state),
-});
-
-export default connect(mapStateToProps)(ModalHeaders);
+export default ModalHeaders;
diff --git a/src/components/GradesView/EditModal/ModalHeaders.test.jsx b/src/components/GradesView/EditModal/ModalHeaders.test.jsx
index 57c23612..382eb6a8 100644
--- a/src/components/GradesView/EditModal/ModalHeaders.test.jsx
+++ b/src/components/GradesView/EditModal/ModalHeaders.test.jsx
@@ -1,93 +1,84 @@
import React from 'react';
import { shallow } from 'enzyme';
-import selectors from 'data/selectors';
+import { useIntl } from '@edx/frontend-platform/i18n';
+import { selectors } from 'data/redux/hooks';
-import {
- ModalHeaders,
- mapStateToProps,
-} from './ModalHeaders';
+import { formatMessage } from 'testUtils';
+
+import HistoryHeader from './HistoryHeader';
+import ModalHeaders, { HistoryKeys } from './ModalHeaders';
+import messages from './messages';
jest.mock('./HistoryHeader', () => 'HistoryHeader');
-jest.mock('data/selectors', () => ({
- __esModule: true,
- default: {
- app: {
- editUpdateData: jest.fn(state => ({ editUpdateData: state })),
- modalState: {
- assignmentName: jest.fn(state => ({ assignmentName: state })),
- updateUserName: jest.fn(state => ({ updateUserName: state })),
- },
- },
- grades: {
- gradeOverrideCurrentEarnedGradedOverride: jest.fn(state => ({ currentGrade: state })),
- gradeOriginalEarnedGraded: jest.fn(state => ({ originalGrade: state })),
- },
+jest.mock('data/redux/hooks', () => ({
+ selectors: {
+ app: { useModalData: jest.fn() },
+ grades: { useGradeData: jest.fn() },
},
}));
-describe('ModalHeaders', () => {
- let el;
- const props = {
- currentGrade: 2,
- originalGrade: 20,
- modalState: {
- assignmentName: 'Qwerty',
- updateUserName: 'Uiop',
- },
- };
- describe('Component', () => {
- describe('snapshots', () => {
- beforeEach(() => {
- });
- describe('gradeOverrideHistoryError is and empty and open is true', () => {
- test('modal open and StatusAlert showing', () => {
- el = shallow(
);
- expect(el).toMatchSnapshot();
- });
- });
- describe('gradeOverrideHistoryError is empty and open is false', () => {
- test('modal closed and StatusAlert closed', () => {
- el = shallow(
-
,
- );
- expect(el).toMatchSnapshot();
- });
- });
+const modalData = {
+ assignmentName: 'test-assignment-name',
+ updateUserName: 'test-user-name',
+};
+selectors.app.useModalData.mockReturnValue(modalData);
+const gradeData = {
+ currentGrade: 'test-current-grade',
+ originalGrade: 'test-original-grade',
+};
+selectors.grades.useGradeData.mockReturnValue(gradeData);
+
+let el;
+describe('ModalHeaders', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ el = shallow(
);
+ });
+ describe('behavior', () => {
+ it('initializes intl', () => {
+ expect(useIntl).toHaveBeenCalled();
+ });
+ it('initializes redux hooks', () => {
+ expect(selectors.app.useModalData).toHaveBeenCalled();
+ expect(selectors.grades.useGradeData).toHaveBeenCalled();
});
});
-
- describe('mapStateToProps', () => {
- const testState = { he: 'lives in a', pineapple: 'under the sea' };
- let mapped;
- beforeEach(() => {
- mapped = mapStateToProps(testState);
+ describe('render', () => {
+ test('snapshot', () => {
+ expect(el).toMatchSnapshot();
});
- describe('modalState', () => {
- test('assignmentName from app.modalState.assignmentName', () => {
- expect(
- mapped.modalState.assignmentName,
- ).toEqual(selectors.app.modalState.assignmentName(testState));
+ test('assignment header', () => {
+ const headerProps = el.find(HistoryHeader).at(0).props();
+ expect(headerProps).toMatchObject({
+ id: HistoryKeys.assignment,
+ label: formatMessage(messages.assignmentHeader),
+ value: modalData.assignmentName,
});
- test('updateUserName from app.modalState.updateUserName', () => {
- expect(
- mapped.modalState.updateUserName,
- ).toEqual(selectors.app.modalState.updateUserName(testState));
+ });
+ test('student header', () => {
+ const headerProps = el.find(HistoryHeader).at(1).props();
+ expect(headerProps).toMatchObject({
+ id: HistoryKeys.student,
+ label: formatMessage(messages.studentHeader),
+ value: modalData.updateUserName,
});
});
- describe('originalGrade', () => {
- test('from grades.gradeOverrideCurrentEarnedGradedOverride', () => {
- expect(mapped.currentGrade).toEqual(
- selectors.grades.gradeOverrideCurrentEarnedGradedOverride(testState),
- );
+ test('originalGrade header', () => {
+ const headerProps = el.find(HistoryHeader).at(2).props();
+ expect(headerProps).toMatchObject({
+ id: HistoryKeys.originalGrade,
+ label: formatMessage(messages.originalGradeHeader),
+ value: gradeData.originalGrade,
});
});
- describe('originalGrade', () => {
- test('from grades.gradeOriginalEarnedGrades', () => {
- expect(mapped.originalGrade).toEqual(
- selectors.grades.gradeOriginalEarnedGraded(testState),
- );
+ test('currentGrade header', () => {
+ const headerProps = el.find(HistoryHeader).at(3).props();
+ expect(headerProps).toMatchObject({
+ id: HistoryKeys.currentGrade,
+ label: formatMessage(messages.currentGradeHeader),
+ value: gradeData.currentGrade,
});
});
});
diff --git a/src/components/GradesView/EditModal/OverrideTable/AdjustedGradeInput.jsx b/src/components/GradesView/EditModal/OverrideTable/AdjustedGradeInput.jsx
deleted file mode 100644
index 9f3a5db5..00000000
--- a/src/components/GradesView/EditModal/OverrideTable/AdjustedGradeInput.jsx
+++ /dev/null
@@ -1,65 +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 { Form } from '@edx/paragon';
-
-import selectors from 'data/selectors';
-import actions from 'data/actions';
-import { getLocalizedSlash } from 'i18n/utils';
-
-/**
- *
- * Input control for adjusting the grade of a unit
- * displays an "/ ${possibleGrade} if there is one in the data model.
- */
-export class AdjustedGradeInput extends React.Component {
- constructor(props) {
- super(props);
- this.onChange = this.onChange.bind(this);
- }
-
- onChange = ({ target }) => {
- this.props.setModalState({ adjustedGradeValue: target.value });
- };
-
- render() {
- return (
-
-
- {this.props.possibleGrade && ` ${getLocalizedSlash()} ${this.props.possibleGrade}`}
-
- );
- }
-}
-AdjustedGradeInput.defaultProps = {
- possibleGrade: null,
-};
-AdjustedGradeInput.propTypes = {
- value: PropTypes.oneOfType([
- PropTypes.string,
- PropTypes.number,
- ]).isRequired,
- possibleGrade: PropTypes.oneOfType([
- PropTypes.string,
- PropTypes.number,
- ]),
- setModalState: PropTypes.func.isRequired,
-};
-
-export const mapStateToProps = (state) => ({
- possibleGrade: selectors.root.editModalPossibleGrade(state),
- value: selectors.app.modalState.adjustedGradeValue(state),
-});
-
-export const mapDispatchToProps = {
- setModalState: actions.app.setModalState,
-};
-
-export default connect(mapStateToProps, mapDispatchToProps)(AdjustedGradeInput);
diff --git a/src/components/GradesView/EditModal/OverrideTable/AdjustedGradeInput.test.jsx b/src/components/GradesView/EditModal/OverrideTable/AdjustedGradeInput.test.jsx
deleted file mode 100644
index 895fc00e..00000000
--- a/src/components/GradesView/EditModal/OverrideTable/AdjustedGradeInput.test.jsx
+++ /dev/null
@@ -1,92 +0,0 @@
-import React from 'react';
-import { shallow } from 'enzyme';
-
-import actions from 'data/actions';
-import selectors from 'data/selectors';
-
-import {
- AdjustedGradeInput,
- mapStateToProps,
- mapDispatchToProps,
-} from './AdjustedGradeInput';
-
-jest.mock('@edx/paragon', () => ({
- Form: { Control: () => 'Form.Control' },
-}));
-jest.mock('data/selectors', () => ({
- __esModule: true,
- default: {
- root: {
- editModalPossibleGrade: jest.fn(state => ({ updateUserName: state })),
- },
- app: {
- modalState: { adjustedGradeValue: jest.fn(state => ({ adjustedGradeValue: state })) },
- },
- },
-}));
-jest.mock('data/actions', () => ({
- __esModule: true,
- default: {
- app: { setModalState: jest.fn() },
- },
-}));
-describe('AdjustedGradeInput', () => {
- let el;
- let props = {
- value: 1,
- possibleGrade: 5,
- };
- beforeEach(() => {
- props = {
- ...props,
- setModalState: jest.fn(),
- };
- });
- describe('Component', () => {
- beforeEach(() => {
- el = shallow(
);
- });
- describe('snapshots', () => {
- test('displays input control and "out of possible grade" label', () => {
- el.instance().onChange = jest.fn().mockName('this.onChange');
- expect(el.instance().render()).toMatchSnapshot();
- });
- });
- describe('behavior', () => {
- describe('onChange', () => {
- it('calls props.setModalState event target value', () => {
- const value = 42;
- el.instance().onChange({ target: { value } });
- expect(props.setModalState).toHaveBeenCalledWith({
- adjustedGradeValue: value,
- });
- });
- });
- });
- });
-
- describe('mapStateToProps', () => {
- const testState = { like: 'no one', ever: 'was' };
- let mapped;
- beforeEach(() => {
- mapped = mapStateToProps(testState);
- });
- describe('modalState', () => {
- test('possibleGrade from root.editModalPossibleGrade', () => {
- expect(
- mapped.possibleGrade,
- ).toEqual(selectors.root.editModalPossibleGrade(testState));
- });
- test('updateUserName from app.modalState.updateUserName', () => {
- expect(
- mapped.value,
- ).toEqual(selectors.app.modalState.adjustedGradeValue(testState));
- });
- });
- });
- describe('mapDispatchToProps', () => {
- test('setModalState from actions.app.setModalState', () => {
- expect(mapDispatchToProps.setModalState).toEqual(actions.app.setModalState);
- });
- });
-});
diff --git a/src/components/GradesView/EditModal/OverrideTable/AdjustedGradeInput/__snapshots__/index.test.jsx.snap b/src/components/GradesView/EditModal/OverrideTable/AdjustedGradeInput/__snapshots__/index.test.jsx.snap
new file mode 100644
index 00000000..b12be4b3
--- /dev/null
+++ b/src/components/GradesView/EditModal/OverrideTable/AdjustedGradeInput/__snapshots__/index.test.jsx.snap
@@ -0,0 +1,13 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`AdjustedGradeInput component render snapshot 1`] = `
+
+
+ some-hint-text
+
+`;
diff --git a/src/components/GradesView/EditModal/OverrideTable/AdjustedGradeInput/hooks.js b/src/components/GradesView/EditModal/OverrideTable/AdjustedGradeInput/hooks.js
new file mode 100644
index 00000000..da315857
--- /dev/null
+++ b/src/components/GradesView/EditModal/OverrideTable/AdjustedGradeInput/hooks.js
@@ -0,0 +1,21 @@
+import { actions, selectors } from 'data/redux/hooks';
+import { getLocalizedSlash } from 'i18n/utils';
+
+const useAdjustedGradeInputData = () => {
+ const possibleGrade = selectors.root.useEditModalPossibleGrade();
+ const value = selectors.app.useModalData().adjustedGradeValue;
+ const setModalState = actions.app.useSetModalState();
+ const hintText = possibleGrade && ` ${getLocalizedSlash()} ${possibleGrade}`;
+
+ const onChange = ({ target }) => {
+ setModalState({ adjustedGradeValue: target.value });
+ };
+
+ return {
+ value,
+ onChange,
+ hintText,
+ };
+};
+
+export default useAdjustedGradeInputData;
diff --git a/src/components/GradesView/EditModal/OverrideTable/AdjustedGradeInput/hooks.test.jsx b/src/components/GradesView/EditModal/OverrideTable/AdjustedGradeInput/hooks.test.jsx
new file mode 100644
index 00000000..e266dc3d
--- /dev/null
+++ b/src/components/GradesView/EditModal/OverrideTable/AdjustedGradeInput/hooks.test.jsx
@@ -0,0 +1,67 @@
+import { getLocalizedSlash } from 'i18n/utils';
+import { actions, selectors } from 'data/redux/hooks';
+import useAdjustedGradeInputData from './hooks';
+
+jest.mock('data/redux/hooks', () => ({
+ selectors: {
+ root: {
+ useEditModalPossibleGrade: jest.fn(),
+ },
+ app: {
+ useModalData: jest.fn(),
+ },
+ },
+ actions: {
+ app: {
+ useSetModalState: jest.fn(),
+ },
+ },
+}));
+jest.mock('i18n/utils', () => ({ getLocalizedSlash: jest.fn() }));
+
+const localizedSlash = 'localized-slash';
+getLocalizedSlash.mockReturnValue(localizedSlash);
+
+const possibleGrade = 105;
+selectors.root.useEditModalPossibleGrade.mockReturnValue(possibleGrade);
+const modalData = { adjustedGradeValue: 70 };
+const setModalState = jest.fn();
+selectors.app.useModalData.mockReturnValue(modalData);
+actions.app.useSetModalState.mockReturnValue(setModalState);
+
+let out;
+describe('useAdjustedGradeInputData hook', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ out = useAdjustedGradeInputData();
+ });
+ describe('behavior', () => {
+ it('initializes redux hooks', () => {
+ expect(selectors.root.useEditModalPossibleGrade).toHaveBeenCalled();
+ expect(selectors.app.useModalData).toHaveBeenCalled();
+ expect(actions.app.useSetModalState).toHaveBeenCalled();
+ });
+ });
+ describe('output', () => {
+ it('forwards adjusted grade value as value from modal data', () => {
+ expect(out.value).toEqual(modalData.adjustedGradeValue);
+ });
+ describe('hintText', () => {
+ it('passes an undefined value if possibleGrade is not available', () => {
+ selectors.root.useEditModalPossibleGrade.mockReturnValueOnce(undefined);
+ out = useAdjustedGradeInputData();
+ expect(out.hintText).toEqual(undefined);
+ });
+ it('passes localized slash and possible grade if available', () => {
+ expect(out.hintText).toEqual(` ${localizedSlash} ${possibleGrade}`);
+ });
+ });
+ describe('onChange', () => {
+ it('sets modal state with event target value', () => {
+ const testValue = 'test-value';
+ out.onChange({ target: { value: testValue } });
+ expect(setModalState).toHaveBeenCalledWith({ adjustedGradeValue: testValue });
+ });
+ });
+ });
+});
diff --git a/src/components/GradesView/EditModal/OverrideTable/AdjustedGradeInput/index.jsx b/src/components/GradesView/EditModal/OverrideTable/AdjustedGradeInput/index.jsx
new file mode 100644
index 00000000..d070b9b5
--- /dev/null
+++ b/src/components/GradesView/EditModal/OverrideTable/AdjustedGradeInput/index.jsx
@@ -0,0 +1,33 @@
+import React from 'react';
+
+import { Form } from '@edx/paragon';
+
+import useAdjustedGradeInputData from './hooks';
+
+/**
+ *
+ * Input control for adjusting the grade of a unit
+ * displays an "/ ${possibleGrade} if there is one in the data model.
+ */
+export const AdjustedGradeInput = () => {
+ const {
+ value,
+ onChange,
+ hintText,
+ } = useAdjustedGradeInputData();
+ return (
+
+
+ {hintText}
+
+ );
+};
+
+AdjustedGradeInput.propTypes = {};
+
+export default AdjustedGradeInput;
diff --git a/src/components/GradesView/EditModal/OverrideTable/AdjustedGradeInput/index.test.jsx b/src/components/GradesView/EditModal/OverrideTable/AdjustedGradeInput/index.test.jsx
new file mode 100644
index 00000000..895dc6ff
--- /dev/null
+++ b/src/components/GradesView/EditModal/OverrideTable/AdjustedGradeInput/index.test.jsx
@@ -0,0 +1,38 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+
+import { Form } from '@edx/paragon';
+
+import useAdjustedGradeInputData from './hooks';
+import AdjustedGradeInput from '.';
+
+jest.mock('./hooks', () => jest.fn());
+
+const hookProps = {
+ hintText: 'some-hint-text',
+ onChange: jest.fn().mockName('hook.onChange'),
+ value: 'test-value',
+};
+useAdjustedGradeInputData.mockReturnValue(hookProps);
+
+let el;
+describe('AdjustedGradeInput component', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ el = shallow(
);
+ });
+ describe('behavior', () => {
+ it('initializes hook data', () => {
+ expect(useAdjustedGradeInputData).toHaveBeenCalled();
+ });
+ });
+ describe('render', () => {
+ test('snapshot', () => {
+ expect(el).toMatchSnapshot();
+ const control = el.find(Form.Control);
+ expect(control.props().value).toEqual(hookProps.value);
+ expect(control.props().onChange).toEqual(hookProps.onChange);
+ expect(el.contains(hookProps.hintText)).toEqual(true);
+ });
+ });
+});
diff --git a/src/components/GradesView/EditModal/OverrideTable/ReasonInput.jsx b/src/components/GradesView/EditModal/OverrideTable/ReasonInput.jsx
deleted file mode 100644
index a9e1c189..00000000
--- a/src/components/GradesView/EditModal/OverrideTable/ReasonInput.jsx
+++ /dev/null
@@ -1,55 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { connect } from 'react-redux';
-
-import { Form } from '@edx/paragon';
-
-import selectors from 'data/selectors';
-import actions from 'data/actions';
-
-/**
- *
- * Input control for the "reason for change" field in the Edit modal.
- */
-export class ReasonInput extends React.Component {
- constructor(props) {
- super(props);
- this.ref = React.createRef();
- this.onChange = this.onChange.bind(this);
- }
-
- componentDidMount() {
- this.ref.current.focus();
- }
-
- onChange = (event) => {
- this.props.setModalState({ reasonForChange: event.target.value });
- };
-
- render() {
- return (
-
- );
- }
-}
-ReasonInput.propTypes = {
- // redux
- setModalState: PropTypes.func.isRequired,
- value: PropTypes.string.isRequired,
-};
-
-export const mapStateToProps = (state) => ({
- value: selectors.app.modalState.reasonForChange(state),
-});
-
-export const mapDispatchToProps = {
- setModalState: actions.app.setModalState,
-};
-
-export default connect(mapStateToProps, mapDispatchToProps)(ReasonInput);
diff --git a/src/components/GradesView/EditModal/OverrideTable/ReasonInput.test.jsx b/src/components/GradesView/EditModal/OverrideTable/ReasonInput.test.jsx
deleted file mode 100644
index 5f9c311d..00000000
--- a/src/components/GradesView/EditModal/OverrideTable/ReasonInput.test.jsx
+++ /dev/null
@@ -1,90 +0,0 @@
-import React from 'react';
-import { shallow } from 'enzyme';
-
-import actions from 'data/actions';
-import selectors from 'data/selectors';
-
-import {
- ReasonInput,
- mapStateToProps,
- mapDispatchToProps,
-} from './ReasonInput';
-
-jest.mock('@edx/paragon', () => ({
- Form: { Control: () => 'Form.Control' },
-}));
-jest.mock('data/selectors', () => ({
- __esModule: true,
- default: {
- app: {
- modalState: { reasonForChange: jest.fn(state => ({ reasonForChange: state })) },
- },
- },
-}));
-jest.mock('data/actions', () => ({
- __esModule: true,
- default: {
- app: { setModalState: jest.fn() },
- },
-}));
-describe('ReasonInput', () => {
- let el;
- let props = {
- value: 'did not answer the question',
- };
- beforeEach(() => {
- props = {
- ...props,
- setModalState: jest.fn(),
- };
- });
- describe('Component', () => {
- beforeEach(() => {
- el = shallow(
, { disableLifecycleMethods: true });
- });
- describe('snapshots', () => {
- test('displays reason for change input control', () => {
- el.instance().onChange = jest.fn().mockName('this.onChange');
- expect(el.instance().render()).toMatchSnapshot();
- });
- });
- describe('behavior', () => {
- describe('onChange', () => {
- it('calls props.setModalState event target value', () => {
- const value = 42;
- el.instance().onChange({ target: { value } });
- expect(props.setModalState).toHaveBeenCalledWith({
- reasonForChange: value,
- });
- });
- });
- describe('componentDidMount', () => {
- it('focuses the input ref', () => {
- const focus = jest.fn();
- expect(el.instance().ref).toEqual({ current: null });
- el.instance().ref.current = { focus };
- el.instance().componentDidMount();
- expect(el.instance().ref.current.focus).toHaveBeenCalledWith();
- });
- });
- });
- });
-
- describe('mapStateToProps', () => {
- const testState = { to: { catchThem: 'my real test', trainThem: 'my cause!' } };
- let mapped;
- beforeEach(() => {
- mapped = mapStateToProps(testState);
- });
- describe('modalState', () => {
- test('value from app.modalState.reasonForChange', () => {
- expect(mapped.value).toEqual(selectors.app.modalState.reasonForChange(testState));
- });
- });
- });
- describe('mapDispatchToProps', () => {
- test('setModalState from actions.app.setModalState', () => {
- expect(mapDispatchToProps.setModalState).toEqual(actions.app.setModalState);
- });
- });
-});
diff --git a/src/components/GradesView/EditModal/OverrideTable/ReasonInput/__snapshots__/index.test.jsx.snap b/src/components/GradesView/EditModal/OverrideTable/ReasonInput/__snapshots__/index.test.jsx.snap
new file mode 100644
index 00000000..153368fb
--- /dev/null
+++ b/src/components/GradesView/EditModal/OverrideTable/ReasonInput/__snapshots__/index.test.jsx.snap
@@ -0,0 +1,11 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ReasonInput component render snapshot 1`] = `
+
+`;
diff --git a/src/components/GradesView/EditModal/OverrideTable/ReasonInput/hooks.js b/src/components/GradesView/EditModal/OverrideTable/ReasonInput/hooks.js
new file mode 100644
index 00000000..be0071ba
--- /dev/null
+++ b/src/components/GradesView/EditModal/OverrideTable/ReasonInput/hooks.js
@@ -0,0 +1,25 @@
+import React from 'react';
+
+import { actions, selectors } from 'data/redux/hooks';
+
+const useReasonInputData = () => {
+ const ref = React.useRef();
+ const { reasonForChange } = selectors.app.useModalData();
+ const setModalState = actions.app.useSetModalState();
+
+ React.useEffect(() => {
+ ref.current.focus();
+ }, [ref]);
+
+ const onChange = (event) => {
+ setModalState({ reasonForChange: event.target.value });
+ };
+
+ return {
+ value: reasonForChange,
+ onChange,
+ ref,
+ };
+};
+
+export default useReasonInputData;
diff --git a/src/components/GradesView/EditModal/OverrideTable/ReasonInput/hooks.test.jsx b/src/components/GradesView/EditModal/OverrideTable/ReasonInput/hooks.test.jsx
new file mode 100644
index 00000000..6b5acec8
--- /dev/null
+++ b/src/components/GradesView/EditModal/OverrideTable/ReasonInput/hooks.test.jsx
@@ -0,0 +1,63 @@
+import React from 'react';
+
+import { actions, selectors } from 'data/redux/hooks';
+import useReasonInputData from './hooks';
+
+jest.mock('data/redux/hooks', () => ({
+ selectors: {
+ app: {
+ useModalData: jest.fn(),
+ },
+ },
+ actions: {
+ app: {
+ useSetModalState: jest.fn(),
+ },
+ },
+}));
+
+const modalData = { reasonForChange: 'test-reason-for-change' };
+const setModalState = jest.fn();
+selectors.app.useModalData.mockReturnValue(modalData);
+actions.app.useSetModalState.mockReturnValue(setModalState);
+
+const ref = { current: { focus: jest.fn() }, useRef: true };
+React.useRef.mockReturnValue(ref);
+
+let out;
+describe('useReasonInputData hook', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ out = useReasonInputData();
+ });
+ describe('behavior', () => {
+ it('initializes ref', () => {
+ expect(React.useRef).toHaveBeenCalled();
+ });
+ it('initializes redux hooks', () => {
+ expect(selectors.app.useModalData).toHaveBeenCalled();
+ expect(actions.app.useSetModalState).toHaveBeenCalled();
+ });
+ it('focuses ref on load', () => {
+ const [[cb, prereqs]] = React.useEffect.mock.calls;
+ expect(prereqs).toEqual([ref]);
+ cb();
+ expect(ref.current.focus).toHaveBeenCalled();
+ });
+ });
+ describe('output', () => {
+ it('forwards reasonForChange as value from modal data', () => {
+ expect(out.value).toEqual(modalData.reasonForChange);
+ });
+ it('forwards ref', () => {
+ expect(out.ref).toEqual(ref);
+ });
+ describe('onChange', () => {
+ it('sets modal state with event target value', () => {
+ const testValue = 'test-value';
+ out.onChange({ target: { value: testValue } });
+ expect(setModalState).toHaveBeenCalledWith({ reasonForChange: testValue });
+ });
+ });
+ });
+});
diff --git a/src/components/GradesView/EditModal/OverrideTable/ReasonInput/index.jsx b/src/components/GradesView/EditModal/OverrideTable/ReasonInput/index.jsx
new file mode 100644
index 00000000..b76bd213
--- /dev/null
+++ b/src/components/GradesView/EditModal/OverrideTable/ReasonInput/index.jsx
@@ -0,0 +1,27 @@
+import React from 'react';
+
+import { Form } from '@edx/paragon';
+
+import useReasonInputData from './hooks';
+
+export const controlTestId = 'reason-input-control';
+
+/**
+ *
+ * Input control for the "reason for change" field in the Edit modal.
+ */
+export const ReasonInput = () => {
+ const { ref, value, onChange } = useReasonInputData();
+ return (
+
+ );
+};
+
+ReasonInput.propTypes = {};
+
+export default ReasonInput;
diff --git a/src/components/GradesView/EditModal/OverrideTable/ReasonInput/index.test.jsx b/src/components/GradesView/EditModal/OverrideTable/ReasonInput/index.test.jsx
new file mode 100644
index 00000000..8cf81342
--- /dev/null
+++ b/src/components/GradesView/EditModal/OverrideTable/ReasonInput/index.test.jsx
@@ -0,0 +1,37 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+
+import { Form } from '@edx/paragon';
+
+import useReasonInputData from './hooks';
+import ReasonInput from '.';
+
+jest.mock('./hooks', () => jest.fn());
+
+const hookProps = {
+ ref: 'reason-input-ref',
+ onChange: jest.fn().mockName('hook.onChange'),
+ value: 'test-value',
+};
+useReasonInputData.mockReturnValue(hookProps);
+
+let el;
+describe('ReasonInput component', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ el = shallow(
);
+ });
+ describe('behavior', () => {
+ it('initializes hook data', () => {
+ expect(useReasonInputData).toHaveBeenCalled();
+ });
+ });
+ describe('render', () => {
+ test('snapshot', () => {
+ expect(el).toMatchSnapshot();
+ const control = el.find(Form.Control);
+ expect(control.props().value).toEqual(hookProps.value);
+ expect(control.props().onChange).toEqual(hookProps.onChange);
+ });
+ });
+});
diff --git a/src/components/GradesView/EditModal/OverrideTable/ReasonInput/ref.test.jsx b/src/components/GradesView/EditModal/OverrideTable/ReasonInput/ref.test.jsx
new file mode 100644
index 00000000..e93bd753
--- /dev/null
+++ b/src/components/GradesView/EditModal/OverrideTable/ReasonInput/ref.test.jsx
@@ -0,0 +1,26 @@
+import React from 'react';
+import { render } from '@testing-library/react';
+
+import useReasonInputData from './hooks';
+import ReasonInput, { controlTestId } from '.';
+
+jest.unmock('react');
+jest.unmock('@edx/paragon');
+jest.mock('./hooks', () => ({ __esModule: true, default: jest.fn() }));
+
+const focus = jest.fn();
+const props = {
+ value: 'test-value',
+ onChange: jest.fn(),
+ ref: { current: { focus }, useRef: jest.fn() },
+};
+useReasonInputData.mockReturnValue(props);
+
+let el;
+describe('ReasonInput ref', () => {
+ it('loads ref from hook', () => {
+ el = render(
);
+ const control = el.getByTestId(controlTestId);
+ expect(control).toEqual(props.ref.current);
+ });
+});
diff --git a/src/components/GradesView/EditModal/OverrideTable/__snapshots__/AdjustedGradeInput.test.jsx.snap b/src/components/GradesView/EditModal/OverrideTable/__snapshots__/AdjustedGradeInput.test.jsx.snap
deleted file mode 100644
index 93a00243..00000000
--- a/src/components/GradesView/EditModal/OverrideTable/__snapshots__/AdjustedGradeInput.test.jsx.snap
+++ /dev/null
@@ -1,13 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`AdjustedGradeInput Component snapshots displays input control and "out of possible grade" label 1`] = `
-
-
- / 5
-
-`;
diff --git a/src/components/GradesView/EditModal/OverrideTable/__snapshots__/ReasonInput.test.jsx.snap b/src/components/GradesView/EditModal/OverrideTable/__snapshots__/ReasonInput.test.jsx.snap
deleted file mode 100644
index 5931cf92..00000000
--- a/src/components/GradesView/EditModal/OverrideTable/__snapshots__/ReasonInput.test.jsx.snap
+++ /dev/null
@@ -1,10 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`ReasonInput Component snapshots displays reason for change input control 1`] = `
-
-`;
diff --git a/src/components/GradesView/EditModal/OverrideTable/__snapshots__/index.test.jsx.snap b/src/components/GradesView/EditModal/OverrideTable/__snapshots__/index.test.jsx.snap
new file mode 100644
index 00000000..9fb2a581
--- /dev/null
+++ b/src/components/GradesView/EditModal/OverrideTable/__snapshots__/index.test.jsx.snap
@@ -0,0 +1,25 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`OverrideTable component render snapshot 1`] = `
+
,
+ "date": Object {
+ "formatted": 2000-01-01T00:00:00.000Z,
+ },
+ "reason":
,
+ },
+ ]
+ }
+ itemCount={2}
+/>
+`;
diff --git a/src/components/GradesView/EditModal/OverrideTable/__snapshots__/test.jsx.snap b/src/components/GradesView/EditModal/OverrideTable/__snapshots__/test.jsx.snap
deleted file mode 100644
index 1f1169b8..00000000
--- a/src/components/GradesView/EditModal/OverrideTable/__snapshots__/test.jsx.snap
+++ /dev/null
@@ -1,64 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`OverrideTable Component snapshots basic snapshot shows a row for each entry and one editable row 1`] = `
-,
- "accessor": "date",
- },
- Object {
- "Header": ,
- "accessor": "grader",
- },
- Object {
- "Header": ,
- "accessor": "reason",
- },
- Object {
- "Header": ,
- "accessor": "adjustedGrade",
- },
- ]
- }
- data={
- Array [
- Object {
- "adjustedGrade": 0,
- "date": "yesterday",
- "grader": "me",
- "reason": "you ate my sandwich",
- },
- Object {
- "adjustedGrade": 20,
- "date": "today",
- "grader": "me",
- "reason": "you brought me a new sandwich",
- },
- Object {
- "adjustedGrade":
,
- "date": "todaaaaaay",
- "reason":
,
- },
- ]
- }
- itemCount={2}
-/>
-`;
diff --git a/src/components/GradesView/EditModal/OverrideTable/hooks.js b/src/components/GradesView/EditModal/OverrideTable/hooks.js
new file mode 100644
index 00000000..d4dfc14c
--- /dev/null
+++ b/src/components/GradesView/EditModal/OverrideTable/hooks.js
@@ -0,0 +1,26 @@
+import { useIntl } from '@edx/frontend-platform/i18n';
+
+import { gradeOverrideHistoryColumns as columns } from 'data/constants/app';
+import { selectors } from 'data/redux/hooks';
+
+import messages from './messages';
+
+const useOverrideTableData = () => {
+ const { formatMessage } = useIntl();
+
+ const hide = selectors.grades.useHasOverrideErrors();
+ const gradeOverrides = selectors.grades.useGradeData().gradeOverrideHistoryResults;
+ const tableProps = {};
+ if (!hide) {
+ tableProps.columns = [
+ { Header: formatMessage(messages.dateHeader), accessor: columns.date },
+ { Header: formatMessage(messages.graderHeader), accessor: columns.grader },
+ { Header: formatMessage(messages.reasonHeader), accessor: columns.reason },
+ { Header: formatMessage(messages.adjustedGradeHeader), accessor: columns.adjustedGrade },
+ ];
+ tableProps.data = gradeOverrides;
+ }
+ return { hide, ...tableProps };
+};
+
+export default useOverrideTableData;
diff --git a/src/components/GradesView/EditModal/OverrideTable/hooks.test.js b/src/components/GradesView/EditModal/OverrideTable/hooks.test.js
new file mode 100644
index 00000000..8e684f00
--- /dev/null
+++ b/src/components/GradesView/EditModal/OverrideTable/hooks.test.js
@@ -0,0 +1,78 @@
+import { useIntl } from '@edx/frontend-platform/i18n';
+import { formatMessage } from 'testUtils';
+
+import { gradeOverrideHistoryColumns as columns } from 'data/constants/app';
+import { selectors } from 'data/redux/hooks';
+
+import useOverrideTableData from './hooks';
+import messages from './messages';
+
+jest.mock('data/redux/hooks', () => ({
+ selectors: {
+ grades: {
+ useHasOverrideErrors: jest.fn(),
+ useGradeData: jest.fn(),
+ },
+ },
+}));
+
+selectors.grades.useHasOverrideErrors.mockReturnValue(false);
+const gradeOverrides = ['some', 'override', 'data'];
+const gradeData = { gradeOverrideHistoryResults: gradeOverrides };
+selectors.grades.useGradeData.mockReturnValue(gradeData);
+
+let out;
+describe('useOverrideTableData', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ out = useOverrideTableData();
+ });
+ describe('behavior', () => {
+ it('initializes intl hook', () => {
+ expect(useIntl).toHaveBeenCalled();
+ });
+ it('initializes redux hooks', () => {
+ expect(selectors.grades.useHasOverrideErrors).toHaveBeenCalled();
+ expect(selectors.grades.useGradeData).toHaveBeenCalled();
+ });
+ });
+ describe('output', () => {
+ describe('no errors', () => {
+ test('hide is false', () => {
+ expect(out.hide).toEqual(false);
+ });
+ describe('columns', () => {
+ test('date column', () => {
+ const { Header, accessor } = out.columns[0];
+ expect(Header).toEqual(formatMessage(messages.dateHeader));
+ expect(accessor).toEqual(columns.date);
+ });
+ test('grader column', () => {
+ const { Header, accessor } = out.columns[1];
+ expect(Header).toEqual(formatMessage(messages.graderHeader));
+ expect(accessor).toEqual(columns.grader);
+ });
+ test('reason column', () => {
+ const { Header, accessor } = out.columns[2];
+ expect(Header).toEqual(formatMessage(messages.reasonHeader));
+ expect(accessor).toEqual(columns.reason);
+ });
+ test('adjustedGrade column', () => {
+ const { Header, accessor } = out.columns[3];
+ expect(Header).toEqual(formatMessage(messages.adjustedGradeHeader));
+ expect(accessor).toEqual(columns.adjustedGrade);
+ });
+ });
+ test('data passed from grade data', () => {
+ expect(out.data).toEqual(gradeOverrides);
+ });
+ });
+ describe('with errors', () => {
+ it('returns hide true and no other fields', () => {
+ selectors.grades.useHasOverrideErrors.mockReturnValue(true);
+ out = useOverrideTableData();
+ expect(out).toEqual({ hide: true });
+ });
+ });
+ });
+});
diff --git a/src/components/GradesView/EditModal/OverrideTable/index.jsx b/src/components/GradesView/EditModal/OverrideTable/index.jsx
index bd5e765a..bab0054a 100644
--- a/src/components/GradesView/EditModal/OverrideTable/index.jsx
+++ b/src/components/GradesView/EditModal/OverrideTable/index.jsx
@@ -1,73 +1,40 @@
/* 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 { DataTable } from '@edx/paragon';
-import { FormattedMessage } from '@edx/frontend-platform/i18n';
-import { gradeOverrideHistoryColumns as columns } from 'data/constants/app';
-import selectors from 'data/selectors';
+import { formatDateForDisplay } from 'utils';
-import messages from './messages';
import ReasonInput from './ReasonInput';
import AdjustedGradeInput from './AdjustedGradeInput';
+import useOverrideTableData from './hooks';
/**
*
* Table containing previous grade override entries, and an "edit" row
* with todays date, an AdjustedGradeInput and a ReasonInput
*/
-export const OverrideTable = ({
- hide,
- gradeOverrides,
- todaysDate,
-}) => {
- if (hide) {
- return null;
- }
+
+export const OverrideTable = () => {
+ const { hide, columns, data } = useOverrideTableData();
+
+ if (hide) { return null; }
+
return (
, accessor: columns.date },
- { Header:
, accessor: columns.grader },
- { Header:
, accessor: columns.reason },
- {
- Header:
,
- accessor: columns.adjustedGrade,
- },
- ]}
+ columns={columns}
data={[
- ...gradeOverrides,
+ ...data,
{
adjustedGrade:
,
- date: todaysDate,
+ date: formatDateForDisplay(new Date()),
reason:
,
},
]}
- itemCount={gradeOverrides.length}
+ itemCount={data.length}
/>
);
};
-OverrideTable.defaultProps = {
- gradeOverrides: [],
-};
-OverrideTable.propTypes = {
- // redux
- gradeOverrides: PropTypes.arrayOf(PropTypes.shape({
- date: PropTypes.string,
- grader: PropTypes.string,
- reason: PropTypes.string,
- adjustedGrade: PropTypes.number,
- })),
- hide: PropTypes.bool.isRequired,
- todaysDate: PropTypes.string.isRequired,
-};
-
-export const mapStateToProps = (state) => ({
- hide: selectors.grades.hasOverrideErrors(state),
- gradeOverrides: selectors.grades.gradeOverrides(state),
- todaysDate: selectors.app.modalState.todaysDate(state),
-});
+OverrideTable.propTypes = {};
-export default connect(mapStateToProps)(OverrideTable);
+export default OverrideTable;
diff --git a/src/components/GradesView/EditModal/OverrideTable/index.test.jsx b/src/components/GradesView/EditModal/OverrideTable/index.test.jsx
new file mode 100644
index 00000000..79210fb7
--- /dev/null
+++ b/src/components/GradesView/EditModal/OverrideTable/index.test.jsx
@@ -0,0 +1,65 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+
+import { DataTable } from '@edx/paragon';
+
+import { formatDateForDisplay } from 'utils';
+
+import AdjustedGradeInput from './AdjustedGradeInput';
+import ReasonInput from './ReasonInput';
+import useOverrideTableData from './hooks';
+import OverrideTable from '.';
+
+jest.mock('utils', () => ({
+ formatDateForDisplay: (date) => ({ formatted: date }),
+}));
+jest.mock('./hooks', () => jest.fn());
+jest.mock('./AdjustedGradeInput', () => 'AdjustedGradeInput');
+jest.mock('./ReasonInput', () => 'ReasonInput');
+
+const hookProps = {
+ hide: false,
+ data: [
+ { test: 'data' },
+ { andOther: 'test-data' },
+ ],
+ columns: 'test-columns',
+};
+useOverrideTableData.mockReturnValue(hookProps);
+
+let el;
+describe('OverrideTable component', () => {
+ beforeEach(() => {
+ jest
+ .clearAllMocks()
+ .useFakeTimers('modern')
+ .setSystemTime(new Date('2000-01-01').getTime());
+ el = shallow(
);
+ });
+ describe('behavior', () => {
+ it('initializes hook data', () => {
+ expect(useOverrideTableData).toHaveBeenCalled();
+ });
+ });
+ describe('render', () => {
+ test('null render if hide', () => {
+ useOverrideTableData.mockReturnValueOnce({ ...hookProps, hide: true });
+ el = shallow(
);
+ expect(el.isEmptyRender()).toEqual(true);
+ });
+ test('snapshot', () => {
+ expect(el).toMatchSnapshot();
+ const table = el.find(DataTable);
+ expect(table.props().columns).toEqual(hookProps.columns);
+ const data = [...table.props().data];
+ const inputRow = data.pop();
+ const formattedDate = formatDateForDisplay(new Date());
+ expect(data).toEqual(hookProps.data);
+ expect(inputRow).toMatchObject({
+ adjustedGrade:
,
+ date: formattedDate,
+ reason:
,
+ });
+ });
+ });
+});
diff --git a/src/components/GradesView/EditModal/OverrideTable/test.jsx b/src/components/GradesView/EditModal/OverrideTable/test.jsx
deleted file mode 100644
index 96149b47..00000000
--- a/src/components/GradesView/EditModal/OverrideTable/test.jsx
+++ /dev/null
@@ -1,81 +0,0 @@
-import React from 'react';
-import { shallow } from 'enzyme';
-
-import selectors from 'data/selectors';
-
-import {
- OverrideTable,
- mapStateToProps,
-} from '.';
-
-jest.mock('@edx/paragon', () => ({ DataTable: () => 'DataTable' }));
-jest.mock('./ReasonInput', () => 'ReasonInput');
-jest.mock('./AdjustedGradeInput', () => 'AdjustedGradeInput');
-
-jest.mock('data/selectors', () => ({
- __esModule: true,
- default: {
- app: {
- modalState: {
- todaysDate: jest.fn(state => ({ todaysDate: state })),
- },
- },
- grades: {
- hasOverrideErrors: jest.fn(state => ({ hasOverrideErrors: state })),
- gradeOverrides: jest.fn(state => ({ gradeOverrides: state })),
- },
- },
-}));
-
-describe('OverrideTable', () => {
- const props = {
- gradeOverrides: [
- {
- date: 'yesterday',
- grader: 'me',
- reason: 'you ate my sandwich',
- adjustedGrade: 0,
- },
- {
- date: 'today',
- grader: 'me',
- reason: 'you brought me a new sandwich',
- adjustedGrade: 20,
- },
- ],
- hide: false,
- todaysDate: 'todaaaaaay',
- };
-
- describe('Component', () => {
- describe('snapshots', () => {
- it('returns null if hide is true', () => {
- expect(shallow(
)).toEqual({});
- });
- describe('basic snapshot', () => {
- test('shows a row for each entry and one editable row', () => {
- expect(shallow(
)).toMatchSnapshot();
- });
- });
- });
- });
-
- describe('mapStateToProps', () => {
- const testState = { I: 'wanna', be: 'the', very: 'best' };
- let mapped;
- beforeEach(() => {
- mapped = mapStateToProps(testState);
- });
- describe('modalState', () => {
- test('hide from grades.hasOverrideErrors', () => {
- expect(mapped.hide).toEqual(selectors.grades.hasOverrideErrors(testState));
- });
- test('gradeOverrides from grades.gradeOverrides', () => {
- expect(mapped.gradeOverrides).toEqual(selectors.grades.gradeOverrides(testState));
- });
- test('todaysData from app.modalState.todaysDate', () => {
- expect(mapped.todaysDate).toEqual(selectors.app.modalState.todaysDate(testState));
- });
- });
- });
-});
diff --git a/src/components/GradesView/EditModal/__snapshots__/ModalHeaders.test.jsx.snap b/src/components/GradesView/EditModal/__snapshots__/ModalHeaders.test.jsx.snap
index 0cce19b4..22efe800 100644
--- a/src/components/GradesView/EditModal/__snapshots__/ModalHeaders.test.jsx.snap
+++ b/src/components/GradesView/EditModal/__snapshots__/ModalHeaders.test.jsx.snap
@@ -1,99 +1,26 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`ModalHeaders Component snapshots gradeOverrideHistoryError is and empty and open is true modal open and StatusAlert showing 1`] = `
+exports[`ModalHeaders render snapshot 1`] = `
- }
- value="Qwerty"
+ label="Assignment"
+ value="test-assignment-name"
/>
- }
- value="Uiop"
+ label="Student"
+ value="test-user-name"
/>
- }
- value={20}
+ label="Original Grade"
+ value="test-original-grade"
/>
- }
- value={2}
- />
-
-`;
-
-exports[`ModalHeaders Component snapshots gradeOverrideHistoryError is empty and open is false modal closed and StatusAlert closed 1`] = `
-
-
- }
- value="Qwerty"
- />
-
- }
- value="Uiop"
- />
-
- }
- value={20}
- />
-
- }
- value={2}
+ label="Current Grade"
+ value="test-current-grade"
/>
`;
diff --git a/src/components/GradesView/EditModal/__snapshots__/index.test.jsx.snap b/src/components/GradesView/EditModal/__snapshots__/index.test.jsx.snap
new file mode 100644
index 00000000..46a0c513
--- /dev/null
+++ b/src/components/GradesView/EditModal/__snapshots__/index.test.jsx.snap
@@ -0,0 +1,91 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`EditModal component render with error snapshot 1`] = `
+
+
+
+
+
+ test-error
+
+
+
+ Showing most recent actions (max 5). To see more, please contact support
+
+
+ Note: Once you save, your changes will be visible to students.
+
+
+
+
+
+
+ Cancel
+
+
+ Save Grades
+
+
+
+
+`;
+
+exports[`EditModal component render without error snapshot 1`] = `
+
+
+
+
+
+
+
+ Showing most recent actions (max 5). To see more, please contact support
+
+
+ Note: Once you save, your changes will be visible to students.
+
+
+
+
+
+
+ Cancel
+
+
+ Save Grades
+
+
+
+
+`;
diff --git a/src/components/GradesView/EditModal/__snapshots__/test.jsx.snap b/src/components/GradesView/EditModal/__snapshots__/test.jsx.snap
deleted file mode 100644
index 3bcdeb7f..00000000
--- a/src/components/GradesView/EditModal/__snapshots__/test.jsx.snap
+++ /dev/null
@@ -1,125 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`EditModal Component snapshots gradeOverrideHistoryError is and empty and open is true modal open and StatusAlert showing 1`] = `
-
-
-
-
-
- Weve been trying to contact you regarding...
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-`;
-
-exports[`EditModal Component snapshots gradeOverrideHistoryError is empty and open is false modal closed and StatusAlert closed 1`] = `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-`;
diff --git a/src/components/GradesView/EditModal/hooks.js b/src/components/GradesView/EditModal/hooks.js
new file mode 100644
index 00000000..a5811b74
--- /dev/null
+++ b/src/components/GradesView/EditModal/hooks.js
@@ -0,0 +1,29 @@
+import { selectors, actions, thunkActions } from 'data/redux/hooks';
+
+export const useEditModalData = () => {
+ const error = selectors.grades.useGradeData().gradeOverrideHistoryError;
+ const isOpen = selectors.app.useModalData().open;
+ const closeModal = actions.app.useCloseModal();
+ const doneViewingAssignment = actions.grades.useDoneViewingAssignment();
+ const updateGrades = thunkActions.grades.useUpdateGrades();
+
+ const onClose = () => {
+ doneViewingAssignment();
+ closeModal();
+ };
+
+ const handleAdjustedGradeClick = () => {
+ updateGrades();
+ doneViewingAssignment();
+ closeModal();
+ };
+
+ return {
+ onClose,
+ error,
+ handleAdjustedGradeClick,
+ isOpen,
+ };
+};
+
+export default useEditModalData;
diff --git a/src/components/GradesView/EditModal/hooks.test.js b/src/components/GradesView/EditModal/hooks.test.js
new file mode 100644
index 00000000..7951a16f
--- /dev/null
+++ b/src/components/GradesView/EditModal/hooks.test.js
@@ -0,0 +1,68 @@
+import { selectors, actions, thunkActions } from 'data/redux/hooks';
+
+import useEditModalData from './hooks';
+
+jest.mock('data/redux/hooks', () => ({
+ actions: {
+ app: { useCloseModal: jest.fn() },
+ grades: { useDoneViewingAssignment: jest.fn() },
+ },
+ selectors: {
+ app: { useModalData: jest.fn() },
+ grades: { useGradeData: jest.fn() },
+ },
+ thunkActions: {
+ grades: { useUpdateGrades: jest.fn() },
+ },
+}));
+
+const closeModal = jest.fn();
+const doneViewingAssignment = jest.fn();
+const updateGrades = jest.fn();
+actions.app.useCloseModal.mockReturnValue(closeModal);
+actions.grades.useDoneViewingAssignment.mockReturnValue(doneViewingAssignment);
+thunkActions.grades.useUpdateGrades.mockReturnValue(updateGrades);
+
+const gradeData = { gradeOverridHistoryError: 'test-error' };
+const modalData = { open: true };
+selectors.app.useModalData.mockReturnValue(modalData);
+selectors.grades.useGradeData.mockReturnValue(gradeData);
+
+let out;
+describe('useEditModalData', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ out = useEditModalData();
+ });
+ describe('behavior', () => {
+ it('initializes redux hooks', () => {
+ expect(selectors.grades.useGradeData).toHaveBeenCalled();
+ expect(selectors.app.useModalData).toHaveBeenCalled();
+ expect(actions.app.useCloseModal).toHaveBeenCalled();
+ expect(actions.grades.useDoneViewingAssignment).toHaveBeenCalled();
+ expect(thunkActions.grades.useUpdateGrades).toHaveBeenCalled();
+ });
+ });
+ describe('output', () => {
+ it('forwards error from gradeData.gradeOverrideHistoryError', () => {
+ expect(out.error).toEqual(gradeData.gradeOverrideHistoryError);
+ });
+ it('forwards isOpen from modalData.open', () => {
+ expect(out.isOpen).toEqual(modalData.open);
+ });
+ describe('handleAdjustedGradeClick', () => {
+ it('updates grades, calls doneViewingAssignment and closeModal', () => {
+ out.handleAdjustedGradeClick();
+ expect(updateGrades).toHaveBeenCalled();
+ expect(doneViewingAssignment).toHaveBeenCalled();
+ expect(closeModal).toHaveBeenCalled();
+ });
+ });
+ test('onClose calls doneViewingAssignment and closeModal', () => {
+ out.onClose();
+ expect(doneViewingAssignment).toHaveBeenCalled();
+ expect(closeModal).toHaveBeenCalled();
+ expect(updateGrades).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/src/components/GradesView/EditModal/index.jsx b/src/components/GradesView/EditModal/index.jsx
index d093be89..519b044c 100644
--- a/src/components/GradesView/EditModal/index.jsx
+++ b/src/components/GradesView/EditModal/index.jsx
@@ -1,7 +1,4 @@
-/* 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 {
Button,
@@ -9,15 +6,12 @@ import {
ModalDialog,
ActionRow,
} from '@edx/paragon';
-import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
+import { useIntl } from '@edx/frontend-platform/i18n';
-import selectors from 'data/selectors';
-import actions from 'data/actions';
-import thunkActions from 'data/thunkActions';
-
-import messages from './messages';
import OverrideTable from './OverrideTable';
import ModalHeaders from './ModalHeaders';
+import useEditModalData from './hooks';
+import messages from './messages';
/**
*
@@ -28,87 +22,48 @@ import ModalHeaders from './ModalHeaders';
* adjusting the grade.
* (also provides a close button that clears the modal state)
*/
-export class EditModal extends React.Component {
- constructor(props) {
- super(props);
- this.closeAssignmentModal = this.closeAssignmentModal.bind(this);
- this.handleAdjustedGradeClick = this.handleAdjustedGradeClick.bind(this);
- }
-
- closeAssignmentModal() {
- this.props.doneViewingAssignment();
- this.props.closeModal();
- }
-
- handleAdjustedGradeClick() {
- this.props.updateGrades();
- this.closeAssignmentModal();
- }
-
- render() {
- return (
-
-
-
-
-
- {this.props.gradeOverrideHistoryError}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
- }
-}
-
-EditModal.defaultProps = {
- gradeOverrideHistoryError: '',
-};
-
-EditModal.propTypes = {
- // redux
- gradeOverrideHistoryError: PropTypes.string,
- open: PropTypes.bool.isRequired,
- closeModal: PropTypes.func.isRequired,
- doneViewingAssignment: PropTypes.func.isRequired,
- updateGrades: PropTypes.func.isRequired,
- // injected
- intl: intlShape.isRequired,
-};
+export const EditModal = () => {
+ const { formatMessage } = useIntl();
+ const {
+ onClose,
+ error,
+ handleAdjustedGradeClick,
+ isOpen,
+ } = useEditModalData();
-export const mapStateToProps = (state) => ({
- gradeOverrideHistoryError: selectors.grades.gradeOverrideHistoryError(state),
- open: selectors.app.modalState.open(state),
-});
+ return (
+
+
+
+
+
+ {error}
+
+
+
{formatMessage(messages.visibility)}
+
{formatMessage(messages.saveVisibility)}
+
+
-export const mapDispatchToProps = {
- closeModal: actions.app.closeModal,
- doneViewingAssignment: actions.grades.doneViewingAssignment,
- updateGrades: thunkActions.grades.updateGrades,
+
+
+
+ {formatMessage(messages.closeText)}
+
+
+ {formatMessage(messages.saveGrade)}
+
+
+
+
+ );
};
-export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(EditModal));
+export default EditModal;
diff --git a/src/components/GradesView/EditModal/index.test.jsx b/src/components/GradesView/EditModal/index.test.jsx
new file mode 100644
index 00000000..8a69126f
--- /dev/null
+++ b/src/components/GradesView/EditModal/index.test.jsx
@@ -0,0 +1,132 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+
+import {
+ ActionRow,
+ ModalDialog,
+} from '@edx/paragon';
+import { useIntl } from '@edx/frontend-platform/i18n';
+
+import { formatMessage } from 'testUtils';
+
+import ModalHeaders from './ModalHeaders';
+import OverrideTable from './OverrideTable';
+import useEditModalData from './hooks';
+import EditModal from '.';
+import messages from './messages';
+
+jest.mock('./hooks', () => jest.fn());
+jest.mock('./ModalHeaders', () => 'ModalHeaders');
+jest.mock('./OverrideTable', () => 'OverrideTable');
+
+const hookProps = {
+ onClose: jest.fn().mockName('hooks.onClose'),
+ error: 'test-error',
+ handleAdjustedGradeClick: jest.fn().mockName('hooks.handleAdjustedGradeClick'),
+ isOpen: 'test-is-open',
+};
+useEditModalData.mockReturnValue(hookProps);
+
+let el;
+describe('EditModal component', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ el = shallow(
);
+ });
+ describe('behavior', () => {
+ it('initializes intl hook', () => {
+ expect(useIntl).toHaveBeenCalled();
+ });
+ it('initializes component hooks', () => {
+ expect(useEditModalData).toHaveBeenCalled();
+ });
+ });
+ describe('render', () => {
+ test('modal props', () => {
+ const modalProps = el.find(ModalDialog).props();
+ expect(modalProps.title).toEqual(formatMessage(messages.title));
+ expect(modalProps.isOpen).toEqual(hookProps.isOpen);
+ expect(modalProps.onClose).toEqual(hookProps.onClose);
+ });
+ const loadBody = () => {
+ const body = el.find(ModalDialog).children().at(0);
+ const children = body.find('div').children();
+ return { body, children };
+ };
+ const testBody = () => {
+ test('type', () => {
+ const { body } = loadBody();
+ expect(body.type()).toEqual('ModalDialog.Body');
+ });
+ test('headers row', () => {
+ const { children } = loadBody();
+ expect(children.at(0)).toMatchObject(shallow(
));
+ });
+ test('table row', () => {
+ const { children } = loadBody();
+ expect(children.at(2)).toMatchObject(shallow(
));
+ });
+ test('messages', () => {
+ const { children } = loadBody();
+ expect(
+ children.at(3).contains(formatMessage(messages.visibility)),
+ ).toEqual(true);
+ expect(
+ children.at(4).contains(formatMessage(messages.saveVisibility)),
+ ).toEqual(true);
+ });
+ };
+ const testFooter = () => {
+ let footer;
+ beforeEach(() => {
+ footer = el.find(ModalDialog).children().at(1);
+ });
+ test('type', () => {
+ expect(footer.type()).toEqual('ModalDialog.Footer');
+ });
+ test('contains action row', () => {
+ expect(footer.children().at(0).type()).toEqual('ActionRow');
+ });
+ test('close button', () => {
+ const button = footer.find(ActionRow).children().at(0);
+ expect(button.contains(formatMessage(messages.closeText))).toEqual(true);
+ expect(button.type()).toEqual('ModalDialog.CloseButton');
+ });
+ test('adjusted grade button', () => {
+ const button = footer.find(ActionRow).children().at(1);
+ expect(button.contains(formatMessage(messages.saveGrade))).toEqual(true);
+ expect(button.type()).toEqual('Button');
+ expect(button.props().onClick).toEqual(hookProps.handleAdjustedGradeClick);
+ });
+ };
+ describe('without error', () => {
+ beforeEach(() => {
+ useEditModalData.mockReturnValueOnce({ ...hookProps, error: undefined });
+ el = shallow(
);
+ });
+ test('snapshot', () => {
+ expect(el).toMatchSnapshot();
+ });
+ testBody();
+ testFooter();
+ test('alert row', () => {
+ const alert = loadBody().children.at(1);
+ expect(alert.type()).toEqual('Alert');
+ expect(alert.props().show).toEqual(false);
+ });
+ });
+ describe('with error', () => {
+ test('snapshot', () => {
+ expect(el).toMatchSnapshot();
+ });
+ testBody();
+ test('alert row', () => {
+ const alert = loadBody().children.at(1);
+ expect(alert.type()).toEqual('Alert');
+ expect(alert.props().show).toEqual(true);
+ expect(alert.contains(hookProps.error)).toEqual(true);
+ });
+ testFooter();
+ });
+ });
+});
diff --git a/src/components/GradesView/EditModal/test.jsx b/src/components/GradesView/EditModal/test.jsx
deleted file mode 100644
index c0f6cbf6..00000000
--- a/src/components/GradesView/EditModal/test.jsx
+++ /dev/null
@@ -1,130 +0,0 @@
-import React from 'react';
-import { shallow } from 'enzyme';
-
-import actions from 'data/actions';
-import selectors from 'data/selectors';
-import thunkActions from 'data/thunkActions';
-
-import {
- EditModal,
- mapDispatchToProps,
- mapStateToProps,
-}
- from '.';
-
-jest.mock('./OverrideTable', () => 'OverrideTable');
-jest.mock('./ModalHeaders', () => 'ModalHeaders');
-jest.mock('data/actions', () => ({
- __esModule: true,
- default: {
- app: { closeModal: jest.fn() },
- grades: { doneViewingAssignment: jest.fn() },
- },
-}));
-jest.mock('data/thunkActions', () => ({
- __esModule: true,
- default: {
- grades: { updateGrades: jest.fn() },
- },
-}));
-jest.mock('data/selectors', () => ({
- __esModule: true,
- default: {
- app: {
- modalState: {
- open: jest.fn(state => ({ isModalOpen: state })),
- },
- },
- grades: {
- gradeOverrideHistoryError: jest.fn(state => ({ overrideHistoryError: state })),
- },
- },
-}));
-describe('EditModal', () => {
- let props;
- beforeEach(() => {
- props = {
- gradeOverrideHistoryError: 'Weve been trying to contact you regarding...',
- open: true,
- closeModal: jest.fn(),
- doneViewingAssignment: jest.fn(),
- updateGrades: jest.fn(),
-
- intl: { formatMessage: (msg) => msg.defaultMessage },
- };
- });
-
- describe('Component', () => {
- describe('behavior', () => {
- let el;
- beforeEach(() => {
- el = shallow(
);
- });
- describe('closeAssignmentModal', () => {
- it('calls props.doneViewingAssignment and props.closeModal', () => {
- el.instance().closeAssignmentModal();
- expect(props.doneViewingAssignment).toHaveBeenCalledWith();
- expect(props.closeModal).toHaveBeenCalledWith();
- });
- });
- describe('handleAdjustedGradeClick', () => {
- it('calls props.updateGardes and this.closeAssignmentModal', () => {
- el.instance().closeAssignmentModal = jest.fn();
- el.instance().handleAdjustedGradeClick();
- expect(props.updateGrades).toHaveBeenCalledWith();
- expect(el.instance().closeAssignmentModal).toHaveBeenCalledWith();
- });
- });
- });
- describe('snapshots', () => {
- let el;
- beforeEach(() => {
- el = shallow(
);
- el.instance().closeAssignmentModal = jest.fn().mockName('this.closeAssignmentModal');
- el.instance().handleAdjustedGradeClick = jest.fn().mockName(
- 'this.handleAdjustedGradeClick',
- );
- });
- describe('gradeOverrideHistoryError is and empty and open is true', () => {
- test('modal open and StatusAlert showing', () => {
- expect(el.instance().render()).toMatchSnapshot();
- });
- });
- describe('gradeOverrideHistoryError is empty and open is false', () => {
- test('modal closed and StatusAlert closed', () => {
- el.setProps({ open: false, gradeOverrideHistoryError: '' });
- expect(el.instance().render()).toMatchSnapshot();
- });
- });
- });
- });
-
- describe('mapStateToProps', () => {
- const testState = { martha: 'why did you say that name?!' };
- let mapped;
- beforeEach(() => {
- mapped = mapStateToProps(testState);
- });
- test('gradeOverrideHistoryError from grades.gradeOverrideHistoryError', () => {
- expect(
- mapped.gradeOverrideHistoryError,
- ).toEqual(selectors.grades.gradeOverrideHistoryError(testState));
- });
- test('open from app.modalState.open', () => {
- expect(mapped.open).toEqual(selectors.app.modalState.open(testState));
- });
- });
- describe('mapDispatchToProps', () => {
- test('closeModal from actions.app.closeModal', () => {
- expect(mapDispatchToProps.closeModal).toEqual(actions.app.closeModal);
- });
- test('doneViewingAssignemtn from actions.grades.doneViewingAssignment', () => {
- expect(
- mapDispatchToProps.doneViewingAssignment,
- ).toEqual(actions.grades.doneViewingAssignment);
- });
- test('updateGrades from thunkActions.grades.updateGrades', () => {
- expect(mapDispatchToProps.updateGrades).toEqual(thunkActions.grades.updateGrades);
- });
- });
-});
diff --git a/src/components/GradesView/FilterBadges/FilterBadge.jsx b/src/components/GradesView/FilterBadges/FilterBadge.jsx
index d6f5dbe7..48e6862b 100644
--- a/src/components/GradesView/FilterBadges/FilterBadge.jsx
+++ b/src/components/GradesView/FilterBadges/FilterBadge.jsx
@@ -1,11 +1,10 @@
import React from 'react';
-import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { Button } from '@edx/paragon';
-import { FormattedMessage } from '@edx/frontend-platform/i18n';
+import { useIntl } from '@edx/frontend-platform/i18n';
-import selectors from 'data/selectors';
+import { selectors } from 'data/redux/hooks';
/**
* FilterBadge
@@ -16,56 +15,43 @@ import selectors from 'data/selectors';
* @param {string} filterName - api filter name (for redux connector)
*/
export const FilterBadge = ({
- config: {
+ filterName,
+ handleClose,
+}) => {
+ const { formatMessage } = useIntl();
+ const {
displayName,
isDefault,
hideValue,
value,
connectedFilters,
- },
- handleClose,
-}) => !isDefault && (
-
-
-
-
-
-
- {!hideValue ? `: ${value}` : ''}
+ } = selectors.root.useFilterBadgeConfig(filterName);
+ if (isDefault) {
+ return null;
+ }
+ return (
+
+
+ {formatMessage(displayName)}
+
+ {!hideValue ? `: ${value}` : ''}
+
+
+ ×
+
-
- ×
-
-
-
-
-);
+
+
+ );
+};
FilterBadge.propTypes = {
handleClose: PropTypes.func.isRequired,
- // eslint-disable-next-line
filterName: PropTypes.string.isRequired,
- // redux
- config: PropTypes.shape({
- connectedFilters: PropTypes.arrayOf(PropTypes.string),
- displayName: PropTypes.shape({
- defaultMessage: PropTypes.string,
- }).isRequired,
- isDefault: PropTypes.bool.isRequired,
- hideValue: PropTypes.bool,
- value: PropTypes.oneOfType([
- PropTypes.string,
- PropTypes.bool,
- ]),
- }).isRequired,
};
-export const mapStateToProps = (state, ownProps) => ({
- config: selectors.root.filterBadgeConfig(state, ownProps.filterName),
-});
-
-export default connect(mapStateToProps)(FilterBadge);
+export default FilterBadge;
diff --git a/src/components/GradesView/FilterBadges/FilterBadge.test.jsx b/src/components/GradesView/FilterBadges/FilterBadge.test.jsx
index 935cf931..30ac3801 100644
--- a/src/components/GradesView/FilterBadges/FilterBadge.test.jsx
+++ b/src/components/GradesView/FilterBadges/FilterBadge.test.jsx
@@ -1,107 +1,95 @@
import React from 'react';
import { shallow } from 'enzyme';
-import { FormattedMessage } from '@edx/frontend-platform/i18n';
+import { useIntl } from '@edx/frontend-platform/i18n';
+import { formatMessage } from 'testUtils';
import { Button } from '@edx/paragon';
-import selectors from 'data/selectors';
-import { FilterBadge, mapStateToProps } from './FilterBadge';
+import { selectors } from 'data/redux/hooks';
+import FilterBadge from './FilterBadge';
jest.mock('@edx/paragon', () => ({
Button: () => 'Button',
}));
-jest.mock('data/selectors', () => ({
- __esModule: true,
- default: {
+jest.mock('data/redux/hooks', () => ({
+ selectors: {
root: {
- filterBadgeConfig: jest.fn(state => ({ filterBadgeConfig: state })),
+ useFilterBadgeConfig: jest.fn(),
},
},
}));
+const handleClose = jest.fn(filters => ({ handleClose: filters }));
+const filterName = 'test-filter-name';
+
+const hookProps = {
+ displayName: {
+ defaultMessage: 'a common name',
+ },
+ isDefault: false,
+ hideValue: false,
+ value: 'a common value',
+ connectedFilters: ['some', 'filters'],
+};
+selectors.root.useFilterBadgeConfig.mockReturnValue(hookProps);
+
+let el;
describe('FilterBadge', () => {
- describe('component', () => {
- const config = {
- displayName: {
- defaultMessage: 'a common name',
- },
- isDefault: false,
- hideValue: false,
- value: 'a common value',
- connectedFilters: ['some', 'filters'],
+ beforeEach(() => {
+ el = shallow(
);
+ });
+ describe('behavior', () => {
+ it('initializes intl hook', () => {
+ expect(useIntl).toHaveBeenCalled();
+ });
+ it('initializes redux hooks', () => {
+ expect(selectors.root.useFilterBadgeConfig).toHaveBeenCalledWith(filterName);
+ });
+ });
+ describe('render', () => {
+ const testDisplayName = () => {
+ test('formatted display name appears on badge', () => {
+ expect(el.contains(formatMessage(hookProps.displayName))).toEqual(true);
+ });
+ };
+ const testCloseButton = () => {
+ test('close button forwards close method', () => {
+ expect(el.find(Button).props().onClick).toEqual(handleClose(hookProps.connectedFilters));
+ });
};
- const filterName = 'api.filter.name';
- let handleClose;
- let el;
- let props;
- beforeEach(() => {
- handleClose = (filters) => ({ handleClose: filters });
- props = { filterName, handleClose, config };
+ test('empty render if isDefault', () => {
+ selectors.root.useFilterBadgeConfig.mockReturnValueOnce({
+ ...hookProps,
+ isDefault: true,
+ });
+ el = shallow(
);
+ expect(el.isEmptyRender()).toEqual(true);
});
- describe('with default value', () => {
+ describe('hide Value', () => {
beforeEach(() => {
- el = shallow(
-
,
- );
+ selectors.root.useFilterBadgeConfig.mockReturnValueOnce({
+ ...hookProps,
+ hideValue: true,
+ });
+ el = shallow(
);
});
- test('snapshot - empty', () => {
+ testDisplayName();
+ testCloseButton();
+ test('snapshot', () => {
expect(el).toMatchSnapshot();
});
- it('does not display', () => {
- expect(el).toEqual({});
+ test('value is note present in the badge', () => {
+ expect(el.contains(hookProps.value)).toEqual(false);
});
});
- describe('with non-default value (active)', () => {
- describe('if hideValue is true', () => {
- beforeEach(() => {
- el = shallow(
-
,
- );
- });
- test('snapshot - shows displayName but not value in span', () => {
- expect(el).toMatchSnapshot();
- });
- it('shows displayName but not value in span', () => {
- expect(el.find('span.badge').childAt(0).getElement()).toEqual(
-
-
- ,
- );
- });
- it('calls a handleClose event for connected filters on button click', () => {
- expect(el.find(Button).props().onClick).toEqual(handleClose(config.connectedFilters));
- });
+ describe('do not hide value', () => {
+ testDisplayName();
+ testCloseButton();
+ test('snapshot', () => {
+ expect(el).toMatchSnapshot();
});
- describe('if hideValue is false (default)', () => {
- beforeEach(() => {
- el = shallow(
);
- });
- test('snapshot', () => {
- expect(el).toMatchSnapshot();
- });
- it('shows displayName and value in span', () => {
- expect(el.find('span.badge').childAt(0).getElement()).toEqual(
-
-
- ,
- );
- expect(el.find('span.badge').childAt(1).getElement()).toEqual(
-
- {`: ${config.value}`}
- ,
- );
- });
- it('calls a handleClose event for connected filters on button click', () => {
- expect(el.find(Button).props().onClick).toEqual(handleClose(config.connectedFilters));
- });
+ test('value is note present in the badge', () => {
+ expect(el.text().includes(hookProps.value)).toEqual(true);
});
});
});
- describe('mapStateToProps', () => {
- const testState = { some: 'kind', of: 'alien' };
- const filterName = 'Lilu Dallas Multipass';
- test('config loads config from root.filterBadgeConfig with ownProps.filterName', () => {
- const { config } = mapStateToProps(testState, { filterName });
- expect(config).toEqual(selectors.root.filterBadgeConfig(testState, filterName));
- });
- });
});
diff --git a/src/components/GradesView/FilterBadges/__snapshots__/FilterBadge.test.jsx.snap b/src/components/GradesView/FilterBadges/__snapshots__/FilterBadge.test.jsx.snap
index 1eb81474..4de3fc52 100644
--- a/src/components/GradesView/FilterBadges/__snapshots__/FilterBadge.test.jsx.snap
+++ b/src/components/GradesView/FilterBadges/__snapshots__/FilterBadge.test.jsx.snap
@@ -1,16 +1,12 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`FilterBadge component with default value snapshot - empty 1`] = `""`;
-
-exports[`FilterBadge component with non-default value (active) if hideValue is false (default) snapshot 1`] = `
+exports[`FilterBadge render do not hide value snapshot 1`] = `
-
+ a common name
: a common value
@@ -38,15 +34,13 @@ exports[`FilterBadge component with non-default value (active) if hideValue is f
`;
-exports[`FilterBadge component with non-default value (active) if hideValue is true snapshot - shows displayName but not value in span 1`] = `
+exports[`FilterBadge render hide Value snapshot 1`] = `
-
+ a common name
(
-
-
-
-);
-
-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);
diff --git a/src/components/GradesView/FilterMenuToggle.test.jsx b/src/components/GradesView/FilterMenuToggle.test.jsx
deleted file mode 100644
index 4d02fca6..00000000
--- a/src/components/GradesView/FilterMenuToggle.test.jsx
+++ /dev/null
@@ -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((
-
- ))).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,
- );
- });
- });
-});
diff --git a/src/components/GradesView/FilterMenuToggle/__snapshots__/index.test.jsx.snap b/src/components/GradesView/FilterMenuToggle/__snapshots__/index.test.jsx.snap
new file mode 100644
index 00000000..c08ee066
--- /dev/null
+++ b/src/components/GradesView/FilterMenuToggle/__snapshots__/index.test.jsx.snap
@@ -0,0 +1,15 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`FilterMenuToggle component render snapshot 1`] = `
+
+
+
+ Edit Filters
+
+`;
diff --git a/src/components/GradesView/FilterMenuToggle/index.jsx b/src/components/GradesView/FilterMenuToggle/index.jsx
new file mode 100644
index 00000000..72f60fdb
--- /dev/null
+++ b/src/components/GradesView/FilterMenuToggle/index.jsx
@@ -0,0 +1,31 @@
+import React from 'react';
+
+import { Button, Icon } from '@edx/paragon';
+import { useIntl } from '@edx/frontend-platform/i18n';
+import { FilterAlt } from '@edx/paragon/icons';
+
+import { thunkActions } from 'data/redux/hooks';
+
+import messages from './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 = () => {
+ const toggleFilterMenu = thunkActions.app.filterMenu.useToggleMenu();
+ const { formatMessage } = useIntl();
+ return (
+
+ {formatMessage(messages.editFilters)}
+
+ );
+};
+
+FilterMenuToggle.propTypes = {};
+
+export default FilterMenuToggle;
diff --git a/src/components/GradesView/FilterMenuToggle/index.test.jsx b/src/components/GradesView/FilterMenuToggle/index.test.jsx
new file mode 100644
index 00000000..ac544957
--- /dev/null
+++ b/src/components/GradesView/FilterMenuToggle/index.test.jsx
@@ -0,0 +1,47 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+
+import { useIntl } from '@edx/frontend-platform/i18n';
+
+import { formatMessage } from 'testUtils';
+import { thunkActions } from 'data/redux/hooks';
+
+import FilterMenuToggle from '.';
+import messages from './messages';
+
+jest.mock('data/redux/hooks', () => ({
+ thunkActions: {
+ app: {
+ filterMenu: {
+ useToggleMenu: jest.fn(),
+ },
+ },
+ },
+}));
+
+const toggleFilterMenu = jest.fn().mockName('hooks.toggleFilterMenu');
+thunkActions.app.filterMenu.useToggleMenu.mockReturnValue(toggleFilterMenu);
+
+let el;
+describe('FilterMenuToggle component', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ el = shallow( );
+ });
+ describe('behavior', () => {
+ it('initializes intl hook', () => {
+ expect(useIntl).toHaveBeenCalled();
+ });
+ it('initializes redux hooks', () => {
+ expect(thunkActions.app.filterMenu.useToggleMenu).toHaveBeenCalled();
+ });
+ });
+ describe('render', () => {
+ test('snapshot', () => {
+ expect(el).toMatchSnapshot();
+ expect(el.type()).toEqual('Button');
+ expect(el.props().onClick).toEqual(toggleFilterMenu);
+ expect(el.text().includes(formatMessage(messages.editFilters)));
+ });
+ });
+});
diff --git a/src/components/GradesView/FilterMenuToggle.messages.js b/src/components/GradesView/FilterMenuToggle/messages.js
similarity index 100%
rename from src/components/GradesView/FilterMenuToggle.messages.js
rename to src/components/GradesView/FilterMenuToggle/messages.js
diff --git a/src/components/GradesView/FilteredUsersLabel.jsx b/src/components/GradesView/FilteredUsersLabel.jsx
deleted file mode 100644
index 3752deed..00000000
--- a/src/components/GradesView/FilteredUsersLabel.jsx
+++ /dev/null
@@ -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';
-
-/**
- *
- * Simple label component displaying the filtered and total users shown
- */
-export const FilteredUsersLabel = ({
- filteredUsersCount,
- totalUsersCount,
-}) => {
- if (!totalUsersCount) {
- return null;
- }
- const bold = (val) => ({val} );
- return (
-
- );
-};
-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);
diff --git a/src/components/GradesView/FilteredUsersLabel.test.jsx b/src/components/GradesView/FilteredUsersLabel.test.jsx
deleted file mode 100644
index c6ec280b..00000000
--- a/src/components/GradesView/FilteredUsersLabel.test.jsx
+++ /dev/null
@@ -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( )).toEqual({});
- });
- test('snapshot - displays label with number of filtered users out of total', () => {
- expect(shallow( )).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));
- });
- });
-});
diff --git a/src/components/GradesView/FilteredUsersLabel/__snapshots__/index.test.jsx.snap b/src/components/GradesView/FilteredUsersLabel/__snapshots__/index.test.jsx.snap
new file mode 100644
index 00000000..be687e16
--- /dev/null
+++ b/src/components/GradesView/FilteredUsersLabel/__snapshots__/index.test.jsx.snap
@@ -0,0 +1,23 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`FilteredUsersLabel component render snapshot 1`] = `
+ ,
+ "totalUsers": ,
+ }
+ }
+/>
+`;
diff --git a/src/components/GradesView/FilteredUsersLabel/index.jsx b/src/components/GradesView/FilteredUsersLabel/index.jsx
new file mode 100644
index 00000000..090e158d
--- /dev/null
+++ b/src/components/GradesView/FilteredUsersLabel/index.jsx
@@ -0,0 +1,37 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import { useIntl } from '@edx/frontend-platform/i18n';
+
+import { selectors } from 'data/redux/hooks';
+import messages from './messages';
+
+export const BoldText = ({ text }) => (
+ {text}
+);
+BoldText.propTypes = {
+ text: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
+};
+
+/**
+ *
+ * Simple label component displaying the filtered and total users shown
+ */
+export const FilteredUsersLabel = () => {
+ const { filteredUsersCount, totalUsersCount } = selectors.grades.useUserCounts();
+ const { formatMessage } = useIntl();
+
+ if (!totalUsersCount) {
+ return null;
+ }
+ return formatMessage(
+ messages.visibilityLabel,
+ {
+ filteredUsers: ,
+ totalUsers: ,
+ },
+ );
+};
+FilteredUsersLabel.propTypes = {};
+
+export default FilteredUsersLabel;
diff --git a/src/components/GradesView/FilteredUsersLabel/index.test.jsx b/src/components/GradesView/FilteredUsersLabel/index.test.jsx
new file mode 100644
index 00000000..46617f72
--- /dev/null
+++ b/src/components/GradesView/FilteredUsersLabel/index.test.jsx
@@ -0,0 +1,56 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+
+import { useIntl } from '@edx/frontend-platform/i18n';
+
+import { formatMessage } from 'testUtils';
+import { selectors } from 'data/redux/hooks';
+
+import FilteredUsersLabel, { BoldText } from '.';
+import messages from './messages';
+
+jest.mock('data/redux/hooks', () => ({
+ selectors: {
+ grades: {
+ useUserCounts: jest.fn(),
+ },
+ },
+}));
+
+const userCounts = {
+ filteredUsersCount: 100,
+ totalUsersCount: 123,
+};
+selectors.grades.useUserCounts.mockReturnValue(userCounts);
+
+let el;
+describe('FilteredUsersLabel component', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ el = shallow( );
+ });
+ describe('behavior', () => {
+ it('initializes intl hook', () => {
+ expect(useIntl).toHaveBeenCalled();
+ });
+ it('initializes redux hooks', () => {
+ expect(selectors.grades.useUserCounts).toHaveBeenCalled();
+ });
+ });
+ describe('render', () => {
+ test('null render if totalUsersCount is 0', () => {
+ selectors.grades.useUserCounts.mockReturnValueOnce({
+ ...userCounts,
+ totalUsersCount: 0,
+ });
+ expect(shallow( ).isEmptyRender()).toEqual(true);
+ });
+ test('snapshot', () => {
+ expect(el).toMatchSnapshot();
+ expect(el).toMatchObject(shallow(formatMessage(messages.visibilityLabel, {
+ filteredUsers: ,
+ totalUsers: ,
+ })));
+ });
+ });
+});
diff --git a/src/components/GradesView/FilteredUsersLabel/messages.js b/src/components/GradesView/FilteredUsersLabel/messages.js
new file mode 100644
index 00000000..d0be2b4e
--- /dev/null
+++ b/src/components/GradesView/FilteredUsersLabel/messages.js
@@ -0,0 +1,11 @@
+import { defineMessages } from '@edx/frontend-platform/i18n';
+
+const messages = defineMessages({
+ visibilityLabel: {
+ id: 'gradebook.GradesTab.usersVisibilityLabel',
+ defaultMessage: 'Showing {filteredUsers} of {totalUsers} total learners',
+ description: 'Users visibility label',
+ },
+});
+
+export default messages;
diff --git a/src/components/GradesView/GradebookTable/GradeButton.jsx b/src/components/GradesView/GradebookTable/GradeButton.jsx
index 67be259c..bbcfea59 100644
--- a/src/components/GradesView/GradebookTable/GradeButton.jsx
+++ b/src/components/GradesView/GradebookTable/GradeButton.jsx
@@ -1,14 +1,31 @@
-/* eslint-disable react/sort-comp, react/button-has-type */
import React from 'react';
import PropTypes from 'prop-types';
-import { connect } from 'react-redux';
import { Button } from '@edx/paragon';
-import selectors from 'data/selectors';
-import thunkActions from 'data/thunkActions';
+import { selectors, thunkActions } from 'data/redux/hooks';
+import transforms from 'data/redux/transforms';
+import * as module from './GradeButton';
-const { subsectionGrade } = selectors.grades;
+export const useGradeButtonData = ({ entry, subsection }) => {
+ const areGradesFrozen = selectors.assignmentTypes.useAreGradesFrozen();
+ const { gradeFormat } = selectors.grades.useGradeData();
+ const setModalState = thunkActions.app.useSetModalStateFromTable();
+ const label = transforms.grades.subsectionGrade({ gradeFormat, subsection });
+
+ const onClick = () => {
+ setModalState({
+ userEntry: entry,
+ subsection,
+ });
+ };
+
+ return {
+ areGradesFrozen,
+ label,
+ onClick,
+ };
+};
/**
* GradeButton
@@ -18,38 +35,24 @@ const { subsectionGrade } = selectors.grades;
* @param {object} entry - user's grade entry
* @param {object} subsection - user's subsection grade from subsection_breakdown
*/
-export class GradeButton extends React.Component {
- constructor(props) {
- super(props);
- this.onClick = this.onClick.bind(this);
- }
-
- get label() {
- return subsectionGrade[this.props.format](this.props.subsection);
- }
-
- onClick() {
- this.props.setModalState({
- userEntry: this.props.entry,
- subsection: this.props.subsection,
- });
- }
-
- render() {
- return this.props.areGradesFrozen
- ? this.label
- : (
-
- {this.label}
-
- );
- }
-}
-
+export const GradeButton = ({ entry, subsection }) => {
+ const {
+ areGradesFrozen,
+ label,
+ onClick,
+ } = module.useGradeButtonData({ entry, subsection });
+ return areGradesFrozen
+ ? label
+ : (
+
+ {label}
+
+ );
+};
GradeButton.propTypes = {
subsection: PropTypes.shape({
attempted: PropTypes.bool,
@@ -62,19 +65,6 @@ GradeButton.propTypes = {
user_id: PropTypes.number,
username: PropTypes.string,
}).isRequired,
- // redux
- areGradesFrozen: PropTypes.bool.isRequired,
- format: PropTypes.string.isRequired,
- setModalState: PropTypes.func.isRequired,
-};
-
-export const mapStateToProps = (state) => ({
- areGradesFrozen: selectors.assignmentTypes.areGradesFrozen(state),
- format: selectors.grades.gradeFormat(state),
-});
-
-export const mapDispatchToProps = {
- setModalState: thunkActions.app.setModalStateFromTable,
};
-export default connect(mapStateToProps, mapDispatchToProps)(GradeButton);
+export default GradeButton;
diff --git a/src/components/GradesView/GradebookTable/GradeButton.test.jsx b/src/components/GradesView/GradebookTable/GradeButton.test.jsx
index 36588ff2..6574f073 100644
--- a/src/components/GradesView/GradebookTable/GradeButton.test.jsx
+++ b/src/components/GradesView/GradebookTable/GradeButton.test.jsx
@@ -1,118 +1,121 @@
import React from 'react';
import { shallow } from 'enzyme';
-import { Button } from '@edx/paragon';
-import selectors from 'data/selectors';
-import thunkActions from 'data/thunkActions';
+import { selectors, thunkActions } from 'data/redux/hooks';
+import transforms from 'data/redux/transforms';
+import { keyStore } from 'utils';
-import {
- GradeButton,
- mapStateToProps,
- mapDispatchToProps,
-} from './GradeButton';
+import * as module from './GradeButton';
-jest.mock('@edx/paragon', () => ({
- Button: () => 'Button',
-}));
+const { useGradeButtonData, default: GradeButton } = module;
-jest.mock('data/selectors', () => ({
- __esModule: true,
- default: {
- assignmentTypes: {
- areGradesFrozen: jest.fn(state => ({ areGradesFrozen: state })),
- },
+jest.mock('data/redux/hooks', () => ({
+ selectors: {
+ assignmentTypes: { useAreGradesFrozen: jest.fn() },
grades: {
- subsectionGrade: {
- percent: jest.fn(subsection => ({ percent: subsection })),
- },
- gradeFormat: jest.fn(state => ({ gradeFormat: state })),
+ useGradeData: jest.fn(),
},
},
+ thunkActions: {
+ app: { useSetModalStateFromTable: jest.fn() },
+ },
}));
-
-jest.mock('data/thunkActions', () => ({
- app: {
- setModalStateFromTable: jest.fn(),
+jest.mock('data/redux/transforms', () => ({
+ grades: {
+ subsectionGrade: jest.fn(),
},
}));
+const props = {
+ subsection: {
+ attempted: false,
+ percent: 23,
+ score_possible: 32,
+ subsection_name: 'the things we do',
+ module_id: 'in potions',
+ },
+ entry: {
+ user_id: 2,
+ username: 'Jessie',
+ },
+};
+const gradeFormat = 'percent';
+const setModalState = jest.fn();
+const subsectionGrade = 'test-subsection-grade';
+selectors.assignmentTypes.useAreGradesFrozen.mockReturnValue(false);
+selectors.grades.useGradeData.mockReturnValue({ gradeFormat });
+thunkActions.app.useSetModalStateFromTable.mockReturnValue(setModalState);
+transforms.grades.subsectionGrade.mockReturnValue(subsectionGrade);
+
+let el;
+let out;
describe('GradeButton', () => {
- let el;
- let props = {
- subsection: {
- attempted: false,
- percent: 23,
- score_possible: 32,
- subsection_name: 'the things we do',
- module_id: 'in potions',
- },
- entry: {
- user_id: 2,
- username: 'Jessie',
- },
- areGradesFrozen: false,
- format: 'percent',
- };
beforeEach(() => {
- props = { ...props, setModalState: jest.fn() };
+ jest.clearAllMocks();
});
- describe('component', () => {
- describe('snapshots', () => {
- test('grades are frozen', () => {
- el = shallow( );
- const label = 'why you gotta label people?';
- jest.spyOn(el.instance(), 'label', 'get').mockReturnValue(label);
- el.instance().onClick = jest.fn().mockName('this.onClick');
- expect(el.instance().render()).toMatchSnapshot();
- expect(el.instance().render()).toEqual(label);
- });
- test('grades are not frozen', () => {
- el = shallow( );
- const label = 'why you gotta label people?';
- jest.spyOn(el.instance(), 'label', 'get').mockReturnValue(label);
- el.instance().onClick = jest.fn().mockName('this.onClick');
- expect(el.instance().render()).toMatchSnapshot();
- expect(el.instance().render().props.children).toEqual(label);
- expect(el.render().is(Button)).toEqual(true);
- });
+ describe('useGradeButton hook', () => {
+ beforeEach(() => {
+ out = useGradeButtonData(props);
});
- describe('label', () => {
- it('calls the appropriate formatter with the subsection prop', () => {
- el = shallow( );
- expect(
- el.instance().label,
- ).toEqual(selectors.grades.subsectionGrade[props.format](props.subsection));
+ describe('behavior', () => {
+ it('initializes redux hooks', () => {
+ expect(selectors.assignmentTypes.useAreGradesFrozen).toHaveBeenCalled();
+ expect(selectors.grades.useGradeData).toHaveBeenCalled();
+ expect(transforms.grades.subsectionGrade).toHaveBeenCalledWith({
+ gradeFormat,
+ subsection: props.subsection,
+ });
+ expect(thunkActions.app.useSetModalStateFromTable).toHaveBeenCalled();
});
});
- describe('onClick', () => {
- it('calls props.setModalState with userEntry and subsection', () => {
- el = shallow( );
- el.instance().onClick();
- expect(props.setModalState).toHaveBeenCalledWith({
+ describe('output', () => {
+ test('forwards areGradesFrozen from redux hook', () => {
+ expect(out.areGradesFrozen).toEqual(false);
+ });
+ test('label passed from subsection grade redux hook', () => {
+ expect(out.label).toEqual(subsectionGrade);
+ });
+ test('onClick sets modal state with user entry and subsection', () => {
+ out.onClick();
+ expect(setModalState).toHaveBeenCalledWith({
userEntry: props.entry,
subsection: props.subsection,
});
});
});
});
- describe('mapStateToProps', () => {
- let mapped;
- const testState = { teams: { rocket: ['jesse', 'james'] } };
+ describe('component', () => {
+ let hookSpy;
+ const moduleKeys = keyStore(module);
+ const hookProps = {
+ areGradesFrozen: false,
+ label: 'test-label',
+ onClick: jest.fn().mockName('hooks.onClick'),
+ };
beforeEach(() => {
- mapped = mapStateToProps(testState);
- });
- test('areGradesFrozen form assignmentTypes.areGradesFrozen', () => {
- expect(
- mapped.areGradesFrozen,
- ).toEqual(selectors.assignmentTypes.areGradesFrozen(testState));
+ hookSpy = jest.spyOn(module, moduleKeys.useGradeButtonData);
});
- test('format form grades.format', () => {
- expect(mapped.format).toEqual(selectors.grades.gradeFormat(testState));
+ describe('frozen grades', () => {
+ beforeEach(() => {
+ hookSpy.mockReturnValue({ ...hookProps, areGradesFrozen: true });
+ el = shallow( );
+ });
+ test('snapshot', () => {
+ expect(el).toMatchSnapshot();
+ expect(el.text()).toEqual(hookProps.label);
+ });
});
- });
- describe('mapDispatchToProps', () => {
- test('setModalState from thunkActions.app.setModalStateFromTable', () => {
- expect(mapDispatchToProps.setModalState).toEqual(thunkActions.app.setModalStateFromTable);
+ describe('not frozen grades', () => {
+ beforeEach(() => {
+ hookSpy.mockReturnValue(hookProps);
+ el = shallow( );
+ });
+ test('snapshot', () => {
+ expect(el).toMatchSnapshot();
+ expect(el.type()).toEqual('Button');
+ expect(el.props().onClick).toEqual(hookProps.onClick);
+ expect(el.contains(hookProps.label)).toEqual(true);
+ });
});
});
});
diff --git a/src/components/GradesView/GradebookTable/LabelReplacements.jsx b/src/components/GradesView/GradebookTable/LabelReplacements.jsx
index 86dd89c1..d134ac39 100644
--- a/src/components/GradesView/GradebookTable/LabelReplacements.jsx
+++ b/src/components/GradesView/GradebookTable/LabelReplacements.jsx
@@ -1,13 +1,13 @@
import React from 'react';
-import { StrictDict } from 'utils';
-
+import { useIntl, getLocale, isRtl } from '@edx/frontend-platform/i18n';
import {
Icon,
OverlayTrigger,
Tooltip,
} from '@edx/paragon';
-import { FormattedMessage, getLocale, isRtl } from '@edx/frontend-platform/i18n';
+
+import { StrictDict } from 'utils';
import messages from './messages';
@@ -18,32 +18,33 @@ export const totalGradePercentageMessage = 'Total Grade values are always displa
* Total Grade column header.
* displays an overlay tooltip with screen-reader text to indicate total grade percentage
*/
-const TotalGradeLabelReplacement = () => (
-
-
-
-
- )}
- >
-
+ );
+};
/**
* Asterisk to display next to heading labels that are only used for masters students
@@ -56,28 +57,34 @@ const mastersOnlyFieldAsterisk = (
*
* Username column header. Lists that Student Key is possibly available
*/
-const UsernameLabelReplacement = () => (
-
+const UsernameLabelReplacement = () => {
+ const { formatMessage } = useIntl();
+ return (
-
-
-
-
- { mastersOnlyFieldAsterisk }
+
+ {formatMessage(messages.usernameHeading)}
+
+
+ {formatMessage(messages.studentKeyLabel)}
+ { mastersOnlyFieldAsterisk }
+
-
-);
+ );
+};
/**
*
* Column header for fields that are only available for masters students
*/
-const MastersOnlyLabelReplacement = (message) => (
-
-
- { mastersOnlyFieldAsterisk }
-
-);
+const MastersOnlyLabelReplacement = (message) => {
+ const { formatMessage } = useIntl();
+ return (
+
+ {formatMessage(message)}
+ { mastersOnlyFieldAsterisk }
+
+ );
+};
export default StrictDict({
TotalGradeLabelReplacement,
diff --git a/src/components/GradesView/GradebookTable/__snapshots__/GradeButton.test.jsx.snap b/src/components/GradesView/GradebookTable/__snapshots__/GradeButton.test.jsx.snap
index 13fa8f89..378fbaef 100644
--- a/src/components/GradesView/GradebookTable/__snapshots__/GradeButton.test.jsx.snap
+++ b/src/components/GradesView/GradebookTable/__snapshots__/GradeButton.test.jsx.snap
@@ -1,13 +1,13 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`GradeButton component snapshots grades are frozen 1`] = `"why you gotta label people?"`;
+exports[`GradeButton component frozen grades snapshot 1`] = `"test-label"`;
-exports[`GradeButton component snapshots grades are not frozen 1`] = `
+exports[`GradeButton component not frozen grades snapshot 1`] = `
- why you gotta label people?
+ test-label
`;
diff --git a/src/components/GradesView/GradebookTable/__snapshots__/LabelReplacements.test.jsx.snap b/src/components/GradesView/GradebookTable/__snapshots__/LabelReplacements.test.jsx.snap
index ed189e28..528c7716 100644
--- a/src/components/GradesView/GradebookTable/__snapshots__/LabelReplacements.test.jsx.snap
+++ b/src/components/GradesView/GradebookTable/__snapshots__/LabelReplacements.test.jsx.snap
@@ -2,11 +2,7 @@
exports[`LabelReplacements MastersOnlyLabelReplacement snapshot 1`] = `
-
+ defaultMessAge
@@ -19,11 +15,7 @@ exports[`LabelReplacements TotalGradeLabelReplacement displays overlay tooltip 1
-
+ Total Grade values are always displayed as a percentage
`;
@@ -35,11 +27,7 @@ exports[`LabelReplacements TotalGradeLabelReplacement snapshot 1`] = `
-
+ Total Grade values are always displayed as a percentage
}
placement="left"
@@ -51,23 +39,13 @@ exports[`LabelReplacements TotalGradeLabelReplacement snapshot 1`] = `
}
>
-
+ Total Grade (%)
- }
+ screenReaderText="Total Grade values are always displayed as a percentage"
/>
@@ -78,20 +56,12 @@ exports[`LabelReplacements TotalGradeLabelReplacement snapshot 1`] = `
exports[`LabelReplacements UsernameLabelReplacement snapshot 1`] = `
-
+ Username
-
+ Student Key
@@ -109,11 +79,7 @@ exports[`snapshot left to right overlay placement 1`] = `
-
+ Total Grade values are always displayed as a percentage
}
placement="right"
@@ -125,23 +91,13 @@ exports[`snapshot left to right overlay placement 1`] = `
}
>
-
+ Total Grade (%)
- }
+ screenReaderText="Total Grade values are always displayed as a percentage"
/>
@@ -157,11 +113,7 @@ exports[`snapshot right to left overlay placement 1`] = `
-
+ Total Grade values are always displayed as a percentage
}
placement="left"
@@ -173,23 +125,13 @@ exports[`snapshot right to left overlay placement 1`] = `
}
>
-
+ Total Grade (%)
- }
+ screenReaderText="Total Grade values are always displayed as a percentage"
/>
diff --git a/src/components/GradesView/GradebookTable/__snapshots__/index.test.jsx.snap b/src/components/GradesView/GradebookTable/__snapshots__/index.test.jsx.snap
new file mode 100644
index 00000000..04bdd0e4
--- /dev/null
+++ b/src/components/GradesView/GradebookTable/__snapshots__/index.test.jsx.snap
@@ -0,0 +1,32 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`GradebookTable snapshot 1`] = `
+
+
+
+
+
+
+
+`;
diff --git a/src/components/GradesView/GradebookTable/__snapshots__/test.jsx.snap b/src/components/GradesView/GradebookTable/__snapshots__/test.jsx.snap
deleted file mode 100644
index 228b171a..00000000
--- a/src/components/GradesView/GradebookTable/__snapshots__/test.jsx.snap
+++ /dev/null
@@ -1,63 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`GradebookTable component snapshot - fields1 and 2 between email and totalGrade, mocked rows 1`] = `
-
- ,
- "accessor": "Username",
- },
- Object {
- "Header": ,
- "accessor": "Full Name",
- },
- Object {
- "Header": ,
- "accessor": "Email",
- },
- Object {
- "Header": "field1",
- "accessor": "field1",
- },
- Object {
- "Header": "field2",
- "accessor": "field2",
- },
- Object {
- "Header": ,
- "accessor": "Total Grade (%)",
- },
- ]
- }
- data={
- Array [
- "mappedRow: 1",
- "mappedRow: 2",
- "mappedRow: 3",
- ]
- }
- hasFixedColumnWidths={true}
- itemCount={3}
- rowHeaderColumnKey="username"
- >
-
-
-
-
-
-`;
diff --git a/src/components/GradesView/GradebookTable/hooks.jsx b/src/components/GradesView/GradebookTable/hooks.jsx
new file mode 100644
index 00000000..99f6ef5d
--- /dev/null
+++ b/src/components/GradesView/GradebookTable/hooks.jsx
@@ -0,0 +1,61 @@
+import React from 'react';
+
+import { useIntl } from '@edx/frontend-platform/i18n';
+
+import { selectors } from 'data/redux/hooks';
+import transforms from 'data/redux/transforms';
+import { Headings } from 'data/constants/grades';
+import { getLocalizedPercentSign } from 'i18n/utils';
+
+import messages from './messages';
+import Fields from './Fields';
+import LabelReplacements from './LabelReplacements';
+import GradeButton from './GradeButton';
+
+const { roundGrade } = transforms.grades;
+
+export const useGradebookTableData = () => {
+ const { formatMessage } = useIntl();
+ const grades = selectors.grades.useAllGrades();
+ const headings = selectors.root.useGetHeadings();
+
+ const mapHeaders = (heading) => {
+ let label;
+ if (heading === Headings.totalGrade) {
+ label = ;
+ } else if (heading === Headings.username) {
+ label = ;
+ } else if (heading === Headings.email) {
+ label = ;
+ } else if (heading === Headings.fullName) {
+ label = ;
+ } else {
+ label = heading;
+ }
+ return { Header: label, accessor: heading };
+ };
+
+ const mapRows = entry => ({
+ [Headings.username]: (
+
+ ),
+ [Headings.email]: ( ),
+ [Headings.totalGrade]: `${roundGrade(entry.percent * 100)}${getLocalizedPercentSign()}`,
+ ...entry.section_breakdown.reduce((acc, subsection) => ({
+ ...acc,
+ [subsection.label]: ,
+ }), {}),
+ });
+
+ const nullMethod = () => null;
+
+ return {
+ columns: headings.map(mapHeaders),
+ data: grades.map(mapRows),
+ grades,
+ nullMethod,
+ emptyContent: formatMessage(messages.noResultsFound),
+ };
+};
+
+export default useGradebookTableData;
diff --git a/src/components/GradesView/GradebookTable/hooks.test.jsx b/src/components/GradesView/GradebookTable/hooks.test.jsx
new file mode 100644
index 00000000..6e95c39f
--- /dev/null
+++ b/src/components/GradesView/GradebookTable/hooks.test.jsx
@@ -0,0 +1,192 @@
+import { shallow } from 'enzyme';
+
+import { useIntl } from '@edx/frontend-platform/i18n';
+
+import { formatMessage } from 'testUtils';
+
+import { getLocalizedPercentSign } from 'i18n/utils';
+import { selectors } from 'data/redux/hooks';
+import transforms from 'data/redux/transforms';
+import { Headings } from 'data/constants/grades';
+import LabelReplacements from './LabelReplacements';
+import Fields from './Fields';
+import GradeButton from './GradeButton';
+
+import messages from './messages';
+
+import useGradebookTableData from './hooks';
+
+jest.mock('i18n/utils', () => ({
+ getLocalizedPercentSign: () => '%',
+}));
+jest.mock('./GradeButton', () => 'GradeButton');
+jest.mock('./Fields', () => jest.requireActual('testUtils').mockNestedComponents({
+ Username: 'Fields.Username',
+ Email: 'Fields.Email',
+}));
+jest.mock('./LabelReplacements', () => jest.requireActual('testUtils').mockNestedComponents({
+ TotalGradeLabelReplacement: 'LabelReplacements.TotalGradeLabelReplacement',
+ UsernameLabelReplacement: 'LabelReplacements.UsernameLabelReplacement',
+ MastersOnlyLabelReplacement: 'LabelReplacements.MastersOnlyLabelReplacement',
+}));
+
+jest.mock('data/redux/hooks', () => ({
+ selectors: {
+ grades: { useAllGrades: jest.fn() },
+ root: { useGetHeadings: jest.fn() },
+ },
+}));
+jest.mock('data/redux/transforms', () => ({
+ grades: { roundGrade: jest.fn() },
+}));
+
+const roundGrade = grade => grade * 20;
+transforms.grades.roundGrade.mockImplementation(roundGrade);
+
+const subsectionLabels = [
+ 'subsectionLabel1',
+ 'subsectionLabel2',
+ 'subsectionLabel3',
+];
+
+const allGrades = [
+ {
+ username: 'test-username-1',
+ external_user_key: 'EKey1',
+ email: 'email-1',
+ fullName: 'test-fullNAME',
+ percent: 0.9,
+ section_breakdown: [
+ { label: subsectionLabels[0] },
+ { label: subsectionLabels[1] },
+ { label: subsectionLabels[2] },
+ ],
+ },
+ {
+ username: 'test-username-2',
+ external_user_key: 'EKey2',
+ email: 'email-2',
+ percent: 0.8,
+ section_breakdown: [
+ { label: subsectionLabels[0] },
+ { label: subsectionLabels[1] },
+ { label: subsectionLabels[2] },
+ ],
+ },
+ {
+ username: 'test-username-3',
+ external_user_key: 'EKey3',
+ email: 'email-3',
+ percent: 0.6,
+ section_breakdown: [
+ { label: subsectionLabels[0] },
+ { label: subsectionLabels[1] },
+ { label: subsectionLabels[2] },
+ ],
+ },
+];
+const testHeading = 'test-heading-value';
+const headings = [
+ Headings.totalGrade,
+ Headings.username,
+ Headings.email,
+ Headings.fullName,
+ testHeading,
+];
+selectors.grades.useAllGrades.mockReturnValue(allGrades);
+selectors.root.useGetHeadings.mockReturnValue(headings);
+
+let out;
+describe('useGradebookTableData', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ out = useGradebookTableData();
+ });
+ describe('behavior', () => {
+ it('initializes intl hook', () => {
+ expect(useIntl).toHaveBeenCalled();
+ });
+ it('initializes redux hooks', () => {
+ expect(selectors.grades.useAllGrades).toHaveBeenCalled();
+ expect(selectors.root.useGetHeadings).toHaveBeenCalled();
+ });
+ });
+ describe('output', () => {
+ describe('columns', () => {
+ test('total grade heading produces TotalGradeLabelReplacement label', () => {
+ const { Header, accessor } = out.columns[0];
+ expect(accessor).toEqual(headings[0]);
+ expect(shallow(Header)).toMatchObject(
+ shallow( ),
+ );
+ });
+ test('username heading produces UsernameLabelReplacement', () => {
+ const { Header, accessor } = out.columns[1];
+ expect(accessor).toEqual(headings[1]);
+ expect(shallow(Header)).toMatchObject(
+ shallow( ),
+ );
+ });
+ test('email heading replaces with email heading message', () => {
+ const { Header, accessor } = out.columns[2];
+ expect(accessor).toEqual(headings[2]);
+ expect(shallow(Header)).toMatchObject(
+ shallow( ),
+ );
+ });
+ test('fullName heading replaces with fullName heading message', () => {
+ const { Header, accessor } = out.columns[3];
+ expect(accessor).toEqual(headings[3]);
+ expect(shallow(Header)).toMatchObject(
+ shallow( ),
+ );
+ });
+ test('other headings are passed through', () => {
+ const { Header, accessor } = out.columns[4];
+ expect(accessor).toEqual(headings[4]);
+ expect(Header).toEqual(headings[4]);
+ });
+ });
+ describe('data', () => {
+ test('username field', () => {
+ allGrades.forEach((entry, index) => {
+ expect(out.data[index][Headings.username]).toMatchObject(
+ ,
+ );
+ });
+ });
+ test('email field', () => {
+ allGrades.forEach((entry, index) => {
+ expect(out.data[index][Headings.email]).toMatchObject(
+ ,
+ );
+ });
+ });
+ test('totalGrade field', () => {
+ allGrades.forEach((entry, index) => {
+ expect(out.data[index][Headings.totalGrade]).toEqual(
+ `${roundGrade(entry.percent * 100)}${getLocalizedPercentSign()}`,
+ );
+ });
+ });
+ test('section breakdown', () => {
+ allGrades.forEach((entry, gradeIndex) => {
+ subsectionLabels.forEach((label, labelIndex) => {
+ expect(out.data[gradeIndex][label]).toMatchObject(
+ ,
+ );
+ });
+ });
+ });
+ });
+ it('forwards grades from redux', () => {
+ expect(out.grades).toEqual(allGrades);
+ });
+ test('nullMethod returns null', () => {
+ expect(out.nullMethod()).toEqual(null);
+ });
+ test('emptyContent', () => {
+ expect(out.emptyContent).toEqual(formatMessage(messages.noResultsFound));
+ });
+ });
+});
diff --git a/src/components/GradesView/GradebookTable/index.jsx b/src/components/GradesView/GradebookTable/index.jsx
index 0fe6d7a2..fd26ffb0 100644
--- a/src/components/GradesView/GradebookTable/index.jsx
+++ b/src/components/GradesView/GradebookTable/index.jsx
@@ -1,21 +1,8 @@
-/* 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 { DataTable } from '@edx/paragon';
-import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
-import selectors from 'data/selectors';
-import { Headings } from 'data/constants/grades';
-import { getLocalizedPercentSign } from 'i18n/utils';
-
-import messages from './messages';
-import Fields from './Fields';
-import LabelReplacements from './LabelReplacements';
-import GradeButton from './GradeButton';
-
-const { roundGrade } = selectors.grades;
+import useGradebookTableData from './hooks';
/**
*
@@ -23,96 +10,33 @@ const { roundGrade } = selectors.grades;
* a row for each user, with a column for their username, email, and total grade,
* along with one for each subsection in their grade entry.
*/
-export class GradebookTable extends React.Component {
- constructor(props) {
- super(props);
- this.mapHeaders = this.mapHeaders.bind(this);
- this.mapRows = this.mapRows.bind(this);
- this.nullMethod = this.nullMethod.bind(this);
- }
-
- mapHeaders(heading) {
- let label;
- if (heading === Headings.totalGrade) {
- label = ;
- } else if (heading === Headings.username) {
- label = ;
- } else if (heading === Headings.email) {
- label = ;
- } else if (heading === Headings.fullName) {
- label = ;
- } else {
- label = heading;
- }
- return { Header: label, accessor: heading };
- }
-
- mapRows = entry => ({
- [Headings.username]: (
-
- ),
- [Headings.fullName]: ( ),
- [Headings.email]: ( ),
- [Headings.totalGrade]: `${roundGrade(entry.percent * 100)}${getLocalizedPercentSign()}`,
- ...entry.section_breakdown.reduce((acc, subsection) => ({
- ...acc,
- [subsection.label]: ,
- }), {}),
- });
-
- nullMethod() {
- return null;
- }
-
- render() {
- return (
-
-
-
-
-
-
-
- );
- }
-}
-
-GradebookTable.defaultProps = {
- grades: [],
-};
-
-GradebookTable.propTypes = {
- // redux
- grades: PropTypes.arrayOf(PropTypes.shape({
- percent: PropTypes.number,
- section_breakdown: PropTypes.arrayOf(PropTypes.shape({
- attempted: PropTypes.bool,
- category: PropTypes.string,
- label: PropTypes.string,
- module_id: PropTypes.string,
- percent: PropTypes.number,
- scoreEarned: PropTypes.number,
- scorePossible: PropTypes.number,
- subsection_name: PropTypes.string,
- })),
- user_id: PropTypes.number,
- user_name: PropTypes.string,
- })),
- headings: PropTypes.arrayOf(PropTypes.string).isRequired,
- // injected
- intl: intlShape.isRequired,
+export const GradebookTable = () => {
+ const {
+ columns,
+ data,
+ grades,
+ nullMethod,
+ emptyContent,
+ } = useGradebookTableData();
+
+ return (
+
+
+
+
+
+
+
+ );
};
-export const mapStateToProps = (state) => ({
- grades: selectors.grades.allGrades(state),
- headings: selectors.root.getHeadings(state),
-});
+GradebookTable.propTypes = {};
-export default injectIntl(connect(mapStateToProps)(GradebookTable));
+export default GradebookTable;
diff --git a/src/components/GradesView/GradebookTable/index.test.jsx b/src/components/GradesView/GradebookTable/index.test.jsx
new file mode 100644
index 00000000..ac97eaaf
--- /dev/null
+++ b/src/components/GradesView/GradebookTable/index.test.jsx
@@ -0,0 +1,39 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+
+import { DataTable } from '@edx/paragon';
+
+import useGradebookTableData from './hooks';
+import GradebookTable from '.';
+
+jest.mock('./hooks', () => jest.fn());
+
+const hookProps = {
+ columns: ['some', 'columns'],
+ data: ['some', 'data'],
+ grades: ['a', 'few', 'grades'],
+ nullMethod: jest.fn().mockName('hooks.nullMethod'),
+ emptyContent: 'empty-table-content',
+};
+useGradebookTableData.mockReturnValue(hookProps);
+
+let el;
+describe('GradebookTable', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ el = shallow( );
+ });
+ test('snapshot', () => {
+ expect(el).toMatchSnapshot();
+ });
+ test('Datatable props', () => {
+ const datatable = el.find(DataTable);
+ const props = datatable.props();
+ expect(props.columns).toEqual(hookProps.columns);
+ expect(props.data).toEqual(hookProps.data);
+ expect(props.itemCount).toEqual(hookProps.grades.length);
+ expect(props.RowStatusComponent).toEqual(hookProps.nullMethod);
+ expect(datatable.children().at(2).type()).toEqual('DataTable.EmptyTable');
+ expect(datatable.children().at(2).props().content).toEqual(hookProps.emptyContent);
+ });
+});
diff --git a/src/components/GradesView/GradebookTable/test.jsx b/src/components/GradesView/GradebookTable/test.jsx
deleted file mode 100644
index ed416a65..00000000
--- a/src/components/GradesView/GradebookTable/test.jsx
+++ /dev/null
@@ -1,186 +0,0 @@
-import React from 'react';
-import { shallow } from 'enzyme';
-
-import { DataTable } from '@edx/paragon';
-
-import selectors from 'data/selectors';
-import { Headings } from 'data/constants/grades';
-import LabelReplacements from './LabelReplacements';
-import Fields from './Fields';
-import messages from './messages';
-import { GradebookTable, mapStateToProps } from '.';
-
-jest.mock('@edx/paragon', () => jest.requireActual('testUtils').mockNestedComponents({
- DataTable: {
- Table: 'DataTable.Table',
- TableControlBar: 'DataTable.TableControlBar',
- EmptyTable: 'DataTable.EmptyTable',
- },
-}));
-jest.mock('./Fields', () => ({
- __esModule: true,
- default: {
- Username: () => 'Fields.Username',
- Text: () => 'Fields.Text',
- },
-}));
-jest.mock('./LabelReplacements', () => ({
- __esModule: true,
- default: {
- TotalGradeLabelReplacement: () => 'TotalGradeLabelReplacement',
- UsernameLabelReplacement: () => 'UsernameLabelReplacement',
- MastersOnlyLabelReplacement: () => 'MastersOnlyLabelReplacement',
- },
-}));
-jest.mock('./GradeButton', () => 'GradeButton');
-jest.mock('data/selectors', () => ({
- __esModule: true,
- default: {
- grades: {
- roundGrade: jest.fn(grade => `roundedGrade: ${grade}`),
- allGrades: jest.fn(state => ({ allGrades: state })),
- },
- root: {
- getHeadings: jest.fn(state => ({ getHeadings: state })),
- },
- },
-}));
-describe('GradebookTable', () => {
- describe('component', () => {
- let el;
- const fields = { field1: 'field1', field2: 'field2' };
- const props = {
- grades: [
- {
- percent: 1,
- section_breakdown: [
- { label: fields.field1, percent: 1.2 },
- { label: fields.field2, percent: 2.3 },
- ],
- },
- {
- percent: 2,
- section_breakdown: [
- { label: fields.field1, percent: 1.2 },
- { label: fields.field2, percent: 2.3 },
- ],
- },
- {
- percent: 3,
- section_breakdown: [
- { label: fields.field1, percent: 1.2 },
- { label: fields.field2, percent: 2.3 },
- ],
- },
- ],
- headings: [
- Headings.username,
- Headings.fullName,
- Headings.email,
- fields.field1,
- fields.field2,
- Headings.totalGrade,
- ],
-
- intl: { formatMessage: (msg) => msg.defaultMessage },
- };
- test('snapshot - fields1 and 2 between email and totalGrade, mocked rows', () => {
- el = shallow( );
- el.instance().nullMethod = jest.fn().mockName('this.nullMethod');
- el.instance().mapRows = (entry) => `mappedRow: ${entry.percent}`;
- expect(el.instance().render()).toMatchSnapshot();
- });
- test('null method returns null for stub component', () => {
- el = shallow( );
- expect(el.instance().nullMethod()).toEqual(null);
- });
- describe('table columns (mapHeaders)', () => {
- let headings;
- beforeEach(() => {
- el = shallow( );
- headings = el.find(DataTable).props().columns;
- });
- test('username sets key and replaces Header with component', () => {
- const heading = headings[0];
- expect(heading.accessor).toEqual(Headings.username);
- expect(heading.Header.type).toEqual(LabelReplacements.UsernameLabelReplacement);
- });
- test('full name sets key and Header from header', () => {
- const heading = headings[1];
- expect(heading.accessor).toEqual(Headings.fullName);
- expect(heading.Header).toEqual( );
- });
- test('email sets key and Header from header', () => {
- const heading = headings[2];
- expect(heading.accessor).toEqual(Headings.email);
- expect(heading.Header).toEqual( );
- });
- test('subsections set key and Header from header', () => {
- expect(headings[3]).toEqual({ accessor: fields.field1, Header: fields.field1 });
- expect(headings[4]).toEqual({ accessor: fields.field2, Header: fields.field2 });
- });
- test('totalGrade sets key and replaces Header with component', () => {
- const heading = headings[5];
- expect(heading.accessor).toEqual(Headings.totalGrade);
- expect(heading.Header.type).toEqual(LabelReplacements.TotalGradeLabelReplacement);
- });
- });
- describe('table data (mapRows)', () => {
- let rows;
- beforeEach(() => {
- el = shallow( );
- rows = el.find(DataTable).props().data;
- });
- describe.each([0, 1, 2])('gradeEntry($percent)', (gradeIndex) => {
- let row;
- const entry = props.grades[gradeIndex];
- beforeEach(() => {
- row = rows[gradeIndex];
- });
- test('username set to Username Field', () => {
- const field = row[Headings.username];
- expect(field.type).toEqual(Fields.Username);
- expect(field.props).toEqual({
- username: entry.username,
- userKey: entry.external_user_key,
- });
- });
- test('fullName set to Text Field', () => {
- const field = row[Headings.fullName];
- expect(field.type).toEqual(Fields.Text);
- expect(field.props).toEqual({ value: entry.full_name });
- });
- test('email set to Text Field', () => {
- const field = row[Headings.email];
- expect(field.type).toEqual(Fields.Text);
- expect(field.props).toEqual({ value: entry.email });
- });
- test('totalGrade set to rounded percent grade * 100', () => {
- expect(
- row[Headings.totalGrade],
- ).toEqual(`${selectors.grades.roundGrade(entry.percent * 100)}%`);
- });
- test('subsections loaded as GradeButtons', () => {
- });
- });
- });
- });
- describe('mapStateToProps', () => {
- let mapped;
- const testState = {
- where: 'did',
- all: 'of',
- these: 'bananas',
- come: 'from?',
- };
- beforeEach(() => {
- mapped = mapStateToProps(testState);
- });
- test('grades from grades.allGrades', () => {
- expect(mapped.grades).toEqual(selectors.grades.allGrades(testState));
- });
- test('headings from root.getHeadings', () => {
- expect(mapped.headings).toEqual(selectors.root.getHeadings(testState));
- });
- });
-});
diff --git a/src/components/GradesView/ImportGradesButton/hooks.test.js b/src/components/GradesView/ImportGradesButton/hooks.test.js
index 753dd093..c4439d04 100644
--- a/src/components/GradesView/ImportGradesButton/hooks.test.js
+++ b/src/components/GradesView/ImportGradesButton/hooks.test.js
@@ -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);
diff --git a/src/components/GradesView/ImportSuccessToast.jsx b/src/components/GradesView/ImportSuccessToast.jsx
deleted file mode 100644
index a38a2e04..00000000
--- a/src/components/GradesView/ImportSuccessToast.jsx
+++ /dev/null
@@ -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';
-
-/**
- *
- * 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 (
-
- {this.props.intl.formatMessage(messages.description)}
-
- );
- }
-}
-
-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));
diff --git a/src/components/GradesView/ImportSuccessToast.test.jsx b/src/components/GradesView/ImportSuccessToast.test.jsx
deleted file mode 100644
index be134925..00000000
--- a/src/components/GradesView/ImportSuccessToast.test.jsx
+++ /dev/null
@@ -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( );
- });
- 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);
- });
- });
-});
diff --git a/src/components/GradesView/ImportSuccessToast/__snapshots__/index.test.jsx.snap b/src/components/GradesView/ImportSuccessToast/__snapshots__/index.test.jsx.snap
new file mode 100644
index 00000000..e3c77217
--- /dev/null
+++ b/src/components/GradesView/ImportSuccessToast/__snapshots__/index.test.jsx.snap
@@ -0,0 +1,11 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ImportSuccessToast component render snapshot 1`] = `
+
+ test-description
+
+`;
diff --git a/src/components/GradesView/ImportSuccessToast/hooks.js b/src/components/GradesView/ImportSuccessToast/hooks.js
new file mode 100644
index 00000000..75db5507
--- /dev/null
+++ b/src/components/GradesView/ImportSuccessToast/hooks.js
@@ -0,0 +1,39 @@
+import { useIntl } from '@edx/frontend-platform/i18n';
+
+import { actions, selectors } from 'data/redux/hooks';
+import { views } from 'data/constants/app';
+import messages from './messages';
+
+/**
+ *
+ * Toast component triggered by successful grade upload.
+ * Provides a link to view the Bulk Management History tab.
+ */
+export const useImportSuccessToastData = () => {
+ const { formatMessage } = useIntl();
+
+ const show = selectors.app.useShowImportSuccessToast();
+ const setAppView = actions.app.useSetView();
+ const setShow = actions.app.useSetShowImportSuccessToast();
+
+ const onClose = () => {
+ setShow(false);
+ };
+
+ const handleShowHistoryView = () => {
+ setAppView(views.bulkManagementHistory);
+ setShow(false);
+ };
+
+ return {
+ action: {
+ label: formatMessage(messages.showHistoryViewBtn),
+ onClick: handleShowHistoryView,
+ },
+ onClose,
+ show,
+ description: formatMessage(messages.description),
+ };
+};
+
+export default useImportSuccessToastData;
diff --git a/src/components/GradesView/ImportSuccessToast/hooks.test.js b/src/components/GradesView/ImportSuccessToast/hooks.test.js
new file mode 100644
index 00000000..85300361
--- /dev/null
+++ b/src/components/GradesView/ImportSuccessToast/hooks.test.js
@@ -0,0 +1,58 @@
+import { useIntl } from '@edx/frontend-platform/i18n';
+
+import { formatMessage } from 'testUtils';
+import { views } from 'data/constants/app';
+import { actions, selectors } from 'data/redux/hooks';
+
+import useImportSuccessToastData from './hooks';
+import messages from './messages';
+
+jest.mock('data/redux/hooks', () => ({
+ actions: {
+ app: {
+ useSetView: jest.fn(),
+ useSetShowImportSuccessToast: jest.fn(),
+ },
+ },
+ selectors: {
+ app: { useShowImportSuccessToast: jest.fn() },
+ },
+}));
+
+const setView = jest.fn().mockName('hooks.setView');
+const setShowToast = jest.fn().mockName('hooks.setShowImportSuccessToast');
+actions.app.useSetView.mockReturnValue(setView);
+actions.app.useSetShowImportSuccessToast.mockReturnValue(setShowToast);
+const showImportSuccessToast = 'test-show-import-success-toast';
+selectors.app.useShowImportSuccessToast.mockReturnValue(showImportSuccessToast);
+
+let out;
+describe('ImportSuccessToast component', () => {
+ beforeAll(() => {
+ out = useImportSuccessToastData();
+ });
+ describe('behavior', () => {
+ it('initializes intl hook', () => {
+ expect(useIntl).toHaveBeenCalledWith();
+ });
+ it('initializes redux hooks', () => {
+ expect(selectors.app.useShowImportSuccessToast).toHaveBeenCalled();
+ expect(actions.app.useSetView).toHaveBeenCalled();
+ expect(actions.app.useSetShowImportSuccessToast).toHaveBeenCalled();
+ });
+ });
+ describe('output', () => {
+ test('action label', () => {
+ expect(out.action.label).toEqual(formatMessage(messages.showHistoryViewBtn));
+ });
+ test('action click event', () => {
+ out.action.onClick();
+ expect(setView).toHaveBeenCalledWith(views.bulkManagementHistory);
+ expect(setShowToast).toHaveBeenCalledWith(false);
+ });
+ test('onClose', () => {
+ out.onClose();
+ expect(setShowToast).toHaveBeenCalledWith(false);
+ });
+ });
+});
diff --git a/src/components/GradesView/ImportSuccessToast/index.jsx b/src/components/GradesView/ImportSuccessToast/index.jsx
new file mode 100644
index 00000000..f9f80f38
--- /dev/null
+++ b/src/components/GradesView/ImportSuccessToast/index.jsx
@@ -0,0 +1,28 @@
+import React from 'react';
+
+import { Toast } from '@edx/paragon';
+
+import useImportSuccessToastData from './hooks';
+
+/**
+ *
+ * Toast component triggered by successful grade upload.
+ * Provides a link to view the Bulk Management History tab.
+ */
+export const ImportSuccessToast = () => {
+ const {
+ action,
+ onClose,
+ show,
+ description,
+ } = useImportSuccessToastData();
+ return (
+
+ {description}
+
+ );
+};
+
+ImportSuccessToast.propTypes = {};
+
+export default ImportSuccessToast;
diff --git a/src/components/GradesView/ImportSuccessToast/index.test.jsx b/src/components/GradesView/ImportSuccessToast/index.test.jsx
new file mode 100644
index 00000000..d9c7c55a
--- /dev/null
+++ b/src/components/GradesView/ImportSuccessToast/index.test.jsx
@@ -0,0 +1,39 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+
+import useImportSuccessToastData from './hooks';
+import ImportSuccessToast from '.';
+
+jest.mock('./hooks', () => jest.fn());
+
+const hookProps = {
+ action: 'test-action',
+ onClose: jest.fn().mockName('hooks.onClose'),
+ show: 'test-show',
+ description: 'test-description',
+};
+useImportSuccessToastData.mockReturnValue(hookProps);
+
+let el;
+describe('ImportSuccessToast component', () => {
+ beforeAll(() => {
+ el = shallow( );
+ });
+ describe('behavior', () => {
+ it('initializes component hook', () => {
+ expect(useImportSuccessToastData).toHaveBeenCalled();
+ });
+ });
+ describe('render', () => {
+ test('snapshot', () => {
+ expect(el).toMatchSnapshot();
+ });
+ test('Toast', () => {
+ expect(el.type()).toEqual('Toast');
+ expect(el.props().action).toEqual(hookProps.action);
+ expect(el.props().onClose).toEqual(hookProps.onClose);
+ expect(el.props().show).toEqual(hookProps.show);
+ expect(el.text()).toEqual(hookProps.description);
+ });
+ });
+});
diff --git a/src/components/GradesView/ImportSuccessToast.messages.js b/src/components/GradesView/ImportSuccessToast/messages.js
similarity index 100%
rename from src/components/GradesView/ImportSuccessToast.messages.js
rename to src/components/GradesView/ImportSuccessToast/messages.js
diff --git a/src/components/GradesView/InterventionsReport.jsx b/src/components/GradesView/InterventionsReport.jsx
deleted file mode 100644
index 5a0402fa..00000000
--- a/src/components/GradesView/InterventionsReport.jsx
+++ /dev/null
@@ -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';
-
-/**
- *
- * 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 && (
-
- );
- }
-}
-
-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);
diff --git a/src/components/GradesView/InterventionsReport.test.jsx b/src/components/GradesView/InterventionsReport.test.jsx
deleted file mode 100644
index 3dfe2b56..00000000
--- a/src/components/GradesView/InterventionsReport.test.jsx
+++ /dev/null
@@ -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( );
- });
- 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( );
- });
- 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,
- );
- });
- });
-});
diff --git a/src/components/GradesView/InterventionsReport/__snapshots__/index.test.jsx.snap b/src/components/GradesView/InterventionsReport/__snapshots__/index.test.jsx.snap
new file mode 100644
index 00000000..2e130dbe
--- /dev/null
+++ b/src/components/GradesView/InterventionsReport/__snapshots__/index.test.jsx.snap
@@ -0,0 +1,30 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`InterventionsReport component output snapshot 1`] = `
+
+
+ Interventions Report
+
+
+
+ Need to find students who may be falling behind? Download the interventions report to obtain engagement metrics such as section attempts and visits.
+
+
+
+
+`;
diff --git a/src/components/GradesView/InterventionsReport/hooks.js b/src/components/GradesView/InterventionsReport/hooks.js
new file mode 100644
index 00000000..493e7f35
--- /dev/null
+++ b/src/components/GradesView/InterventionsReport/hooks.js
@@ -0,0 +1,19 @@
+import { actions, selectors } from 'data/redux/hooks';
+
+const useInterventionsReportData = () => {
+ const interventionExportUrl = selectors.root.useInterventionExportUrl();
+ const showBulkManagement = selectors.root.useShowBulkManagement();
+ const downloadInterventionReport = actions.grades.useDownloadInterventionReport();
+
+ const handleClick = () => {
+ downloadInterventionReport();
+ window.location.assign(interventionExportUrl);
+ };
+
+ return {
+ show: showBulkManagement,
+ handleClick,
+ };
+};
+
+export default useInterventionsReportData;
diff --git a/src/components/GradesView/InterventionsReport/hooks.test.js b/src/components/GradesView/InterventionsReport/hooks.test.js
new file mode 100644
index 00000000..357b2982
--- /dev/null
+++ b/src/components/GradesView/InterventionsReport/hooks.test.js
@@ -0,0 +1,56 @@
+import { actions, selectors } from 'data/redux/hooks';
+
+import useInterventionsReportData from './hooks';
+
+jest.mock('data/redux/hooks', () => ({
+ actions: {
+ grades: {
+ useDownloadInterventionReport: jest.fn(),
+ },
+ },
+ selectors: {
+ root: {
+ useInterventionExportUrl: jest.fn(),
+ useShowBulkManagement: jest.fn(),
+ },
+ },
+}));
+
+const downloadReport = jest.fn();
+actions.grades.useDownloadInterventionReport.mockReturnValue(downloadReport);
+selectors.root.useShowBulkManagement.mockReturnValue(true);
+const exportUrl = 'test-intervention-export-url';
+selectors.root.useInterventionExportUrl.mockReturnValue(exportUrl);
+
+let hook;
+let oldLocation;
+describe('useInterventionsReportData hooks', () => {
+ beforeEach(() => {
+ oldLocation = window.location;
+ delete window.location;
+ window.location = { assign: jest.fn() };
+ hook = useInterventionsReportData();
+ });
+ afterEach(() => {
+ window.location = oldLocation;
+ });
+ describe('behavior', () => {
+ it('initializes hooks', () => {
+ expect(selectors.root.useInterventionExportUrl).toHaveBeenCalled();
+ expect(selectors.root.useShowBulkManagement).toHaveBeenCalled();
+ expect(actions.grades.useDownloadInterventionReport).toHaveBeenCalled();
+ });
+ });
+ describe('output', () => {
+ test('show from showBulkManagement selector', () => {
+ expect(hook.show).toEqual(true);
+ });
+ describe('handleClick', () => {
+ it('downloads interventions report and navigates to export url', () => {
+ hook.handleClick();
+ expect(downloadReport).toHaveBeenCalled();
+ expect(window.location.assign).toHaveBeenCalledWith(exportUrl);
+ });
+ });
+ });
+});
diff --git a/src/components/GradesView/InterventionsReport/index.jsx b/src/components/GradesView/InterventionsReport/index.jsx
new file mode 100644
index 00000000..f19e96e2
--- /dev/null
+++ b/src/components/GradesView/InterventionsReport/index.jsx
@@ -0,0 +1,43 @@
+import React from 'react';
+
+import { useIntl } from '@edx/frontend-platform/i18n';
+
+import NetworkButton from 'components/NetworkButton';
+
+import messages from './messages';
+import useInterventionsReportData from './hooks';
+
+/**
+ *
+ * Provides download buttons for Bulk Management and Intervention reports, only if
+ * showBulkManagement is set in redus.
+ */
+export const InterventionsReport = () => {
+ const { show, handleClick } = useInterventionsReportData();
+ const { formatMessage } = useIntl();
+
+ if (!show) {
+ return null;
+ }
+
+ return (
+
+
+ {formatMessage(messages.title)}
+
+
+
+ {formatMessage(messages.description)}
+
+
+
+
+ );
+};
+
+export default InterventionsReport;
diff --git a/src/components/GradesView/InterventionsReport/index.test.jsx b/src/components/GradesView/InterventionsReport/index.test.jsx
new file mode 100644
index 00000000..0d1c82c8
--- /dev/null
+++ b/src/components/GradesView/InterventionsReport/index.test.jsx
@@ -0,0 +1,42 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+
+import { useIntl } from '@edx/frontend-platform/i18n';
+
+import NetworkButton from 'components/NetworkButton';
+
+import messages from './messages';
+import useInterventionsReportData from './hooks';
+import InterventionsReport from '.';
+
+jest.mock('components/NetworkButton', () => 'NetworkButton');
+jest.mock('./hooks', () => jest.fn());
+
+const hookProps = { show: true, handleClick: jest.fn() };
+useInterventionsReportData.mockReturnValue(hookProps);
+
+let el;
+describe('InterventionsReport component', () => {
+ beforeEach(() => {
+ el = shallow( );
+ });
+ describe('behavior', () => {
+ it('initializes hooks', () => {
+ expect(useInterventionsReportData).toHaveBeenCalledWith();
+ expect(useIntl).toHaveBeenCalledWith();
+ });
+ });
+ describe('output', () => {
+ it('does now render if show is false', () => {
+ useInterventionsReportData.mockReturnValueOnce({ ...hookProps, show: false });
+ el = shallow( );
+ expect(el.isEmptyRender()).toEqual(true);
+ });
+ test('snapshot', () => {
+ expect(el).toMatchSnapshot();
+ const btnProps = el.find(NetworkButton).props();
+ expect(btnProps.label).toEqual(messages.downloadBtn);
+ expect(btnProps.onClick).toEqual(hookProps.handleClick);
+ });
+ });
+});
diff --git a/src/components/GradesView/InterventionsReport.messages.js b/src/components/GradesView/InterventionsReport/messages.js
similarity index 100%
rename from src/components/GradesView/InterventionsReport.messages.js
rename to src/components/GradesView/InterventionsReport/messages.js
diff --git a/src/components/GradesView/PageButtons/PageButtons.test.jsx b/src/components/GradesView/PageButtons/PageButtons.test.jsx
deleted file mode 100644
index 41680226..00000000
--- a/src/components/GradesView/PageButtons/PageButtons.test.jsx
+++ /dev/null
@@ -1,96 +0,0 @@
-import React from 'react';
-import { shallow } from 'enzyme';
-
-import selectors from 'data/selectors';
-import thunkActions from 'data/thunkActions';
-
-import { PageButtons, mapStateToProps, mapDispatchToProps } from '.';
-
-jest.mock('@edx/paragon', () => ({
- Button: () => 'Button',
-}));
-jest.mock('data/selectors', () => ({
- __esModule: true,
- default: {
- grades: {
- nextPage: jest.fn(state => ({ nextPage: state })),
- prevPage: jest.fn(state => ({ prevPage: state })),
- },
- },
-}));
-
-jest.mock('data/thunkActions', () => ({
- __esModule: true,
- default: {
- grades: {
- fetchPrevNextGrades: jest.fn(),
- },
- },
-}));
-
-let props;
-let el;
-describe('PageButtons component', () => {
- beforeEach(() => {
- props = {
- getPrevNextGrades: jest.fn(),
- nextPage: 'NEXT PAGE',
- prevPage: 'prev PAGE',
- };
- });
- describe('snapshots', () => {
- beforeEach(() => {
- el = shallow( );
- el.instance.fetchNextGrades = jest.fn().mockName('fetchNextGrades');
- el.instance.fetchPrevGrades = jest.fn().mockName('fetchPrevGrades');
- });
- test('buttons enabled with both endpoints provided', () => {
- expect(el.instance().render()).toMatchSnapshot();
- });
- test('nextPage disabled if not provided', () => {
- el.setProps({ nextPage: undefined });
- expect(el.instance().render()).toMatchSnapshot();
- });
- test('prevPage disabled if not provided', () => {
- el.setProps({ prevPage: undefined });
- expect(el.instance().render()).toMatchSnapshot();
- });
- });
- describe('behavior', () => {
- beforeEach(() => {
- el = shallow( );
- });
- describe('getPrevGrades', () => {
- it('calls props.getPrevNextGrades with props.prevPage', () => {
- el.instance().getPrevGrades();
- expect(props.getPrevNextGrades).toHaveBeenCalledWith(props.prevPage);
- });
- });
- describe('getNextGrades', () => {
- it('calls props.getPrevNextGrades with props.nextPage', () => {
- el.instance().getNextGrades();
- expect(props.getPrevNextGrades).toHaveBeenCalledWith(props.nextPage);
- });
- });
- });
- describe('mapStateToProps', () => {
- const testState = { l: 'eeeerroooooy', j: 'jjjjeeeeeeenkins' };
- let mapped;
- beforeEach(() => {
- mapped = mapStateToProps(testState);
- });
- test('nextPage from grades.nextPage', () => {
- expect(mapped.nextPage).toEqual(selectors.grades.nextPage(testState));
- });
- test('prevPage from grades.prevPage', () => {
- expect(mapped.prevPage).toEqual(selectors.grades.prevPage(testState));
- });
- });
- describe('mapDispatchToProps', () => {
- test('getPrevNextGrades from thunkActions.grades.fetchPrevNextGrades', () => {
- expect(
- mapDispatchToProps.getPrevNextGrades,
- ).toEqual(thunkActions.grades.fetchPrevNextGrades);
- });
- });
-});
diff --git a/src/components/GradesView/PageButtons/__snapshots__/PageButtons.test.jsx.snap b/src/components/GradesView/PageButtons/__snapshots__/PageButtons.test.jsx.snap
deleted file mode 100644
index 144c7c01..00000000
--- a/src/components/GradesView/PageButtons/__snapshots__/PageButtons.test.jsx.snap
+++ /dev/null
@@ -1,133 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`PageButtons component snapshots buttons enabled with both endpoints provided 1`] = `
-
-
-
-
-
-
-
-
-`;
-
-exports[`PageButtons component snapshots nextPage disabled if not provided 1`] = `
-
-
-
-
-
-
-
-
-`;
-
-exports[`PageButtons component snapshots prevPage disabled if not provided 1`] = `
-
-
-
-
-
-
-
-
-`;
diff --git a/src/components/GradesView/PageButtons/__snapshots__/index.test.jsx.snap b/src/components/GradesView/PageButtons/__snapshots__/index.test.jsx.snap
new file mode 100644
index 00000000..8fb8ffa2
--- /dev/null
+++ b/src/components/GradesView/PageButtons/__snapshots__/index.test.jsx.snap
@@ -0,0 +1,37 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`PageButtons component render snapshot 1`] = `
+
+
+ prev-text
+
+
+ next-text
+
+
+`;
diff --git a/src/components/GradesView/PageButtons/hooks.js b/src/components/GradesView/PageButtons/hooks.js
new file mode 100644
index 00000000..f1023422
--- /dev/null
+++ b/src/components/GradesView/PageButtons/hooks.js
@@ -0,0 +1,34 @@
+import { useIntl } from '@edx/frontend-platform/i18n';
+
+import { selectors, thunkActions } from 'data/redux/hooks';
+import messages from './messages';
+
+export const usePageButtonsData = () => {
+ const { formatMessage } = useIntl();
+
+ const { nextPage, prevPage } = selectors.grades.useGradeData();
+ const getPrevNextGrades = thunkActions.grades.useFetchPrevNextGrades();
+
+ const getPrevGrades = () => {
+ getPrevNextGrades(prevPage);
+ };
+
+ const getNextGrades = () => {
+ getPrevNextGrades(nextPage);
+ };
+
+ return {
+ prev: {
+ disabled: !prevPage,
+ onClick: getPrevGrades,
+ text: formatMessage(messages.prevPage),
+ },
+ next: {
+ disabled: !nextPage,
+ onClick: getNextGrades,
+ text: formatMessage(messages.nextPage),
+ },
+ };
+};
+
+export default usePageButtonsData;
diff --git a/src/components/GradesView/PageButtons/hooks.test.js b/src/components/GradesView/PageButtons/hooks.test.js
new file mode 100644
index 00000000..8d96bec7
--- /dev/null
+++ b/src/components/GradesView/PageButtons/hooks.test.js
@@ -0,0 +1,77 @@
+import { useIntl } from '@edx/frontend-platform/i18n';
+
+import { formatMessage } from 'testUtils';
+import { selectors, thunkActions } from 'data/redux/hooks';
+
+import usePageButtonsData from './hooks';
+import messages from './messages';
+
+jest.mock('data/redux/hooks', () => ({
+ selectors: {
+ grades: { useGradeData: jest.fn() },
+ },
+ thunkActions: {
+ grades: { useFetchPrevNextGrades: jest.fn() },
+ },
+}));
+
+const gradeData = { nextPage: 'test-next-page', prevPage: 'test-prev-page' };
+selectors.grades.useGradeData.mockReturnValue(gradeData);
+
+const fetchGrades = jest.fn();
+thunkActions.grades.useFetchPrevNextGrades.mockReturnValue(fetchGrades);
+
+let out;
+describe('usePageButtonsData', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ out = usePageButtonsData();
+ });
+ describe('behavior', () => {
+ it('initializes intl hook', () => {
+ expect(useIntl).toHaveBeenCalled();
+ });
+ it('initializes redux hooks', () => {
+ expect(selectors.grades.useGradeData).toHaveBeenCalled();
+ expect(thunkActions.grades.useFetchPrevNextGrades).toHaveBeenCalled();
+ });
+ });
+ describe('output', () => {
+ describe('prev button entry', () => {
+ it('is disabled iff prevPage is not provided', () => {
+ expect(out.prev.disabled).toEqual(false);
+ selectors.grades.useGradeData.mockReturnValueOnce({
+ ...gradeData,
+ prevPage: undefined,
+ });
+ out = usePageButtonsData();
+ expect(out.prev.disabled).toEqual(true);
+ });
+ it('calls fetch with prevPage on click', () => {
+ out.prev.onClick();
+ expect(fetchGrades).toHaveBeenCalledWith(gradeData.prevPage);
+ });
+ test('text display', () => {
+ expect(out.prev.text).toEqual(formatMessage(messages.prevPage));
+ });
+ });
+ describe('next button entry', () => {
+ it('is disabled iff nextPage is not provided', () => {
+ expect(out.next.disabled).toEqual(false);
+ selectors.grades.useGradeData.mockReturnValueOnce({
+ ...gradeData,
+ nextPage: undefined,
+ });
+ out = usePageButtonsData();
+ expect(out.next.disabled).toEqual(true);
+ });
+ it('calls fetch with prevPage on click', () => {
+ out.next.onClick();
+ expect(fetchGrades).toHaveBeenCalledWith(gradeData.nextPage);
+ });
+ test('text display', () => {
+ expect(out.next.text).toEqual(formatMessage(messages.nextPage));
+ });
+ });
+ });
+});
diff --git a/src/components/GradesView/PageButtons/index.jsx b/src/components/GradesView/PageButtons/index.jsx
index 0c2fb1b8..bb377f47 100644
--- a/src/components/GradesView/PageButtons/index.jsx
+++ b/src/components/GradesView/PageButtons/index.jsx
@@ -1,75 +1,37 @@
import React from 'react';
-import PropTypes from 'prop-types';
-import { connect } from 'react-redux';
import { Button } from '@edx/paragon';
-import { FormattedMessage } from '@edx/frontend-platform/i18n';
-import selectors from 'data/selectors';
-import thunkActions from 'data/thunkActions';
-import messages from './messages';
-
-export class PageButtons extends React.Component {
- constructor(props) {
- super(props);
- this.getPrevGrades = this.getPrevGrades.bind(this);
- this.getNextGrades = this.getNextGrades.bind(this);
- }
-
- getPrevGrades() {
- this.props.getPrevNextGrades(this.props.prevPage);
- }
-
- getNextGrades() {
- this.props.getPrevNextGrades(this.props.nextPage);
- }
-
- render() {
- return (
- {
+ const { prev, next } = usePageButtonsData();
+
+ return (
+
+
-
-
-
-
-
-
-
- );
- }
-}
-
-PageButtons.defaultProps = {
- nextPage: '',
- prevPage: '',
-};
-
-PageButtons.propTypes = {
- // redux
- getPrevNextGrades: PropTypes.func.isRequired,
- nextPage: PropTypes.string,
- prevPage: PropTypes.string,
+ {prev.text}
+
+
+ {next.text}
+
+
+ );
};
-export const mapStateToProps = (state) => ({
- nextPage: selectors.grades.nextPage(state),
- prevPage: selectors.grades.prevPage(state),
-});
-
-export const mapDispatchToProps = {
- getPrevNextGrades: thunkActions.grades.fetchPrevNextGrades,
-};
+PageButtons.propTypes = {};
-export default connect(mapStateToProps, mapDispatchToProps)(PageButtons);
+export default PageButtons;
diff --git a/src/components/GradesView/PageButtons/index.test.jsx b/src/components/GradesView/PageButtons/index.test.jsx
new file mode 100644
index 00000000..3922408e
--- /dev/null
+++ b/src/components/GradesView/PageButtons/index.test.jsx
@@ -0,0 +1,53 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+
+import { Button } from '@edx/paragon';
+
+import usePageButtonsData from './hooks';
+import PageButtons from '.';
+
+jest.mock('./hooks', () => jest.fn());
+
+const hookProps = {
+ prev: {
+ disabled: 'prev-disabled',
+ onClick: jest.fn().mockName('hooks.prev.onClick'),
+ text: 'prev-text',
+ },
+ next: {
+ disabled: 'next-disabled',
+ onClick: jest.fn().mockName('hooks.next.onClick'),
+ text: 'next-text',
+ },
+};
+usePageButtonsData.mockReturnValue(hookProps);
+
+let el;
+describe('PageButtons component', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ el = shallow( );
+ });
+ describe('behavior', () => {
+ it('initializes component hooks', () => {
+ expect(usePageButtonsData).toHaveBeenCalled();
+ });
+ });
+ describe('render', () => {
+ test('snapshot', () => {
+ expect(el).toMatchSnapshot();
+ });
+ test('prev button', () => {
+ const button = el.find(Button).at(0);
+ expect(button.props().disabled).toEqual(hookProps.prev.disabled);
+ expect(button.props().onClick).toEqual(hookProps.prev.onClick);
+ expect(button.text()).toEqual(hookProps.prev.text);
+ });
+ test('next button', () => {
+ const button = el.find(Button).at(1);
+ expect(button.props().disabled).toEqual(hookProps.next.disabled);
+ expect(button.props().onClick).toEqual(hookProps.next.onClick);
+ expect(button.text()).toEqual(hookProps.next.text);
+ });
+ });
+});
diff --git a/src/components/GradesView/ScoreViewInput.jsx b/src/components/GradesView/ScoreViewInput.jsx
deleted file mode 100644
index 6759573b..00000000
--- a/src/components/GradesView/ScoreViewInput.jsx
+++ /dev/null
@@ -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';
-
-/**
- *
- * redux-connected select control for grade format (percent vs absolute)
- */
-export const ScoreViewInput = ({ format, intl, toggleFormat }) => (
-
- :
-
- {intl.formatMessage(messages.percent)}
- {intl.formatMessage(messages.absolute)}
-
-
-);
-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));
diff --git a/src/components/GradesView/ScoreViewInput.test.jsx b/src/components/GradesView/ScoreViewInput.test.jsx
deleted file mode 100644
index 1c734189..00000000
--- a/src/components/GradesView/ScoreViewInput.test.jsx
+++ /dev/null
@@ -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( );
- });
- 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);
- });
- });
-});
diff --git a/src/components/GradesView/ScoreViewInput/__snapshots__/index.test.jsx.snap b/src/components/GradesView/ScoreViewInput/__snapshots__/index.test.jsx.snap
new file mode 100644
index 00000000..d687f4cb
--- /dev/null
+++ b/src/components/GradesView/ScoreViewInput/__snapshots__/index.test.jsx.snap
@@ -0,0 +1,28 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ScoreViewInput component render snapshot 1`] = `
+
+
+ Score View
+ :
+
+
+
+ Percent
+
+
+ Absolute
+
+
+
+`;
diff --git a/src/components/GradesView/ScoreViewInput/index.jsx b/src/components/GradesView/ScoreViewInput/index.jsx
new file mode 100644
index 00000000..09519c55
--- /dev/null
+++ b/src/components/GradesView/ScoreViewInput/index.jsx
@@ -0,0 +1,33 @@
+import React from 'react';
+
+import { Form } from '@edx/paragon';
+import { useIntl } from '@edx/frontend-platform/i18n';
+
+import { actions, selectors } from 'data/redux/hooks';
+import messages from './messages';
+
+/**
+ *
+ * redux-connected select control for grade format (percent vs absolute)
+ */
+export const ScoreViewInput = () => {
+ const { formatMessage } = useIntl();
+ const { gradeFormat } = selectors.grades.useGradeData();
+ const toggleFormat = actions.grades.useToggleGradeFormat();
+ return (
+
+ {formatMessage(messages.scoreView)}:
+
+ {formatMessage(messages.percent)}
+ {formatMessage(messages.absolute)}
+
+
+ );
+};
+ScoreViewInput.propTypes = {};
+
+export default ScoreViewInput;
diff --git a/src/components/GradesView/ScoreViewInput/index.test.jsx b/src/components/GradesView/ScoreViewInput/index.test.jsx
new file mode 100644
index 00000000..bb2462f3
--- /dev/null
+++ b/src/components/GradesView/ScoreViewInput/index.test.jsx
@@ -0,0 +1,67 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+
+import { useIntl } from '@edx/frontend-platform/i18n';
+import { GradeFormats } from 'data/constants/grades';
+
+import { formatMessage } from 'testUtils';
+import { actions, selectors } from 'data/redux/hooks';
+import ScoreViewInput from '.';
+import messages from './messages';
+
+jest.mock('data/redux/hooks', () => ({
+ actions: {
+ grades: { useToggleGradeFormat: jest.fn() },
+ },
+ selectors: {
+ grades: { useGradeData: jest.fn() },
+ },
+}));
+
+const toggleGradeFormat = jest.fn().mockName('hooks.toggleGradeFormat');
+actions.grades.useToggleGradeFormat.mockReturnValue(toggleGradeFormat);
+const gradeFormat = 'test-grade-format';
+selectors.grades.useGradeData.mockReturnValue({ gradeFormat });
+
+let el;
+describe('ScoreViewInput component', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ el = shallow( );
+ });
+ describe('behavior', () => {
+ it('initializes intl hook', () => {
+ expect(useIntl).toHaveBeenCalled();
+ });
+ it('initializes redux hooks', () => {
+ expect(actions.grades.useToggleGradeFormat).toHaveBeenCalled();
+ expect(selectors.grades.useGradeData).toHaveBeenCalled();
+ });
+ });
+ describe('render', () => {
+ test('snapshot', () => {
+ expect(el).toMatchSnapshot();
+ });
+ test('label', () => {
+ const label = el.children().at(0);
+ expect(label.text()).toEqual(`${formatMessage(messages.scoreView)}:`);
+ });
+ describe('form control', () => {
+ let control;
+ beforeEach(() => {
+ control = el.children().at(1);
+ });
+ test('value and onChange from redux hooks', () => {
+ expect(control.props().value).toEqual(gradeFormat);
+ expect(control.props().onChange).toEqual(toggleGradeFormat);
+ });
+ test('absolute and percent options', () => {
+ const children = control.children();
+ expect(children.at(0).props().value).toEqual(GradeFormats.percent);
+ expect(children.at(0).text()).toEqual(formatMessage(messages.percent));
+ expect(children.at(1).props().value).toEqual(GradeFormats.absolute);
+ expect(children.at(1).text()).toEqual(formatMessage(messages.absolute));
+ });
+ });
+ });
+});
diff --git a/src/components/GradesView/ScoreViewInput.messages.js b/src/components/GradesView/ScoreViewInput/messages.js
similarity index 100%
rename from src/components/GradesView/ScoreViewInput.messages.js
rename to src/components/GradesView/ScoreViewInput/messages.js
diff --git a/src/components/GradesView/SearchControls.jsx b/src/components/GradesView/SearchControls.jsx
deleted file mode 100644
index 08124fee..00000000
--- a/src/components/GradesView/SearchControls.jsx
+++ /dev/null
@@ -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 (
-
- }
- onBlur={this.onBlur}
- onClear={this.onClear}
- value={this.props.searchValue}
- />
-
-
-
-
- );
- }
-}
-
-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);
diff --git a/src/components/GradesView/SearchControls.test.jsx b/src/components/GradesView/SearchControls.test.jsx
deleted file mode 100644
index b181d915..00000000
--- a/src/components/GradesView/SearchControls.test.jsx
+++ /dev/null
@@ -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( );
- };
-
- 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);
- });
- });
- });
-});
diff --git a/src/components/GradesView/SearchControls/__snapshots__/index.test.jsx.snap b/src/components/GradesView/SearchControls/__snapshots__/index.test.jsx.snap
new file mode 100644
index 00000000..d9f5c94e
--- /dev/null
+++ b/src/components/GradesView/SearchControls/__snapshots__/index.test.jsx.snap
@@ -0,0 +1,18 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`SearchControls component render snapshot 1`] = `
+
+
+
+ test-hint-text
+
+
+`;
diff --git a/src/components/GradesView/SearchControls/hooks.js b/src/components/GradesView/SearchControls/hooks.js
new file mode 100644
index 00000000..8ce49734
--- /dev/null
+++ b/src/components/GradesView/SearchControls/hooks.js
@@ -0,0 +1,41 @@
+import { useIntl } from '@edx/frontend-platform/i18n';
+
+import { actions, selectors, thunkActions } from 'data/redux/hooks';
+
+import messages from './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 useSearchControlsData = () => {
+ const { formatMessage } = useIntl();
+ const searchValue = selectors.app.useSearchValue();
+ const fetchGrades = thunkActions.grades.useFetchGrades();
+ const setSearchValue = actions.app.useSetSearchValue();
+
+ const onBlur = (e) => {
+ setSearchValue(e.target.value);
+ };
+
+ const onClear = () => {
+ setSearchValue('');
+ fetchGrades();
+ };
+
+ const onSubmit = (newValue) => {
+ setSearchValue(newValue);
+ fetchGrades();
+ };
+
+ return {
+ onSubmit,
+ onBlur,
+ onClear,
+ searchValue,
+ inputLabel: formatMessage(messages.label),
+ hintText: formatMessage(messages.hint),
+ };
+};
+
+export default useSearchControlsData;
diff --git a/src/components/GradesView/SearchControls/hooks.test.js b/src/components/GradesView/SearchControls/hooks.test.js
new file mode 100644
index 00000000..db0c291e
--- /dev/null
+++ b/src/components/GradesView/SearchControls/hooks.test.js
@@ -0,0 +1,71 @@
+import { useIntl } from '@edx/frontend-platform/i18n';
+
+import { formatMessage } from 'testUtils';
+import { actions, selectors, thunkActions } from 'data/redux/hooks';
+
+import useSearchControlsData from './hooks';
+import messages from './messages';
+
+jest.mock('data/redux/hooks', () => ({
+ actions: {
+ app: { useSetSearchValue: jest.fn() },
+ },
+ selectors: {
+ app: { useSearchValue: jest.fn() },
+ },
+ thunkActions: {
+ grades: { useFetchGrades: jest.fn() },
+ },
+}));
+
+const searchValue = 'test-search-value';
+selectors.app.useSearchValue.mockReturnValue(searchValue);
+const setSearchValue = jest.fn();
+actions.app.useSetSearchValue.mockReturnValue(setSearchValue);
+const fetchGrades = jest.fn();
+thunkActions.grades.useFetchGrades.mockReturnValue(fetchGrades);
+
+const testValue = 'test-value';
+let out;
+describe('useSearchControlsData', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ out = useSearchControlsData();
+ });
+ describe('behavior', () => {
+ it('initializes intl hook', () => {
+ expect(useIntl).toHaveBeenCalled();
+ });
+ it('initializes redux hooks', () => {
+ expect(actions.app.useSetSearchValue).toHaveBeenCalled();
+ expect(selectors.app.useSearchValue).toHaveBeenCalled();
+ expect(thunkActions.grades.useFetchGrades).toHaveBeenCalled();
+ });
+ });
+ describe('output', () => {
+ test('onSubmit sets search value and fetches grades', () => {
+ out.onSubmit(testValue);
+ expect(setSearchValue).toHaveBeenCalledWith(testValue);
+ expect(fetchGrades).toHaveBeenCalled();
+ });
+ test('onBlur sets search value to event target', () => {
+ out.onBlur({ target: { value: testValue } });
+ expect(setSearchValue).toHaveBeenCalledWith(testValue);
+ expect(fetchGrades).not.toHaveBeenCalled();
+ });
+ test('onClear clears search value and fetches grades', () => {
+ out.onClear();
+ expect(setSearchValue).toHaveBeenCalledWith('');
+ expect(fetchGrades).toHaveBeenCalled();
+ });
+ it('forwards searchValue from redux', () => {
+ expect(out.searchValue).toEqual(searchValue);
+ });
+ test('input label message', () => {
+ expect(out.inputLabel).toEqual(formatMessage(messages.label));
+ });
+ test('hint text message', () => {
+ expect(out.hintText).toEqual(formatMessage(messages.hint));
+ });
+ });
+});
diff --git a/src/components/GradesView/SearchControls/index.jsx b/src/components/GradesView/SearchControls/index.jsx
new file mode 100644
index 00000000..17f418b7
--- /dev/null
+++ b/src/components/GradesView/SearchControls/index.jsx
@@ -0,0 +1,38 @@
+import React from 'react';
+
+import { SearchField } from '@edx/paragon';
+import useSearchControlsData from './hooks';
+
+/**
+ * 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 SearchControls = () => {
+ const {
+ onSubmit,
+ onBlur,
+ onClear,
+ searchValue,
+ inputLabel,
+ hintText,
+ } = useSearchControlsData();
+
+ return (
+
+
+
+ {hintText}
+
+
+ );
+};
+
+SearchControls.propTypes = {};
+
+export default SearchControls;
diff --git a/src/components/GradesView/SearchControls/index.test.jsx b/src/components/GradesView/SearchControls/index.test.jsx
new file mode 100644
index 00000000..ae9a1020
--- /dev/null
+++ b/src/components/GradesView/SearchControls/index.test.jsx
@@ -0,0 +1,48 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+
+import { SearchField } from '@edx/paragon';
+
+import useSearchControlsData from './hooks';
+import SearchControls from '.';
+
+jest.mock('./hooks', () => jest.fn());
+
+const hookProps = {
+ onSubmit: jest.fn().mockName('hooks.onSubmit'),
+ onBlur: jest.fn().mockName('hooks.onBlur'),
+ onClear: jest.fn().mockName('hooks.onClear'),
+ searchValue: 'test-search-value',
+ inputLabel: 'test-input-label',
+ hintText: 'test-hint-text',
+};
+useSearchControlsData.mockReturnValue(hookProps);
+
+let el;
+describe('SearchControls component', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ el = shallow( );
+ });
+ describe('behavior', () => {
+ it('initializes component hooks', () => {
+ expect(useSearchControlsData).toHaveBeenCalled();
+ });
+ });
+ describe('render', () => {
+ test('snapshot', () => {
+ expect(el).toMatchSnapshot();
+ });
+ test('search field', () => {
+ const props = el.find(SearchField).props();
+ expect(props.onSubmit).toEqual(hookProps.onSubmit);
+ expect(props.onBlur).toEqual(hookProps.onBlur);
+ expect(props.onClear).toEqual(hookProps.onClear);
+ expect(props.inputLabel).toEqual(hookProps.inputLabel);
+ expect(props.value).toEqual(hookProps.searchValue);
+ });
+ test('hint text', () => {
+ expect(el.find('small').text()).toEqual(hookProps.hintText);
+ });
+ });
+});
diff --git a/src/components/GradesView/SearchControls.messages.js b/src/components/GradesView/SearchControls/messages.js
similarity index 100%
rename from src/components/GradesView/SearchControls.messages.js
rename to src/components/GradesView/SearchControls/messages.js
diff --git a/src/components/GradesView/SpinnerIcon.jsx b/src/components/GradesView/SpinnerIcon.jsx
index 398201c6..da044d63 100644
--- a/src/components/GradesView/SpinnerIcon.jsx
+++ b/src/components/GradesView/SpinnerIcon.jsx
@@ -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';
/**
*
* Simmple redux-connected icon component that shows a spinner overlay only if
* redux state says it should.
*/
-export const SpinnerIcon = ({ show }) => show && (
-
-
-
-);
-SpinnerIcon.defaultProps = {
- show: false,
+export const SpinnerIcon = () => {
+ const show = selectors.root.useShouldShowSpinner();
+ return show && (
+
+
+
+ );
};
-SpinnerIcon.propTypes = {
- show: PropTypes.bool,
-};
-
-export const mapStateToProps = (state) => ({
- show: selectors.root.shouldShowSpinner(state),
-});
+SpinnerIcon.propTypes = {};
-export default connect(mapStateToProps)(SpinnerIcon);
+export default SpinnerIcon;
diff --git a/src/components/GradesView/SpinnerIcon.test.jsx b/src/components/GradesView/SpinnerIcon.test.jsx
index 0ffd0886..031cf1ba 100644
--- a/src/components/GradesView/SpinnerIcon.test.jsx
+++ b/src/components/GradesView/SpinnerIcon.test.jsx
@@ -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', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ el = shallow( );
+ });
+ describe('behavior', () => {
+ it('initializes redux hook', () => {
+ expect(selectors.root.useShouldShowSpinner).toHaveBeenCalled();
+ });
+ });
describe('component', () => {
- it('snapshot - does not render if show: false', () => {
- expect(shallow( )).toMatchSnapshot();
+ it('does not render if show: false', () => {
+ selectors.root.useShouldShowSpinner.mockReturnValueOnce(false);
+ el = shallow( );
+ expect(el.isEmptyRender()).toEqual(true);
});
test('snapshot - displays spinner overlay with spinner icon', () => {
- expect(shallow( )).toMatchSnapshot();
- });
- });
- describe('mapStateToProps', () => {
- const testState = { a: 'nice', day: 'for', some: 'sun' };
- test('show from root.shouldShowSpinner', () => {
- expect(mapStateToProps(testState).show).toEqual(selectors.root.shouldShowSpinner(testState));
+ expect(el).toMatchSnapshot();
});
});
});
diff --git a/src/components/GradesView/StatusAlerts.jsx b/src/components/GradesView/StatusAlerts.jsx
deleted file mode 100644
index 74f60fb3..00000000
--- a/src/components/GradesView/StatusAlerts.jsx
+++ /dev/null
@@ -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)
- ? ''
- : ;
- }
-
- get maxValidityMessage() {
- return (this.props.limitValidity.isMaxValid)
- ? ''
- : ;
- }
-
- get courseGradeFilterAlertDialogText() {
- return (
- <>
- {this.minValidityMessage}{this.maxValidityMessage}
- >
- );
- }
-
- render() {
- return (
- <>
-
-
-
-
- {this.courseGradeFilterAlertDialogText}
-
- >
- );
- }
-}
-
-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);
diff --git a/src/components/GradesView/StatusAlerts.test.jsx b/src/components/GradesView/StatusAlerts.test.jsx
deleted file mode 100644
index ba2d7997..00000000
--- a/src/components/GradesView/StatusAlerts.test.jsx
+++ /dev/null
@@ -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( );
- 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( );
- expect(
- el.instance().isCourseGradeFilterAlertOpen,
- ).toEqual(
- !isMinValid || !isMaxValid,
- );
- if (!isMaxValid) {
- if (!isMinValid) {
- expect(el.instance().courseGradeFilterAlertDialogText).toEqual(
- <>
-
-
- >,
- );
- } else {
- expect(
- el.instance().courseGradeFilterAlertDialogText,
- // eslint-disable-next-line react/jsx-curly-brace-presence
- ).toEqual(<>{''} >);
- }
- } else if (!isMinValid) {
- expect(
- el.instance().courseGradeFilterAlertDialogText,
- // eslint-disable-next-line react/jsx-curly-brace-presence
- ).toEqual(<> {''}>);
- }
- });
- });
-
- 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);
- });
- });
-});
diff --git a/src/components/GradesView/StatusAlerts/__snapshots__/index.test.jsx.snap b/src/components/GradesView/StatusAlerts/__snapshots__/index.test.jsx.snap
new file mode 100644
index 00000000..a842973d
--- /dev/null
+++ b/src/components/GradesView/StatusAlerts/__snapshots__/index.test.jsx.snap
@@ -0,0 +1,20 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`StatusAlerts component render snapshot 1`] = `
+
+
+ hooks.success-banner-text
+
+
+ hooks.grade-filter-text
+
+
+`;
diff --git a/src/components/GradesView/StatusAlerts/hooks.js b/src/components/GradesView/StatusAlerts/hooks.js
new file mode 100644
index 00000000..efcd8a56
--- /dev/null
+++ b/src/components/GradesView/StatusAlerts/hooks.js
@@ -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;
diff --git a/src/components/GradesView/StatusAlerts/hooks.test.js b/src/components/GradesView/StatusAlerts/hooks.test.js
new file mode 100644
index 00000000..4b0e11f6
--- /dev/null
+++ b/src/components/GradesView/StatusAlerts/hooks.test.js
@@ -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)}`,
+ );
+ });
+ });
+ });
+ });
+});
diff --git a/src/components/GradesView/StatusAlerts/index.jsx b/src/components/GradesView/StatusAlerts/index.jsx
new file mode 100644
index 00000000..f7883ff9
--- /dev/null
+++ b/src/components/GradesView/StatusAlerts/index.jsx
@@ -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 (
+ <>
+
+ {successBanner.text}
+
+
+ {gradeFilter.text}
+
+ >
+ );
+};
+
+StatusAlerts.propTypes = {};
+
+export default StatusAlerts;
diff --git a/src/components/GradesView/StatusAlerts/index.test.jsx b/src/components/GradesView/StatusAlerts/index.test.jsx
new file mode 100644
index 00000000..408044d5
--- /dev/null
+++ b/src/components/GradesView/StatusAlerts/index.test.jsx
@@ -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( );
+ });
+ 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);
+ });
+ });
+});
diff --git a/src/components/GradesView/StatusAlerts.messages.js b/src/components/GradesView/StatusAlerts/messages.js
similarity index 100%
rename from src/components/GradesView/StatusAlerts.messages.js
rename to src/components/GradesView/StatusAlerts/messages.js
diff --git a/src/components/GradesView/UsersLabel.jsx b/src/components/GradesView/UsersLabel.jsx
deleted file mode 100644
index ac6df0fb..00000000
--- a/src/components/GradesView/UsersLabel.jsx
+++ /dev/null
@@ -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';
-
-/**
- *
- * Simple label component displaying the filtered and total users shown
- */
-export const UsersLabel = ({
- filteredUsersCount,
- totalUsersCount,
-}) => {
- if (!totalUsersCount) {
- return null;
- }
- const bold = (val) => ({val} );
- return (
-
- );
-};
-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);
diff --git a/src/components/GradesView/UsersLabel.test.jsx b/src/components/GradesView/UsersLabel.test.jsx
deleted file mode 100644
index 43a43359..00000000
--- a/src/components/GradesView/UsersLabel.test.jsx
+++ /dev/null
@@ -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( )).toEqual({});
- });
- test('snapshot - displays label with number of filtered users out of total', () => {
- expect(shallow( )).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));
- });
- });
-});
diff --git a/src/components/GradesView/__snapshots__/FilterMenuToggle.test.jsx.snap b/src/components/GradesView/__snapshots__/FilterMenuToggle.test.jsx.snap
deleted file mode 100644
index 9b66b380..00000000
--- a/src/components/GradesView/__snapshots__/FilterMenuToggle.test.jsx.snap
+++ /dev/null
@@ -1,19 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`FilterMenuToggle component snapshots basic snapshot 1`] = `
-
-
-
-
-
-`;
diff --git a/src/components/GradesView/__snapshots__/FilteredUsersLabel.test.jsx.snap b/src/components/GradesView/__snapshots__/FilteredUsersLabel.test.jsx.snap
deleted file mode 100644
index 2086de89..00000000
--- a/src/components/GradesView/__snapshots__/FilteredUsersLabel.test.jsx.snap
+++ /dev/null
@@ -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`] = `
-
- 23
- ,
- "totalUsers":
- 140
- ,
- }
- }
-/>
-`;
diff --git a/src/components/GradesView/__snapshots__/ImportSuccessToast.test.jsx.snap b/src/components/GradesView/__snapshots__/ImportSuccessToast.test.jsx.snap
deleted file mode 100644
index b42d9940..00000000
--- a/src/components/GradesView/__snapshots__/ImportSuccessToast.test.jsx.snap
+++ /dev/null
@@ -1,16 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`ImportSuccessToast component snapshots snapshot 1`] = `
-
- Import Successful! Grades will be updated momentarily.
-
-`;
diff --git a/src/components/GradesView/__snapshots__/InterventionsReport.test.jsx.snap b/src/components/GradesView/__snapshots__/InterventionsReport.test.jsx.snap
deleted file mode 100644
index 44210b18..00000000
--- a/src/components/GradesView/__snapshots__/InterventionsReport.test.jsx.snap
+++ /dev/null
@@ -1,38 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`InterventionsReport component snapshots snapshot 1`] = `
-
-`;
diff --git a/src/components/GradesView/__snapshots__/ScoreViewInput.test.jsx.snap b/src/components/GradesView/__snapshots__/ScoreViewInput.test.jsx.snap
deleted file mode 100644
index 44b805ac..00000000
--- a/src/components/GradesView/__snapshots__/ScoreViewInput.test.jsx.snap
+++ /dev/null
@@ -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`] = `
-
-
-
- :
-
-
-
- Percent
-
-
- Absolute
-
-
-
-`;
diff --git a/src/components/GradesView/__snapshots__/SearchControls.test.jsx.snap b/src/components/GradesView/__snapshots__/SearchControls.test.jsx.snap
deleted file mode 100644
index 43e3bb74..00000000
--- a/src/components/GradesView/__snapshots__/SearchControls.test.jsx.snap
+++ /dev/null
@@ -1,28 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`SearchControls Component Snapshots basic snapshot 1`] = `
-
-
- }
- onBlur={[MockFunction onBlur]}
- onClear={[MockFunction onClear]}
- onSubmit={[MockFunction onSubmit]}
- value="alice"
- />
-
-
-
-
-`;
diff --git a/src/components/GradesView/__snapshots__/SpinnerIcon.test.jsx.snap b/src/components/GradesView/__snapshots__/SpinnerIcon.test.jsx.snap
index 6a99def2..c530fb32 100644
--- a/src/components/GradesView/__snapshots__/SpinnerIcon.test.jsx.snap
+++ b/src/components/GradesView/__snapshots__/SpinnerIcon.test.jsx.snap
@@ -9,5 +9,3 @@ exports[`SpinnerIcon component snapshot - displays spinner overlay with spinner
/>
`;
-
-exports[`SpinnerIcon component snapshot - does not render if show: false 1`] = `""`;
diff --git a/src/components/GradesView/__snapshots__/StatusAlerts.test.jsx.snap b/src/components/GradesView/__snapshots__/StatusAlerts.test.jsx.snap
deleted file mode 100644
index 7edc860a..00000000
--- a/src/components/GradesView/__snapshots__/StatusAlerts.test.jsx.snap
+++ /dev/null
@@ -1,24 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`StatusAlerts snapshots basic snapshot 1`] = `
-
-
-
-
-
- the quiCk brown does somEthing or other
-
-
-`;
diff --git a/src/components/GradesView/__snapshots__/UsersLabel.test.jsx.snap b/src/components/GradesView/__snapshots__/UsersLabel.test.jsx.snap
deleted file mode 100644
index 92631592..00000000
--- a/src/components/GradesView/__snapshots__/UsersLabel.test.jsx.snap
+++ /dev/null
@@ -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`] = `
-
- 23
- ,
- "totalUsers":
- 140
- ,
- }
- }
-/>
-`;
diff --git a/src/components/GradesView/__snapshots__/index.test.jsx.snap b/src/components/GradesView/__snapshots__/index.test.jsx.snap
new file mode 100644
index 00000000..cc927fae
--- /dev/null
+++ b/src/components/GradesView/__snapshots__/index.test.jsx.snap
@@ -0,0 +1,41 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`GradesView component render snapshot 1`] = `
+
+
+
+
+ filter-step-heading
+
+
+
+
+
+
+
+
+ gradebook-step-heading
+
+
+
+
+
+
+
+
+
+ *
+ test-masters-hint
+
+
+
+
+`;
diff --git a/src/components/GradesView/__snapshots__/test.jsx.snap b/src/components/GradesView/__snapshots__/test.jsx.snap
deleted file mode 100644
index d208f4bd..00000000
--- a/src/components/GradesView/__snapshots__/test.jsx.snap
+++ /dev/null
@@ -1,53 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`GradesView Component snapshots basic snapshot 1`] = `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- *
-
-
-
-
-
-`;
diff --git a/src/components/GradesView/hooks.js b/src/components/GradesView/hooks.js
new file mode 100644
index 00000000..b8f65048
--- /dev/null
+++ b/src/components/GradesView/hooks.js
@@ -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;
diff --git a/src/components/GradesView/hooks.test.js b/src/components/GradesView/hooks.test.js
new file mode 100644
index 00000000..7e444e0b
--- /dev/null
+++ b/src/components/GradesView/hooks.test.js
@@ -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();
+ });
+ });
+ });
+});
diff --git a/src/components/GradesView/index.jsx b/src/components/GradesView/index.jsx
index 47373b13..5c4ddee7 100644
--- a/src/components/GradesView/index.jsx
+++ b/src/components/GradesView/index.jsx
@@ -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 (
- <>
-
+ return (
+ <>
+
-
-
-
-
+
+
+ {stepHeadings.filter}
+
-
-
-
-
+
+
+
+
-
-
+
+
-
+ {stepHeadings.gradebook}
-
-
-
-
+
+
+
+
-
+
-
+
-
- *
-
+
+ * {mastersHint}
+
-
- >
- );
- }
-}
-
-GradesView.defaultProps = {};
+
+ >
+ );
+};
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;
diff --git a/src/components/GradesView/index.test.jsx b/src/components/GradesView/index.test.jsx
new file mode 100644
index 00000000..22790717
--- /dev/null
+++ b/src/components/GradesView/index.test.jsx
@@ -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( );
+ });
+ 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,
+ );
+ });
+ });
+});
diff --git a/src/components/GradesView/test.jsx b/src/components/GradesView/test.jsx
deleted file mode 100644
index 847e989e..00000000
--- a/src/components/GradesView/test.jsx
+++ /dev/null
@@ -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( );
- });
- 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( );
- 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,
- );
- });
- });
- });
-});
diff --git a/src/data/redux/hooks/actions.js b/src/data/redux/hooks/actions.js
index bfe11490..526971d8 100644
--- a/src/data/redux/hooks/actions.js
+++ b/src/data/redux/hooks/actions.js
@@ -4,6 +4,11 @@ import { actionHook } from './utils';
const app = StrictDict({
useSetLocalFilter: actionHook(actions.app.setLocalFilter),
+ useSetSearchValue: actionHook(actions.app.setSearchValue),
+ useSetShowImportSuccessToast: actionHook(actions.app.setShowImportSuccessToast),
+ useSetView: actionHook(actions.app.setView),
+ useCloseModal: actionHook(actions.app.closeModal),
+ useSetModalState: actionHook(actions.app.setModalState),
});
const filters = StrictDict({
@@ -14,9 +19,19 @@ const filters = StrictDict({
useUpdateCourseGradeLimits: actionHook(actions.filters.update.courseGradeLimits),
useUpdateIncludeCourseRoleMembers: actionHook(actions.filters.update.includeCourseRoleMembers),
useUpdateTrack: actionHook(actions.filters.update.track),
+ useResetFilters: actionHook(actions.filters.reset),
+});
+
+const grades = StrictDict({
+ useDoneViewingAssignment: actionHook(actions.grades.doneViewingAssignment),
+ useDownloadBulkGradesReport: actionHook(actions.grades.downloadReport.bulkGrades),
+ useDownloadInterventionReport: actionHook(actions.grades.downloadReport.intervention),
+ useToggleGradeFormat: actionHook(actions.grades.toggleGradeFormat),
+ useCloseBanner: actionHook(actions.grades.banner.close),
});
export default StrictDict({
app,
filters,
+ grades,
});
diff --git a/src/data/redux/hooks/actions.test.js b/src/data/redux/hooks/actions.test.js
index 95cf0157..9f7de29e 100644
--- a/src/data/redux/hooks/actions.test.js
+++ b/src/data/redux/hooks/actions.test.js
@@ -4,17 +4,6 @@ import actions from 'data/actions';
import { actionHook } from './utils';
import actionHooks from './actions';
-jest.mock('data/actions', () => ({
- app: {
- setLocalFilter: jest.fn(),
- },
- filters: {
- update: {
- assignment: jest.fn(),
- assignmentLimits: jest.fn(),
- },
- },
-}));
jest.mock('./utils', () => ({
actionHook: (action) => ({ actionHook: action }),
}));
@@ -24,6 +13,7 @@ let hooks;
const testActionHook = (hookKey, action) => {
test(hookKey, () => {
expect(hooks[hookKey]).toEqual(actionHook(action));
+ expect(hooks[hookKey]).not.toEqual(undefined);
});
};
@@ -32,6 +22,11 @@ describe('action hooks', () => {
const hookKeys = keyStore(actionHooks.app);
beforeEach(() => { hooks = actionHooks.app; });
testActionHook(hookKeys.useSetLocalFilter, actions.app.setLocalFilter);
+ testActionHook(hookKeys.useSetSearchValue, actions.app.setSearchValue);
+ testActionHook(hookKeys.useSetShowImportSuccessToast, actions.app.setShowImportSuccessToast);
+ testActionHook(hookKeys.useSetView, actions.app.setView);
+ testActionHook(hookKeys.useCloseModal, actions.app.closeModal);
+ testActionHook(hookKeys.useSetModalState, actions.app.setModalState);
});
describe('filters', () => {
const hookKeys = keyStore(actionHooks.filters);
@@ -39,12 +34,23 @@ describe('action hooks', () => {
beforeEach(() => { hooks = actionHooks.filters; });
testActionHook(hookKeys.useUpdateAssignment, actionGroup.assignment);
testActionHook(hookKeys.useUpdateAssignmentLimits, actionGroup.assignmentLimits);
- testActionHook(hookKeys.useUpdateCohort, actionGroup.updateCohort);
+ testActionHook(hookKeys.useUpdateAssignmentType, actionGroup.assignmentType);
+ testActionHook(hookKeys.useUpdateCohort, actionGroup.cohort);
testActionHook(hookKeys.useUpdateCourseGradeLimits, actionGroup.courseGradeLimits);
testActionHook(
hookKeys.useUpdateIncludeCourseRoleMembers,
- actionGroup.updateIncludeCourseRoleMembers,
+ actionGroup.includeCourseRoleMembers,
);
- testActionHook(hookKeys.useUpdateTrack, actionGroup.updateTrack);
+ testActionHook(hookKeys.useResetFilters, actions.filters.reset);
+ });
+ describe('grades', () => {
+ const hookKeys = keyStore(actionHooks.grades);
+ const actionGroup = actions.grades;
+ beforeEach(() => { hooks = actionHooks.grades; });
+ testActionHook(hookKeys.useDoneViewingAssignment, actionGroup.doneViewingAssignment);
+ testActionHook(hookKeys.useDownloadBulkGradesReport, actionGroup.downloadReport.bulkGrades);
+ testActionHook(hookKeys.useDownloadInterventionReport, actionGroup.downloadReport.intervention);
+ testActionHook(hookKeys.useToggleGradeFormat, actionGroup.toggleGradeFormat);
+ testActionHook(hookKeys.useCloseBanner, actionGroup.banner.close);
});
});
diff --git a/src/data/redux/hooks/selectors.js b/src/data/redux/hooks/selectors.js
index 4b865a65..6f27ab0c 100644
--- a/src/data/redux/hooks/selectors.js
+++ b/src/data/redux/hooks/selectors.js
@@ -3,40 +3,75 @@ import { useSelector } from 'react-redux';
import { StrictDict } from 'utils';
import selectors from 'data/selectors';
+const selectorHook = (selector) => () => useSelector(selector);
+
export const root = StrictDict({
- useGradeExportUrl: () => useSelector(selectors.root.gradeExportUrl),
- useSelectedCohortEntry: () => useSelector(selectors.root.selectedCohortEntry),
- useSelectedTrackEntry: () => useSelector(selectors.root.selectedTrackEntry),
+ useEditModalPossibleGrade: selectorHook(selectors.root.editModalPossibleGrade),
+ useGetHeadings: selectorHook(selectors.root.getHeadings),
+ useGradeExportUrl: selectorHook(selectors.root.gradeExportUrl),
+ useInterventionExportUrl: selectorHook(selectors.root.interventionExportUrl),
+ useSelectedCohortEntry: selectorHook(selectors.root.selectedCohortEntry),
+ useSelectedTrackEntry: selectorHook(selectors.root.selectedTrackEntry),
+ useShouldShowSpinner: selectorHook(selectors.root.shouldShowSpinner),
+ useShowBulkManagement: selectorHook(selectors.root.showBulkManagement),
+ useFilterBadgeConfig: (filterName) => useSelector(
+ (state) => selectors.root.filterBadgeConfig(state, filterName),
+ ),
});
export const app = StrictDict({
- useAssignmentGradeLimits: () => useSelector(selectors.app.assignmentGradeLimits),
- useAreCourseGradeFiltersValid: () => useSelector(selectors.app.areCourseGradeFiltersValid),
- useCourseGradeLimits: () => useSelector(selectors.app.courseGradeLimits),
+ useActiveView: selectorHook(selectors.app.activeView),
+ useAssignmentGradeLimits: selectorHook(selectors.app.assignmentGradeLimits),
+ useAreCourseGradeFiltersValid: selectorHook(selectors.app.areCourseGradeFiltersValid),
+ useCourseGradeLimits: selectorHook(selectors.app.courseGradeLimits),
+ useCourseGradeFilterValidity: selectorHook(selectors.app.courseGradeFilterValidity),
+ useCourseId: selectorHook(selectors.app.courseId),
+ useModalData: selectorHook(selectors.app.modalData),
+ useSearchValue: selectorHook(selectors.app.searchValue),
+ useShowImportSuccessToast: selectorHook(selectors.app.showImportSuccessToast),
});
export const assignmentTypes = StrictDict({
- useAllAssignmentTypes: () => useSelector(selectors.assignmentTypes.allAssignmentTypes),
+ useAllAssignmentTypes: selectorHook(selectors.assignmentTypes.allAssignmentTypes),
+ useAreGradesFrozen: selectorHook(selectors.assignmentTypes.areGradesFrozen),
});
export const cohorts = StrictDict({
- useAllCohorts: () => useSelector(selectors.cohorts.allCohorts),
+ useAllCohorts: selectorHook(selectors.cohorts.allCohorts),
// maybe not needed?
- useCohortsByName: () => useSelector(selectors.cohorts.cohortsByName),
+ useCohortsByName: selectorHook(selectors.cohorts.cohortsByName),
});
export const filters = StrictDict({
- useData: () => useSelector(selectors.filters.allFilters),
- useIncludeCourseRoleMembers: () => useSelector(selectors.filters.includeCourseRoleMembers),
- useSelectableAssignmentLabels: () => useSelector(selectors.filters.selectableAssignmentLabels),
- useSelectedAssignmentLabel: () => useSelector(selectors.filters.selectedAssignmentLabel),
- useAssignmentType: () => useSelector(selectors.filters.assignmentType),
+ useData: selectorHook(selectors.filters.allFilters),
+ useIncludeCourseRoleMembers: selectorHook(selectors.filters.includeCourseRoleMembers),
+ useSelectableAssignmentLabels: selectorHook(selectors.filters.selectableAssignmentLabels),
+ useSelectedAssignmentLabel: selectorHook(selectors.filters.selectedAssignmentLabel),
+ useAssignmentType: selectorHook(selectors.filters.assignmentType),
+});
+
+export const grades = StrictDict({
+ useAllGrades: selectorHook(selectors.grades.allGrades),
+ useUserCounts: () => ({
+ filteredUsersCount: useSelector(selectors.grades.filteredUsersCount),
+ totalUsersCount: useSelector(selectors.grades.totalUsersCount),
+ }),
+ useGradeData: selectorHook(selectors.grades.gradeData),
+ useHasOverrideErrors: selectorHook(selectors.grades.hasOverrideErrors),
+ useShowSuccess: selectorHook(selectors.grades.showSuccess),
+ useSubsectionGrade: ({ gradeFormat, subsection }) => () => (
+ selectors.grades.subsectionGrade[gradeFormat](subsection)
+ ),
+});
+
+export const roles = StrictDict({
+ useCanUserViewGradebook: selectorHook(selectors.roles.canUserViewGradebook),
});
export const tracks = StrictDict({
- useAllTracks: () => useSelector(selectors.tracks.allTracks),
+ useAllTracks: selectorHook(selectors.tracks.allTracks),
// maybe not needed?
- useTracksByName: () => useSelector(selectors.tracks.tracksByName),
+ useTracksByName: selectorHook(selectors.tracks.tracksByName),
});
export default StrictDict({
@@ -44,6 +79,8 @@ export default StrictDict({
assignmentTypes,
cohorts,
filters,
+ grades,
+ roles,
tracks,
root,
});
diff --git a/src/data/redux/hooks/selectors.test.js b/src/data/redux/hooks/selectors.test.js
index c19ad9a3..ad74e30a 100644
--- a/src/data/redux/hooks/selectors.test.js
+++ b/src/data/redux/hooks/selectors.test.js
@@ -7,85 +7,102 @@ jest.mock('react-redux', () => ({
useSelector: (selector) => ({ useSelector: selector }),
}));
-jest.mock('data/selectors', () => ({
- app: {
- assignmentGradeLimits: jest.fn(),
- areCourseGradeFiltersValid: jest.fn(),
- courseGradelimits: jest.fn(),
- },
- assignmentTypes: { allAssignmentTypes: jest.fn() },
- cohorts: {
- allCohorts: jest.fn(),
- cohortsByName: jest.fn(),
- },
- filters: {
- allFilters: jest.fn(),
- includeCourseRoleMembers: jest.fn(),
- selectableAssignmentLabels: jest.fn(),
- selectedAssignmentLabel: jest.fn(),
- assignmentType: jest.fn(),
- },
- tracks: {
- allTracks: jest.fn(),
- tracksByName: jest.fn(),
- },
- root: {
- gradeExportUrl: jest.fn(),
- selectedCohortEntry: jest.fn(),
- selectedTrackEntry: jest.fn(),
- },
-}));
+const testValue = 'test-value';
+const testState = { test: 'state value' };
+let hookKeys;
let hooks;
-const testHook = (hookKey, selector) => {
+let selKeys;
+let selectorGroup;
+
+const loadSelectorGroup = (hookGroup, selGroup) => {
+ hookKeys = keyStore(hookGroup);
+ selKeys = keyStore(selGroup);
+ beforeEach(() => {
+ hooks = hookGroup;
+ selectorGroup = selGroup;
+ });
+};
+
+const testHook = (hookKey, selectorKey) => {
test(hookKey, () => {
- expect(hooks[hookKey]()).toEqual(useSelector(selector));
+ expect(hooks[hookKey]()).toEqual(useSelector(selectorGroup[selectorKey]));
});
};
+
describe('selector hooks', () => {
describe('root selectors', () => {
- const hookKeys = keyStore(selectorHooks.root);
- beforeEach(() => { hooks = selectorHooks.root; });
- testHook(hookKeys.useGradeExportUrl, selectors.root.gradeExportUrl);
- testHook(hookKeys.useSelectedCohortEntry, selectors.root.selectedCohortEntry);
- testHook(hookKeys.useSelectedTrackEntry, selectors.root.selectedTrackEntry);
+ loadSelectorGroup(selectorHooks.root, selectors.root);
+ testHook(hookKeys.useEditModalPossibleGrade, selKeys.editModalPossibleGrade);
+ testHook(hookKeys.useGetHeadings, selKeys.getHeadings);
+ testHook(hookKeys.useGradeExportUrl, selKeys.gradeExportUrl);
+ testHook(hookKeys.useInterventionExportUrl, selKeys.interventionExportUrl);
+ testHook(hookKeys.useSelectedCohortEntry, selKeys.selectedCohortEntry);
+ testHook(hookKeys.useSelectedTrackEntry, selKeys.selectedTrackEntry);
+ testHook(hookKeys.useShouldShowSpinner, selKeys.shouldShowSpinner);
+ testHook(hookKeys.useShowBulkManagement, selKeys.showBulkManagement);
+ describe(hookKeys.useFilterBadgeConfig, () => {
+ test('calls filterBadgeConfig selector with passed filterName', () => {
+ const filterBadgeConfig = (state, filterName) => ({
+ filterBadgeConfig: { state, filterName },
+ });
+ const rootKeys = keyStore(selectors.root);
+ jest.spyOn(selectors.root, rootKeys.filterBadgeConfig)
+ .mockImplementation(filterBadgeConfig);
+ const out = hooks.useFilterBadgeConfig(testValue);
+ expect(out.useSelector(testState)).toEqual(filterBadgeConfig(testState, testValue));
+ });
+ });
});
describe('app', () => {
- const hookKeys = keyStore(selectorHooks.app);
- const selGroup = selectors.app;
- beforeEach(() => { hooks = selectorHooks.app; });
- testHook(hookKeys.useAssignmentGradeLimits, selGroup.assignmentGradeLimits);
- testHook(hookKeys.useAreCourseGradeFiltersValid, selGroup.areCourseGradeFiltersValid);
- testHook(hookKeys.useCourseGradeLimits, selGroup.courseGradeLimits);
+ loadSelectorGroup(selectorHooks.app, selectors.app);
+ testHook(hookKeys.useActiveView, selKeys.activeView);
+ testHook(hookKeys.useAssignmentGradeLimits, selKeys.assignmentGradeLimits);
+ testHook(hookKeys.useAreCourseGradeFiltersValid, selKeys.areCourseGradeFiltersValid);
+ testHook(hookKeys.useCourseGradeLimits, selKeys.courseGradeLimits);
+ testHook(hookKeys.useCourseId, selKeys.courseId);
+ testHook(hookKeys.useModalData, selKeys.modalData);
+ testHook(hookKeys.useSearchValue, selKeys.searchValue);
+ testHook(hookKeys.useShowImportSuccessToast, selKeys.showImportSuccessToast);
});
describe('assignmentTypes', () => {
- const hookKeys = keyStore(selectorHooks.assignmentTypes);
- const selGroup = selectors.assignmentTypes;
- beforeEach(() => { hooks = selectorHooks.assignmentTypes; });
- testHook(hookKeys.useAllAssignmentTypes, selGroup.allAssignmentTypes);
+ loadSelectorGroup(selectorHooks.assignmentTypes, selectors.assignmentTypes);
+ testHook(hookKeys.useAllAssignmentTypes, selKeys.allAssignmentTypes);
+ testHook(hookKeys.useAreGradesFrozen, selKeys.areGradesFrozen);
});
describe('cohorts', () => {
- const hookKeys = keyStore(selectorHooks.cohorts);
- const selGroup = selectors.cohorts;
- beforeEach(() => { hooks = selectorHooks.cohorts; });
- testHook(hookKeys.useAllCohorts, selGroup.allCohorts);
- testHook(hookKeys.useCohortsByName, selGroup.cohortsByName);
+ loadSelectorGroup(selectorHooks.cohorts, selectors.cohorts);
+ testHook(hookKeys.useAllCohorts, selKeys.allCohorts);
+ testHook(hookKeys.useCohortsByName, selKeys.cohortsByName);
});
describe('filters', () => {
- const hookKeys = keyStore(selectorHooks.filters);
- const selGroup = selectors.filters;
- beforeEach(() => { hooks = selectorHooks.filters; });
- testHook(hookKeys.useData, selGroup.allFilters);
- testHook(hookKeys.useIncludeCourseRoleMembers, selGroup.includeCourseRoleMembers);
- testHook(hookKeys.useSelectableAssignmentLabels, selGroup.selectableAssignmentLabels);
- testHook(hookKeys.useSelectedAssignmentLabel, selGroup.selectedAssignmentLabel);
- testHook(hookKeys.useAssignmentType, selGroup.assignmentType);
+ loadSelectorGroup(selectorHooks.filters, selectors.filters);
+ testHook(hookKeys.useData, selKeys.allFilters);
+ testHook(hookKeys.useIncludeCourseRoleMembers, selKeys.includeCourseRoleMembers);
+ testHook(hookKeys.useSelectableAssignmentLabels, selKeys.selectableAssignmentLabels);
+ testHook(hookKeys.useSelectedAssignmentLabel, selKeys.selectedAssignmentLabel);
+ testHook(hookKeys.useAssignmentType, selKeys.assignmentType);
+ });
+ describe('grades', () => {
+ loadSelectorGroup(selectorHooks.grades, selectors.grades);
+ testHook(hookKeys.useAllGrades, selKeys.allGrades);
+ testHook(hookKeys.useGradeData, selKeys.gradeData);
+ testHook(hookKeys.useHasOverrideErrors, selKeys.hasOverrideErrors);
+ testHook(hookKeys.useShowSuccess, selKeys.showSuccess);
+ test(hookKeys.useUserCounts, () => {
+ expect(hooks.useUserCounts()).toEqual({
+ filteredUsersCount: useSelector(selectors.grades.filteredUsersCount),
+ totalUsersCount: useSelector(selectors.grades.totalUsersCount),
+ });
+ });
+ });
+ describe('roles', () => {
+ loadSelectorGroup(selectorHooks.roles, selectors.roles);
+ testHook(hookKeys.useCanUserViewGradebook, selKeys.canUserViewGradebook);
});
describe('tracks', () => {
- const hookKeys = keyStore(selectorHooks.tracks);
- const selGroup = selectors.tracks;
- beforeEach(() => { hooks = selectorHooks.tracks; });
- testHook(hookKeys.useAllTracks, selGroup.allTracks);
- testHook(hookKeys.useTracksByName, selGroup.tracksByName);
+ loadSelectorGroup(selectorHooks.tracks, selectors.tracks);
+ testHook(hookKeys.useAllTracks, selKeys.allTracks);
+ testHook(hookKeys.useTracksByName, selKeys.tracksByName);
});
});
diff --git a/src/data/redux/hooks/thunkActions.js b/src/data/redux/hooks/thunkActions.js
index 6b5bc5bc..5017c5a9 100644
--- a/src/data/redux/hooks/thunkActions.js
+++ b/src/data/redux/hooks/thunkActions.js
@@ -3,15 +3,22 @@ import thunkActions from 'data/thunkActions';
import { actionHook } from './utils';
const app = StrictDict({
- useCloseFilterMenu: actionHook(thunkActions.app.filterMenu.close),
+ filterMenu: {
+ useCloseMenu: actionHook(thunkActions.app.filterMenu.close),
+ useHandleTransitionEnd: actionHook(thunkActions.app.filterMenu.handleTransitionEnd),
+ useToggleMenu: actionHook(thunkActions.app.filterMenu.toggle),
+ },
+ useSetModalStateFromTable: actionHook(thunkActions.app.setModalStateFromTable),
});
const grades = StrictDict({
useFetchGradesIfAssignmentGradeFiltersSet: actionHook(
thunkActions.grades.fetchGradesIfAssignmentGradeFiltersSet,
),
+ useFetchPrevNextGrades: actionHook(thunkActions.grades.fetchPrevNextGrades),
useFetchGrades: actionHook(thunkActions.grades.fetchGrades),
useSubmitImportGradesButtonData: actionHook(thunkActions.grades.submitImportGradesButtonData),
+ useUpdateGrades: actionHook(thunkActions.grades.updateGrades),
});
export default StrictDict({
diff --git a/src/data/redux/hooks/thunkActions.test.js b/src/data/redux/hooks/thunkActions.test.js
index 7ec8ebb3..51499f01 100644
--- a/src/data/redux/hooks/thunkActions.test.js
+++ b/src/data/redux/hooks/thunkActions.test.js
@@ -5,7 +5,11 @@ import thunkActionHooks from './thunkActions';
jest.mock('data/thunkActions', () => ({
app: {
- filterMenu: { close: jest.fn() },
+ filterMenu: {
+ close: jest.fn(),
+ handleTransitionEnd: jest.fn(),
+ toggle: jest.fn(),
+ },
},
grades: {
fetchGrades: jest.fn(),
@@ -25,24 +29,38 @@ const testActionHook = (hookKey, action) => {
expect(hooks[hookKey]).toEqual(actionHook(action));
});
};
+let hookKeys;
describe('thunkAction hooks', () => {
describe('app', () => {
- const hookKeys = keyStore(thunkActionHooks.app);
+ hookKeys = keyStore(thunkActionHooks.app);
beforeEach(() => { hooks = thunkActionHooks.app; });
- testActionHook(hookKeys.useCloseFilterMenu, thunkActions.app.filterMenu.close);
+ testActionHook(hookKeys.useSetModalStateFromTable, thunkActions.app.setModalStateFromTable);
+
+ describe('filterMenu', () => {
+ hookKeys = keyStore(thunkActionHooks.app.filterMenu);
+ beforeEach(() => { hooks = thunkActionHooks.app.filterMenu; });
+ testActionHook(hookKeys.useCloseMenu, thunkActions.app.filterMenu.close);
+ testActionHook(
+ hookKeys.useHandleTransitionEnd,
+ thunkActions.app.filterMenu.handleTransitionEnd,
+ );
+ testActionHook(hookKeys.useToggleMenu, thunkActions.app.filterMenu.toggle);
+ });
});
describe('grades', () => {
- const hookKeys = keyStore(thunkActionHooks.grades);
+ hookKeys = keyStore(thunkActionHooks.grades);
const actionGroup = thunkActions.grades;
beforeEach(() => { hooks = thunkActionHooks.grades; });
- testActionHook(hookKeys.useFetchGrades, actionGroup.fetchGrades);
testActionHook(
hookKeys.useFetchGradesIfAssignmentGradeFiltersSet,
actionGroup.fetchGradesIfAssignmentGradeFiltersSet,
);
+ testActionHook(hookKeys.useFetchPrevNextGrades, actionGroup.fetchPrevNextGrades);
+ testActionHook(hookKeys.useFetchGrades, actionGroup.fetchGrades);
testActionHook(
hookKeys.useSubmitImportGradesButtonData,
actionGroup.submitImportGradesButtonData,
);
+ testActionHook(hookKeys.useUpdateGrades, actionGroup.updateGrades);
});
});
diff --git a/src/data/redux/transforms.js b/src/data/redux/transforms.js
new file mode 100644
index 00000000..3980b3a4
--- /dev/null
+++ b/src/data/redux/transforms.js
@@ -0,0 +1,13 @@
+import { StrictDict } from 'utils';
+import selectors from 'data/selectors';
+
+export const grades = StrictDict({
+ subsectionGrade: ({ gradeFormat, subsection }) => () => (
+ selectors.grades.subsectionGrade[gradeFormat](subsection)
+ ),
+ roundGrade: selectors.grades.roundGrade,
+});
+
+export default StrictDict({
+ grades,
+});
diff --git a/src/data/redux/transforms.test.js b/src/data/redux/transforms.test.js
new file mode 100644
index 00000000..c639aba5
--- /dev/null
+++ b/src/data/redux/transforms.test.js
@@ -0,0 +1,38 @@
+import selectors from 'data/selectors';
+
+import { GradeFormats } from 'data/constants/grades';
+import transforms from './transforms';
+
+jest.mock('data/selectors', () => {
+ const {
+ GradeFormats: { absolute, percent },
+ } = jest.requireActual('data/constants/grades');
+ return {
+ grades: {
+ subsectionGrade: {
+ [absolute]: jest.fn(v => ({ absolute: v })),
+ [percent]: jest.fn(v => ({ percent: v })),
+ },
+ roundGrade: jest.fn(),
+ },
+ };
+});
+
+describe('redux transforms', () => {
+ describe('grades transforms', () => {
+ test('subsectionGrade', () => {
+ const subsection = 'test-subsection';
+ expect(transforms.grades.subsectionGrade({
+ gradeFormat: GradeFormats.absolute,
+ subsection,
+ })()).toEqual(selectors.grades.subsectionGrade.absolute(subsection));
+ expect(transforms.grades.subsectionGrade({
+ gradeFormat: GradeFormats.percent,
+ subsection,
+ })()).toEqual(selectors.grades.subsectionGrade.percent(subsection));
+ });
+ test('roundGrade', () => {
+ expect(transforms.grades.roundGrade).toEqual(selectors.grades.roundGrade);
+ });
+ });
+});
diff --git a/src/data/selectors/app.js b/src/data/selectors/app.js
index 6fd32bdd..4ef2f7c8 100644
--- a/src/data/selectors/app.js
+++ b/src/data/selectors/app.js
@@ -89,6 +89,16 @@ const modalSelectors = simpleSelectorFactory(
],
);
+const modalData = ({ app: { modalState } }) => ({
+ assignmentName: modalState.assignmentName,
+ adjustedGradePossible: modalState.adjustedGradePossible,
+ adjustedGradeValue: modalState.adjustedGradeValue,
+ open: modalState.open,
+ reasonForChange: modalState.reasonForChange,
+ todaysDate: modalState.todaysDate,
+ updateUserName: modalState.updateUserName,
+});
+
const filterMenuSelectors = simpleSelectorFactory(
({ app: { filterMenu } }) => filterMenu,
['open', 'transitioning'],
@@ -115,6 +125,7 @@ export default StrictDict({
isFilterMenuOpening,
...simpleSelectors,
modalState: StrictDict(modalSelectors),
+ modalData,
filterMenu: StrictDict({
...filterMenuSelectors,
isClosed: isFilterMenuClosed,
diff --git a/src/data/selectors/grades.js b/src/data/selectors/grades.js
index 7bc3f12b..e5d22b68 100644
--- a/src/data/selectors/grades.js
+++ b/src/data/selectors/grades.js
@@ -266,12 +266,26 @@ const simpleSelectors = simpleSelectorFactory(
'gradeOverrideHistoryError',
'gradeOriginalEarnedGraded',
'gradeOriginalPossibleGraded',
- 'nextPage',
- 'prevPage',
'showSuccess',
],
);
+const gradeData = ({ grades }) => ({
+ courseId: grades.courseId,
+ filteredUsersCount: grades.filteredUsersCount,
+ totalUsersCount: grades.totalUsersCount,
+ gradeFormat: grades.gradeFormat,
+ showSpinner: grades.showSpinner,
+ gradeOverrideCurrentEarnedGradedOverride: grades.gradeOverrideCurrentEarnedGradedOverride,
+ gradeOverrideHistoryError: grades.gradeOverrideHistoryError,
+ gradeOverrideHistoryResults: grades.gradeOverrideHistoryResults,
+ gradeOriginalEarnedGraded: grades.gradeOriginalEarnedGraded,
+ gradeOriginalPossibleGraded: grades.gradeOriginalPossibleGraded,
+ nextPage: grades.nextPage,
+ prevPage: grades.prevPage,
+ showSuccess: grades.showSuccess,
+});
+
export default StrictDict({
bulkImportError,
formatGradeOverrideForDisplay,
@@ -286,6 +300,7 @@ export default StrictDict({
subsectionGrade,
...simpleSelectors,
+ gradeData,
allGrades,
bulkManagementHistoryEntries,
getExampleSectionBreakdown,
diff --git a/src/data/services/lms/urls.js b/src/data/services/lms/urls.js
index 79737b73..d430db8f 100644
--- a/src/data/services/lms/urls.js
+++ b/src/data/services/lms/urls.js
@@ -36,6 +36,10 @@ export const sectionOverrideHistoryUrl = (subsectionId, userId) => stringifyUrl(
{ user_id: userId, history_record_limit: historyRecordLimit },
);
+export const instructorDashboardUrl = () => (
+ `${getConfig().LMS_BASE_URL}/courses/${courseId}/instructor`
+);
+
export default StrictDict({
getUrlPrefix,
getBulkGradesUrl,
diff --git a/src/setupTest.js b/src/setupTest.js
index 9cda7ed5..5416431b 100755
--- a/src/setupTest.js
+++ b/src/setupTest.js
@@ -39,6 +39,11 @@ jest.mock('@edx/frontend-component-footer', () => ({
messages: ['some', 'messages'],
}));
+jest.mock('@edx/paragon/icons', () => ({
+ FilterAlt: 'FilterAlt',
+ Close: 'Close',
+}));
+
jest.mock('@edx/paragon', () => jest.requireActual('testUtils').mockNestedComponents({
Alert: 'Alert',
ActionRow: 'ActionRow',
@@ -75,6 +80,7 @@ jest.mock('@edx/paragon', () => jest.requireActual('testUtils').mockNestedCompon
Hyperlink: 'Hyperlink',
Icon: 'Icon',
IconButton: 'IconButton',
+ Input: 'Input',
ModalDialog: {
Body: 'ModalDialog.Body',
CloseButton: 'ModalDialog.CloseButton',
@@ -84,8 +90,10 @@ jest.mock('@edx/paragon', () => jest.requireActual('testUtils').mockNestedCompon
},
OverlayTrigger: 'OverlayTrigger',
Row: 'Row',
- StatefulButton: 'StatefulButton',
+ SearchField: 'SearchField',
Spinner: 'Spinner',
+ StatefulButton: 'StatefulButton',
+ Toast: 'Toast',
useCheckboxSetValues: () => jest.fn().mockImplementation((values) => ([values, {
add: jest.fn().mockName('useCheckboxSetValues.add'),
diff --git a/src/utils/formatDate.js b/src/utils/formatDate.js
new file mode 100644
index 00000000..bce70dac
--- /dev/null
+++ b/src/utils/formatDate.js
@@ -0,0 +1,20 @@
+export const options = {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ timeZone: 'UTC',
+};
+export const timeOptions = {
+ hour: '2-digit',
+ minute: '2-digit',
+ timeZone: 'UTC',
+ timeZoneName: 'short',
+};
+
+const formatDateForDisplay = (inputDate) => {
+ const date = inputDate.toLocaleDateString('en-US', options);
+ const time = inputDate.toLocaleTimeString('en-US', timeOptions);
+ return `${date} at ${time}`;
+};
+
+export default formatDateForDisplay;
diff --git a/src/utils/index.js b/src/utils/index.js
index 678ea705..2c038dc8 100644
--- a/src/utils/index.js
+++ b/src/utils/index.js
@@ -1,3 +1,4 @@
/* eslint-disable import/prefer-default-export */
export { default as StrictDict } from './StrictDict';
export { default as keyStore } from './keyStore';
+export { default as formatDateForDisplay } from './formatDate';