pull GradebookHeader out of monolith (#185)
* pull GradebookHeader out of monolith * v1.4.30
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
59
src/components/Gradebook/GradebookHeader.jsx
Normal file
59
src/components/Gradebook/GradebookHeader.jsx
Normal 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);
|
||||
54
src/components/Gradebook/GradebookHeader.test.jsx
Normal file
54
src/components/Gradebook/GradebookHeader.test.jsx
Normal 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));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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"
|
||||
>
|
||||
<<
|
||||
</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"
|
||||
>
|
||||
<<
|
||||
</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"
|
||||
>
|
||||
<<
|
||||
</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>
|
||||
`;
|
||||
@@ -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,
|
||||
}),
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user