pull GradebookHeader out of monolith (#185)

* pull GradebookHeader out of monolith

* v1.4.30
This commit is contained in:
Ben Warzeski
2021-05-26 15:05:43 -04:00
committed by GitHub
parent 189152f51b
commit 2ee522352e
8 changed files with 276 additions and 105 deletions

View File

@@ -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",

View File

@@ -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),
};
};

View File

@@ -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 && (
<div>
<StatefulButton
variant="outline-primary"
@@ -63,21 +64,31 @@ export class BulkManagementControls extends React.Component {
BulkManagementControls.defaultProps = {
courseId: '',
showBulkManagement: false,
showSpinner: false,
};
BulkManagementControls.propTypes = {
courseId: PropTypes.string,
gradeExportUrl: PropTypes.string.isRequired,
interventionExportUrl: PropTypes.string.isRequired,
showSpinner: PropTypes.bool,
// redux
downloadBulkGradesReport: PropTypes.func.isRequired,
downloadInterventionReport: PropTypes.func.isRequired,
gradeExportUrl: PropTypes.string.isRequired,
interventionExportUrl: PropTypes.string.isRequired,
showSpinner: PropTypes.bool,
showBulkManagement: PropTypes.bool,
};
export const mapStateToProps = () => ({ });
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,

View File

@@ -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 (
<div className="gradebook-header">
<a
href={this.lmsInstructorDashboardUrl(this.props.courseId)}
className="mb-3"
>
<span aria-hidden="true">{'<< '}</span> Back to Dashboard
</a>
<h1>Gradebook</h1>
<h3> {this.props.courseId}</h3>
{this.props.areGradesFrozen
&& (
<div className="alert alert-warning" role="alert">
The grades for this course are now frozen. Editing of grades is no longer allowed.
</div>
)}
{(this.props.canUserViewGradebook === false) && (
<div className="alert alert-warning" role="alert">
You are not authorized to view the gradebook for this course.
</div>
)}
</div>
);
}
}
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);

View File

@@ -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(<GradebookHeader {...props} />)).toMatchSnapshot();
});
});
describe('grades frozen, cannot view', () => {
test('unauthorized warning, and grades frozen warning.', () => {
const props = { courseId, areGradesFrozen: true, canUserViewGradebook: false };
expect(shallow(<GradebookHeader {...props} />)).toMatchSnapshot();
});
});
describe('grades frozen, can view.', () => {
test('grades frozen warning but no unauthorized warning', () => {
const props = { courseId, areGradesFrozen: true, canUserViewGradebook: true };
expect(shallow(<GradebookHeader {...props} />)).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));
});
});
});

View File

