Skip to content

Commit

Permalink
Split SearchControls into separate component (#174)
Browse files Browse the repository at this point in the history
* Split SearchControls into separate component

* Add testing config

* Add webpack config

* Add snapshot tests for Search Controls

* bump version and update package-lock format
  • Loading branch information
nsprenkle authored Apr 28, 2021
1 parent 2729644 commit 84a7531
Show file tree
Hide file tree
Showing 10 changed files with 43,263 additions and 2,467 deletions.
11 changes: 11 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
const { createConfig } = require('@edx/frontend-build');

module.exports = createConfig('jest', {
setupFilesAfterEnv: [
'<rootDir>/src/setupTest.js',
],
modulePaths: ['<rootDir>/src/'],
snapshotSerializers: [
'enzyme-to-json/serializer',
],
});
45,381 changes: 42,945 additions & 2,436 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@edx/frontend-app-gradebook",
"version": "1.4.20",
"version": "1.4.21",
"description": "edx editable gradebook-ui to manipulate grade overrides on subsections",
"repository": {
"type": "git",
Expand Down Expand Up @@ -38,6 +38,7 @@
"classnames": "^2.2.6",
"core-js": "3.6.5",
"email-prop-type": "^1.1.7",
"enzyme-to-json": "^3.6.2",
"font-awesome": "4.7.0",
"history": "4.10.1",
"node-sass": "^4.14.1",
Expand Down Expand Up @@ -69,6 +70,7 @@
"fetch-mock": "^6.5.2",
"husky": "2.7.0",
"identity-obj-proxy": "^3.0.0",
"jest": "24.9.0",
"react-dev-utils": "^5.0.3",
"react-test-renderer": "^16.10.1",
"redux-mock-store": "^1.5.3",
Expand Down
110 changes: 110 additions & 0 deletions src/components/Gradebook/SearchControls.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';

import { Button, Icon, SearchField } from '@edx/paragon';

import {
fetchGrades,
fetchMatchingUserGrades,
} from '../../data/actions/grades';

/**
* 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.onSubmit = this.onSubmit.bind(this);
this.onChange = this.onChange.bind(this);
this.onClear = this.onClear.bind(this);
}

/** Submitting searches for user matching the username/email in `value` */
onSubmit(value) {
this.props.searchForUser(
this.props.courseId,
value,
this.props.selectedCohort,
this.props.selectedTrack,
this.props.selectedAssignmentType,
);
}

/** Changing the search value stores the key in Gradebook. Currently unused */
onChange(filterValue) {
this.props.setFilterValue(filterValue);
}

/** Clearing the search box falls back to showing students with already applied filters */
onClear() {
this.props.getUserGrades(
this.props.courseId,
this.props.selectedCohort,
this.props.selectedTrack,
this.props.selectedAssignmentType,
);
}

render() {
return (
<>
<h4>Step 1: Filter the Grade Report</h4>
<div className="d-flex justify-content-between">
<Button
id="edit-filters-btn"
className="btn-primary align-self-start"
onClick={this.props.toggleFilterDrawer}
>
<Icon className="fa fa-filter" /> Edit Filters
</Button>
<div>
<SearchField
onSubmit={this.onSubmit}
inputLabel="Search for a learner"
onChange={this.onChange}
onClear={this.onClear}
value={this.props.filterValue}
/>
<small className="form-text text-muted search-help-text">Search by username, email, or student key</small>
</div>
</div>
</>
);
}
}

SearchControls.defaultProps = {
courseId: '',
filterValue: '',
selectedAssignmentType: '',
selectedCohort: null,
selectedTrack: null,
};

SearchControls.propTypes = {
courseId: PropTypes.string,
filterValue: PropTypes.string,
setFilterValue: PropTypes.func.isRequired,
toggleFilterDrawer: PropTypes.func.isRequired,
// From Redux
getUserGrades: PropTypes.func.isRequired,
searchForUser: PropTypes.func.isRequired,
selectedAssignmentType: PropTypes.string,
selectedCohort: PropTypes.string,
selectedTrack: PropTypes.string,
};

export const mapStateToProps = (state) => ({
selectedAssignmentType: state.filters.assignmentType,
selectedTrack: state.filters.track,
selectedCohort: state.filters.cohort,
});

export const mapDispatchToProps = {
getUserGrades: fetchGrades,
searchForUser: fetchMatchingUserGrades,
};

export default connect(mapStateToProps, mapDispatchToProps)(SearchControls);
118 changes: 118 additions & 0 deletions src/components/Gradebook/SearchControls.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import React from 'react';
import { shallow } from 'enzyme';

import {
fetchGrades,
fetchMatchingUserGrades,
} from '../../data/actions/grades';
import { mapDispatchToProps, mapStateToProps, SearchControls } from './SearchControls';

jest.mock('@edx/paragon', () => ({
Icon: 'Icon',
Button: 'Button',
SearchField: 'SearchField',
}));

describe('SearchControls', () => {
let props;

beforeEach(() => {
jest.resetAllMocks();
props = {
courseId: 'course-v1:edX+DEV101+T1',
filterValue: 'alice',
selectedAssignmentType: 'homework',
selectedCohort: 'spring term',
selectedTrack: 'masters',
getUserGrades: jest.fn(),
searchForUser: jest.fn(),
setFilterValue: jest.fn(),
toggleFilterDrawer: jest.fn().mockName('toggleFilterDrawer'),
};
});

const searchControls = (overriddenProps) => {
props = { ...props, ...overriddenProps };
return shallow(<SearchControls {...props} />);
};

describe('Component', () => {
describe('onSubmit', () => {
it('calls props.searchForUser with correct data', () => {
const wrapper = searchControls();
wrapper.instance().onSubmit('bob');

expect(props.searchForUser).toHaveBeenCalledWith(
props.courseId,
'bob',
props.selectedCohort,
props.selectedTrack,
props.selectedAssignmentType,
);
});
});

describe('onChange', () => {
it('saves the changed search value to Gradebook state', () => {
const wrapper = searchControls();
wrapper.instance().onChange('bob');
expect(props.setFilterValue).toHaveBeenCalledWith('bob');
});
});

describe('onClear', () => {
it('re-runs search with existing filters', () => {
const wrapper = searchControls();
wrapper.instance().onClear();
expect(props.getUserGrades).toHaveBeenCalledWith(
props.courseId,
props.selectedCohort,
props.selectedTrack,
props.selectedAssignmentType,
);
});
});

describe('mapStateToProps', () => {
const state = {
filters: {
assignmentType: 'labs',
track: 'honor',
cohort: 'fall term',
},
};

it('maps assignment type filter correctly', () => {
expect(mapStateToProps(state).selectedAssignmentType).toEqual(state.filters.assignmentType);
});

it('maps track filter correctly', () => {
expect(mapStateToProps(state).selectedTrack).toEqual(state.filters.track);
});

it('maps cohort filter correctly', () => {
expect(mapStateToProps(state).selectedCohort).toEqual(state.filters.cohort);
});
});

describe('mapDispatchToProps', () => {
test('getUserGrades', () => {
expect(mapDispatchToProps.getUserGrades).toEqual(fetchGrades);
});

test('searchForUser', () => {
expect(mapDispatchToProps.searchForUser).toEqual(fetchMatchingUserGrades);
});
});

describe('Snapshots', () => {
test('basic snapshot', () => {
const wrapper = searchControls();
wrapper.instance().onChange = jest.fn().mockName('onChange');
wrapper.instance().onClear = jest.fn().mockName('onClear');
wrapper.instance().onSubmit = jest.fn().mockName('onSubmit');
expect(wrapper.instance().render()).toMatchSnapshot();
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`SearchControls Component Snapshots basic snapshot 1`] = `
<React.Fragment>
<h4>
Step 1: Filter the Grade Report
</h4>
<div
className="d-flex justify-content-between"
>
<Button
className="btn-primary align-self-start"
id="edit-filters-btn"
onClick={[MockFunction toggleFilterDrawer]}
>
<Icon
className="fa fa-filter"
/>
Edit Filters
</Button>
<div>
<SearchField
inputLabel="Search for a learner"
onChange={[MockFunction onChange]}
onClear={[MockFunction onClear]}
onSubmit={[MockFunction onSubmit]}
value="alice"
/>
<small
className="form-text text-muted search-help-text"
>
Search by username, email, or student key
</small>
</div>
</div>
</React.Fragment>
`;
41 changes: 13 additions & 28 deletions src/components/Gradebook/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,10 @@ import PropTypes from 'prop-types';
import {
Button,
Collapsible,
Icon,
CheckBox,
Icon,
InputSelect,
InputText,
SearchField,
StatusAlert,
Tab,
Tabs,
Expand All @@ -27,6 +26,7 @@ import BulkManagement from './BulkManagement';
import BulkManagementControls from './BulkManagementControls';
import EditModal from './EditModal';
import GradebookTable from './GradebookTable';
import SearchControls from './SearchControls';

export default class Gradebook extends React.Component {
constructor(props) {
Expand Down Expand Up @@ -259,6 +259,7 @@ export default class Gradebook extends React.Component {
'adjustedGradePossible',
'adjustedGradeValue',
'assignmentName',
'filterValue',
'modalOpen',
'reasonForChange',
'todaysDate',
Expand Down Expand Up @@ -294,32 +295,17 @@ export default class Gradebook extends React.Component {
)}
<Tabs defaultActiveKey="grades">
<Tab eventKey="grades" title="Grades">
<h4>Step 1: Filter the Grade Report</h4>
<div className="d-flex justify-content-between">
{this.props.showSpinner && <div className="spinner-overlay"><Icon className="fa fa-spinner fa-spin fa-5x color-black" /></div>}
<Button className="btn-primary align-self-start" onClick={toggleFilterDrawer}><FontAwesomeIcon icon={faFilter} /> Edit Filters</Button>
<div>
<SearchField
onSubmit={value => this.props.searchForUser(
this.props.courseId,
value,
this.props.selectedCohort,
this.props.selectedTrack,
this.props.selectedAssignmentType,
)}
inputLabel="Search for a learner"
onChange={filterValue => this.setState({ filterValue })}
onClear={() => this.props.getUserGrades(
this.props.courseId,
this.props.selectedCohort,
this.props.selectedTrack,
this.props.selectedAssignmentType,
)}
value={this.state.filterValue}
/>
<small className="form-text text-muted search-help-text">Search by username, email, or student key</small>
{this.props.showSpinner && (
<div className="spinner-overlay">
<Icon className="fa fa-spinner fa-spin fa-5x color-black" />
</div>
</div>
)}
<SearchControls
courseId={this.props.courseId}
filterValue={this.state.filterValue}
setFilterValue={this.createStateFieldSetter('filterValue')}
toggleFilterDrawer={toggleFilterDrawer}
/>
<ConnectedFilterBadges
handleFilterBadgeClose={this.handleFilterBadgeClose}
/>
Expand Down Expand Up @@ -527,7 +513,6 @@ Gradebook.propTypes = {
search: PropTypes.string,
}),
resetFilters: PropTypes.func.isRequired,
searchForUser: PropTypes.func.isRequired,
selectedAssignmentType: PropTypes.string,
selectedCohort: PropTypes.string,
selectedTrack: PropTypes.string,
Expand Down
2 changes: 0 additions & 2 deletions src/containers/GradebookPage/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {
closeBanner,
fetchGradeOverrideHistory,
fetchGrades,
fetchMatchingUserGrades,
fetchPrevNextGrades,
filterAssignmentType,
submitFileUploadFormData,
Expand Down Expand Up @@ -132,7 +131,6 @@ const mapDispatchToProps = {
getUserGrades: fetchGrades,
initializeFilters,
resetFilters,
searchForUser: fetchMatchingUserGrades,
submitFileUploadFormData,
toggleFormat: toggleGradeFormat,
updateAssignmentFilter,
Expand Down
Loading

0 comments on commit 84a7531

Please sign in to comment.