diff --git a/package.json b/package.json
index f1415c2..26c24c8 100755
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@edx/frontend-app-gradebook",
- "version": "1.4.32",
+ "version": "1.4.33",
"description": "edx editable gradebook-ui to manipulate grade overrides on subsections",
"repository": {
"type": "git",
diff --git a/src/App.scss b/src/App.scss
index f63c0bf..1af5934 100755
--- a/src/App.scss
+++ b/src/App.scss
@@ -11,5 +11,5 @@ $input-focus-box-shadow: $input-box-shadow; // hack to get upgrade to paragon 4.
@import "~@edx/frontend-component-footer/dist/_footer";
-@import "./components/Gradebook/gradebook";
-@import "./components/Drawer/Drawer";
+@import "./components/GradesTab/GradesTab";
+@import "./components/WithSidebar/WithSidebar";
diff --git a/src/components/Gradebook/BulkManagement.jsx b/src/components/BulkManagement.jsx
similarity index 100%
rename from src/components/Gradebook/BulkManagement.jsx
rename to src/components/BulkManagement.jsx
diff --git a/src/components/Drawer/index.jsx b/src/components/Drawer/index.jsx
deleted file mode 100644
index 1052f9e..0000000
--- a/src/components/Drawer/index.jsx
+++ /dev/null
@@ -1,85 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import classNames from 'classnames';
-import { Button } from '@edx/paragon';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { faTimes } from '@fortawesome/free-solid-svg-icons';
-
-export default class Drawer extends React.Component {
- constructor(props) {
- super(props);
- this.state = {
- open: props.initiallyOpen,
- transitioning: false,
- };
- }
-
- close = () => {
- if (this.state.open) {
- this.toggleOpen();
- }
- };
-
- toggleOpen = () => {
- this.setState({ transitioning: true });
- // defer the transition to the next repaint so we can be sure that
- // opening drawer is visible before it transitions
- // (the start state of the opening animation doesn't work if the element starts hidden)
- this.deferToNextRepaint(() => this.setState(prevState => ({ open: !prevState.open })));
- };
-
- handleSlideDone = (e) => {
- if (e.currentTarget === e.target) {
- this.setState({ transitioning: false });
- }
- };
-
- deferToNextRepaint(callback) {
- window.requestAnimationFrame(() => window.setTimeout(callback, 0));
- }
-
- render() {
- return (
-
-
-
- {this.props.mainContent(this.toggleOpen)}
-
-
- );
- }
-}
-
-Drawer.propTypes = {
- initiallyOpen: PropTypes.bool.isRequired,
- children: PropTypes.node.isRequired,
- mainContent: PropTypes.func.isRequired,
- title: PropTypes.node.isRequired,
-};
diff --git a/src/components/Gradebook/index.jsx b/src/components/Gradebook/index.jsx
deleted file mode 100644
index 237e308..0000000
--- a/src/components/Gradebook/index.jsx
+++ /dev/null
@@ -1,133 +0,0 @@
-/* eslint-disable react/sort-comp, react/button-has-type, import/no-named-as-default */
-import React from 'react';
-import PropTypes from 'prop-types';
-import {
- Tab,
- Tabs,
-} from '@edx/paragon';
-import queryString from 'query-string';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { faFilter } from '@fortawesome/free-solid-svg-icons';
-import PageButtons from '../PageButtons';
-import Drawer from '../Drawer';
-import ConnectedFilterBadges from '../FilterBadges';
-
-import GradebookHeader from './GradebookHeader';
-import BulkManagement from './BulkManagement';
-import BulkManagementControls from './BulkManagementControls';
-import EditModal from './EditModal';
-import GradebookFilters from './GradebookFilters';
-import GradebookTable from './GradebookTable';
-import SearchControls from './SearchControls';
-import StatusAlerts from './StatusAlerts';
-import SpinnerIcon from './SpinnerIcon';
-import ScoreViewInput from './ScoreViewInput';
-import UsersLabel from './UsersLabel';
-
-export default class Gradebook extends React.Component {
- constructor(props) {
- super(props);
- this.myRef = React.createRef();
- }
-
- componentDidMount() {
- const urlQuery = queryString.parse(this.props.location.search);
- this.props.initializeApp(this.props.courseId, urlQuery);
- }
-
- getActiveTabs = () => (
- this.props.showBulkManagement ? ['Grades', 'BulkManagement'] : ['Grades']
- );
-
- updateQueryParams = (queryParams) => {
- const parsed = queryString.parse(this.props.location.search);
- Object.keys(queryParams).forEach((key) => {
- if (queryParams[key]) {
- parsed[key] = queryParams[key];
- } else {
- delete parsed[key];
- }
- });
- this.props.history.push(`?${queryString.stringify(parsed)}`);
- };
-
- handleFilterBadgeClose = filterNames => () => {
- this.props.resetFilters(filterNames);
- this.updateQueryParams(filterNames.reduce(
- (obj, filterName) => ({ ...obj, [filterName]: false }),
- {},
- ));
- this.props.fetchGrades();
- }
-
- render() {
- return (
- (
-
-
-
-
-
-
-
-
-
- Step 2: View or Modify Individual Grades
-
-
-
-
-
-
-
-
-
-
-
- * available for learners in the Master's track only
-
-
- {this.props.showBulkManagement
- && (
-
-
-
- )}
-
-
- )}
- initiallyOpen={false}
- title={(
- <>
- Filter By...
- >
- )}
- >
-
-
- );
- }
-}
-
-Gradebook.defaultProps = {
- courseId: '',
- location: {
- search: '',
- },
- showBulkManagement: false,
-};
-
-Gradebook.propTypes = {
- courseId: PropTypes.string,
- fetchGrades: PropTypes.func.isRequired,
- history: PropTypes.shape({
- push: PropTypes.func,
- }).isRequired,
- initializeApp: PropTypes.func.isRequired,
- location: PropTypes.shape({
- search: PropTypes.string,
- }),
- resetFilters: PropTypes.func.isRequired,
- showBulkManagement: PropTypes.bool,
-};
diff --git a/src/components/Gradebook/GradebookFilters/AssignmentFilter/__snapshots__/test.jsx.snap b/src/components/GradebookFilters/AssignmentFilter/__snapshots__/test.jsx.snap
similarity index 100%
rename from src/components/Gradebook/GradebookFilters/AssignmentFilter/__snapshots__/test.jsx.snap
rename to src/components/GradebookFilters/AssignmentFilter/__snapshots__/test.jsx.snap
diff --git a/src/components/Gradebook/GradebookFilters/AssignmentFilter/index.jsx b/src/components/GradebookFilters/AssignmentFilter/index.jsx
similarity index 100%
rename from src/components/Gradebook/GradebookFilters/AssignmentFilter/index.jsx
rename to src/components/GradebookFilters/AssignmentFilter/index.jsx
diff --git a/src/components/Gradebook/GradebookFilters/AssignmentFilter/test.jsx b/src/components/GradebookFilters/AssignmentFilter/test.jsx
similarity index 100%
rename from src/components/Gradebook/GradebookFilters/AssignmentFilter/test.jsx
rename to src/components/GradebookFilters/AssignmentFilter/test.jsx
diff --git a/src/components/Gradebook/GradebookFilters/AssignmentGradeFilter/__snapshots__/test.jsx.snap b/src/components/GradebookFilters/AssignmentGradeFilter/__snapshots__/test.jsx.snap
similarity index 100%
rename from src/components/Gradebook/GradebookFilters/AssignmentGradeFilter/__snapshots__/test.jsx.snap
rename to src/components/GradebookFilters/AssignmentGradeFilter/__snapshots__/test.jsx.snap
diff --git a/src/components/Gradebook/GradebookFilters/AssignmentGradeFilter/index.jsx b/src/components/GradebookFilters/AssignmentGradeFilter/index.jsx
similarity index 100%
rename from src/components/Gradebook/GradebookFilters/AssignmentGradeFilter/index.jsx
rename to src/components/GradebookFilters/AssignmentGradeFilter/index.jsx
diff --git a/src/components/Gradebook/GradebookFilters/AssignmentGradeFilter/test.jsx b/src/components/GradebookFilters/AssignmentGradeFilter/test.jsx
similarity index 100%
rename from src/components/Gradebook/GradebookFilters/AssignmentGradeFilter/test.jsx
rename to src/components/GradebookFilters/AssignmentGradeFilter/test.jsx
diff --git a/src/components/Gradebook/GradebookFilters/AssignmentTypeFilter/__snapshots__/test.jsx.snap b/src/components/GradebookFilters/AssignmentTypeFilter/__snapshots__/test.jsx.snap
similarity index 100%
rename from src/components/Gradebook/GradebookFilters/AssignmentTypeFilter/__snapshots__/test.jsx.snap
rename to src/components/GradebookFilters/AssignmentTypeFilter/__snapshots__/test.jsx.snap
diff --git a/src/components/Gradebook/GradebookFilters/AssignmentTypeFilter/index.jsx b/src/components/GradebookFilters/AssignmentTypeFilter/index.jsx
similarity index 100%
rename from src/components/Gradebook/GradebookFilters/AssignmentTypeFilter/index.jsx
rename to src/components/GradebookFilters/AssignmentTypeFilter/index.jsx
diff --git a/src/components/Gradebook/GradebookFilters/AssignmentTypeFilter/test.jsx b/src/components/GradebookFilters/AssignmentTypeFilter/test.jsx
similarity index 100%
rename from src/components/Gradebook/GradebookFilters/AssignmentTypeFilter/test.jsx
rename to src/components/GradebookFilters/AssignmentTypeFilter/test.jsx
diff --git a/src/components/Gradebook/GradebookFilters/CourseGradeFilter/__snapshots__/test.jsx.snap b/src/components/GradebookFilters/CourseGradeFilter/__snapshots__/test.jsx.snap
similarity index 100%
rename from src/components/Gradebook/GradebookFilters/CourseGradeFilter/__snapshots__/test.jsx.snap
rename to src/components/GradebookFilters/CourseGradeFilter/__snapshots__/test.jsx.snap
diff --git a/src/components/Gradebook/GradebookFilters/CourseGradeFilter/index.jsx b/src/components/GradebookFilters/CourseGradeFilter/index.jsx
similarity index 100%
rename from src/components/Gradebook/GradebookFilters/CourseGradeFilter/index.jsx
rename to src/components/GradebookFilters/CourseGradeFilter/index.jsx
diff --git a/src/components/Gradebook/GradebookFilters/CourseGradeFilter/test.jsx b/src/components/GradebookFilters/CourseGradeFilter/test.jsx
similarity index 100%
rename from src/components/Gradebook/GradebookFilters/CourseGradeFilter/test.jsx
rename to src/components/GradebookFilters/CourseGradeFilter/test.jsx
diff --git a/src/components/Gradebook/GradebookFilters/PercentGroup.jsx b/src/components/GradebookFilters/PercentGroup.jsx
similarity index 100%
rename from src/components/Gradebook/GradebookFilters/PercentGroup.jsx
rename to src/components/GradebookFilters/PercentGroup.jsx
diff --git a/src/components/Gradebook/GradebookFilters/PercentGroup.test.jsx b/src/components/GradebookFilters/PercentGroup.test.jsx
similarity index 100%
rename from src/components/Gradebook/GradebookFilters/PercentGroup.test.jsx
rename to src/components/GradebookFilters/PercentGroup.test.jsx
diff --git a/src/components/Gradebook/GradebookFilters/SelectGroup.jsx b/src/components/GradebookFilters/SelectGroup.jsx
similarity index 100%
rename from src/components/Gradebook/GradebookFilters/SelectGroup.jsx
rename to src/components/GradebookFilters/SelectGroup.jsx
diff --git a/src/components/Gradebook/GradebookFilters/SelectGroup.test.jsx b/src/components/GradebookFilters/SelectGroup.test.jsx
similarity index 100%
rename from src/components/Gradebook/GradebookFilters/SelectGroup.test.jsx
rename to src/components/GradebookFilters/SelectGroup.test.jsx
diff --git a/src/components/Gradebook/GradebookFilters/StudentGroupsFilter/__snapshots__/test.jsx.snap b/src/components/GradebookFilters/StudentGroupsFilter/__snapshots__/test.jsx.snap
similarity index 100%
rename from src/components/Gradebook/GradebookFilters/StudentGroupsFilter/__snapshots__/test.jsx.snap
rename to src/components/GradebookFilters/StudentGroupsFilter/__snapshots__/test.jsx.snap
diff --git a/src/components/Gradebook/GradebookFilters/StudentGroupsFilter/index.jsx b/src/components/GradebookFilters/StudentGroupsFilter/index.jsx
similarity index 100%
rename from src/components/Gradebook/GradebookFilters/StudentGroupsFilter/index.jsx
rename to src/components/GradebookFilters/StudentGroupsFilter/index.jsx
diff --git a/src/components/Gradebook/GradebookFilters/StudentGroupsFilter/test.jsx b/src/components/GradebookFilters/StudentGroupsFilter/test.jsx
similarity index 100%
rename from src/components/Gradebook/GradebookFilters/StudentGroupsFilter/test.jsx
rename to src/components/GradebookFilters/StudentGroupsFilter/test.jsx
diff --git a/src/components/Gradebook/GradebookFilters/__snapshots__/PercentGroup.test.jsx.snap b/src/components/GradebookFilters/__snapshots__/PercentGroup.test.jsx.snap
similarity index 100%
rename from src/components/Gradebook/GradebookFilters/__snapshots__/PercentGroup.test.jsx.snap
rename to src/components/GradebookFilters/__snapshots__/PercentGroup.test.jsx.snap
diff --git a/src/components/Gradebook/GradebookFilters/__snapshots__/SelectGroup.test.jsx.snap b/src/components/GradebookFilters/__snapshots__/SelectGroup.test.jsx.snap
similarity index 100%
rename from src/components/Gradebook/GradebookFilters/__snapshots__/SelectGroup.test.jsx.snap
rename to src/components/GradebookFilters/__snapshots__/SelectGroup.test.jsx.snap
diff --git a/src/components/Gradebook/GradebookFilters/__snapshots__/test.jsx.snap b/src/components/GradebookFilters/__snapshots__/test.jsx.snap
similarity index 100%
rename from src/components/Gradebook/GradebookFilters/__snapshots__/test.jsx.snap
rename to src/components/GradebookFilters/__snapshots__/test.jsx.snap
diff --git a/src/components/Gradebook/GradebookFilters/index.jsx b/src/components/GradebookFilters/index.jsx
similarity index 97%
rename from src/components/Gradebook/GradebookFilters/index.jsx
rename to src/components/GradebookFilters/index.jsx
index 0a27cb9..9b639ed 100644
--- a/src/components/Gradebook/GradebookFilters/index.jsx
+++ b/src/components/GradebookFilters/index.jsx
@@ -3,7 +3,10 @@ import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
-import { Collapsible, Form } from '@edx/paragon';
+import {
+ Collapsible,
+ Form,
+} from '@edx/paragon';
import actions from 'data/actions';
import selectors from 'data/selectors';
@@ -73,10 +76,11 @@ GradebookFilters.defaultProps = {
includeCourseRoleMembers: false,
};
GradebookFilters.propTypes = {
+ updateQueryParams: PropTypes.func.isRequired,
+ // redux
fetchGrades: PropTypes.func.isRequired,
includeCourseRoleMembers: PropTypes.bool,
updateIncludeCourseRoleMembers: PropTypes.func.isRequired,
- updateQueryParams: PropTypes.func.isRequired,
};
export const mapStateToProps = (state) => ({
diff --git a/src/components/Gradebook/GradebookFilters/test.jsx b/src/components/GradebookFilters/test.jsx
similarity index 100%
rename from src/components/Gradebook/GradebookFilters/test.jsx
rename to src/components/GradebookFilters/test.jsx
diff --git a/src/components/GradebookFiltersHeader.jsx b/src/components/GradebookFiltersHeader.jsx
new file mode 100644
index 0000000..0883d2e
--- /dev/null
+++ b/src/components/GradebookFiltersHeader.jsx
@@ -0,0 +1,38 @@
+/* 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 {
+ Icon,
+ IconButton,
+} from '@edx/paragon';
+import { Close } from '@edx/paragon/icons';
+
+import thunkActions from 'data/thunkActions';
+
+export const GradebookFiltersHeader = ({ closeMenu }) => (
+ <>
+
+
+ >
+);
+GradebookFiltersHeader.propTypes = {
+ // redux
+ closeMenu: PropTypes.func.isRequired,
+};
+
+export const mapStateToProps = () => ({});
+
+export const mapDispatchToProps = {
+ closeMenu: thunkActions.app.filterMenu.close,
+};
+
+export default connect(mapStateToProps, mapDispatchToProps)(GradebookFiltersHeader);
diff --git a/src/components/GradebookFiltersHeader.test.jsx b/src/components/GradebookFiltersHeader.test.jsx
new file mode 100644
index 0000000..6a43f7c
--- /dev/null
+++ b/src/components/GradebookFiltersHeader.test.jsx
@@ -0,0 +1,56 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+
+import thunkActions from 'data/thunkActions';
+
+import {
+ GradebookFiltersHeader,
+ mapDispatchToProps,
+} from './GradebookFiltersHeader';
+
+jest.mock('@edx/paragon', () => ({
+ Icon: jest.fn().mockName('Paragon.Icon'),
+ IconButton: () => 'IconButton',
+}));
+
+jest.mock('@edx/paragon/icons', () => ({
+ close: jest.fn().mockName('Paragon.icons.Close'),
+}));
+
+jest.mock('data/thunkActions', () => ({
+ __esModule: true,
+ default: {
+ app: {
+ filterMenu: {
+ close: jest.fn().mockName('closeFilterMenu'),
+ },
+ },
+ },
+}));
+
+describe('GradebookFiltersHeader', () => {
+ let props;
+ beforeEach(() => {
+ props = {
+ closeMenu: jest.fn().mockName('props.closeMenu'),
+ };
+ });
+
+ describe('Component', () => {
+ describe('snapshots', () => {
+ test('basic snapshot', () => {
+ const el = shallow();
+ expect(el).toMatchSnapshot();
+ });
+ });
+ });
+ describe('mapDispatchToProps', () => {
+ describe('closeMenu', () => {
+ test('from thunkActions.app.filterMenu.close', () => {
+ expect(mapDispatchToProps.closeMenu).toEqual(
+ thunkActions.app.filterMenu.close,
+ );
+ });
+ });
+ });
+});
diff --git a/src/components/Gradebook/GradebookHeader.jsx b/src/components/GradebookHeader.jsx
similarity index 100%
rename from src/components/Gradebook/GradebookHeader.jsx
rename to src/components/GradebookHeader.jsx
diff --git a/src/components/Gradebook/GradebookHeader.test.jsx b/src/components/GradebookHeader.test.jsx
similarity index 100%
rename from src/components/Gradebook/GradebookHeader.test.jsx
rename to src/components/GradebookHeader.test.jsx
diff --git a/src/components/Gradebook/BulkManagementControls.jsx b/src/components/GradesTab/BulkManagementControls.jsx
similarity index 100%
rename from src/components/Gradebook/BulkManagementControls.jsx
rename to src/components/GradesTab/BulkManagementControls.jsx
diff --git a/src/components/Gradebook/EditModal/ModalHeaders.jsx b/src/components/GradesTab/EditModal/ModalHeaders.jsx
similarity index 100%
rename from src/components/Gradebook/EditModal/ModalHeaders.jsx
rename to src/components/GradesTab/EditModal/ModalHeaders.jsx
diff --git a/src/components/Gradebook/EditModal/OverrideTable/AdjustedGradeInput.jsx b/src/components/GradesTab/EditModal/OverrideTable/AdjustedGradeInput.jsx
similarity index 100%
rename from src/components/Gradebook/EditModal/OverrideTable/AdjustedGradeInput.jsx
rename to src/components/GradesTab/EditModal/OverrideTable/AdjustedGradeInput.jsx
diff --git a/src/components/Gradebook/EditModal/OverrideTable/ReasonInput.jsx b/src/components/GradesTab/EditModal/OverrideTable/ReasonInput.jsx
similarity index 100%
rename from src/components/Gradebook/EditModal/OverrideTable/ReasonInput.jsx
rename to src/components/GradesTab/EditModal/OverrideTable/ReasonInput.jsx
diff --git a/src/components/Gradebook/EditModal/OverrideTable/index.jsx b/src/components/GradesTab/EditModal/OverrideTable/index.jsx
similarity index 100%
rename from src/components/Gradebook/EditModal/OverrideTable/index.jsx
rename to src/components/GradesTab/EditModal/OverrideTable/index.jsx
diff --git a/src/components/Gradebook/EditModal/index.jsx b/src/components/GradesTab/EditModal/index.jsx
similarity index 100%
rename from src/components/Gradebook/EditModal/index.jsx
rename to src/components/GradesTab/EditModal/index.jsx
diff --git a/src/components/Gradebook/GradebookTable/Fields.jsx b/src/components/GradesTab/GradebookTable/Fields.jsx
similarity index 100%
rename from src/components/Gradebook/GradebookTable/Fields.jsx
rename to src/components/GradesTab/GradebookTable/Fields.jsx
diff --git a/src/components/Gradebook/GradebookTable/GradeButton.jsx b/src/components/GradesTab/GradebookTable/GradeButton.jsx
similarity index 100%
rename from src/components/Gradebook/GradebookTable/GradeButton.jsx
rename to src/components/GradesTab/GradebookTable/GradeButton.jsx
diff --git a/src/components/Gradebook/GradebookTable/LabelReplacements.jsx b/src/components/GradesTab/GradebookTable/LabelReplacements.jsx
similarity index 100%
rename from src/components/Gradebook/GradebookTable/LabelReplacements.jsx
rename to src/components/GradesTab/GradebookTable/LabelReplacements.jsx
diff --git a/src/components/Gradebook/GradebookTable/index.jsx b/src/components/GradesTab/GradebookTable/index.jsx
similarity index 100%
rename from src/components/Gradebook/GradebookTable/index.jsx
rename to src/components/GradesTab/GradebookTable/index.jsx
diff --git a/src/components/Gradebook/gradebook.scss b/src/components/GradesTab/GradesTab.scss
similarity index 100%
rename from src/components/Gradebook/gradebook.scss
rename to src/components/GradesTab/GradesTab.scss
diff --git a/src/components/Gradebook/ScoreViewInput.jsx b/src/components/GradesTab/ScoreViewInput.jsx
similarity index 100%
rename from src/components/Gradebook/ScoreViewInput.jsx
rename to src/components/GradesTab/ScoreViewInput.jsx
diff --git a/src/components/Gradebook/SearchControls.jsx b/src/components/GradesTab/SearchControls.jsx
similarity index 97%
rename from src/components/Gradebook/SearchControls.jsx
rename to src/components/GradesTab/SearchControls.jsx
index 732d417..49fb324 100644
--- a/src/components/Gradebook/SearchControls.jsx
+++ b/src/components/GradesTab/SearchControls.jsx
@@ -60,11 +60,11 @@ export class SearchControls extends React.Component {
}
SearchControls.propTypes = {
- toggleFilterDrawer: PropTypes.func.isRequired,
// From Redux
fetchGrades: PropTypes.func.isRequired,
searchValue: PropTypes.string.isRequired,
setSearchValue: PropTypes.func.isRequired,
+ toggleFilterDrawer: PropTypes.func.isRequired,
};
export const mapStateToProps = (state) => ({
@@ -74,6 +74,7 @@ export const mapStateToProps = (state) => ({
export const mapDispatchToProps = {
fetchGrades: thunkActions.grades.fetchGrades,
setSearchValue: actions.app.setSearchValue,
+ toggleFilterDrawer: thunkActions.app.filterMenu.toggle,
};
export default connect(mapStateToProps, mapDispatchToProps)(SearchControls);
diff --git a/src/components/Gradebook/SearchControls.test.jsx b/src/components/GradesTab/SearchControls.test.jsx
similarity index 87%
rename from src/components/Gradebook/SearchControls.test.jsx
rename to src/components/GradesTab/SearchControls.test.jsx
index ebb9cd8..05419f1 100644
--- a/src/components/Gradebook/SearchControls.test.jsx
+++ b/src/components/GradesTab/SearchControls.test.jsx
@@ -23,7 +23,10 @@ jest.mock('data/thunkActions', () => ({
__esModule: true,
default: {
grades: {
- fetchGrades: jest.fn(),
+ fetchGrades: jest.fn().mockName('thunkActions.grades.fetchGrades'),
+ },
+ app: {
+ filterMenu: { toggle: jest.fn().mockName('thunkActions.app.filterMenu') },
},
},
}));
@@ -47,6 +50,15 @@ describe('SearchControls', () => {
};
describe('Component', () => {
+ describe('Snapshots', () => {
+ test('basic snapshot', () => {
+ const wrapper = searchControls();
+ wrapper.instance().onChange = jest.fn().mockName('onChange');
+ wrapper.instance().onClear = jest.fn().mockName('onClear');
+ expect(wrapper.instance().render()).toMatchSnapshot();
+ });
+ });
+
describe('onChange', () => {
it('saves the changed search value to Gradebook state', () => {
const wrapper = searchControls();
@@ -80,14 +92,11 @@ describe('SearchControls', () => {
test('setSearchValue from actions.app.setSearchValue', () => {
expect(mapDispatchToProps.setSearchValue).toEqual(actions.app.setSearchValue);
});
- });
- describe('Snapshots', () => {
- test('basic snapshot', () => {
- const wrapper = searchControls();
- wrapper.instance().onChange = jest.fn().mockName('onChange');
- wrapper.instance().onClear = jest.fn().mockName('onClear');
- expect(wrapper.instance().render()).toMatchSnapshot();
+ test('toggleFilterDrawer from thunkActions.app.filterMenu.toggle', () => {
+ expect(
+ mapDispatchToProps.toggleFilterDrawer,
+ ).toEqual(thunkActions.app.filterMenu.toggle);
});
});
});
diff --git a/src/components/Gradebook/SpinnerIcon.jsx b/src/components/GradesTab/SpinnerIcon.jsx
similarity index 100%
rename from src/components/Gradebook/SpinnerIcon.jsx
rename to src/components/GradesTab/SpinnerIcon.jsx
diff --git a/src/components/Gradebook/StatusAlerts.jsx b/src/components/GradesTab/StatusAlerts.jsx
similarity index 100%
rename from src/components/Gradebook/StatusAlerts.jsx
rename to src/components/GradesTab/StatusAlerts.jsx
diff --git a/src/components/Gradebook/StatusAlerts.test.jsx b/src/components/GradesTab/StatusAlerts.test.jsx
similarity index 100%
rename from src/components/Gradebook/StatusAlerts.test.jsx
rename to src/components/GradesTab/StatusAlerts.test.jsx
diff --git a/src/components/Gradebook/UsersLabel.jsx b/src/components/GradesTab/UsersLabel.jsx
similarity index 100%
rename from src/components/Gradebook/UsersLabel.jsx
rename to src/components/GradesTab/UsersLabel.jsx
diff --git a/src/components/Gradebook/__snapshots__/SearchControls.test.jsx.snap b/src/components/GradesTab/__snapshots__/SearchControls.test.jsx.snap
similarity index 100%
rename from src/components/Gradebook/__snapshots__/SearchControls.test.jsx.snap
rename to src/components/GradesTab/__snapshots__/SearchControls.test.jsx.snap
diff --git a/src/components/Gradebook/__snapshots__/StatusAlerts.test.jsx.snap b/src/components/GradesTab/__snapshots__/StatusAlerts.test.jsx.snap
similarity index 100%
rename from src/components/Gradebook/__snapshots__/StatusAlerts.test.jsx.snap
rename to src/components/GradesTab/__snapshots__/StatusAlerts.test.jsx.snap
diff --git a/src/components/GradesTab/__snapshots__/test.jsx.snap b/src/components/GradesTab/__snapshots__/test.jsx.snap
new file mode 100644
index 0000000..9c9fe20
--- /dev/null
+++ b/src/components/GradesTab/__snapshots__/test.jsx.snap
@@ -0,0 +1,28 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`GradesTab Component snapshots basic snapshot 1`] = `
+
+
+
+
+
+
+ Step 2: View or Modify Individual Grades
+
+
+
+
+
+
+
+
+
+ * available for learners in the Master's track only
+
+
+
+`;
diff --git a/src/components/GradesTab/index.jsx b/src/components/GradesTab/index.jsx
new file mode 100644
index 0000000..e206026
--- /dev/null
+++ b/src/components/GradesTab/index.jsx
@@ -0,0 +1,81 @@
+/* eslint-disable react/sort-comp, react/button-has-type, import/no-named-as-default */
+import React from 'react';
+import PropTypes from 'prop-types';
+import { connect } from 'react-redux';
+
+import actions from 'data/actions';
+import thunkActions from 'data/thunkActions';
+
+import PageButtons from '../PageButtons';
+import FilterBadges from '../FilterBadges';
+
+import BulkManagementControls from './BulkManagementControls';
+import EditModal from './EditModal';
+import GradebookTable from './GradebookTable';
+import SearchControls from './SearchControls';
+import StatusAlerts from './StatusAlerts';
+import SpinnerIcon from './SpinnerIcon';
+import ScoreViewInput from './ScoreViewInput';
+import UsersLabel from './UsersLabel';
+
+export class GradesTab extends React.Component {
+ constructor(props) {
+ super(props);
+ this.handleFilterBadgeClose = this.handleFilterBadgeClose.bind(this);
+ }
+
+ handleFilterBadgeClose(filterNames) {
+ return () => {
+ this.props.resetFilters(filterNames);
+ this.props.updateQueryParams(filterNames.reduce(
+ (obj, filterName) => ({ ...obj, [filterName]: false }),
+ {},
+ ));
+ this.props.fetchGrades();
+ };
+ }
+
+ render() {
+ return (
+ <>
+
+
+
+
+
+ Step 2: View or Modify Individual Grades
+
+
+
+
+
+
+
+
+
+
+ * available for learners in the Master's track only
+
+ >
+ );
+ }
+}
+
+GradesTab.defaultProps = {};
+
+GradesTab.propTypes = {
+ updateQueryParams: PropTypes.func.isRequired,
+
+ // redux
+ fetchGrades: PropTypes.func.isRequired,
+ resetFilters: PropTypes.func.isRequired,
+};
+
+export const mapStateToProps = () => ({});
+
+export const mapDispatchToProps = {
+ fetchGrades: thunkActions.grades.fetchGrades,
+ resetFilters: actions.filters.reset,
+};
+
+export default connect(mapStateToProps, mapDispatchToProps)(GradesTab);
diff --git a/src/components/GradesTab/test.jsx b/src/components/GradesTab/test.jsx
new file mode 100644
index 0000000..7607d84
--- /dev/null
+++ b/src/components/GradesTab/test.jsx
@@ -0,0 +1,97 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+
+import actions from 'data/actions';
+import thunkActions from 'data/thunkActions';
+
+import {
+ GradesTab,
+ mapDispatchToProps,
+} from '.';
+
+jest.mock('data/actions', () => ({
+ __esModule: true,
+ default: {
+ filters: { resetFilters: jest.fn() },
+ },
+}));
+jest.mock('data/thunkActions', () => ({
+ __esModule: true,
+ default: {
+ grades: { fetchGrades: jest.fn() },
+ },
+}));
+
+jest.mock('../PageButtons', () => 'PageButtons');
+jest.mock('../FilterBadges', () => 'FilterBadges');
+jest.mock('./BulkManagementControls', () => 'BulkManagementControls');
+jest.mock('./EditModal', () => 'EditModal');
+jest.mock('./GradebookTable', () => 'GradebookTable');
+jest.mock('./SearchControls', () => 'SearchControls');
+jest.mock('./StatusAlerts', () => 'StatusAlerts');
+jest.mock('./SpinnerIcon', () => 'SpinnerIcon');
+jest.mock('./ScoreViewInput', () => 'ScoreViewInput');
+jest.mock('./UsersLabel', () => 'UsersLabel');
+
+describe('GradesTab', () => {
+ let props;
+ beforeEach(() => {
+ props = {
+ updateQueryParams: jest.fn(),
+ fetchGrades: jest.fn(),
+ resetFilters: jest.fn(),
+ };
+ });
+
+ describe('Component', () => {
+ const filterNames = ['duck', 'Duck', 'Duuuuuck', 'GOOOOSE!'];
+ describe('behavior', () => {
+ let el;
+ beforeEach(() => {
+ el = shallow();
+ });
+ describe('handleFilterBadgeClose', () => {
+ beforeEach(() => {
+ el.instance().handleFilterBadgeClose(filterNames)();
+ });
+ it('calls props.resetFilters with the filters', () => {
+ expect(props.resetFilters).toHaveBeenCalledWith(filterNames);
+ });
+ it('calls props.updateQueryParams with a reset-filters obj', () => {
+ expect(props.updateQueryParams).toHaveBeenCalledWith({
+ [filterNames[0]]: false,
+ [filterNames[1]]: false,
+ [filterNames[2]]: false,
+ [filterNames[3]]: false,
+ });
+ });
+ it('calls fetchGrades', () => {
+ expect(props.fetchGrades).toHaveBeenCalledWith();
+ });
+ });
+ });
+ describe('snapshots', () => {
+ test('basic snapshot', () => {
+ const el = shallow();
+ el.instance().handleFilterBadgeClose = jest.fn().mockName('this.handleFilterBadgeClose');
+ expect(el.instance().render()).toMatchSnapshot();
+ });
+ });
+ });
+ describe('mapDispatchToProps', () => {
+ describe('fetchGrades', () => {
+ test('from thunkActions.grades.fetchGrades', () => {
+ expect(mapDispatchToProps.fetchGrades).toEqual(
+ thunkActions.grades.fetchGrades,
+ );
+ });
+ });
+ describe('resetFilters', () => {
+ test('from actions.filters.reset', () => {
+ expect(mapDispatchToProps.resetFilters).toEqual(
+ actions.filters.reset,
+ );
+ });
+ });
+ });
+});
diff --git a/src/components/Drawer/Drawer.scss b/src/components/WithSidebar/WithSidebar.scss
similarity index 54%
rename from src/components/Drawer/Drawer.scss
rename to src/components/WithSidebar/WithSidebar.scss
index 372d273..03f63b9 100644
--- a/src/components/Drawer/Drawer.scss
+++ b/src/components/WithSidebar/WithSidebar.scss
@@ -1,45 +1,45 @@
-$drawer-width: 350px;
+$sidebar-width: 350px;
-.drawer-contents {
+.sidebar-contents {
overflow-x: auto;
transition: margin 300ms cubic-bezier(0.4,0,0.2,1);
margin-left: 0;
- .drawer.open + & {
- margin-left: $drawer-width;
+ .sidebar.open + & {
+ margin-left: $sidebar-width;
}
- &.opened {
- width: calc(100vw - #{$drawer-width});
+ &.opening {
+ width: calc(100vw - #{$sidebar-width});
}
}
-.drawer-contents {
+.sidebar-contents {
overflow-x: auto;
transition: margin 300ms cubic-bezier(0.4,0,0.2,1);
margin-left: 0;
- .drawer.open + & {
- margin-left: $drawer-width;
+ .sidebar.open + & {
+ margin-left: $sidebar-width;
}
- &.opened {
- width: calc(100vw - #{$drawer-width});
+ &.opening {
+ width: calc(100vw - #{$sidebar-width});
}
}
-.drawer-header {
+.sidebar-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
padding: 15px;
}
-.drawer-container .collapsible {
+.sidebar-container .collapsible {
margin-bottom: 1em;
}
-.drawer {
+.sidebar {
height: 100%;
- width: $drawer-width;
+ width: $sidebar-width;
position: absolute;
- transform: translateX(-$drawer-width);
+ transform: translateX(-$sidebar-width);
flex-direction: column;
transition: transform 300ms cubic-bezier(0.4,0,0.2,1);
&.open {
diff --git a/src/components/WithSidebar/__snapshots__/test.jsx.snap b/src/components/WithSidebar/__snapshots__/test.jsx.snap
new file mode 100644
index 0000000..6a433fc
--- /dev/null
+++ b/src/components/WithSidebar/__snapshots__/test.jsx.snap
@@ -0,0 +1,30 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`WithSidebar Component snapshots basic snapshot 1`] = `
+
+
+
+
+ aby in a bi
+
+
+
+`;
diff --git a/src/components/WithSidebar/index.jsx b/src/components/WithSidebar/index.jsx
new file mode 100644
index 0000000..814649f
--- /dev/null
+++ b/src/components/WithSidebar/index.jsx
@@ -0,0 +1,72 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { connect } from 'react-redux';
+import classNames from 'classnames';
+
+import selectors from 'data/selectors';
+import thunkActions from 'data/thunkActions';
+
+/**
+ * WithSidebar
+ * Simple wrapper around a content component, with a collapsible sidebar, whose open/closed
+ * state is drawn from redux.
+ *
+ * Wraps child react content in a container to enable the sidebar behavior.
+ *
+ * @param {JSX} children - page content
+ * @param {JSX} sidebar - sidebar content
+ * @param {JSX} sidebarHeader - sidebar header content
+ *
+ * Ex Usage:
+ * {children}
+ */
+export class WithSidebar extends React.Component {
+ get sidebarClassNames() {
+ return classNames('sidebar', { open: this.props.open, 'd-none': this.props.isClosed });
+ }
+
+ get contentClassNames() {
+ return classNames('sidebar-contents', 'position-relative', {
+ opening: this.props.isOpening,
+ });
+ }
+
+ render() {
+ return (
+
+
+
+ { this.props.children}
+
+
+ );
+ }
+}
+
+WithSidebar.propTypes = {
+ children: PropTypes.node.isRequired,
+ sidebar: PropTypes.node.isRequired,
+ sidebarHeader: PropTypes.node.isRequired,
+ // redux
+ isClosed: PropTypes.bool.isRequired,
+ isOpening: PropTypes.bool.isRequired,
+ open: PropTypes.bool.isRequired,
+ handleSlideDone: PropTypes.func.isRequired,
+};
+
+export const mapStateToProps = (state) => ({
+ open: selectors.app.filterMenu.open(state),
+ isClosed: selectors.app.filterMenu.isClosed(state),
+ isOpening: selectors.app.filterMenu.isOpening(state),
+});
+
+export const mapDispatchToProps = {
+ handleSlideDone: thunkActions.app.filterMenu.handleTransitionEnd,
+};
+
+export default connect(mapStateToProps, mapDispatchToProps)(WithSidebar);
diff --git a/src/components/WithSidebar/test.jsx b/src/components/WithSidebar/test.jsx
new file mode 100644
index 0000000..2c91cdf
--- /dev/null
+++ b/src/components/WithSidebar/test.jsx
@@ -0,0 +1,124 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+
+import selectors from 'data/selectors';
+import thunkActions from 'data/thunkActions';
+
+import {
+ WithSidebar,
+ mapStateToProps,
+ mapDispatchToProps,
+} from '.';
+
+jest.mock('data/selectors', () => ({
+ __esModule: true,
+ default: {
+ app: {
+ filterMenu: {
+ isClosed: jest.fn(state => ({ isClosed: state })),
+ isOpening: jest.fn(state => ({ isOpening: state })),
+ open: jest.fn(state => ({ open: state })),
+ },
+ },
+ },
+}));
+jest.mock('data/thunkActions', () => ({
+ __esModule: true,
+ default: {
+ app: {
+ filterMenu: {
+ handleTransitionEnd: jest.fn().mockName('handleTransitionEnd'),
+ },
+ },
+ },
+}));
+
+describe('WithSidebar', () => {
+ let props = {
+ sidebar: (Some Sidebar Content
),
+ sidebarHeader: (A really nice sidebar header
),
+ children: (aby in a bi),
+ isClosed: true,
+ isOpening: false,
+ open: false,
+ };
+
+ beforeEach(() => {
+ props = {
+ ...props,
+ handleSlideDone: jest.fn().mockName('handleSlideDone'),
+ };
+ });
+
+ describe('Component', () => {
+ describe('behavior', () => {
+ let el;
+ beforeEach(() => {
+ el = shallow();
+ });
+ describe('sidebarClassNames', () => {
+ const getVal = () => el.instance().sidebarClassNames.split(' ');
+ it('returns a "sidebar" classname', () => {
+ expect(getVal()).toContain('sidebar');
+ });
+ it('includes an open className iff props.open', () => {
+ expect(getVal()).not.toContain('open');
+ el.setProps({ open: true });
+ expect(getVal()).toContain('open');
+ });
+ it('includes a d-none className iff props.isClosed', () => {
+ expect(getVal()).toContain('d-none');
+ el.setProps({ isClosed: false });
+ expect(getVal()).not.toContain('d-none');
+ });
+ });
+ describe('contentClassNames', () => {
+ const getVal = () => el.instance().contentClassNames.split(' ');
+ it('includes sidebar-contents and position-relative classNames', () => {
+ expect(getVal()).toContain('sidebar-contents');
+ expect(getVal()).toContain('position-relative');
+ });
+ it('includes an opening class iff props.isOpening', () => {
+ expect(getVal()).not.toContain('opening');
+ el.setProps({ isOpening: true });
+ expect(getVal()).toContain('opening');
+ });
+ });
+ });
+ describe('snapshots', () => {
+ test('basic snapshot', () => {
+ const el = shallow();
+ const sidebarClassNames = 'sidebar-class-names';
+ const contentClassNames = 'content-class-names';
+ jest.spyOn(el.instance(), 'sidebarClassNames', 'get').mockReturnValue(sidebarClassNames);
+ jest.spyOn(el.instance(), 'contentClassNames', 'get').mockReturnValue(contentClassNames);
+ expect(el.instance().render()).toMatchSnapshot();
+ });
+ });
+ });
+ describe('mapStateToProps', () => {
+ const testState = { A: 'laska' };
+ let mapped;
+ beforeEach(() => {
+ mapped = mapStateToProps(testState);
+ });
+ test('open from app.filterMenu.open', () => {
+ expect(mapped.open).toEqual(selectors.app.filterMenu.open(testState));
+ });
+ test('isClosed from app.filterMenu.isClosed', () => {
+ expect(mapped.isClosed).toEqual(selectors.app.filterMenu.isClosed(testState));
+ });
+ test('open from app.filterMenu.isOpening', () => {
+ expect(mapped.isOpening).toEqual(selectors.app.filterMenu.isOpening(testState));
+ });
+ });
+ describe('mapDispatchToProps', () => {
+ describe('handleSlideDone', () => {
+ test('from thunkActions.app.filterMenu.handleTransitionEnd', () => {
+ expect(mapDispatchToProps.handleSlideDone).toEqual(
+ thunkActions.app.filterMenu.handleTransitionEnd,
+ );
+ });
+ });
+ });
+});
diff --git a/src/components/__snapshots__/GradebookFiltersHeader.test.jsx.snap b/src/components/__snapshots__/GradebookFiltersHeader.test.jsx.snap
new file mode 100644
index 0000000..a9f0ee4
--- /dev/null
+++ b/src/components/__snapshots__/GradebookFiltersHeader.test.jsx.snap
@@ -0,0 +1,18 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`GradebookFiltersHeader Component snapshots basic snapshot 1`] = `
+
+
+
+
+
+
+`;
diff --git a/src/components/Gradebook/__snapshots__/GradebookHeader.test.jsx.snap b/src/components/__snapshots__/GradebookHeader.test.jsx.snap
similarity index 100%
rename from src/components/Gradebook/__snapshots__/GradebookHeader.test.jsx.snap
rename to src/components/__snapshots__/GradebookHeader.test.jsx.snap
diff --git a/src/containers/GradebookPage/index.jsx b/src/containers/GradebookPage/index.jsx
index 08b10b7..7516b8a 100644
--- a/src/containers/GradebookPage/index.jsx
+++ b/src/containers/GradebookPage/index.jsx
@@ -1,45 +1,83 @@
+/* eslint-disable import/no-named-as-default */
+import React from 'react';
import { connect } from 'react-redux';
+import PropTypes from 'prop-types';
+import queryString from 'query-string';
+
+import { Tab, Tabs } from '@edx/paragon';
-import thunkActions from 'data/thunkActions';
-import actions from 'data/actions';
import selectors from 'data/selectors';
+import thunkActions from 'data/thunkActions';
-import Gradebook from 'components/Gradebook';
+import WithSidebar from 'components/WithSidebar';
+import GradebookHeader from 'components/GradebookHeader';
+import GradesTab from 'components/GradesTab';
+import GradebookFilters from 'components/GradebookFilters';
+import GradebookFiltersHeader from 'components/GradebookFiltersHeader';
+import BulkManagement from 'components/BulkManagement';
-const mapStateToProps = (state, ownProps) => {
- const {
- root,
- filters,
- grades,
- } = selectors;
+export class GradebookPage extends React.Component {
+ constructor(props) {
+ super(props);
+ this.updateQueryParams = this.updateQueryParams.bind(this);
+ }
- const { courseId } = ownProps.match.params;
- return {
- courseId,
- filteredUsersCount: grades.filteredUsersCount(state),
- gradeExportUrl: root.gradeExportUrl(state, { courseId }),
- interventionExportUrl: root.interventionExportUrl(state, { courseId }),
- selectedTrack: filters.track(state),
- selectedCohort: filters.cohort(state),
- selectedAssignmentType: filters.assignmentType(state),
- showBulkManagement: root.showBulkManagement(state, { courseId }),
- showSpinner: root.shouldShowSpinner(state),
- totalUsersCount: grades.totalUsersCount(state),
- };
+ componentDidMount() {
+ const urlQuery = queryString.parse(this.props.location.search);
+ this.props.initializeApp(this.props.courseId, urlQuery);
+ }
+
+ updateQueryParams(queryParams) {
+ const parsed = queryString.parse(this.props.location.search);
+ Object.keys(queryParams).forEach((key) => {
+ if (queryParams[key]) {
+ parsed[key] = queryParams[key];
+ } else {
+ delete parsed[key];
+ }
+ });
+ }
+
+ render() {
+ return (
+ }
+ sidebarHeader={}
+ >
+
+
+
+
+
+
+ {this.props.showBulkManagement && (
+
+
+
+ )}
+
+
+
+ );
+ }
+}
+GradebookPage.defaultProps = {
+ location: { search: '' },
+};
+GradebookPage.propTypes = {
+ location: PropTypes.shape({ search: PropTypes.string }),
+ courseId: PropTypes.string.isRequired,
+ initializeApp: PropTypes.func.isRequired,
+ showBulkManagement: PropTypes.bool.isRequired,
};
-const mapDispatchToProps = {
- toggleFormat: actions.grades.toggleGradeFormat,
- resetFilters: actions.filters.reset,
+export const mapStateToProps = (state, ownProps) => ({
+ courseId: ownProps.match.params.courseId,
+ showBulkManagement: selectors.root.showBulkManagement(state),
+});
+
+export const mapDispatchToProps = {
initializeApp: thunkActions.app.initialize,
- fetchGrades: thunkActions.grades.fetchGrades,
- getRoles: thunkActions.roles.fetchRoles,
- getTracks: thunkActions.tracks.fetchTracks,
};
-const GradebookPage = connect(
- mapStateToProps,
- mapDispatchToProps,
-)(Gradebook);
-
-export default GradebookPage;
+export default connect(mapStateToProps, mapDispatchToProps)(GradebookPage);
diff --git a/src/data/actions/app.js b/src/data/actions/app.js
index ef99538..9ed8807 100644
--- a/src/data/actions/app.js
+++ b/src/data/actions/app.js
@@ -12,11 +12,19 @@ const closeModal = createAction('closeModal');
* @param {string} courseId - course ID from app context
*/
const setCourseId = createAction('setCourseId');
+
+const filterMenu = StrictDict({
+ endTransition: createAction('filterMenu/endTransition'),
+ startTransition: createAction('filterMenu/startTransition'),
+ toggle: createAction('filterMenu/toggle'),
+});
+
/**
* setModalStateFromTable({ userEntry, subsection })
* sets modalState
- * */
+ */
const setModalStateFromTable = createAction('setModalStateFromTable');
+
/**
* setSearchValue(searchValue)
* sets searchValue in local state
@@ -48,9 +56,10 @@ const setModalState = createAction('setModalState', (modalState) => ({
export default StrictDict({
closeModal,
+ filterMenu,
setCourseId,
setModalState,
+ setModalStateFromTable,
setSearchValue,
setLocalFilter,
- setModalStateFromTable,
});
diff --git a/src/data/reducers/app.js b/src/data/reducers/app.js
index 9750313..8af1ad4 100644
--- a/src/data/reducers/app.js
+++ b/src/data/reducers/app.js
@@ -22,6 +22,10 @@ const initialState = {
updateUserId: null,
updateUserName: null,
},
+ filterMenu: {
+ open: false,
+ transitioning: false,
+ },
searchValue: '',
};
@@ -31,6 +35,21 @@ const app = (state = initialState, { type, payload }) => {
return { ...state, modalState: { ...initialState.modalState } };
case actions.setCourseId.toString():
return { ...state, courseId: payload };
+ case actions.filterMenu.startTransition.toString():
+ return {
+ ...state,
+ filterMenu: { ...state.filterMenu, transitioning: true },
+ };
+ case actions.filterMenu.endTransition.toString():
+ return {
+ ...state,
+ filterMenu: { ...state.filterMenu, transitioning: false },
+ };
+ case actions.filterMenu.toggle.toString():
+ return {
+ ...state,
+ filterMenu: { ...state.filterMenu, open: !state.filterMenu.open },
+ };
case actions.setLocalFilter.toString():
return {
...state,
diff --git a/src/data/selectors/app.js b/src/data/selectors/app.js
index 0690eee..186b89d 100644
--- a/src/data/selectors/app.js
+++ b/src/data/selectors/app.js
@@ -68,6 +68,14 @@ const areCourseGradeFiltersValid = (state) => {
return validity.isMinValid && validity.isMaxValid;
};
+const isFilterMenuClosed = ({ app: { filterMenu } }) => (
+ !filterMenu.open && !filterMenu.transitioning
+);
+
+const isFilterMenuOpening = ({ app: { filterMenu } }) => (
+ filterMenu.transitioning && filterMenu.open
+);
+
const modalSelectors = simpleSelectorFactory(
({ app: { modalState } }) => modalState,
[
@@ -81,6 +89,11 @@ const modalSelectors = simpleSelectorFactory(
],
);
+const filterMenuSelectors = simpleSelectorFactory(
+ ({ app: { filterMenu } }) => filterMenu,
+ ['open', 'transitioning'],
+);
+
const simpleSelectors = simpleSelectorFactory(
({ app }) => app,
[
@@ -98,4 +111,9 @@ export default StrictDict({
editUpdateData,
...simpleSelectors,
modalState: StrictDict(modalSelectors),
+ filterMenu: StrictDict({
+ ...filterMenuSelectors,
+ isClosed: isFilterMenuClosed,
+ isOpening: isFilterMenuOpening,
+ }),
});
diff --git a/src/data/selectors/app.test.js b/src/data/selectors/app.test.js
index 40c7fad..60418a2 100644
--- a/src/data/selectors/app.test.js
+++ b/src/data/selectors/app.test.js
@@ -93,6 +93,50 @@ describe('app selectors', () => {
selectors.courseGradeFilterValidity = old;
});
});
+
+ describe('filterMenu', () => {
+ describe('isClosed', () => {
+ const testSelector = (open, transitioning, expected) => {
+ expect(exportedSelectors.filterMenu.isClosed({
+ app: { filterMenu: { open, transitioning } },
+ })).toEqual(expected);
+ };
+ it('returns true if filterMenu is !open and !transitioning', () => {
+ testSelector(false, false, true);
+ });
+ it('returns false if filterMenu is not (!open and !transitioning)', () => {
+ testSelector(true, false, false);
+ testSelector(false, true, false);
+ testSelector(true, true, false);
+ });
+ });
+ describe('isOpening', () => {
+ const testSelector = (open, transitioning, expected) => {
+ expect(exportedSelectors.filterMenu.isOpening({
+ app: { filterMenu: { open, transitioning } },
+ })).toEqual(expected);
+ };
+ it('returns true if filter menu is transitioning AND open', () => {
+ testSelector(true, true, true);
+ });
+ it('returns true if filter menu is not (transitioning AND open)', () => {
+ testSelector(false, false, false);
+ testSelector(true, false, false);
+ testSelector(false, true, false);
+ });
+ });
+ describe('simpleSelectors', () => {
+ const testFilterMenuSelector = (key) => {
+ test(key, () => {
+ expect(
+ exportedSelectors.filterMenu[key]({ app: { filterMenu: { [key]: testVal } } }),
+ ).toEqual(testVal);
+ });
+ };
+ testFilterMenuSelector('open');
+ testFilterMenuSelector('transitioning');
+ });
+ });
describe('modalSelectors', () => {
const testModalSelector = (key) => {
test(key, () => {
diff --git a/src/data/thunkActions/app.js b/src/data/thunkActions/app.js
index c90bc69..9604a26 100644
--- a/src/data/thunkActions/app.js
+++ b/src/data/thunkActions/app.js
@@ -1,8 +1,36 @@
+/* eslint-disable import/no-self-import */
import { StrictDict } from 'utils';
import actions from 'data/actions';
+import selectors from 'data/selectors';
import { fetchGradeOverrideHistory } from './grades';
import { fetchRoles } from './roles';
+import * as module from './app';
+
+export const initialize = (courseId, urlQuery) => (dispatch) => {
+ dispatch(actions.app.setCourseId(courseId));
+ dispatch(actions.filters.initialize(urlQuery));
+ dispatch(fetchRoles());
+};
+
+export const filterMenu = StrictDict({
+ close: () => (dispatch, getState) => {
+ if (selectors.app.filterMenu.open(getState())) {
+ dispatch(module.filterMenu.toggle());
+ }
+ },
+ handleTransitionEnd: (event) => (dispatch) => {
+ if (event.currentTarget === event.target) {
+ dispatch(actions.app.filterMenu.endTransition());
+ }
+ },
+ toggle: () => (dispatch) => {
+ dispatch(actions.app.filterMenu.startTransition());
+ const toggleMenu = () => dispatch(actions.app.filterMenu.toggle());
+ const animationCb = () => window.setTimeout(toggleMenu);
+ window.requestAnimationFrame(animationCb);
+ },
+});
export const setModalStateFromTable = ({ userEntry, subsection }) => (
(dispatch) => {
@@ -11,13 +39,8 @@ export const setModalStateFromTable = ({ userEntry, subsection }) => (
}
);
-export const initialize = (courseId, urlQuery) => (dispatch) => {
- dispatch(actions.app.setCourseId(courseId));
- dispatch(actions.filters.initialize(urlQuery));
- dispatch(fetchRoles());
-};
-
export default StrictDict({
initialize,
+ filterMenu,
setModalStateFromTable,
});
diff --git a/src/data/thunkActions/app.test.js b/src/data/thunkActions/app.test.js
index 625c74e..3d77fd9 100644
--- a/src/data/thunkActions/app.test.js
+++ b/src/data/thunkActions/app.test.js
@@ -1,4 +1,6 @@
import actions from 'data/actions';
+import selectors from 'data/selectors';
+
import * as thunkActions from './app';
import { fetchGradeOverrideHistory } from './grades';
import { fetchRoles } from './roles';
@@ -9,6 +11,16 @@ jest.mock('./grades', () => ({
jest.mock('./roles', () => ({
fetchRoles: jest.fn(() => ({ type: 'fetchRoles' })),
}));
+jest.mock('data/selectors', () => ({
+ __esModule: true,
+ default: {
+ app: {
+ filterMenu: {
+ open: jest.fn(state => ({ menuOpen: state })),
+ },
+ },
+ },
+}));
describe('app thunkActions', () => {
describe('setModalStateFromTable', () => {
@@ -25,6 +37,48 @@ describe('app thunkActions', () => {
]);
});
});
+ describe('filterMenu', () => {
+ describe('close', () => {
+ it('calls filterMenu.toggle iff menu is open', () => {
+ const { toggle } = thunkActions.filterMenu;
+ const dispatch = jest.fn();
+ thunkActions.filterMenu.toggle = jest.fn(() => ({ type: 'filterMenuToggle' }));
+ selectors.app.filterMenu.open.mockReturnValue(false);
+ thunkActions.filterMenu.close()(dispatch, jest.fn());
+ expect(dispatch).not.toHaveBeenCalled();
+ selectors.app.filterMenu.open.mockReturnValue(true);
+ thunkActions.filterMenu.close()(dispatch, jest.fn());
+ expect(dispatch).toHaveBeenCalledWith(thunkActions.filterMenu.toggle());
+ thunkActions.filterMenu.toggle = toggle;
+ });
+ });
+ describe('handleTransitionEnd', () => {
+ it('ends filterMenu transition iff event target has not changed', () => {
+ const dispatch = jest.fn();
+ thunkActions.filterMenu.handleTransitionEnd({ target: 1, currentTarget: 2 })(dispatch);
+ expect(dispatch).not.toHaveBeenCalled();
+ thunkActions.filterMenu.handleTransitionEnd({ target: 1, currentTarget: 1 })(dispatch);
+ expect(dispatch).toHaveBeenCalled();
+ });
+ });
+ describe('toggle', () => {
+ it('starts transition and toggles on timeout at next animation frame', () => {
+ const dispatch = jest.fn(action => ({ dispatch: action }));
+ const reqAnimFrame = window.requestAnimationFrame;
+ const { setTimeout } = window;
+ window.requestAnimationFrame = jest.fn();
+ window.setTimeout = jest.fn(fn => ({ setTimeout: fn() }));
+ thunkActions.filterMenu.toggle()(dispatch);
+ expect(dispatch).toHaveBeenCalled();
+ expect(dispatch.mock.calls[0][0]).toEqual(actions.app.filterMenu.startTransition());
+ const animCb = window.requestAnimationFrame.mock.calls[0][0];
+ expect(animCb()).toEqual({ setTimeout: dispatch(actions.app.filterMenu.toggle()) });
+ expect(dispatch.mock.calls[1][0]).toEqual(actions.app.filterMenu.toggle());
+ window.requestAnimationFrame = reqAnimFrame;
+ window.setTimeout = setTimeout;
+ });
+ });
+ });
describe('initialize', () => {
it('loads course id, and initailzes filters from urlQuery before fetching roles', () => {
const courseId = 'an ID';
diff --git a/src/index.jsx b/src/index.jsx
index a863a25..6991185 100755
--- a/src/index.jsx
+++ b/src/index.jsx
@@ -1,3 +1,4 @@
+/* eslint-disable import/no-named-as-default */
import 'core-js/stable';
import 'regenerator-runtime/runtime';