@@ -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`] = `
<div
className="gradebook-header"
>
<a
className="mb-3"
href="http://localhost:18000/courses/fakeID/instructor"
>
<span
aria-hidden="true"
>
&lt;&lt;
</span>
Back to Dashboard
</a>
<h1>
Gradebook
</h1>
<h3>
fakeID
</h3>
<div
className="alert alert-warning"
role="alert"
>
You are not authorized to view the gradebook for this course.
</div>
</div>
`;
exports[`GradebookHeader component snapshots grades frozen, can view. grades frozen warning but no unauthorized warning 1`] = `
<div
className="gradebook-header"
>
<a
className="mb-3"
href="http://localhost:18000/courses/fakeID/instructor"
>
<span
aria-hidden="true"
>
&lt;&lt;
</span>
Back to Dashboard
</a>
<h1>
Gradebook
</h1>
<h3>
fakeID
</h3>
<div
className="alert alert-warning"
role="alert"
>
The grades for this course are now frozen. Editing of grades is no longer allowed.
</div>
</div>
`;
exports[`GradebookHeader component snapshots grades frozen, cannot view unauthorized warning, and grades frozen warning. 1`] = `
<div
className="gradebook-header"
>
<a
className="mb-3"
href="http://localhost:18000/courses/fakeID/instructor"
>
<span
aria-hidden="true"
>
&lt;&lt;
</span>
Back to Dashboard
</a>
<h1>
Gradebook
</h1>
<h3>
fakeID
</h3>
<div
className="alert alert-warning"
role="alert"
>
The grades for this course are now frozen. Editing of grades is no longer allowed.
</div>
<div
className="alert alert-warning"
role="alert"
>
You are not authorized to view the gradebook for this course.
</div>
</div>
`;

View File

@@ -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) => (<span className="font-weight-bold">{val}</span>);
const { filteredUsersCount, totalUsersCount } = this.props;
return (
<>
Showing {bold(filteredUsersCount)} of {bold(totalUsersCount)} total learners
</>
);
};
scoreViewInput = () => (
<InputSelect
label="Score View:"
name="ScoreView"
value="percent"
options={[{ label: 'Percent', value: 'percent' }, { label: 'Absolute', value: 'absolute' }]}
onChange={this.props.toggleFormat}
/>
);
spinnerIcon = () => {
if (!this.props.showSpinner) {
return null;
}
return (
<div className="spinner-overlay">
<Icon className="fa fa-spinner fa-spin fa-5x color-black" />
</div>
);
}
render() {
return (
<Drawer
mainContent={toggleFilterDrawer => (
<div className="px-3 gradebook-content">
<a
href={this.lmsInstructorDashboardUrl(this.props.courseId)}
className="mb-3"
>
<span aria-hidden="true">{'<< '}</span> Back to Dashboard
</a>
<h1>Gradebook</h1>
<h3> {this.props.courseId}</h3>
{this.props.areGradesFrozen
&& (
<div className="alert alert-warning" role="alert">
The grades for this course are now frozen. Editing of grades is no longer allowed.
</div>
)}
{(this.props.canUserViewGradebook === false)
&& (
<div className="alert alert-warning" role="alert">
You are not authorized to view the gradebook for this course.
</div>
)}
<GradebookHeader courseId={this.props.courseId} />
<Tabs defaultActiveKey="grades">
<Tab eventKey="grades" title="Grades">
{this.props.showSpinner && (
<div className="spinner-overlay">
<Icon className="fa fa-spinner fa-spin fa-5x color-black" />
</div>
)}
{this.spinnerIcon()}
<SearchControls
courseId={this.props.courseId}
filterValue={this.state.filterValue}
@@ -196,33 +202,10 @@ export default class Gradebook extends React.Component {
isMaxCourseGradeFilterValid={this.state.isMaxCourseGradeFilterValid}
/>
<h4>Step 2: View or Modify Individual Grades</h4>
{this.props.totalUsersCount
? (
<div>
Showing
<span className="font-weight-bold"> {this.props.filteredUsersCount} </span>
of
<span className="font-weight-bold"> {this.props.totalUsersCount} </span>
total learners
</div>
)
: null}
{this.usersLabel()}
<div className="d-flex justify-content-between align-items-center mb-2">
<InputSelect
label="Score View:"
name="ScoreView"
value="percent"
options={[{ label: 'Percent', value: 'percent' }, { label: 'Absolute', value: 'absolute' }]}
onChange={this.props.toggleFormat}
/>
{this.props.showBulkManagement && (
<BulkManagementControls
courseId={this.props.courseId}
gradeExportUrl={this.props.gradeExportUrl}
interventionExportUrl={this.props.interventionExportUrl}
showSpinner={this.props.showSpinner}
/>
)}
{this.scoreViewInput()}
<BulkManagementControls courseId={this.props.courseId} />
</div>
<GradebookTable setGradebookState={this.safeSetState} />
{PageButtons(this.props)}
@@ -244,15 +227,11 @@ export default class Gradebook extends React.Component {
updateUserId={this.state.updateUserId}
updateUserName={this.state.updateUserName}
/>
</Tab>
{this.props.showBulkManagement
&& (
<Tab eventKey="bulk_management" title="Bulk Management">
<BulkManagement
courseId={this.props.courseId}
gradeExportUrl={this.props.gradeExportUrl}
/>
<BulkManagement courseId={this.props.courseId} />
</Tab>
)}
</Tabs>
@@ -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,
}),

View File

@@ -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(