From ee2c5730175a3aa160f58c0f8dd849a395d54a6d Mon Sep 17 00:00:00 2001 From: Nathan Sprenkle Date: Fri, 7 May 2021 12:44:00 -0400 Subject: [PATCH] Clean up MFE/Redux usage (#179) * refactor: clean up/standardize selector usage * fix: fix eslint errors * chore: bump version to 1.4.25 Co-authored-by: Ben Warzeski --- package-lock.json | 2 +- package.json | 2 +- src/components/Gradebook/BulkManagement.jsx | 20 +-- src/components/Gradebook/EditModal.jsx | 29 +++-- .../AssignmentFilter/index.jsx | 19 +-- .../AssignmentFilter/test.jsx | 53 ++++---- .../AssignmentGradeFilter/index.jsx | 16 ++- .../AssignmentTypeFilter/index.jsx | 8 +- .../AssignmentTypeFilter/test.jsx | 42 ++++--- .../CourseGradeFilter/index.jsx | 14 ++- .../StudentGroupsFilter/index.jsx | 18 +-- .../Gradebook/GradebookFilters/index.jsx | 3 +- src/components/Gradebook/GradebookTable.jsx | 19 +-- src/components/Gradebook/SearchControls.jsx | 14 ++- src/components/Gradebook/StatusAlerts.jsx | 3 +- src/containers/GradebookPage/index.jsx | 118 +++++------------- src/data/actions/filters.js | 6 +- src/data/actions/grades.js | 16 ++- src/data/actions/roles.js | 6 +- src/data/actions/tracks.js | 17 +-- src/data/reducers/filters.js | 6 +- src/data/selectors/assignmentTypes.js | 6 + src/data/selectors/cohorts.js | 12 +- src/data/selectors/filters.js | 63 +++++++--- src/data/selectors/grades.js | 95 ++++++++------ src/data/selectors/grades.test.js | 12 +- src/data/selectors/index.js | 71 +++++++++++ src/data/selectors/roles.js | 5 + src/data/selectors/special.js | 11 +- src/data/selectors/tracks.js | 14 ++- src/data/utils.js | 19 +++ src/data/utils.test.js | 27 ++++ 32 files changed, 489 insertions(+), 277 deletions(-) create mode 100644 src/data/selectors/assignmentTypes.js create mode 100644 src/data/selectors/index.js create mode 100644 src/data/selectors/roles.js create mode 100644 src/data/utils.js create mode 100644 src/data/utils.test.js diff --git a/package-lock.json b/package-lock.json index ccd9fe5..288a889 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@edx/frontend-app-gradebook", - "version": "1.4.24", + "version": "1.4.25", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 60805ac..9e1312c 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@edx/frontend-app-gradebook", - "version": "1.4.24", + "version": "1.4.25", "description": "edx editable gradebook-ui to manipulate grade overrides on subsections", "repository": { "type": "git", diff --git a/src/components/Gradebook/BulkManagement.jsx b/src/components/Gradebook/BulkManagement.jsx index b23df38..e0336ff 100644 --- a/src/components/Gradebook/BulkManagement.jsx +++ b/src/components/Gradebook/BulkManagement.jsx @@ -10,10 +10,10 @@ import { } from '@edx/paragon'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faDownload } from '@fortawesome/free-solid-svg-icons'; -import { configuration } from '../../config'; +import selectors from 'data/selectors'; +import { configuration } from '../../config'; import { submitFileUploadFormData } from '../../data/actions/grades'; -import { getBulkManagementHistory } from '../../data/selectors/grades'; export class BulkManagement extends React.Component { constructor(props) { @@ -183,14 +183,14 @@ BulkManagement.propTypes = { uploadSuccess: PropTypes.bool, }; -export const mapStateToProps = (state) => ({ - bulkImportError: state.grades.bulkManagement - && state.grades.bulkManagement.errorMessages - ? `Errors while processing: ${state.grades.bulkManagement.errorMessages.join(', ')}` - : '', - bulkManagementHistory: getBulkManagementHistory(state), - uploadSuccess: !!(state.grades.bulkManagement && state.grades.bulkManagement.uploadSuccess), -}); +export const mapStateToProps = (state) => { + const { grades } = selectors; + return { + bulkImportError: grades.bulkImportError(state), + bulkManagementHistory: grades.bulkManagementHistoryEntries(state), + uploadSuccess: grades.uploadSuccess(state), + }; +}; export const mapDispatchToProps = { submitFileUploadFormData, diff --git a/src/components/Gradebook/EditModal.jsx b/src/components/Gradebook/EditModal.jsx index 849236b..59cffa6 100644 --- a/src/components/Gradebook/EditModal.jsx +++ b/src/components/Gradebook/EditModal.jsx @@ -10,14 +10,18 @@ import { Table, } from '@edx/paragon'; +import selectors from 'data/selectors'; import { doneViewingAssignment, updateGrades, } from '../../data/actions/grades'; -const GRADE_OVERRIDE_HISTORY_COLUMNS = [{ label: 'Date', key: 'date' }, { label: 'Grader', key: 'grader' }, +const GRADE_OVERRIDE_HISTORY_COLUMNS = [ + { label: 'Date', key: 'date' }, + { label: 'Grader', key: 'grader' }, { label: 'Reason', key: 'reason' }, - { label: 'Adjusted grade', key: 'adjustedGrade' }]; + { label: 'Adjusted grade', key: 'adjustedGrade' }, +]; export class EditModal extends React.Component { constructor(props) { @@ -185,15 +189,18 @@ EditModal.propTypes = { updateGrades: PropTypes.func.isRequired, }; -export const mapStateToProps = (state) => ({ - gradeOverrides: state.grades.gradeOverrideHistoryResults, - gradeOverrideCurrentEarnedGradedOverride: state.grades.gradeOverrideCurrentEarnedGradedOverride, - gradeOverrideHistoryError: state.grades.gradeOverrideHistoryError, - gradeOriginalEarnedGraded: state.grades.gradeOriginalEarnedGraded, - grdaeOriginalPossibleGraded: state.grades.grdaeOriginalPossibleGraded, - selectedCohort: state.filters.cohort, - selectedTrack: state.filters.track, -}); +export const mapStateToProps = (state) => { + const { filters, grades } = selectors; + return { + gradeOverrides: grades.gradeOverrides(state), + gradeOverrideCurrentEarnedGradedOverride: grades.gradeOverrideCurrentEarnedGradedOverride(state), + gradeOverrideHistoryError: grades.gradeOverrideHistoryError(state), + gradeOriginalEarnedGraded: grades.gradeOriginalEarnedGraded(state), + gradeOriginalPossibleGraded: grades.gradeOriginalPossibleGraded(state), + selectedCohort: filters.cohort(state), + selectedTrack: filters.track(state), + }; +}; export const mapDispatchToProps = { doneViewingAssignment, diff --git a/src/components/Gradebook/GradebookFilters/AssignmentFilter/index.jsx b/src/components/Gradebook/GradebookFilters/AssignmentFilter/index.jsx index 020cfcf..ef89f11 100644 --- a/src/components/Gradebook/GradebookFilters/AssignmentFilter/index.jsx +++ b/src/components/Gradebook/GradebookFilters/AssignmentFilter/index.jsx @@ -3,9 +3,9 @@ import React from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; -import { selectableAssignmentLabels } from 'data/selectors/filters'; import * as gradesActions from 'data/actions/grades'; import * as filterActions from 'data/actions/filters'; +import selectors from 'data/selectors'; import SelectGroup from '../SelectGroup'; @@ -85,13 +85,16 @@ AssignmentFilter.propTypes = { updateAssignmentFilter: PropTypes.func.isRequired, }; -export const mapStateToProps = (state) => ({ - assignmentFilterOptions: selectableAssignmentLabels(state), - selectedAssignment: (state.filters.assignment || {}).label, - selectedAssignmentType: state.filters.assignmentType, - selectedCohort: state.filters.cohort, - selectedTrack: state.filters.track, -}); +export const mapStateToProps = (state) => { + const { filters } = selectors; + return { + assignmentFilterOptions: filters.selectableAssignmentLabels(state), + selectedAssignment: filters.selectedAssignmentLabel(state), + selectedAssignmentType: filters.assignmentType(state), + selectedCohort: filters.cohort(state), + selectedTrack: filters.track(state), + }; +}; export const mapDispatchToProps = { updateAssignmentFilter: filterActions.updateAssignmentFilter, diff --git a/src/components/Gradebook/GradebookFilters/AssignmentFilter/test.jsx b/src/components/Gradebook/GradebookFilters/AssignmentFilter/test.jsx index 2d16e19..a31a015 100644 --- a/src/components/Gradebook/GradebookFilters/AssignmentFilter/test.jsx +++ b/src/components/Gradebook/GradebookFilters/AssignmentFilter/test.jsx @@ -1,6 +1,7 @@ import React from 'react'; import { mount, shallow } from 'enzyme'; +import selectors from 'data/selectors'; import { updateAssignmentFilter } from 'data/actions/filters'; import { updateGradesIfAssignmentGradeFiltersSet } from 'data/actions/grades'; import { @@ -9,12 +10,20 @@ import { mapDispatchToProps, } from '.'; -jest.mock('data/selectors/filters', () => ({ +jest.mock('data/selectors', () => ({ /** Mocking to use passed state for validation purposes */ - selectableAssignmentLabels: jest.fn().mockImplementation((state) => ({ - state, - selectableLabels: 'selectableLabels', - })), + filters: { + selectableAssignmentLabels: jest.fn(() => ([{ + label: 'assigNment', + subsectionLabel: 'subsection', + type: 'assignMentType', + id: 'subsectionId', + }])), + selectedAssignmentLabel: jest.fn(() => 'assigNment'), + assignmentType: jest.fn(() => 'assignMentType'), + cohort: jest.fn(() => 'COhort'), + track: jest.fn(() => 'traCK'), + }, })); describe('AssignmentFilter', () => { @@ -103,51 +112,47 @@ describe('AssignmentFilter', () => { }, }; describe('assignmentFilterOptions', () => { - it('is drawn from selectableAssignmentLabels', () => { - expect(mapStateToProps(state).assignmentFilterOptions).toEqual({ - state, - selectableLabels: 'selectableLabels', - }); + it('is selected from filters.selectableAssignmentLabels', () => { + expect( + mapStateToProps(state).assignmentFilterOptions, + ).toEqual( + selectors.filters.selectableAssignmentLabels(state), + ); }); }); - describe('selectedAsssignment', () => { - it('is undefined if no assignment is passed', () => { - expect( - mapStateToProps({ filters: {} }).selectedAssignment, - ).toEqual(undefined); - }); - it('returns the label of selected assignment if there is one', () => { + describe('selectedAssignment', () => { + it('is selected from filters.selectedAssignmentLabel', () => { expect( mapStateToProps(state).selectedAssignment, ).toEqual( - state.filters.assignment.label, + selectors.filters.selectedAssignmentLabel(state), ); }); }); describe('selectedAssignmentType', () => { - it('is drawn from state.filters.assignmentType', () => { + it('is selected from filters.assignmentType', () => { expect( mapStateToProps(state).selectedAssignmentType, ).toEqual( - state.filters.assignmentType, + selectors.filters.assignmentType(state), ); }); }); describe('selectedCohort', () => { - it('is drawn from state.filters.cohort', () => { + it('is selected from filters.cohort', () => { expect( mapStateToProps(state).selectedCohort, ).toEqual( - state.filters.cohort, + selectors.filters.cohort(state), ); }); }); describe('selectedTrack', () => { - it('is drawn from state.filters.track', () => { + it('is selected from filters.track', () => { expect( mapStateToProps(state).selectedTrack, ).toEqual( - state.filters.track, + selectors.filters.track(state), ); }); }); diff --git a/src/components/Gradebook/GradebookFilters/AssignmentGradeFilter/index.jsx b/src/components/Gradebook/GradebookFilters/AssignmentGradeFilter/index.jsx index c1aa92d..e962a88 100644 --- a/src/components/Gradebook/GradebookFilters/AssignmentGradeFilter/index.jsx +++ b/src/components/Gradebook/GradebookFilters/AssignmentGradeFilter/index.jsx @@ -7,6 +7,7 @@ import { Button } from '@edx/paragon'; import * as gradesActions from 'data/actions/grades'; import * as filterActions from 'data/actions/filters'; +import selectors from 'data/selectors'; import PercentGroup from '../PercentGroup'; @@ -106,12 +107,15 @@ AssignmentGradeFilter.propTypes = { updateAssignmentLimits: PropTypes.func.isRequired, }; -export const mapStateToProps = (state) => ({ - selectedAssignment: (state.filters.assignment || {}).label, - selectedAssignmentType: state.filters.assignmentType, - selectedCohort: state.filters.cohort, - selectedTrack: state.filters.track, -}); +export const mapStateToProps = (state) => { + const { filters } = selectors; + return { + selectedAssignment: filters.selectedAssignmentLabel(state), + selectedAssignmentType: filters.assignmentType(state), + selectedCohort: filters.cohort(state), + selectedTrack: filters.track(state), + }; +}; export const mapDispatchToProps = { getUserGrades: gradesActions.fetchGrades, diff --git a/src/components/Gradebook/GradebookFilters/AssignmentTypeFilter/index.jsx b/src/components/Gradebook/GradebookFilters/AssignmentTypeFilter/index.jsx index 0f14c24..787cb87 100644 --- a/src/components/Gradebook/GradebookFilters/AssignmentTypeFilter/index.jsx +++ b/src/components/Gradebook/GradebookFilters/AssignmentTypeFilter/index.jsx @@ -3,8 +3,8 @@ import React from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; -import { selectableAssignmentLabels } from 'data/selectors/filters'; import * as gradesActions from 'data/actions/grades'; +import selectors from 'data/selectors'; import SelectGroup from '../SelectGroup'; @@ -66,9 +66,9 @@ AssignmentTypeFilter.propTypes = { }; export const mapStateToProps = (state) => ({ - assignmentTypes: state.assignmentTypes.results, - assignmentFilterOptions: selectableAssignmentLabels(state), - selectedAssignmentType: state.filters.assignmentType, + assignmentTypes: selectors.assignmentTypes.allAssignmentTypes(state), + assignmentFilterOptions: selectors.filters.selectableAssignmentLabels(state), + selectedAssignmentType: selectors.filters.assignmentType(state), }); export const mapDispatchToProps = { diff --git a/src/components/Gradebook/GradebookFilters/AssignmentTypeFilter/test.jsx b/src/components/Gradebook/GradebookFilters/AssignmentTypeFilter/test.jsx index 8f87583..b755630 100644 --- a/src/components/Gradebook/GradebookFilters/AssignmentTypeFilter/test.jsx +++ b/src/components/Gradebook/GradebookFilters/AssignmentTypeFilter/test.jsx @@ -1,6 +1,7 @@ import React from 'react'; import { shallow } from 'enzyme'; +import selectors from 'data/selectors'; import { filterAssignmentType } from 'data/actions/grades'; import { @@ -9,12 +10,20 @@ import { mapDispatchToProps, } from '.'; -jest.mock('data/selectors/filters', () => ({ +jest.mock('data/selectors', () => ({ /** Mocking to use passed state for validation purposes */ - selectableAssignmentLabels: jest.fn().mockImplementation((state) => ({ - state, - selectableLabels: 'selectableLabels', - })), + assignmentTypes: { + allAssignmentTypes: jest.fn(() => (['assignment', 'labs'])), + }, + filters: { + selectableAssignmentLabels: jest.fn(() => ([{ + label: 'assigNment', + subsectionLabel: 'subsection', + type: 'assignMentType', + id: 'subsectionId', + }])), + assignmentType: jest.fn(() => 'assignMentType'), + }, })); describe('AssignmentTypeFilter', () => { @@ -89,26 +98,29 @@ describe('AssignmentTypeFilter', () => { }, }; describe('assignmentTypes', () => { - it('is drawn from assignmentTypes.results', () => { - expect(mapStateToProps(state).assignmentTypes).toEqual( - state.assignmentTypes.results, + it('is selected from assignmentTypes.allAssignmentTypes', () => { + expect( + mapStateToProps(state).assignmentTypes, + ).toEqual( + selectors.assignmentTypes.allAssignmentTypes(state), ); }); }); describe('assignmentFilterOptions', () => { - it('is drawn from selectableAssignmentLabels', () => { - expect(mapStateToProps(state).assignmentFilterOptions).toEqual({ - state, - selectableLabels: 'selectableLabels', - }); + it('is selected from filters.selectableAssignmentLabels', () => { + expect( + mapStateToProps(state).assignmentFilterOptions, + ).toEqual( + selectors.filters.selectableAssignmentLabels(state), + ); }); }); describe('selectedAssignmentType', () => { - it('is drawn from state.filters.assignmentType', () => { + it('is selected from filters.assignmentType', () => { expect( mapStateToProps(state).selectedAssignmentType, ).toEqual( - state.filters.assignmentType, + selectors.filters.assignmentType(state), ); }); }); diff --git a/src/components/Gradebook/GradebookFilters/CourseGradeFilter/index.jsx b/src/components/Gradebook/GradebookFilters/CourseGradeFilter/index.jsx index f85806c..fe14bd9 100644 --- a/src/components/Gradebook/GradebookFilters/CourseGradeFilter/index.jsx +++ b/src/components/Gradebook/GradebookFilters/CourseGradeFilter/index.jsx @@ -8,6 +8,7 @@ import { import { updateCourseGradeFilter } from 'data/actions/filters'; import { fetchGrades } from 'data/actions/grades'; +import selectors from 'data/selectors'; import PercentGroup from '../PercentGroup'; export class CourseGradeFilter extends React.Component { @@ -118,11 +119,14 @@ CourseGradeFilter.propTypes = { updateFilter: PropTypes.func.isRequired, }; -export const mapStateToProps = (state) => ({ - selectedCohort: state.filters.cohort, - selectedTrack: state.filters.track, - selectedAssignmentType: state.filters.assignmentType, -}); +export const mapStateToProps = (state) => { + const { filters } = selectors; + return { + selectedCohort: filters.cohort(state), + selectedTrack: filters.track(state), + selectedAssignmentType: filters.assignmentType(state), + }; +}; export const mapDispatchToProps = { updateFilter: updateCourseGradeFilter, diff --git a/src/components/Gradebook/GradebookFilters/StudentGroupsFilter/index.jsx b/src/components/Gradebook/GradebookFilters/StudentGroupsFilter/index.jsx index cba3842..dcfd0e4 100644 --- a/src/components/Gradebook/GradebookFilters/StudentGroupsFilter/index.jsx +++ b/src/components/Gradebook/GradebookFilters/StudentGroupsFilter/index.jsx @@ -4,6 +4,7 @@ import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import { fetchGrades } from 'data/actions/grades'; +import selectors from 'data/selectors'; import SelectGroup from '../SelectGroup'; export class StudentGroupsFilter extends React.Component { @@ -134,13 +135,16 @@ StudentGroupsFilter.propTypes = { })), }; -export const mapStateToProps = (state) => ({ - cohorts: state.cohorts.results, - selectedAssignmentType: state.filters.assignmentType, - selectedCohort: state.filters.cohort, - selectedTrack: state.filters.track, - tracks: state.tracks.results, -}); +export const mapStateToProps = (state) => { + const { filters, cohorts, tracks } = selectors; + return { + cohorts: cohorts.allCohorts(state), + selectedAssignmentType: filters.assignmentType(state), + selectedCohort: filters.cohort(state), + selectedTrack: filters.track(state), + tracks: tracks.allTracks(state), + }; +}; export const mapDispatchToProps = { getUserGrades: fetchGrades, diff --git a/src/components/Gradebook/GradebookFilters/index.jsx b/src/components/Gradebook/GradebookFilters/index.jsx index bba27ff..8613893 100644 --- a/src/components/Gradebook/GradebookFilters/index.jsx +++ b/src/components/Gradebook/GradebookFilters/index.jsx @@ -6,6 +6,7 @@ import { connect } from 'react-redux'; import { Collapsible, Form } from '@edx/paragon'; import * as filterActions from 'data/actions/filters'; +import selectors from 'data/selectors'; import AssignmentTypeFilter from './AssignmentTypeFilter'; import AssignmentFilter from './AssignmentFilter'; @@ -109,7 +110,7 @@ GradebookFilters.propTypes = { }; export const mapStateToProps = (state) => ({ - includeCourseRoleMembers: state.filters.includeCourseRoleMembers, + includeCourseRoleMembers: selectors.filters.includeCourseRoleMembers(state), }); export const mapDispatchToProps = { diff --git a/src/components/Gradebook/GradebookTable.jsx b/src/components/Gradebook/GradebookTable.jsx index bd2a764..b4a8ffe 100644 --- a/src/components/Gradebook/GradebookTable.jsx +++ b/src/components/Gradebook/GradebookTable.jsx @@ -2,13 +2,15 @@ import React from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; + import { Table, OverlayTrigger, Tooltip, Icon, } from '@edx/paragon'; + import { formatDateForDisplay } from '../../data/actions/utils'; -import { getHeadings } from '../../data/selectors/grades'; import { fetchGradeOverrideHistory } from '../../data/actions/grades'; import { EMAIL_HEADING, TOTAL_COURSE_GRADE_HEADING, USERNAME_HEADING } from '../../data/constants/grades'; +import selectors from '../../data/selectors'; const DECIMAL_PRECISION = 2; @@ -215,12 +217,15 @@ GradebookTable.propTypes = { fetchGradeOverrideHistory: PropTypes.func.isRequired, }; -export const mapStateToProps = (state) => ({ - areGradesFrozen: state.assignmentTypes.areGradesFrozen, - format: state.grades.gradeFormat, - grades: state.grades.results, - headings: getHeadings(state), -}); +export const mapStateToProps = (state) => { + const { assignmentTypes, grades, root } = selectors; + return { + areGradesFrozen: assignmentTypes.areGradesFrozen(state), + format: grades.gradeFormat(state), + grades: grades.allGrades(state), + headings: root.getHeadings(state), + }; +}; export const mapDispatchToProps = { fetchGradeOverrideHistory, diff --git a/src/components/Gradebook/SearchControls.jsx b/src/components/Gradebook/SearchControls.jsx index 4c59a95..d02af0b 100644 --- a/src/components/Gradebook/SearchControls.jsx +++ b/src/components/Gradebook/SearchControls.jsx @@ -4,6 +4,7 @@ import { connect } from 'react-redux'; import { Button, Icon, SearchField } from '@edx/paragon'; +import selectors from 'data/selectors'; import { fetchGrades, fetchMatchingUserGrades, @@ -96,11 +97,14 @@ SearchControls.propTypes = { selectedTrack: PropTypes.string, }; -export const mapStateToProps = (state) => ({ - selectedAssignmentType: state.filters.assignmentType, - selectedTrack: state.filters.track, - selectedCohort: state.filters.cohort, -}); +export const mapStateToProps = (state) => { + const { filters } = selectors; + return { + selectedAssignmentType: filters.assignmentType(state), + selectedTrack: filters.track(state), + selectedCohort: filters.cohort(state), + }; +}; export const mapDispatchToProps = { getUserGrades: fetchGrades, diff --git a/src/components/Gradebook/StatusAlerts.jsx b/src/components/Gradebook/StatusAlerts.jsx index de2b68f..8c67b5f 100644 --- a/src/components/Gradebook/StatusAlerts.jsx +++ b/src/components/Gradebook/StatusAlerts.jsx @@ -4,6 +4,7 @@ import { connect } from 'react-redux'; import { StatusAlert } from '@edx/paragon'; +import selectors from 'data/selectors'; import { closeBanner } from '../../data/actions/grades'; export const maxCourseGradeInvalidMessage = 'Maximum course grade value must be between 0 and 100. '; @@ -61,7 +62,7 @@ StatusAlerts.propTypes = { }; export const mapStateToProps = (state) => ({ - showSuccessBanner: state.grades.showSuccess, + showSuccessBanner: selectors.grades.showSuccess(state), }); export const mapDispatchToProps = { diff --git a/src/containers/GradebookPage/index.jsx b/src/containers/GradebookPage/index.jsx index 42ecac4..1d92f42 100644 --- a/src/containers/GradebookPage/index.jsx +++ b/src/containers/GradebookPage/index.jsx @@ -1,5 +1,6 @@ import { connect } from 'react-redux'; +import selectors from 'data/selectors'; import Gradebook from '../../components/Gradebook'; import { fetchGradeOverrideHistory, @@ -19,96 +20,45 @@ import { updateAssignmentFilter, updateAssignmentLimits, } from '../../data/actions/filters'; -import stateHasMastersTrack from '../../data/selectors/tracks'; -import { - getBulkManagementHistory, - getHeadings, - formatMinAssignmentGrade, - formatMaxAssignmentGrade, - formatMinCourseGrade, - formatMaxCourseGrade, -} from '../../data/selectors/grades'; -import { selectableAssignmentLabels } from '../../data/selectors/filters'; -import { hasSpecialBulkManagementAccess } from '../../data/selectors/special'; -import { getCohortNameById } from '../../data/selectors/cohorts'; import { fetchAssignmentTypes } from '../../data/actions/assignmentTypes'; import { getRoles } from '../../data/actions/roles'; -import LmsApiService from '../../data/services/LmsApiService'; -function shouldShowSpinner(state) { - if (state.roles.canUserViewGradebook === true) { - return state.grades.showSpinner; - } if (state.roles.canUserViewGradebook === false) { - return false; - } // canUserViewGradebook === null - return true; -} +const mapStateToProps = (state, ownProps) => { + const { + root, + assignmentTypes, + filters, + grades, + roles, + } = selectors; -const mapStateToProps = (state, ownProps) => ( - { - areGradesFrozen: state.assignmentTypes.areGradesFrozen, - assignmentTypes: state.assignmentTypes.results, - assignmentFilterOptions: selectableAssignmentLabels(state), - bulkImportError: state.grades.bulkManagement - && state.grades.bulkManagement.errorMessages - ? `Errors while processing: ${state.grades.bulkManagement.errorMessages.join(', ')}` - : '', - bulkManagementHistory: getBulkManagementHistory(state), - courseId: ownProps.match.params.courseId, - canUserViewGradebook: state.roles.canUserViewGradebook, - filteredUsersCount: state.grades.filteredUsersCount, - format: state.grades.gradeFormat, - gradeExportUrl: LmsApiService.getGradeExportCsvUrl(ownProps.match.params.courseId, { - cohort: getCohortNameById(state, state.filters.cohort), - track: state.filters.track, - assignment: (state.filters.assignment || {}).id, - assignmentType: state.filters.assignmentType, - assignmentGradeMin: formatMinAssignmentGrade( - state.filters.assignmentGradeMin, - { assignmentId: (state.filters.assignment || {}).id }, - ), - assignmentGradeMax: formatMaxAssignmentGrade( - state.filters.assignmentGradeMax, - { assignmentId: (state.filters.assignment || {}).id }, - ), - courseGradeMin: formatMinCourseGrade(state.filters.courseGradeMin), - courseGradeMax: formatMaxCourseGrade(state.filters.courseGradeMax), - excludedCourseRoles: state.filters.includeCourseRoleMembers ? '' : 'all', - }), - grades: state.grades.results, - headings: getHeadings(state), - interventionExportUrl: - LmsApiService.getInterventionExportCsvUrl(ownProps.match.params.courseId, { - cohort: getCohortNameById(state, state.filters.cohort), - assignment: (state.filters.assignment || {}).id, - assignmentType: state.filters.assignmentType, - assignmentGradeMin: formatMinAssignmentGrade( - state.filters.assignmentGradeMin, - { assignmentId: (state.filters.assignment || {}).id }, - ), - assignmentGradeMax: formatMaxAssignmentGrade( - state.filters.assignmentGradeMax, - { assignmentId: (state.filters.assignment || {}).id }, - ), - courseGradeMin: formatMinCourseGrade(state.filters.courseGradeMin), - courseGradeMax: formatMaxCourseGrade(state.filters.courseGradeMax), - }), + const { courseId } = ownProps.match.params; + return { + courseId, + areGradesFrozen: assignmentTypes.areGradesFrozen(state), + assignmentTypes: assignmentTypes.allAssignmentTypes(state), + assignmentFilterOptions: filters.selectableAssignmentLabels(state), + bulkImportError: grades.bulkImportError(state), + bulkManagementHistory: grades.bulkManagementHistoryEntries(state), + canUserViewGradebook: roles.canUserViewGradebook(state), + filteredUsersCount: grades.filteredUsersCount(state), + format: grades.gradeFormat(state), + gradeExportUrl: root.gradeExportUrl(state, { courseId }), + grades: grades.allGrades(state), + headings: root.getHeadings(state), + interventionExportUrl: root.interventionExportUrl(state, { courseId }), nextPage: state.grades.nextPage, prevPage: state.grades.prevPage, - selectedTrack: state.filters.track, - selectedCohort: state.filters.cohort, - selectedAssignmentType: state.filters.assignmentType, - selectedAssignment: (state.filters.assignment || {}).label, - showBulkManagement: ( - hasSpecialBulkManagementAccess(ownProps.match.params.courseId) - || (stateHasMastersTrack(state) && state.config.bulkManagementAvailable) - ), - showSpinner: shouldShowSpinner(state), - totalUsersCount: state.grades.totalUsersCount, - uploadSuccess: !!(state.grades.bulkManagement - && state.grades.bulkManagement.uploadSuccess), - } -); + selectedTrack: filters.track(state), + selectedCohort: filters.cohort(state), + selectedAssignmentType: filters.assignmentType(state), + selectedAssignment: filters.selectedAssignmentLabel(state), + showBulkManagement: root.showBulkManagement(state, { courseId }), + showSpinner: root.shouldShowSpinner(state), + totalUsersCount: grades.totalUsersCount(state), + uploadSuccess: grades.uploadSuccess(state), + }; +}; const mapDispatchToProps = { downloadBulkGradesReport, diff --git a/src/data/actions/filters.js b/src/data/actions/filters.js index 92a0008..aa7e04a 100644 --- a/src/data/actions/filters.js +++ b/src/data/actions/filters.js @@ -1,3 +1,4 @@ +import filterSelectors from 'data/selectors/filters'; import initialFilters from '../constants/filters'; import { INITIALIZE_FILTERS, @@ -7,9 +8,10 @@ import { UPDATE_COURSE_GRADE_LIMITS, UPDATE_INCLUDE_COURSE_ROLE_MEMBERS, } from '../constants/actionTypes/filters'; -import { getFilters } from '../selectors/filters'; import { fetchGrades } from './grades'; +const { allFilters } = filterSelectors; + const initializeFilters = ({ assignment = initialFilters.assignment, assignmentType = initialFilters.assignmentType, @@ -69,7 +71,7 @@ const updateIncludeCourseRoleMembersFilter = (includeCourseRoleMembers) => ({ const updateIncludeCourseRoleMembers = includeCourseRoleMembers => (dispatch, getState) => { dispatch(updateIncludeCourseRoleMembersFilter(includeCourseRoleMembers)); const state = getState(); - const { cohort, track, assignmentType } = getFilters(state); + const { cohort, track, assignmentType } = allFilters(state); dispatch(fetchGrades(state.grades.courseId, cohort, track, assignmentType)); }; diff --git a/src/data/actions/grades.js b/src/data/actions/grades.js index c9b51b4..3c83931 100644 --- a/src/data/actions/grades.js +++ b/src/data/actions/grades.js @@ -1,4 +1,6 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; +import gradesSelectors from 'data/selectors/grades'; +import filtersSelectors from 'data/selectors/filters'; import { STARTED_FETCHING_GRADES, FINISHED_FETCHING_GRADES, @@ -27,10 +29,14 @@ import { import GRADE_OVERRIDE_HISTORY_ERROR_DEFAULT_MSG from '../constants/errors'; import LmsApiService from '../services/LmsApiService'; import { sortAlphaAsc, formatDateForDisplay } from './utils'; -import { - formatMaxAssignmentGrade, formatMinAssignmentGrade, formatMaxCourseGrade, formatMinCourseGrade, -} from '../selectors/grades'; -import { getFilters } from '../selectors/filters'; + +const { + formatMaxAssignmentGrade, + formatMinAssignmentGrade, + formatMaxCourseGrade, + formatMinCourseGrade, +} = gradesSelectors; +const { allFilters } = filtersSelectors; const defaultAssignmentFilter = 'All'; @@ -142,7 +148,7 @@ const fetchGrades = ( courseGradeMin, courseGradeMax, includeCourseRoleMembers, - } = getFilters(getState()); + } = allFilters(getState()); const { id: assignmentId } = assignment || {}; const assignmentGradeMax = formatMaxAssignmentGrade(assignmentMax, { assignmentId }); const assignmentGradeMin = formatMinAssignmentGrade(assignmentMin, { assignmentId }); diff --git a/src/data/actions/roles.js b/src/data/actions/roles.js index 6d81634..9b3b42b 100644 --- a/src/data/actions/roles.js +++ b/src/data/actions/roles.js @@ -1,3 +1,4 @@ +import filtersSelectors from 'data/selectors/filters'; import { GOT_ROLES, ERROR_FETCHING_ROLES, @@ -6,9 +7,10 @@ import { fetchGrades } from './grades'; import { fetchTracks } from './tracks'; import { fetchCohorts } from './cohorts'; import { fetchAssignmentTypes } from './assignmentTypes'; -import { getFilters } from '../selectors/filters'; import LmsApiService from '../services/LmsApiService'; +const { allFilters } = filtersSelectors; + const allowedRoles = ['staff', 'instructor', 'support']; const gotRoles = (canUserViewGradebook, courseId) => ({ @@ -26,7 +28,7 @@ const getRoles = courseId => ( || (response.roles.some(role => (role.course_id === courseId) && allowedRoles.includes(role.role))); dispatch(gotRoles(canUserViewGradebook, courseId)); - const { cohort, track, assignmentType } = getFilters(getState()); + const { cohort, track, assignmentType } = allFilters(getState()); if (canUserViewGradebook) { dispatch(fetchGrades(courseId, cohort, track, assignmentType)); dispatch(fetchTracks(courseId)); diff --git a/src/data/actions/tracks.js b/src/data/actions/tracks.js index 39eb79a..8d79461 100644 --- a/src/data/actions/tracks.js +++ b/src/data/actions/tracks.js @@ -1,24 +1,27 @@ +/* eslint-disable camelcase */ +import tracksSelectors from 'data/selectors/tracks'; import { STARTED_FETCHING_TRACKS, GOT_TRACKS, ERROR_FETCHING_TRACKS, } from '../constants/actionTypes/tracks'; -import { hasMastersTrack } from '../selectors/tracks'; import { fetchBulkUpgradeHistory } from './grades'; import LmsApiService from '../services/LmsApiService'; +const { hasMastersTrack } = tracksSelectors; + const startedFetchingTracks = () => ({ type: STARTED_FETCHING_TRACKS }); const errorFetchingTracks = () => ({ type: ERROR_FETCHING_TRACKS }); -const gotTracks = tracks => ({ type: GOT_TRACKS, tracks }); +const gotTracks = (tracks) => ({ type: GOT_TRACKS, tracks }); -const fetchTracks = courseId => ( +const fetchTracks = (courseId) => ( (dispatch) => { dispatch(startedFetchingTracks()); return LmsApiService.fetchTracks(courseId) - .then(response => response.data) - .then((data) => { - dispatch(gotTracks(data.course_modes)); - if (hasMastersTrack(data.course_modes)) { + .then(({ data }) => data) + .then(({ course_modes }) => { + dispatch(gotTracks(course_modes)); + if (hasMastersTrack(course_modes)) { dispatch(fetchBulkUpgradeHistory(courseId)); } }) diff --git a/src/data/reducers/filters.js b/src/data/reducers/filters.js index d41d8c5..e8a71cc 100644 --- a/src/data/reducers/filters.js +++ b/src/data/reducers/filters.js @@ -1,5 +1,5 @@ +import filterSelectors from 'data/selectors/filters'; import { GOT_GRADES, FILTER_BY_ASSIGNMENT_TYPE } from '../constants/actionTypes/grades'; - import { INITIALIZE_FILTERS, UPDATE_ASSIGNMENT_FILTER, @@ -8,11 +8,9 @@ import { RESET_FILTERS, UPDATE_INCLUDE_COURSE_ROLE_MEMBERS, } from '../constants/actionTypes/filters'; - import initialFilters from '../constants/filters'; -import { getAssignmentsFromResultsSubstate, chooseRelevantAssignmentData } from '../selectors/filters'; - +const { getAssignmentsFromResultsSubstate, chooseRelevantAssignmentData } = filterSelectors; const initialState = {}; const reducer = (state = initialState, action) => { diff --git a/src/data/selectors/assignmentTypes.js b/src/data/selectors/assignmentTypes.js new file mode 100644 index 0000000..4feff21 --- /dev/null +++ b/src/data/selectors/assignmentTypes.js @@ -0,0 +1,6 @@ +const selectors = { + areGradesFrozen: ({ assignmentTypes }) => assignmentTypes.areGradesFrozen, + allAssignmentTypes: ({ assignmentTypes }) => assignmentTypes.results, +}; + +export default selectors; diff --git a/src/data/selectors/cohorts.js b/src/data/selectors/cohorts.js index 647c7d6..0f218c6 100644 --- a/src/data/selectors/cohorts.js +++ b/src/data/selectors/cohorts.js @@ -1,10 +1,16 @@ -const getCohorts = state => state.cohorts.results || []; +const allCohorts = state => state.cohorts.results || []; const getCohortById = (state, selectedCohortId) => { - const cohort = getCohorts(state).find(coh => coh.id === selectedCohortId); + const cohort = allCohorts(state).find(coh => coh.id === selectedCohortId); return cohort; }; const getCohortNameById = (state, selectedCohortId) => (getCohortById(state, selectedCohortId) || {}).name; -export { getCohortById, getCohortNameById, getCohorts }; +const selectors = { + getCohortById, + getCohortNameById, + allCohorts, +}; + +export default selectors; diff --git a/src/data/selectors/filters.js b/src/data/selectors/filters.js index df64030..f0853d1 100644 --- a/src/data/selectors/filters.js +++ b/src/data/selectors/filters.js @@ -1,38 +1,73 @@ -const getFilters = state => state.filters || {}; +import simpleSelectorFactory from '../utils'; -const getAssignmentsFromResultsSubstate = results => (results[0] || {}).section_breakdown || []; +const allFilters = (state) => state.filters || {}; + +const getAssignmentsFromResultsSubstate = (results) => ( + (results[0] || {}).section_breakdown || [] +); const selectableAssignments = (state) => { - const selectedAssignmentType = getFilters(state).assignmentType; + const selectedAssignmentType = allFilters(state).assignmentType; const needToFilter = selectedAssignmentType && selectedAssignmentType !== 'All'; const allAssignments = getAssignmentsFromResultsSubstate(state.grades.results); if (needToFilter) { - return allAssignments.filter(assignment => assignment.category === selectedAssignmentType); + return allAssignments.filter( + (assignment) => assignment.category === selectedAssignmentType, + ); } return allAssignments; }; -const chooseRelevantAssignmentData = assignment => ({ - label: assignment.label, - subsectionLabel: assignment.subsection_name, - type: assignment.category, - id: assignment.module_id, +const chooseRelevantAssignmentData = ({ + label, + subsection_name: subsectionLabel, + category, + module_id: id, +}) => ({ + label, subsectionLabel, category, id, }); -const selectableAssignmentLabels = state => selectableAssignments(state).map(chooseRelevantAssignmentData); +const selectableAssignmentLabels = (state) => ( + selectableAssignments(state).map(chooseRelevantAssignmentData) +); const typeOfSelectedAssignment = (state) => { - const selectedAssignmentLabel = getFilters(state).assignment; + const selectedAssignmentLabel = allFilters(state).assignment; const sectionBreakdown = (state.grades.results[0] || {}).section_breakdown || []; - const selectedAssignment = sectionBreakdown.find(section => section.label === selectedAssignmentLabel); + const selectedAssignment = sectionBreakdown.find( + ({ label }) => label === selectedAssignmentLabel, + ); return selectedAssignment && selectedAssignment.category; }; -export { +const simpleSelectors = simpleSelectorFactory( + ({ filters }) => filters, + [ + 'assignment', + 'assignmentGradeMax', + 'assignmentGradeMin', + 'assignmentType', + 'cohort', + 'courseGradeMax', + 'courseGradeMin', + 'track', + 'includeCourseRoleMembers', + ], +); +const selectedAssignmentId = (state) => (simpleSelectors.assignment(state) || {}).id; +const selectedAssignmentLabel = (state) => (simpleSelectors.assignment(state) || {}).label; + +const selectors = { + ...simpleSelectors, + selectedAssignmentId, + selectedAssignmentLabel, + selectableAssignmentLabels, selectableAssignments, - getFilters, + allFilters, typeOfSelectedAssignment, chooseRelevantAssignmentData, getAssignmentsFromResultsSubstate, }; + +export default selectors; diff --git a/src/data/selectors/grades.js b/src/data/selectors/grades.js index d08378a..ec9e721 100644 --- a/src/data/selectors/grades.js +++ b/src/data/selectors/grades.js @@ -1,5 +1,5 @@ import { formatDateForDisplay } from '../actions/utils'; -import { getFilters } from './filters'; +import simpleSelectorFactory from '../utils'; import { EMAIL_HEADING, TOTAL_COURSE_GRADE_HEADING, USERNAME_HEADING } from '../constants/grades'; const getRowsProcessed = (data) => { @@ -16,26 +16,25 @@ const getRowsProcessed = (data) => { }; }; -const transformHistoryEntry = (historyRow) => { - const { - modified, - original_filename: originalFilename, - data, - ...rest - } = historyRow; +const transformHistoryEntry = ({ + modified, + original_filename: originalFilename, + data, + ...rest +}) => ({ + timeUploaded: formatDateForDisplay(new Date(modified)), + originalFilename, + summaryOfRowsProcessed: getRowsProcessed(data), + ...rest, +}); - const timeUploaded = formatDateForDisplay(new Date(modified)); - const summaryOfRowsProcessed = getRowsProcessed(data); +const bulkManagementHistory = ({ grades: { bulkManagement } }) => ( + (bulkManagement.history || []) +); - return { - timeUploaded, - originalFilename, - summaryOfRowsProcessed, - ...rest, - }; -}; -const getBulkManagementHistoryFromState = state => state.grades.bulkManagement.history || []; -const getBulkManagementHistory = state => getBulkManagementHistoryFromState(state).map(transformHistoryEntry); +const bulkManagementHistoryEntries = (state) => ( + bulkManagementHistory(state).map(transformHistoryEntry) +); const headingMapper = (category, label = 'All') => { const filters = { @@ -67,19 +66,9 @@ const headingMapper = (category, label = 'All') => { }; }; -const getExampleSectionBreakdown = state => (state.grades.results[0] || {}).section_breakdown || []; - -const getHeadings = (state) => { - const filters = getFilters(state) || {}; - const { - assignmentType: selectedAssignmentType, - assignment: selectedAssignment, - } = filters; - const assignments = getExampleSectionBreakdown(state); - const type = selectedAssignmentType || 'All'; - const assignment = (selectedAssignment || {}).label || 'All'; - return headingMapper(type, assignment)(assignments); -}; +const getExampleSectionBreakdown = ({ grades }) => ( + (grades.results[0] || {}).section_breakdown || [] +); const composeFilters = (...predicates) => (percentGrade, options = {}) => predicates.reduce((accum, predicate) => { if (predicate(percentGrade, options)) { @@ -102,20 +91,56 @@ const assignmentIdIsDefined = (percentGrade, { assignmentId }) => ( const formatMaxCourseGrade = composeFilters(percentGradeIsMax); const formatMinCourseGrade = composeFilters(percentGradeIsMin); + const formatMaxAssignmentGrade = composeFilters( percentGradeIsMax, assignmentIdIsDefined, ); + const formatMinAssignmentGrade = composeFilters( percentGradeIsMin, assignmentIdIsDefined, ); -export { - getBulkManagementHistory, - getHeadings, +const simpleSelectors = simpleSelectorFactory( + ({ grades }) => grades, + [ + 'filteredUsersCount', + 'totalUsersCount', + 'gradeFormat', + 'showSpinner', + 'gradeOverrideCurrentEarnedGradedOverride', + 'gradeOverrideHistoryError', + 'gradeOriginalEarnedGraded', + 'gradeOriginalPossibleGraded', + 'showSuccess', + ], +); + +const allGrades = ({ grades: { results } }) => results; +const uploadSuccess = ({ grades: { bulkManagement } }) => (!!bulkManagement && bulkManagement.uploadSuccess); + +const bulkImportError = ({ grades: { bulkManagement } }) => ( + (!!bulkManagement && bulkManagement.errorMessages) + ? `Errors while processing: ${bulkManagement.errorMessages.join(', ')}` + : '' +); +const gradeOverrides = ({ grades }) => grades.gradeOverrideHistoryResults; + +const selectors = { + bulkImportError, formatMinAssignmentGrade, formatMaxAssignmentGrade, formatMaxCourseGrade, formatMinCourseGrade, + getExampleSectionBreakdown, + headingMapper, + + ...simpleSelectors, + allGrades, + uploadSuccess, + bulkManagementHistoryEntries, + gradeOverrides, }; + +export default selectors; diff --git a/src/data/selectors/grades.test.js b/src/data/selectors/grades.test.js index 17ef29b..e25bf37 100644 --- a/src/data/selectors/grades.test.js +++ b/src/data/selectors/grades.test.js @@ -1,4 +1,4 @@ -import { getBulkManagementHistory } from './grades'; +import selectors from './grades'; const genericHistoryRow = { id: 5, @@ -15,14 +15,14 @@ const genericHistoryRow = { }, }; -describe('getBulkManagementHistory', () => { +describe('bulkManagementHistoryEntries', () => { it('handles history being as-yet unloaded', () => { - const result = getBulkManagementHistory({ grades: { bulkManagement: {} } }); + const result = selectors.bulkManagementHistoryEntries({ grades: { bulkManagement: {} } }); expect(result).toEqual([]); }); it('formats dates for us', () => { - const result = getBulkManagementHistory({ + const result = selectors.bulkManagementHistoryEntries({ grades: { bulkManagement: { history: [ @@ -37,7 +37,7 @@ describe('getBulkManagementHistory', () => { }); const exerciseGetRowsProcessed = (input, expectation) => { - const result = getBulkManagementHistory({ + const result = selectors.bulkManagementHistoryEntries({ grades: { bulkManagement: { history: [ @@ -50,7 +50,7 @@ describe('getBulkManagementHistory', () => { expect(summaryOfRowsProcessed).toEqual(expect.objectContaining(expectation)); }; - it('calculates skippage', () => { + it('calculates skipped rows', () => { exerciseGetRowsProcessed({ total_rows: 100, processed_rows: 10, diff --git a/src/data/selectors/index.js b/src/data/selectors/index.js new file mode 100644 index 0000000..06efd54 --- /dev/null +++ b/src/data/selectors/index.js @@ -0,0 +1,71 @@ +import LmsApiService from 'data/services/LmsApiService'; + +import assignmentTypes from './assignmentTypes'; +import cohorts from './cohorts'; +import filters from './filters'; +import grades from './grades'; +import roles from './roles'; +import special from './special'; +import tracks from './tracks'; + +const lmsApiServiceArgs = (state) => ({ + cohort: cohorts.getCohortNameById(state, filters.cohort(state)), + assignment: filters.selectedAssignmentId(state), + assignmentType: filters.assignmentType(state), + assignmentGradeMin: grades.formatMinAssignmentGrade( + filters.assignmentGradeMin(state), + { assignmentId: filters.selectedAssignmentId(state) }, + ), + assignmentGradeMax: grades.formatMaxAssignmentGrade( + filters.assignmentGradeMax(state), + { assignmentId: filters.selectedAssignmentId(state) }, + ), + courseGradeMin: grades.formatMinCourseGrade(filters.courseGradeMin(state)), + courseGradeMax: grades.formatMaxCourseGrade(filters.courseGradeMax(state)), +}); + +const gradeExportUrl = (state, { courseId }) => ( + LmsApiService.getGradeExportCsvUrl(courseId, { + ...lmsApiServiceArgs(state), + excludeCourseRoles: filters.includeCourseRoleMembers(state) ? '' : 'all', + }) +); + +const interventionExportUrl = (state, { courseId }) => ( + LmsApiService.getInterventionExportCsvUrl( + courseId, + lmsApiServiceArgs(state), + ) +); + +const showBulkManagement = (state, { courseId }) => ( + special.hasSpecialBulkManagementAccess(courseId) + || (tracks.stateHasMastersTrack(state) && state.config.bulkManagementAvailable) +); + +const shouldShowSpinner = (state) => { + const canView = roles.canUserViewGradebook(state); + return canView && grades.showSpinner(state); +}; + +const getHeadings = (state) => grades.headingMapper( + filters.assignmentType(state) || 'All', + filters.selectedAssignmentLabel(state) || 'All', +)(grades.getExampleSectionBreakdown(state)); + +export default { + root: { + getHeadings, + gradeExportUrl, + interventionExportUrl, + showBulkManagement, + shouldShowSpinner, + }, + assignmentTypes, + cohorts, + filters, + grades, + roles, + special, + tracks, +}; diff --git a/src/data/selectors/roles.js b/src/data/selectors/roles.js new file mode 100644 index 0000000..d46a11f --- /dev/null +++ b/src/data/selectors/roles.js @@ -0,0 +1,5 @@ +const selectors = { + canUserViewGradebook: ({ roles }) => roles.canUserViewGradebook, +}; + +export default selectors; diff --git a/src/data/selectors/special.js b/src/data/selectors/special.js index 43063cb..1055d80 100644 --- a/src/data/selectors/special.js +++ b/src/data/selectors/special.js @@ -3,10 +3,11 @@ // Note that this does not affect whether or not the backend // LMS API will permit usage of the tool. -const hasSpecialBulkManagementAccess = courseId => { - const specialIdList = process.env.BULK_MANAGEMENT_SPECIAL_ACCESS_COURSE_IDS || ''; - return specialIdList.split(',').includes(courseId); +const selectors = { + hasSpecialBulkManagementAccess: (courseId) => { + const specialIdList = process.env.BULK_MANAGEMENT_SPECIAL_ACCESS_COURSE_IDS || ''; + return specialIdList.split(',').includes(courseId); + }, }; -export { hasSpecialBulkManagementAccess }; -export default hasSpecialBulkManagementAccess; +export default selectors; diff --git a/src/data/selectors/tracks.js b/src/data/selectors/tracks.js index eed1b39..5b78000 100644 --- a/src/data/selectors/tracks.js +++ b/src/data/selectors/tracks.js @@ -3,10 +3,16 @@ const compose = (...fns) => { return (...args) => rest.reduce((accum, fn) => fn(accum), firstFunc(...args)); }; -const getTracks = state => state.tracks.results || []; +const allTracks = state => state.tracks.results || []; const trackIsMasters = track => track.slug === 'masters'; const hasMastersTrack = tracks => tracks.some(trackIsMasters); -const stateHasMastersTrack = compose(hasMastersTrack, getTracks); +const stateHasMastersTrack = compose(hasMastersTrack, allTracks); -export { hasMastersTrack, trackIsMasters }; -export default stateHasMastersTrack; +const selectors = { + allTracks, + hasMastersTrack, + stateHasMastersTrack, + trackIsMasters, +}; + +export default selectors; diff --git a/src/data/utils.js b/src/data/utils.js new file mode 100644 index 0000000..40bcfc4 --- /dev/null +++ b/src/data/utils.js @@ -0,0 +1,19 @@ +/** + * Simple selector factory. + * Takes a list of string keys, and returns a simple slector for each. + * + * @function + * @param {Object|string[]} keys - If passed as object, Object.keys(keys) is used. + * @return {Object} - object of `{[key]: ({key}) => key}` + */ +const simpleSelectorFactory = (transformer, keys) => { + const selKeys = Array.isArray(keys) ? keys : Object.keys(keys); + return selKeys.reduce( + (obj, key) => ({ + ...obj, [key]: (state) => transformer(state)[key], + }), + { root: (state) => transformer(state) }, + ); +}; + +export default simpleSelectorFactory; diff --git a/src/data/utils.test.js b/src/data/utils.test.js new file mode 100644 index 0000000..4267154 --- /dev/null +++ b/src/data/utils.test.js @@ -0,0 +1,27 @@ +import simpleSelectorFactory from './utils'; + +describe('Redux utilities - creators', () => { + describe('simpleSelectors', () => { + const data = { a: 1, b: 2, c: 3 }; + const state = { + testGroup: data, + other: 'stuff', + }; + const transformer = ({ testGroup }) => testGroup; + + test('given a list of strings, returns a dict w/ a simple selector per string', () => { + const keys = ['a', 'b']; + const selectors = simpleSelectorFactory(transformer, keys); + expect(Object.keys(selectors)).toEqual(['root', ...keys]); + expect(selectors.a(state)).toEqual(data.a); + expect(selectors.b(state)).toEqual(data.b); + }); + test('given an object for keys, returns a dict w/ simple selector per key', () => { + const selectors = simpleSelectorFactory(transformer, data); + expect(Object.keys(selectors)).toEqual(['root', ...Object.keys(data)]); + expect(selectors.a(state)).toEqual(data.a); + expect(selectors.b(state)).toEqual(data.b); + expect(selectors.c(state)).toEqual(data.c); + }); + }); +});