group filters under GradebookFilters sub-view

This commit is contained in:
Ben Warzeski
2021-04-26 22:06:40 -04:00
parent c60358941e
commit 12d32efe08
29 changed files with 488 additions and 259 deletions

View File

@@ -7,7 +7,7 @@ import {
AssignmentFilter,
mapStateToProps,
mapDispatchToProps,
} from './AssignmentFilter';
} from '.';
jest.mock('data/selectors/filters', () => ({
/** Mocking to use passed state for validation purposes */
@@ -43,11 +43,9 @@ describe('AssignmentFilter', () => {
beforeEach(() => {
props = {
...props,
updateQueryParams: jest.fn().mockName('updateQueryParams'),
updateGradesIfAssignmentGradeFiltersSet: jest.fn().mockName(
'updateGradesIfAssignmentGradeFiltersSet',
),
updateAssignmentFilter: jest.fn().mockName('updateAssignmentFilter'),
updateQueryParams: jest.fn(),
updateGradesIfAssignmentGradeFiltersSet: jest.fn(),
updateAssignmentFilter: jest.fn(),
};
});

View File

@@ -22,7 +22,7 @@ export class AssignmentGradeFilter extends React.Component {
const {
assignmentGradeMin,
assignmentGradeMax,
} = this.props;
} = this.props.filterValues;
this.props.updateAssignmentLimits(
assignmentGradeMin,
@@ -41,11 +41,11 @@ export class AssignmentGradeFilter extends React.Component {
}
handleSetMax(event) {
this.props.setAssignmentGradeMax(event.target.value);
this.props.setFilters({ assignmentGradeMax: event.target.value });
}
handleSetMin(event) {
this.props.setAssignmentGradeMin(event.target.value);
this.props.setFilters({ assignmentGradeMin: event.target.value });
}
render() {
@@ -54,14 +54,14 @@ export class AssignmentGradeFilter extends React.Component {
<PercentGroup
id="assignmentGradeMin"
label="Min Grade"
value={this.props.assignmentGradeMin}
value={this.props.filterValues.assignmentGradeMin}
disabled={!this.props.selectedAssignment}
onChange={this.handleSetMin}
/>
<PercentGroup
id="assignmentGradeMax"
label="Max Grade"
value={this.props.assignmentGradeMax}
value={this.props.filterValues.assignmentGradeMax}
disabled={!this.props.selectedAssignment}
onChange={this.handleSetMax}
/>
@@ -89,11 +89,12 @@ AssignmentGradeFilter.defaultProps = {
};
AssignmentGradeFilter.propTypes = {
assignmentGradeMin: PropTypes.string.isRequired,
assignmentGradeMax: PropTypes.string.isRequired,
courseId: PropTypes.string.isRequired,
setAssignmentGradeMin: PropTypes.func.isRequired,
setAssignmentGradeMax: PropTypes.func.isRequired,
filterValues: PropTypes.shape({
assignmentGradeMin: PropTypes.string.isRequired,
assignmentGradeMax: PropTypes.string.isRequired,
}).isRequired,
setFilters: PropTypes.func.isRequired,
updateQueryParams: PropTypes.func.isRequired,
// redux

View File

@@ -8,12 +8,14 @@ import {
AssignmentGradeFilter,
mapStateToProps,
mapDispatchToProps,
} from './AssignmentGradeFilter';
} from '.';
describe('AssignmentGradeFilter', () => {
let props = {
assignmentGradeMin: '1',
assignmentGradeMax: '100',
filterValues: {
assignmentGradeMin: '1',
assignmentGradeMax: '100',
},
courseId: '12345',
selectedAssignmentType: 'assgnFilterLabel1',
@@ -25,8 +27,7 @@ describe('AssignmentGradeFilter', () => {
beforeEach(() => {
props = {
...props,
setAssignmentGradeMin: jest.fn(),
setAssignmentGradeMax: jest.fn(),
setFilters: jest.fn(),
updateQueryParams: jest.fn(),
getUserGrades: jest.fn(),
updateAssignmentLimits: jest.fn(),
@@ -45,8 +46,8 @@ describe('AssignmentGradeFilter', () => {
});
it('calls props.updateAssignmentLimits with min and max', () => {
expect(props.updateAssignmentLimits).toHaveBeenCalledWith(
props.assignmentGradeMin,
props.assignmentGradeMax,
props.filterValues.assignmentGradeMin,
props.filterValues.assignmentGradeMax,
);
});
it('calls getUserGrades w/ selection', () => {
@@ -59,8 +60,28 @@ describe('AssignmentGradeFilter', () => {
});
it('updates queryParams with assignment grade min and max', () => {
expect(props.updateQueryParams).toHaveBeenCalledWith({
assignmentGradeMin: props.assignmentGradeMin,
assignmentGradeMax: props.assignmentGradeMax,
assignmentGradeMin: props.filterValues.assignmentGradeMin,
assignmentGradeMax: props.filterValues.assignmentGradeMax,
});
});
});
describe('handleSetMin', () => {
it('calls setFilters for assignmentGradeMin', () => {
const testVal = 23;
const el = mount(<AssignmentGradeFilter {...props} />);
el.instance().handleSetMin({ target: { value: testVal } });
expect(props.setFilters).toHaveBeenCalledWith({
assignmentGradeMin: testVal,
});
});
});
describe('handleSetMax', () => {
it('calls setFilters for assignmentGradeMax', () => {
const testVal = 92;
const el = mount(<AssignmentGradeFilter {...props} />);
el.instance().handleSetMax({ target: { value: testVal } });
expect(props.setFilters).toHaveBeenCalledWith({
assignmentGradeMax: testVal,
});
});
});

View File

@@ -7,7 +7,7 @@ import {
AssignmentTypeFilter,
mapStateToProps,
mapDispatchToProps,
} from './AssignmentTypeFilter';
} from '.';
jest.mock('data/selectors/filters', () => ({
/** Mocking to use passed state for validation purposes */

View File

@@ -1,11 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CourseGradeFilters Component snapshots basic snapshot 1`] = `
<Collapsible
className="filter-group mb-3"
defaultOpen={true}
title="Overall Grade"
>
exports[`CourseGradeFilter Component snapshots basic snapshot 1`] = `
<React.Fragment>
<div
className="grade-filter-inputs"
>
@@ -34,5 +30,5 @@ exports[`CourseGradeFilters Component snapshots basic snapshot 1`] = `
Apply
</Button>
</div>
</Collapsible>
</React.Fragment>
`;

View File

@@ -4,14 +4,13 @@ import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import {
Button,
Collapsible,
} from '@edx/paragon';
import { updateCourseGradeFilter } from 'data/actions/filters';
import { fetchGrades } from 'data/actions/grades';
import PercentGroup from '../PercentGroup';
export class CourseGradeFilters extends React.Component {
export class CourseGradeFilter extends React.Component {
constructor(props) {
super(props);
this.handleApplyClick = this.handleApplyClick.bind(this);
@@ -21,11 +20,14 @@ export class CourseGradeFilters extends React.Component {
}
handleApplyClick() {
const isMinValid = this.isGradeFilterValueInRange(this.props.courseGradeMin);
const isMaxValid = this.isGradeFilterValueInRange(this.props.courseGradeMax);
const { courseGradeMin, courseGradeMax } = this.props.filterValues;
const isMinValid = this.isGradeFilterValueInRange(courseGradeMin);
const isMaxValid = this.isGradeFilterValueInRange(courseGradeMax);
this.props.setIsMinCourseGradeFilterValid(isMinValid);
this.props.setIsMaxCourseGradeFilterValid(isMaxValid);
this.props.setFilters({
isMinCourseGradeFilterValid: isMinValid,
isMaxCourseGradeFilterValid: isMaxValid,
});
if (isMinValid && isMaxValid) {
this.updateAPI();
@@ -33,7 +35,7 @@ export class CourseGradeFilters extends React.Component {
}
updateAPI() {
const { courseGradeMin, courseGradeMax } = this.props;
const { courseGradeMin, courseGradeMax } = this.props.filterValues;
this.props.updateFilter(
courseGradeMin,
courseGradeMax,
@@ -50,11 +52,11 @@ export class CourseGradeFilters extends React.Component {
}
handleUpdateMin(event) {
this.props.setCourseGradeMin(event.target.value);
this.props.setFilters({ courseGradeMin: event.target.value });
}
handleUpdateMax(event) {
this.props.setCourseGradeMax(event.target.value);
this.props.setFilters({ courseGradeMax: event.target.value });
}
isGradeFilterValueInRange = (value) => {
@@ -64,18 +66,18 @@ export class CourseGradeFilters extends React.Component {
render() {
return (
<Collapsible title="Overall Grade" defaultOpen className="filter-group mb-3">
<>
<div className="grade-filter-inputs">
<PercentGroup
id="minimum-grade"
label="Min Grade"
value={this.props.courseGradeMin}
value={this.props.filterValues.courseGradeMin}
onChange={this.handleUpdateMin}
/>
<PercentGroup
id="maximum-grade"
label="Max Grade"
value={this.props.courseGradeMax}
value={this.props.filterValues.courseGradeMax}
onChange={this.handleUpdateMax}
/>
</div>
@@ -87,27 +89,27 @@ export class CourseGradeFilters extends React.Component {
Apply
</Button>
</div>
</Collapsible>
</>
);
}
}
CourseGradeFilters.defaultProps = {
CourseGradeFilter.defaultProps = {
courseId: '',
selectedAssignmentType: '',
selectedCohort: null,
selectedTrack: null,
};
CourseGradeFilters.propTypes = {
courseGradeMin: PropTypes.string.isRequired,
courseGradeMax: PropTypes.string.isRequired,
CourseGradeFilter.propTypes = {
courseId: PropTypes.string,
setCourseGradeMin: PropTypes.func.isRequired,
setCourseGradeMax: PropTypes.func.isRequired,
setIsMaxCourseGradeFilterValid: PropTypes.func.isRequired,
setIsMinCourseGradeFilterValid: PropTypes.func.isRequired,
filterValues: PropTypes.shape({
courseGradeMin: PropTypes.string.isRequired,
courseGradeMax: PropTypes.string.isRequired,
}).isRequired,
setFilters: PropTypes.func.isRequired,
updateQueryParams: PropTypes.func.isRequired,
// Redux
getUserGrades: PropTypes.func.isRequired,
selectedAssignmentType: PropTypes.string,
@@ -127,4 +129,4 @@ export const mapDispatchToProps = {
getUserGrades: fetchGrades,
};
export default connect(mapStateToProps, mapDispatchToProps)(CourseGradeFilters);
export default connect(mapStateToProps, mapDispatchToProps)(CourseGradeFilter);

View File

@@ -6,7 +6,7 @@ import { shallow } from 'enzyme';
import { updateCourseGradeFilter } from 'data/actions/filters';
import { fetchGrades } from 'data/actions/grades';
import {
CourseGradeFilters,
CourseGradeFilter,
mapStateToProps,
mapDispatchToProps,
} from '.';
@@ -16,10 +16,12 @@ jest.mock('@edx/paragon', () => ({
Collapsible: 'Collapsible',
}));
describe('CourseGradeFilters', () => {
describe('CourseGradeFilter', () => {
let props = {
courseGradeMin: '5',
courseGradeMax: '92',
filterValues: {
courseGradeMin: '5',
courseGradeMax: '92',
},
courseId: '12345',
selectedAssignmentType: 'assignMent type 1',
selectedCohort: 'COHort',
@@ -30,10 +32,7 @@ describe('CourseGradeFilters', () => {
props = {
...props,
getUserGrades: jest.fn(),
setCourseGradeMin: jest.fn(),
setCourseGradeMax: jest.fn(),
setIsMinCourseGradeFilterValid: jest.fn(),
setIsMaxCourseGradeFilterValid: jest.fn(),
setFilters: jest.fn(),
updateQueryParams: jest.fn(),
updateFilter: jest.fn(),
};
@@ -42,7 +41,7 @@ describe('CourseGradeFilters', () => {
describe('Component', () => {
describe('snapshots', () => {
test('basic snapshot', () => {
const el = shallow(<CourseGradeFilters {...props} />);
const el = shallow(<CourseGradeFilter {...props} />);
el.instance().handleUpdateMin = jest.fn().mockName(
'handleUpdateMin',
);
@@ -60,9 +59,32 @@ describe('CourseGradeFilters', () => {
let el;
const testVal = 'TESTvalue';
beforeEach(() => {
el = shallow(<CourseGradeFilters {...props} />);
el = shallow(<CourseGradeFilter {...props} />);
});
describe('handleApplyClick', () => {
beforeEach(() => {
el.instance().updateAPI = jest.fn();
});
it('calls setFilters for isMin(Max)CourseGradeFilterValid', () => {
el.instance().isGradeFilterValueInRange = jest.fn().mockImplementation(v => v >= 50);
el.instance().handleApplyClick();
expect(props.setFilters).toHaveBeenCalledWith({
isMinCourseGradeFilterValid: false,
isMaxCourseGradeFilterValid: true,
});
});
it('calls updateAPI only if both min and max are valid', () => {
const isValid = jest.fn().mockImplementation(v => v >= 50);
el.instance().isGradeFilterValueInRange = isValid;
el.instance().handleApplyClick();
expect(el.instance().updateAPI).not.toHaveBeenCalled();
isValid.mockImplementation(v => v <= 50);
el.instance().handleApplyClick();
expect(el.instance().updateAPI).not.toHaveBeenCalled();
isValid.mockImplementation(v => v >= 0);
el.instance().handleApplyClick();
expect(el.instance().updateAPI).toHaveBeenCalled();
});
});
describe('updateAPI', () => {
beforeEach(() => {
@@ -70,8 +92,8 @@ describe('CourseGradeFilters', () => {
});
it('calls props.updateFilter with selection', () => {
expect(props.updateFilter).toHaveBeenCalledWith(
props.courseGradeMin,
props.courseGradeMax,
props.filterValues.courseGradeMin,
props.filterValues.courseGradeMax,
props.courseId,
);
});
@@ -82,15 +104,15 @@ describe('CourseGradeFilters', () => {
props.selectedTrack,
props.selectedAssignmentType,
{
courseGradeMin: props.courseGradeMin,
courseGradeMax: props.courseGradeMax,
courseGradeMin: props.filterValues.courseGradeMin,
courseGradeMax: props.filterValues.courseGradeMax,
},
);
});
it('updates query params with courseGradeMin and courseGradeMax', () => {
expect(props.updateQueryParams).toHaveBeenCalledWith({
courseGradeMin: props.courseGradeMin,
courseGradeMax: props.courseGradeMax,
courseGradeMin: props.filterValues.courseGradeMin,
courseGradeMax: props.filterValues.courseGradeMax,
});
});
});
@@ -99,7 +121,9 @@ describe('CourseGradeFilters', () => {
el.instance().handleUpdateMin(
{ target: { value: testVal } },
);
expect(props.setCourseGradeMin).toHaveBeenCalledWith(testVal);
expect(props.setFilters).toHaveBeenCalledWith({
courseGradeMin: testVal,
});
});
});
describe('handleUpdateMax', () => {
@@ -107,7 +131,9 @@ describe('CourseGradeFilters', () => {
el.instance().handleUpdateMax(
{ target: { value: testVal } },
);
expect(props.setCourseGradeMax).toHaveBeenCalledWith(testVal);
expect(props.setFilters).toHaveBeenCalledWith({
courseGradeMax: testVal,
});
});
});
describe('isFilterValueInRange', () => {

View File

@@ -1,11 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`StudentGroupsFilters Component snapshots basic snapshot 1`] = `
<Collapsible
className="filter-group mb-3"
defaultOpen={true}
title="Student Groups"
>
exports[`StudentGroupsFilter Component snapshots basic snapshot 1`] = `
<React.Fragment>
<SelectGroup
disabled={false}
id="Tracks"
@@ -68,10 +64,10 @@ exports[`StudentGroupsFilters Component snapshots basic snapshot 1`] = `
}
value="cohorT3"
/>
</Collapsible>
</React.Fragment>
`;
exports[`StudentGroupsFilters Component snapshots mapCohortsEntries cohort options: [Cohort-All, <{slug, name}...>] 1`] = `
exports[`StudentGroupsFilter Component snapshots mapCohortsEntries cohort options: [Cohort-All, <{slug, name}...>] 1`] = `
Array [
<option
value="Cohort-All"
@@ -96,7 +92,7 @@ Array [
]
`;
exports[`StudentGroupsFilters Component snapshots mapTracksEntries cohort options: [Track-All, <{id, name}...>] 1`] = `
exports[`StudentGroupsFilter Component snapshots mapTracksEntries cohort options: [Track-All, <{id, name}...>] 1`] = `
Array [
<option
value="Track-All"

View File

@@ -3,14 +3,10 @@ import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import {
Collapsible,
} from '@edx/paragon';
import { fetchGrades } from 'data/actions/grades';
import SelectGroup from '../SelectGroup';
export class StudentGroupsFilters extends React.Component {
export class StudentGroupsFilter extends React.Component {
constructor(props) {
super(props);
this.updateCohorts = this.updateCohorts.bind(this);
@@ -89,11 +85,7 @@ export class StudentGroupsFilters extends React.Component {
render() {
return (
<Collapsible
title="Student Groups"
defaultOpen
className="filter-group mb-3"
>
<>
<SelectGroup
id="Tracks"
label="Tracks"
@@ -109,12 +101,12 @@ export class StudentGroupsFilters extends React.Component {
onChange={this.updateCohorts}
options={this.mapCohortsEntries()}
/>
</Collapsible>
</>
);
}
}
StudentGroupsFilters.defaultProps = {
StudentGroupsFilter.defaultProps = {
/** testing
cohorts: [
{ name: 'Fake Cohort 1', id: 'fake_cohort_1' },
@@ -134,7 +126,7 @@ StudentGroupsFilters.defaultProps = {
tracks: [],
};
StudentGroupsFilters.propTypes = {
StudentGroupsFilter.propTypes = {
courseId: PropTypes.string,
updateQueryParams: PropTypes.func.isRequired,
@@ -165,4 +157,4 @@ export const mapDispatchToProps = {
getUserGrades: fetchGrades,
};
export default connect(mapStateToProps, mapDispatchToProps)(StudentGroupsFilters);
export default connect(mapStateToProps, mapDispatchToProps)(StudentGroupsFilter);

View File

@@ -5,7 +5,7 @@ import { shallow } from 'enzyme';
import { fetchGrades } from 'data/actions/grades';
import {
StudentGroupsFilters,
StudentGroupsFilter,
mapStateToProps,
mapDispatchToProps,
} from '.';
@@ -14,7 +14,7 @@ jest.mock('@edx/paragon', () => ({
Collapsible: 'Collapsible',
}));
describe('StudentGroupsFilters', () => {
describe('StudentGroupsFilter', () => {
let props = {
courseId: '12345',
cohorts: [
@@ -44,7 +44,7 @@ describe('StudentGroupsFilters', () => {
describe('snapshots', () => {
let el;
beforeEach(() => {
el = shallow(<StudentGroupsFilters {...props} />);
el = shallow(<StudentGroupsFilter {...props} />);
});
test('basic snapshot', () => {
el.instance().updateTracks = jest.fn().mockName(
@@ -70,7 +70,7 @@ describe('StudentGroupsFilters', () => {
describe('behavior', () => {
let el;
beforeEach(() => {
el = shallow(<StudentGroupsFilters {...props} />);
el = shallow(<StudentGroupsFilter {...props} />);
});
describe('mapSelectedCohortEntry', () => {
it('returns the name of the cohort with the same numerical id', () => {
@@ -135,7 +135,7 @@ describe('StudentGroupsFilters', () => {
describe('updateTracks', () => {
const selectedSlug = 'SLUG';
beforeEach(() => {
el = shallow(<StudentGroupsFilters {...props} />);
el = shallow(<StudentGroupsFilter {...props} />);
jest.spyOn(
el.instance(),
'selectedTrackSlugFromEvent',
@@ -159,7 +159,7 @@ describe('StudentGroupsFilters', () => {
describe('updateCohorts', () => {
const selectedId = 23;
beforeEach(() => {
el = shallow(<StudentGroupsFilters {...props} />);
el = shallow(<StudentGroupsFilter {...props} />);
jest.spyOn(
el.instance(),
'selectedCohortIdFromEvent',

View File

@@ -0,0 +1,135 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`GradebookFilters Component snapshots basic snapshot 1`] = `
<React.Fragment>
<ForwardRef
className="filter-group mb-3"
defaultOpen={true}
iconWhenClosed={
<withDeprecatedProps(Icon)
src={[Function]}
/>
}
iconWhenOpen={
<withDeprecatedProps(Icon)
src={[Function]}
/>
}
styling="card"
title="Assignments"
>
<div>
<Connect(AssignmentTypeFilter)
updateQueryParams={[MockFunction]}
/>
<Connect(AssignmentFilter)
courseId="12345"
updateQueryParams={[MockFunction]}
/>
<Connect(AssignmentGradeFilter)
courseId="12345"
filterValues={
Object {
"assignmentGradeMax": "90",
"assignmentGradeMin": "10",
"courseGradeMax": "80",
"courseGradeMin": "20",
}
}
setFilters={[MockFunction]}
updateQueryParams={[MockFunction]}
/>
</div>
</ForwardRef>
<ForwardRef
className="filter-group mb-3"
defaultOpen={true}
iconWhenClosed={
<withDeprecatedProps(Icon)
src={[Function]}
/>
}
iconWhenOpen={
<withDeprecatedProps(Icon)
src={[Function]}
/>
}
styling="card"
title="Overall Grade"
>
<Connect(CourseGradeFilter)
courseId="12345"
filterValues={
Object {
"assignmentGradeMax": "90",
"assignmentGradeMin": "10",
"courseGradeMax": "80",
"courseGradeMin": "20",
}
}
setFilters={[MockFunction]}
updateQueryParams={[MockFunction]}
/>
</ForwardRef>
<ForwardRef
className="filter-group mb-3"
defaultOpen={true}
iconWhenClosed={
<withDeprecatedProps(Icon)
src={[Function]}
/>
}
iconWhenOpen={
<withDeprecatedProps(Icon)
src={[Function]}
/>
}
styling="card"
title="Student Groups"
>
<Connect(StudentGroupsFilter)
courseId="12345"
updateQueryParams={[MockFunction]}
/>
</ForwardRef>
<ForwardRef
className="filter-group mb-3"
defaultOpen={true}
iconWhenClosed={
<withDeprecatedProps(Icon)
src={[Function]}
/>
}
iconWhenOpen={
<withDeprecatedProps(Icon)
src={[Function]}
/>
}
styling="card"
title="Include Course Team Members"
>
<ForwardRef
checked={true}
controlAs={
Object {
"$$typeof": Symbol(react.forward_ref),
"defaultProps": Object {
"className": undefined,
"isIndeterminate": false,
},
"propTypes": Object {
"className": [Function],
"isIndeterminate": [Function],
},
"render": [Function],
}
}
isInvalid={false}
isValid={false}
onChange={[MockFunction handleIncludeTeamMembersChange]}
>
Include Course Team Members
</ForwardRef>
</ForwardRef>
</React.Fragment>
`;

View File

@@ -0,0 +1,115 @@
/* eslint-disable react/sort-comp, import/no-named-as-default */
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Collapsible, Form } from '@edx/paragon';
import * as filterActions from 'data/actions/filters';
import AssignmentTypeFilter from './AssignmentTypeFilter';
import AssignmentFilter from './AssignmentFilter';
import AssignmentGradeFilter from './AssignmentGradeFilter';
import CourseGradeFilter from './CourseGradeFilter';
import StudentGroupsFilter from './StudentGroupsFilter';
export class GradebookFilters extends React.Component {
constructor(props) {
super(props);
this.state = {
includeCourseRoleMembers: this.props.includeCourseRoleMembers,
};
this.handleIncludeTeamMembersChange = this.handleIncludeTeamMembersChange.bind(this);
}
handleIncludeTeamMembersChange(includeCourseRoleMembers) {
this.setState({ includeCourseRoleMembers });
this.props.updateIncludeCourseRoleMembers(includeCourseRoleMembers);
this.props.updateQueryParams({ includeCourseRoleMembers });
}
collapsibleGroup = (title, content) => (
<Collapsible title={title} defaultOpen className="filter-group mb-3">
{content}
</Collapsible>
);
render() {
const {
courseId,
filterValues,
setFilters,
updateQueryParams,
} = this.props;
return (
<>
{this.collapsibleGroup('Assignments', (
<div>
<AssignmentTypeFilter
updateQueryParams={updateQueryParams}
/>
<AssignmentFilter
courseId={courseId}
updateQueryParams={updateQueryParams}
/>
<AssignmentGradeFilter
{...{
courseId,
filterValues,
setFilters,
updateQueryParams,
}}
/>
</div>
))}
{this.collapsibleGroup('Overall Grade', (
<CourseGradeFilter
{...{
filterValues,
setFilters,
courseId,
updateQueryParams,
}}
/>
))}
{this.collapsibleGroup('Student Groups', (
<StudentGroupsFilter
courseId={courseId}
updateQueryParams={updateQueryParams}
/>
))}
{this.collapsibleGroup('Include Course Team Members', (
<Form.Checkbox
checked={this.state.includeCourseRoleMembers}
onChange={this.handleIncludeTeamMembersChange}
>
Include Course Team Members
</Form.Checkbox>
))}
</>
);
}
}
GradebookFilters.propTypes = {
courseId: PropTypes.string.isRequired,
filterValues: PropTypes.shape({
assignmentGradeMin: PropTypes.string,
assignmentGradeMax: PropTypes.string,
courseGradeMin: PropTypes.string,
courseGradeMax: PropTypes.string,
}).isRequired,
setFilters: PropTypes.func.isRequired,
includeCourseRoleMembers: PropTypes.bool.isRequired,
updateIncludeCourseRoleMembers: PropTypes.func.isRequired,
updateQueryParams: PropTypes.func.isRequired,
};
export const mapStateToProps = (state) => ({
includeCourseRoleMembers: state.filters.includeCourseRoleMembers,
});
export const mapDispatchToProps = {
updateIncludeCourseRoleMembers: filterActions.updateIncludeCourseRoleMembers,
};
export default connect(mapStateToProps, mapDispatchToProps)(GradebookFilters);

View File

@@ -0,0 +1,85 @@
import React from 'react';
import { shallow } from 'enzyme';
import { updateIncludeCourseRoleMembers } from 'data/actions/filters';
import {
GradebookFilters,
mapStateToProps,
mapDispatchToProps,
} from '.';
describe('GradebookFilters', () => {
let props = {
courseId: '12345',
filterValues: {
assignmentGradeMin: '10',
assignmentGradeMax: '90',
courseGradeMin: '20',
courseGradeMax: '80',
},
includeCourseRoleMembers: true,
};
beforeEach(() => {
props = {
...props,
updateQueryParams: jest.fn(),
updateIncludeCourseRoleMembers: jest.fn(),
setFilters: jest.fn(),
};
});
describe('Component', () => {
describe('behavior', () => {
describe('handleIncludeTeamMembersChange', () => {
let el;
beforeEach(() => {
el = shallow(<GradebookFilters {...props} />);
});
it('calls props.updateIncludeCourseRoleMembers with newVal', () => {
el.instance().handleIncludeTeamMembersChange(false);
expect(
props.updateIncludeCourseRoleMembers,
).toHaveBeenCalledWith(false);
});
it('calls props.updateIncludeCourseRoleMembers with newVal', () => {
el.instance().handleIncludeTeamMembersChange(true);
expect(
props.updateQueryParams,
).toHaveBeenCalledWith({ includeCourseRoleMembers: true });
});
});
});
describe('snapshots', () => {
test('basic snapshot', () => {
const el = shallow(<GradebookFilters {...props} />);
el.instance().handleIncludeTeamMembersChange = jest.fn().mockName(
'handleIncludeTeamMembersChange',
);
expect(el.instance().render()).toMatchSnapshot();
});
});
});
describe('mapStateToProps', () => {
const state = {
filters: {
includeCourseRoleMembers: 'plz do',
},
};
describe('includeCourseRoleMembers', () => {
it('is drawn from filters.includeCourseRoleMembers', () => {
expect(mapStateToProps(state).includeCourseRoleMembers).toEqual(
state.filters.includeCourseRoleMembers,
);
});
});
});
describe('mapDispatchToProps', () => {
test('updateIncludeCourseRoleMembers', () => {
expect(mapDispatchToProps.updateIncludeCourseRoleMembers).toEqual(
updateIncludeCourseRoleMembers,
);
});
});
});

View File

@@ -1,27 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Assignments Component snapshots basic snapshot 1`] = `
<Collapsible
className="filter-group mb-3"
defaultOpen={true}
title="Assignments"
>
<div>
<Connect(AssignmentTypeFilter)
updateQueryParams={[MockFunction updateQueryParams]}
/>
<Connect(AssignmentFilter)
courseId="12345"
updateQueryParams={[MockFunction updateQueryParams]}
/>
<Connect(AssignmentGradeFilter)
assignmentGradeMax="92"
assignmentGradeMin="5"
courseId="12345"
setAssignmentGradeMax={[MockFunction setAssignmentGradeMax]}
setAssignmentGradeMin={[MockFunction setAssignmentGradeMin]}
updateQueryParams={[MockFunction updateQueryParams]}
/>
</div>
</Collapsible>
`;

View File

@@ -1,49 +0,0 @@
/* eslint-disable react/sort-comp, import/no-named-as-default */
import React from 'react';
import PropTypes from 'prop-types';
import { Collapsible } from '@edx/paragon';
import AssignmentTypeFilter from './AssignmentTypeFilter';
import AssignmentFilter from './AssignmentFilter';
import AssignmentGradeFilter from './AssignmentGradeFilter';
export const AssignmentFilters = ({
courseId,
assignmentGradeMax,
assignmentGradeMin,
setAssignmentGradeMax,
setAssignmentGradeMin,
updateQueryParams,
}) => (
<Collapsible title="Assignments" defaultOpen className="filter-group mb-3">
<div>
<AssignmentTypeFilter
updateQueryParams={updateQueryParams}
/>
<AssignmentFilter
courseId={courseId}
updateQueryParams={updateQueryParams}
/>
<AssignmentGradeFilter
courseId={courseId}
assignmentGradeMin={assignmentGradeMin}
assignmentGradeMax={assignmentGradeMax}
setAssignmentGradeMin={setAssignmentGradeMin}
setAssignmentGradeMax={setAssignmentGradeMax}
updateQueryParams={updateQueryParams}
/>
</div>
</Collapsible>
);
AssignmentFilters.propTypes = {
assignmentGradeMin: PropTypes.string.isRequired,
assignmentGradeMax: PropTypes.string.isRequired,
courseId: PropTypes.string.isRequired,
setAssignmentGradeMin: PropTypes.func.isRequired,
setAssignmentGradeMax: PropTypes.func.isRequired,
updateQueryParams: PropTypes.func.isRequired,
};
export default AssignmentFilters;

View File

@@ -1,36 +0,0 @@
/* eslint-disable import/no-named-as-default */
import React from 'react';
import { shallow } from 'enzyme';
import AssignmentFilters from '.';
jest.mock('@edx/paragon', () => ({
Collapsible: 'Collapsible',
}));
describe('Assignments', () => {
const props = {
assignmentGradeMin: '5',
assignmentGradeMax: '92',
courseId: '12345',
setAssignmentGradeMin: jest.fn().mockName('setAssignmentGradeMin'),
setAssignmentGradeMax: jest.fn().mockName('setAssignmentGradeMax'),
updateQueryParams: jest.fn().mockName('updateQueryParams'),
};
describe('Component', () => {
describe('snapshots', () => {
test('basic snapshot', () => {
const el = shallow(<AssignmentFilters {...props} />);
expect(el).toMatchSnapshot();
});
});
});
describe('mapStateToProps', () => {
});
describe('mapDispatchToProps', () => {
});
});

View File

@@ -3,8 +3,6 @@ import React from 'react';
import PropTypes from 'prop-types';
import {
Button,
Collapsible,
CheckBox,
Icon,
InputSelect,
SearchField,
@@ -26,9 +24,7 @@ import BulkManagementControls from './BulkManagementControls';
import EditModal from './EditModal';
import GradebookTable from './GradebookTable';
import SearchControls from './SearchControls';
import AssignmentFilters from './filters/AssignmentFilters';
import CourseGradeFilters from './filters/CourseGradeFilters';
import StudentGroupsFilters from './filters/StudentGroupsFilters';
import GradebookFilters from './GradebookFilters';
export default class Gradebook extends React.Component {
constructor(props) {
@@ -241,11 +237,6 @@ export default class Gradebook extends React.Component {
);
}
handleIncludeTeamMembersChange = (includeCourseRoleMembers) => {
this.props.updateIncludeCourseRoleMembers(includeCourseRoleMembers);
this.updateQueryParams({ includeCourseRoleMembers });
};
createStateFieldSetter = (key) => (value) => this.setState({ [key]: value });
createStateFieldOnChange = (key) => ({ target }) => this.setState({ [key]: target.value });
@@ -270,6 +261,22 @@ export default class Gradebook extends React.Component {
'updateUserName',
);
setFilters = this.createLimitedSetter(
'assignmentGradeMin',
'assignmentGradeMax',
'courseGradeMin',
'courseGradeMax',
'isMinCourseGradeFilterValid',
'isMaxCourseGradeFilterValid',
);
filterValues = () => ({
assignmentGradeMin: this.state.assignmentGradeMin,
assignmentGradeMax: this.state.assignmentGradeMax,
courseGradeMin: this.state.courseGradeMin,
courseGradeMax: this.state.courseGradeMax,
});
render() {
return (
<Drawer
@@ -396,39 +403,12 @@ export default class Gradebook extends React.Component {
</>
)}
>
<AssignmentFilters
assignmentGradeMin={this.state.assignmentGradeMin}
assignmentGradeMax={this.state.assignmentGradeMax}
courseId={this.props.courseId}
setAssignmentGradeMin={this.createStateFieldSetter('assignmentGradeMin')}
setAssignmentGradeMax={this.createStateFieldSetter('assignmentGradeMax')}
<GradebookFilters
setFilters={this.setFilters}
filterValues={this.filterValues()}
updateQueryParams={this.updateQueryParams}
/>
<CourseGradeFilters
{...{
courseGradeMin: this.state.courseGradeMin,
courseGradeMax: this.state.courseGradeMax,
courseId: this.props.courseId,
setCourseGradeMin: this.createStateFieldSetter('courseGradeMin'),
setCourseGradeMax: this.createStateFieldSetter('courseGradeMax'),
setIsMaxCourseGradeFilterValid: this.createStateFieldSetter('isMinCourseGradeFilterValid'),
setIsMinCourseGradeFilterValid: this.createStateFieldSetter('isMaxCourseGradeFilterValid'),
updateQueryParams: this.updateQueryParams,
}}
/>
<StudentGroupsFilters
courseId={this.props.courseId}
updateQueryParams={this.updateQueryParams}
/>
<Collapsible title="Include Course Team Members" className="filter-group mb-3">
<CheckBox
name="include-course-team-members"
aria-label="Include Course Team Members"
label="Include Course Team Members"
checked={this.props.includeCourseRoleMembers}
onChange={this.handleIncludeTeamMembersChange}
/>
</Collapsible>
</Drawer>
);
}
@@ -452,7 +432,6 @@ Gradebook.defaultProps = {
showBulkManagement: false,
showSpinner: false,
totalUsersCount: null,
includeCourseRoleMembers: false,
tracks: [
{ name: 'Fake Track 1', id: 'fake_track_1' },
{ name: 'Fake Track 2', id: 'fake_track_2' },
@@ -493,6 +472,4 @@ Gradebook.propTypes = {
name: PropTypes.string,
})),
updateCourseGradeFilter: PropTypes.func.isRequired,
includeCourseRoleMembers: PropTypes.bool,
updateIncludeCourseRoleMembers: PropTypes.func.isRequired,
};

View File

@@ -20,7 +20,6 @@ import {
updateAssignmentFilter,
updateAssignmentLimits,
updateCourseGradeFilter,
updateIncludeCourseRoleMembers,
} from '../../data/actions/filters';
import stateHasMastersTrack from '../../data/selectors/tracks';
import {
@@ -113,7 +112,6 @@ const mapStateToProps = (state, ownProps) => (
tracks: state.tracks.results,
uploadSuccess: !!(state.grades.bulkManagement
&& state.grades.bulkManagement.uploadSuccess),
includeCourseRoleMembers: state.filters.includeCourseRoleMembers,
}
);
@@ -136,7 +134,6 @@ const mapDispatchToProps = {
updateAssignmentFilter,
updateAssignmentLimits,
updateCourseGradeFilter,
updateIncludeCourseRoleMembers,
};
const GradebookPage = connect(