diff --git a/package.json b/package.json index 989e61a..833d44e 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@edx/frontend-app-gradebook", - "version": "1.4.29", + "version": "1.4.30", "description": "edx editable gradebook-ui to manipulate grade overrides on subsections", "repository": { "type": "git", diff --git a/src/components/Gradebook/BulkManagement.jsx b/src/components/Gradebook/BulkManagement.jsx index 17621e4..05dcc01 100644 --- a/src/components/Gradebook/BulkManagement.jsx +++ b/src/components/Gradebook/BulkManagement.jsx @@ -183,11 +183,12 @@ BulkManagement.propTypes = { uploadSuccess: PropTypes.bool, }; -export const mapStateToProps = (state) => { +export const mapStateToProps = (state, ownProps) => { const { grades } = selectors; return { bulkImportError: grades.bulkImportError(state), bulkManagementHistory: grades.bulkManagementHistoryEntries(state), + gradeExportUrl: selectors.root.gradeExportUrl(state, { courseId: ownProps.courseId }), uploadSuccess: grades.uploadSuccess(state), }; }; diff --git a/src/components/Gradebook/BulkManagementControls.jsx b/src/components/Gradebook/BulkManagementControls.jsx index b89f831..c0bf894 100644 --- a/src/components/Gradebook/BulkManagementControls.jsx +++ b/src/components/Gradebook/BulkManagementControls.jsx @@ -8,6 +8,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faDownload, faSpinner } from '@fortawesome/free-solid-svg-icons'; import actions from 'data/actions'; +import selectors from 'data/selectors'; export class BulkManagementControls extends React.Component { handleClickDownloadInterventions = () => { @@ -25,7 +26,7 @@ export class BulkManagementControls extends React.Component { }; render() { - return ( + return this.props.showBulkManagement && (
({ }); +export const mapStateToProps = (state, ownProps) => ({ + gradeExportUrl: selectors.root.gradeExportUrl(state, { courseId: ownProps.courseId }), + interventionExportUrl: selectors.root.interventionExportUrl( + state, + { courseId: ownProps.courseId }, + ), + showBulkManagement: selectors.root.showBulkManagement(state, { courseId: ownProps.courseId }), + showSpinner: selectors.root.shouldShowSpinner(state), +}); export const mapDispatchToProps = { downloadBulkGradesReport: actions.grades.downloadReport.bulkGrades, diff --git a/src/components/Gradebook/GradebookHeader.jsx b/src/components/Gradebook/GradebookHeader.jsx new file mode 100644 index 0000000..e4a43d0 --- /dev/null +++ b/src/components/Gradebook/GradebookHeader.jsx @@ -0,0 +1,59 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; + +import { configuration } from 'config'; +import selectors from 'data/selectors'; + +export class GradebookHeader extends React.Component { + lmsInstructorDashboardUrl = courseId => ( + `${configuration.LMS_BASE_URL}/courses/${courseId}/instructor` + ); + + render() { + return ( +
+ + Back to Dashboard + +

Gradebook

+

{this.props.courseId}

+ {this.props.areGradesFrozen + && ( +
+ The grades for this course are now frozen. Editing of grades is no longer allowed. +
+ )} + {(this.props.canUserViewGradebook === false) && ( +
+ You are not authorized to view the gradebook for this course. +
+ )} +
+ ); + } +} + +GradebookHeader.defaultProps = { + courseId: '', + // redux + areGradesFrozen: false, + canUserViewGradebook: false, +}; + +GradebookHeader.propTypes = { + courseId: PropTypes.string, + // redux + areGradesFrozen: PropTypes.bool, + canUserViewGradebook: PropTypes.bool, +}; + +export const mapStateToProps = (state) => ({ + areGradesFrozen: selectors.assignmentTypes.areGradesFrozen(state), + canUserViewGradebook: selectors.roles.canUserViewGradebook(state), +}); + +export default connect(mapStateToProps)(GradebookHeader); diff --git a/src/components/Gradebook/GradebookHeader.test.jsx b/src/components/Gradebook/GradebookHeader.test.jsx new file mode 100644 index 0000000..5554e35 --- /dev/null +++ b/src/components/Gradebook/GradebookHeader.test.jsx @@ -0,0 +1,54 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import selectors from 'data/selectors'; +import { GradebookHeader, mapStateToProps } from './GradebookHeader'; + +jest.mock('data/selectors', () => ({ + __esModule: true, + default: { + assignmentTypes: { areGradesFrozen: jest.fn(state => ({ areGradesFrozen: state })) }, + roles: { canUserViewGradebook: jest.fn(state => ({ canUserViewGradebook: state })) }, + }, +})); + +const courseId = 'fakeID'; +describe('GradebookHeader component', () => { + describe('snapshots', () => { + describe('default values (grades frozen, cannot view).', () => { + test('unauthorized warning, but no grades frozen warning', () => { + const props = { courseId, areGradesFrozen: false, canUserViewGradebook: false }; + expect(shallow()).toMatchSnapshot(); + }); + }); + describe('grades frozen, cannot view', () => { + test('unauthorized warning, and grades frozen warning.', () => { + const props = { courseId, areGradesFrozen: true, canUserViewGradebook: false }; + expect(shallow()).toMatchSnapshot(); + }); + }); + describe('grades frozen, can view.', () => { + test('grades frozen warning but no unauthorized warning', () => { + const props = { courseId, areGradesFrozen: true, canUserViewGradebook: true }; + expect(shallow()).toMatchSnapshot(); + }); + }); + }); + describe('mapStateToProps', () => { + let mapped; + const testState = { a: 'test', example: 'state' }; + beforeEach(() => { + mapped = mapStateToProps(testState); + }); + it('maps areGradesFrozen from assignmentTypes selector', () => { + expect( + mapped.areGradesFrozen, + ).toEqual(selectors.assignmentTypes.areGradesFrozen(testState)); + }); + it('maps canUserViewGradebook from roles selector', () => { + expect( + mapped.canUserViewGradebook, + ).toEqual(selectors.roles.canUserViewGradebook(testState)); + }); + }); +}); diff --git a/src/components/Gradebook/__snapshots__/GradebookHeader.test.jsx.snap b/src/components/Gradebook/__snapshots__/GradebookHeader.test.jsx.snap new file mode 100644 index 0000000..95223c4 --- /dev/null +++ b/src/components/Gradebook/__snapshots__/GradebookHeader.test.jsx.snap @@ -0,0 +1,100 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`GradebookHeader component snapshots default values (grades frozen, cannot view). unauthorized warning, but no grades frozen warning 1`] = ` +
+ + + Back to Dashboard + +

+ Gradebook +

+

+ + fakeID +

+
+ You are not authorized to view the gradebook for this course. +
+
+`; + +exports[`GradebookHeader component snapshots grades frozen, can view. grades frozen warning but no unauthorized warning 1`] = ` +
+ + + Back to Dashboard + +

+ Gradebook +

+

+ + fakeID +

+
+ The grades for this course are now frozen. Editing of grades is no longer allowed. +
+
+`; + +exports[`GradebookHeader component snapshots grades frozen, cannot view unauthorized warning, and grades frozen warning. 1`] = ` +
+ + + Back to Dashboard + +

+ Gradebook +

+

+ + fakeID +

+
+ The grades for this course are now frozen. Editing of grades is no longer allowed. +
+
+ You are not authorized to view the gradebook for this course. +
+
+`; diff --git a/src/components/Gradebook/index.jsx b/src/components/Gradebook/index.jsx index 1572e91..182ca79 100644 --- a/src/components/Gradebook/index.jsx +++ b/src/components/Gradebook/index.jsx @@ -10,12 +10,12 @@ import { import queryString from 'query-string'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faFilter } from '@fortawesome/free-solid-svg-icons'; -import { configuration } from '../../config'; import PageButtons from '../PageButtons'; import Drawer from '../Drawer'; import initialFilters from '../../data/constants/filters'; import ConnectedFilterBadges from '../FilterBadges'; +import GradebookHeader from './GradebookHeader'; import BulkManagement from './BulkManagement'; import BulkManagementControls from './BulkManagementControls'; import EditModal from './EditModal'; @@ -66,12 +66,9 @@ export default class Gradebook extends React.Component { this.setState({ [e.target.name]: e.target.value }); } - getActiveTabs = () => { - if (this.props.showBulkManagement) { - return ['Grades', 'Bulk Management']; - } - return ['Grades']; - }; + getActiveTabs = () => ( + this.props.showBulkManagement ? ['Grades', 'BulkManagement'] : ['Grades'] + ); updateQueryParams = (queryParams) => { const parsed = queryString.parse(this.props.location.search); @@ -85,8 +82,6 @@ export default class Gradebook extends React.Component { this.props.history.push(`?${queryString.stringify(parsed)}`); }; - lmsInstructorDashboardUrl = courseId => `${configuration.LMS_BASE_URL}/courses/${courseId}/instructor`; - handleFilterBadgeClose = filterNames => () => { this.props.resetFilters(filterNames); const queryParams = {}; @@ -150,38 +145,49 @@ export default class Gradebook extends React.Component { courseGradeMax: this.state.courseGradeMax, }); + usersLabel = () => { + if (!this.props.totalUsersCount) { + return null; + } + const bold = (val) => ({val}); + const { filteredUsersCount, totalUsersCount } = this.props; + return ( + <> + Showing {bold(filteredUsersCount)} of {bold(totalUsersCount)} total learners + + ); + }; + + scoreViewInput = () => ( + + ); + + spinnerIcon = () => { + if (!this.props.showSpinner) { + return null; + } + return ( +
+ +
+ ); + } + render() { return ( (
- - Back to Dashboard - -

Gradebook

-

{this.props.courseId}

- {this.props.areGradesFrozen - && ( -
- The grades for this course are now frozen. Editing of grades is no longer allowed. -
- )} - {(this.props.canUserViewGradebook === false) - && ( -
- You are not authorized to view the gradebook for this course. -
- )} + - {this.props.showSpinner && ( -
- -
- )} + {this.spinnerIcon()}

Step 2: View or Modify Individual Grades

- {this.props.totalUsersCount - ? ( -
- Showing - {this.props.filteredUsersCount} - of - {this.props.totalUsersCount} - total learners -
- ) - : null} + {this.usersLabel()}
- - {this.props.showBulkManagement && ( - - )} + {this.scoreViewInput()} +
{PageButtons(this.props)} @@ -244,15 +227,11 @@ export default class Gradebook extends React.Component { updateUserId={this.state.updateUserId} updateUserName={this.state.updateUserName} /> -
{this.props.showBulkManagement && ( - + )}
@@ -277,8 +256,6 @@ export default class Gradebook extends React.Component { } Gradebook.defaultProps = { - areGradesFrozen: false, - canUserViewGradebook: false, courseId: '', filteredUsersCount: null, location: { @@ -293,18 +270,14 @@ Gradebook.defaultProps = { }; Gradebook.propTypes = { - areGradesFrozen: PropTypes.bool, - canUserViewGradebook: PropTypes.bool, courseId: PropTypes.string, filteredUsersCount: PropTypes.number, getRoles: PropTypes.func.isRequired, getUserGrades: PropTypes.func.isRequired, - gradeExportUrl: PropTypes.string.isRequired, history: PropTypes.shape({ push: PropTypes.func, }).isRequired, initializeFilters: PropTypes.func.isRequired, - interventionExportUrl: PropTypes.string.isRequired, location: PropTypes.shape({ search: PropTypes.string, }), diff --git a/src/containers/GradebookPage/index.jsx b/src/containers/GradebookPage/index.jsx index 2ab8e86..315ade8 100644 --- a/src/containers/GradebookPage/index.jsx +++ b/src/containers/GradebookPage/index.jsx @@ -9,59 +9,32 @@ import Gradebook from 'components/Gradebook'; const mapStateToProps = (state, ownProps) => { const { root, - assignmentTypes, filters, grades, - roles, } = selectors; const { courseId } = ownProps.match.params; return { courseId, - areGradesFrozen: assignmentTypes.areGradesFrozen(state), - assignmentTypes: assignmentTypes.allAssignmentTypes(state), - assignmentFilterOptions: filters.selectableAssignmentLabels(state), - bulkImportError: grades.bulkImportError(state), - bulkManagementHistory: grades.bulkManagementHistoryEntries(state), - canUserViewGradebook: roles.canUserViewGradebook(state), filteredUsersCount: grades.filteredUsersCount(state), - format: grades.gradeFormat(state), gradeExportUrl: root.gradeExportUrl(state, { courseId }), - grades: grades.allGrades(state), - headings: root.getHeadings(state), interventionExportUrl: root.interventionExportUrl(state, { courseId }), - nextPage: state.grades.nextPage, - prevPage: state.grades.prevPage, selectedTrack: filters.track(state), selectedCohort: filters.cohort(state), selectedAssignmentType: filters.assignmentType(state), - selectedAssignment: filters.selectedAssignmentLabel(state), showBulkManagement: root.showBulkManagement(state, { courseId }), showSpinner: root.shouldShowSpinner(state), totalUsersCount: grades.totalUsersCount(state), - uploadSuccess: grades.uploadSuccess(state), }; }; const mapDispatchToProps = { - downloadBulkGradesReport: actions.grades.downloadReport.bulkGrades, - downloadInterventionReport: actions.grades.downloadReport.intervention, toggleFormat: actions.grades.toggleGradeFormat, - - filterAssignmentType: actions.filters.update.assignmentType, initializeFilters: actions.filters.initialize, resetFilters: actions.filters.reset, - updateAssignmentFilter: actions.filters.update.assignment, - updateAssignmentLimits: actions.filters.update.assignmentLimits, - - fetchGradeOverrideHistory: thunkActions.grades.fetchGradeOverrideHistory, - getAssignmentTypes: thunkActions.assignmentTypes.fetchAssignmentTypes, - getCohorts: thunkActions.cohorts.fetchCohorts, - getPrevNextGrades: thunkActions.grades.fetchPrevNextGrades, getRoles: thunkActions.roles.fetchRoles, getTracks: thunkActions.tracks.fetchTracks, getUserGrades: thunkActions.grades.fetchGrades, - submitFileUploadFormData: thunkActions.grades.submitFileUploadFormData, }; const GradebookPage = connect(