From a1de3a86128abffc9a1d4e62bf66165a92fcab37 Mon Sep 17 00:00:00 2001 From: Ben Warzeski Date: Thu, 26 Aug 2021 10:37:35 -0400 Subject: [PATCH] feat: Bulk Management reorganization, phase 1 (#206) * move GradesTab and BulkHistoryTab to views and control from container through redux * add data logic for view control and import success toast * add NetworkButton component for download/upload buttons * remove download button from Bulk History view and update heading and help text * add View control button to GradebookHeader if bulk management available * remove FilterMenuToggle from SearchControls * update BulkManagementControls to now include upload/download grades buttons * add Import Success toast * rename UserLabel to FilteredUsersLabel for clarity * add InterventionsReport component * update GradesView top-level component * messageing update (separate messages into per-component files) * style updates * update test plan * clean up css and add docstrings * typo fix * fix typo in bulk management view header --- documentation/testing/test-plan.md | 31 ++-- src/App.scss | 3 +- .../BulkManagementAlerts.jsx | 0 .../BulkManagementAlerts.test.jsx | 0 .../BulkManagementHistoryView.scss | 6 + .../HistoryTable.jsx | 8 - .../HistoryTable.test.jsx | 14 +- .../ResultsSummary.jsx | 0 .../ResultsSummary.test.jsx | 0 .../BulkManagementAlerts.test.jsx.snap | 4 +- .../__snapshots__/HistoryTable.test.jsx.snap | 15 +- .../ResultsSummary.test.jsx.snap | 0 .../__snapshots__/index.test.jsx.snap | 26 ++++ .../index.jsx | 15 +- .../index.test.jsx | 14 +- .../BulkManagementHistoryView/messages.js | 21 +++ .../__snapshots__/index.test.jsx.snap | 16 -- src/components/BulkManagementTab/messages.js | 36 ----- .../__snapshots__/test.jsx.snap | 142 ++++++++++++++++-- src/components/GradebookHeader/index.jsx | 43 +++++- src/components/GradebookHeader/messages.js | 10 ++ src/components/GradebookHeader/test.jsx | 113 ++++++++++++-- .../GradesTab/BulkManagementControls.jsx | 108 ------------- .../SearchControls.test.jsx.snap | 56 ------- src/components/GradesTab/messages.js | 76 ---------- .../GradesView/BulkManagementControls.jsx | 71 +++++++++ .../BulkManagementControls.messages.js | 11 ++ .../BulkManagementControls.test.jsx | 66 ++------ .../EditModal/HistoryHeader.jsx | 0 .../EditModal/HistoryHeader.test.jsx | 0 .../EditModal/ModalHeaders.jsx | 0 .../EditModal/ModalHeaders.test.jsx | 0 .../OverrideTable/AdjustedGradeInput.jsx | 0 .../OverrideTable/AdjustedGradeInput.test.jsx | 0 .../EditModal/OverrideTable/ReasonInput.jsx | 0 .../OverrideTable/ReasonInput.test.jsx | 0 .../AdjustedGradeInput.test.jsx.snap | 0 .../__snapshots__/ReasonInput.test.jsx.snap | 0 .../OverrideTable/__snapshots__/test.jsx.snap | 8 +- .../EditModal/OverrideTable/index.jsx | 0 .../EditModal/OverrideTable/messages.js | 8 +- .../EditModal/OverrideTable/test.jsx | 0 .../__snapshots__/HistoryHeader.test.jsx.snap | 0 .../__snapshots__/ModalHeaders.test.jsx.snap | 16 +- .../EditModal/__snapshots__/test.jsx.snap | 20 +-- .../EditModal/index.jsx | 0 .../EditModal/messages.js | 18 +-- .../EditModal/test.jsx | 0 .../FilterBadges/FilterBadge.jsx | 0 .../FilterBadges/FilterBadge.test.jsx | 0 .../__snapshots__/FilterBadge.test.jsx.snap | 0 .../FilterBadges/__snapshots__/test.jsx.snap | 0 .../FilterBadges/index.jsx | 0 .../FilterBadges/test.jsx | 0 .../GradesView/FilterMenuToggle.jsx | 37 +++++ .../GradesView/FilterMenuToggle.messages.js | 11 ++ .../GradesView/FilterMenuToggle.test.jsx | 42 ++++++ .../GradesView/FilteredUsersLabel.jsx | 44 ++++++ .../GradesView/FilteredUsersLabel.test.jsx | 46 ++++++ .../GradebookTable/Fields.jsx | 0 .../GradebookTable/Fields.test.jsx | 0 .../GradebookTable/GradeButton.jsx | 0 .../GradebookTable/GradeButton.test.jsx | 0 .../GradebookTable/LabelReplacements.jsx | 0 .../GradebookTable/LabelReplacements.test.jsx | 0 .../__snapshots__/Fields.test.jsx.snap | 0 .../__snapshots__/GradeButton.test.jsx.snap | 0 .../LabelReplacements.test.jsx.snap | 12 +- .../__snapshots__/test.jsx.snap | 2 +- .../GradebookTable/index.jsx | 0 .../GradebookTable/messages.js | 12 +- .../GradebookTable/test.jsx | 0 .../GradesView.scss} | 11 +- .../ImportGradesButton.jsx} | 27 ++-- .../GradesView/ImportGradesButton.messages.js | 16 ++ .../ImportGradesButton.test.jsx} | 48 +++--- .../GradesView/ImportSuccessToast.jsx | 72 +++++++++ .../GradesView/ImportSuccessToast.messages.js | 16 ++ .../GradesView/ImportSuccessToast.test.jsx | 110 ++++++++++++++ .../GradesView/InterventionsReport.jsx | 72 +++++++++ .../InterventionsReport.messages.js | 21 +++ .../GradesView/InterventionsReport.test.jsx | 107 +++++++++++++ .../PageButtons/PageButtons.test.jsx | 0 .../__snapshots__/PageButtons.test.jsx.snap | 12 +- .../PageButtons/index.jsx | 0 .../PageButtons/messages.js | 4 +- .../ScoreViewInput.jsx | 2 +- .../GradesView/ScoreViewInput.messages.js | 21 +++ .../ScoreViewInput.test.jsx | 0 .../SearchControls.jsx | 42 ++---- .../GradesView/SearchControls.messages.js | 16 ++ .../SearchControls.test.jsx | 7 - .../{GradesTab => GradesView}/SpinnerIcon.jsx | 0 .../SpinnerIcon.test.jsx | 0 .../StatusAlerts.jsx | 2 +- .../GradesView/StatusAlerts.messages.js | 21 +++ .../StatusAlerts.test.jsx | 2 +- .../{GradesTab => GradesView}/UsersLabel.jsx | 0 .../UsersLabel.test.jsx | 0 .../FilterMenuToggle.test.jsx.snap | 19 +++ .../FilteredUsersLabel.test.jsx.snap | 23 +++ .../ImportGradesButton.test.jsx.snap} | 28 ++-- .../ImportSuccessToast.test.jsx.snap | 16 ++ .../InterventionsReport.test.jsx.snap | 38 +++++ .../ScoreViewInput.test.jsx.snap | 2 +- .../SearchControls.test.jsx.snap | 28 ++++ .../__snapshots__/SpinnerIcon.test.jsx.snap | 0 .../__snapshots__/StatusAlerts.test.jsx.snap | 2 +- .../__snapshots__/UsersLabel.test.jsx.snap | 0 .../__snapshots__/test.jsx.snap | 26 +++- .../{GradesTab => GradesView}/index.jsx | 41 +++-- src/components/GradesView/messages.js | 21 +++ .../{GradesTab => GradesView}/test.jsx | 24 +-- .../NetworkButton/__snapshots__/test.jsx.snap | 41 +++++ src/components/NetworkButton/index.jsx | 88 +++++++++++ src/components/NetworkButton/test.jsx | 89 +++++++++++ .../GradebookPage/__snapshots__/test.jsx.snap | 38 +---- src/containers/GradebookPage/index.jsx | 31 ++-- src/containers/GradebookPage/test.jsx | 57 +++---- src/data/actions/app.js | 18 ++- src/data/actions/app.test.js | 6 + src/data/constants/app.js | 5 + src/data/reducers/app.js | 10 ++ src/data/reducers/app.test.js | 22 +++ src/data/reducers/grades.js | 3 + src/data/selectors/app.js | 2 + src/data/selectors/app.test.js | 2 + src/data/thunkActions/grades.js | 4 +- src/data/thunkActions/grades.test.js | 4 +- 129 files changed, 1796 insertions(+), 702 deletions(-) rename src/components/{BulkManagementTab => BulkManagementHistoryView}/BulkManagementAlerts.jsx (100%) rename src/components/{BulkManagementTab => BulkManagementHistoryView}/BulkManagementAlerts.test.jsx (100%) create mode 100644 src/components/BulkManagementHistoryView/BulkManagementHistoryView.scss rename src/components/{BulkManagementTab => BulkManagementHistoryView}/HistoryTable.jsx (88%) rename src/components/{BulkManagementTab => BulkManagementHistoryView}/HistoryTable.test.jsx (85%) rename src/components/{BulkManagementTab => BulkManagementHistoryView}/ResultsSummary.jsx (100%) rename src/components/{BulkManagementTab => BulkManagementHistoryView}/ResultsSummary.test.jsx (100%) rename src/components/{BulkManagementTab => BulkManagementHistoryView}/__snapshots__/BulkManagementAlerts.test.jsx.snap (90%) rename src/components/{BulkManagementTab => BulkManagementHistoryView}/__snapshots__/HistoryTable.test.jsx.snap (83%) rename src/components/{BulkManagementTab => BulkManagementHistoryView}/__snapshots__/ResultsSummary.test.jsx.snap (100%) create mode 100644 src/components/BulkManagementHistoryView/__snapshots__/index.test.jsx.snap rename src/components/{BulkManagementTab => BulkManagementHistoryView}/index.jsx (58%) rename src/components/{BulkManagementTab => BulkManagementHistoryView}/index.test.jsx (73%) create mode 100644 src/components/BulkManagementHistoryView/messages.js delete mode 100644 src/components/BulkManagementTab/__snapshots__/index.test.jsx.snap delete mode 100644 src/components/BulkManagementTab/messages.js delete mode 100644 src/components/GradesTab/BulkManagementControls.jsx delete mode 100644 src/components/GradesTab/__snapshots__/SearchControls.test.jsx.snap delete mode 100644 src/components/GradesTab/messages.js create mode 100644 src/components/GradesView/BulkManagementControls.jsx create mode 100644 src/components/GradesView/BulkManagementControls.messages.js rename src/components/{GradesTab => GradesView}/BulkManagementControls.test.jsx (54%) rename src/components/{GradesTab => GradesView}/EditModal/HistoryHeader.jsx (100%) rename src/components/{GradesTab => GradesView}/EditModal/HistoryHeader.test.jsx (100%) rename src/components/{GradesTab => GradesView}/EditModal/ModalHeaders.jsx (100%) rename src/components/{GradesTab => GradesView}/EditModal/ModalHeaders.test.jsx (100%) rename src/components/{GradesTab => GradesView}/EditModal/OverrideTable/AdjustedGradeInput.jsx (100%) rename src/components/{GradesTab => GradesView}/EditModal/OverrideTable/AdjustedGradeInput.test.jsx (100%) rename src/components/{GradesTab => GradesView}/EditModal/OverrideTable/ReasonInput.jsx (100%) rename src/components/{GradesTab => GradesView}/EditModal/OverrideTable/ReasonInput.test.jsx (100%) rename src/components/{GradesTab => GradesView}/EditModal/OverrideTable/__snapshots__/AdjustedGradeInput.test.jsx.snap (100%) rename src/components/{GradesTab => GradesView}/EditModal/OverrideTable/__snapshots__/ReasonInput.test.jsx.snap (100%) rename src/components/{GradesTab => GradesView}/EditModal/OverrideTable/__snapshots__/test.jsx.snap (84%) rename src/components/{GradesTab => GradesView}/EditModal/OverrideTable/index.jsx (100%) rename src/components/{GradesTab => GradesView}/EditModal/OverrideTable/messages.js (70%) rename src/components/{GradesTab => GradesView}/EditModal/OverrideTable/test.jsx (100%) rename src/components/{GradesTab => GradesView}/EditModal/__snapshots__/HistoryHeader.test.jsx.snap (100%) rename src/components/{GradesTab => GradesView}/EditModal/__snapshots__/ModalHeaders.test.jsx.snap (79%) rename src/components/{GradesTab => GradesView}/EditModal/__snapshots__/test.jsx.snap (83%) rename src/components/{GradesTab => GradesView}/EditModal/index.jsx (100%) rename src/components/{GradesTab => GradesView}/EditModal/messages.js (70%) rename src/components/{GradesTab => GradesView}/EditModal/test.jsx (100%) rename src/components/{GradesTab => GradesView}/FilterBadges/FilterBadge.jsx (100%) rename src/components/{GradesTab => GradesView}/FilterBadges/FilterBadge.test.jsx (100%) rename src/components/{GradesTab => GradesView}/FilterBadges/__snapshots__/FilterBadge.test.jsx.snap (100%) rename src/components/{GradesTab => GradesView}/FilterBadges/__snapshots__/test.jsx.snap (100%) rename src/components/{GradesTab => GradesView}/FilterBadges/index.jsx (100%) rename src/components/{GradesTab => GradesView}/FilterBadges/test.jsx (100%) create mode 100644 src/components/GradesView/FilterMenuToggle.jsx create mode 100644 src/components/GradesView/FilterMenuToggle.messages.js create mode 100644 src/components/GradesView/FilterMenuToggle.test.jsx create mode 100644 src/components/GradesView/FilteredUsersLabel.jsx create mode 100644 src/components/GradesView/FilteredUsersLabel.test.jsx rename src/components/{GradesTab => GradesView}/GradebookTable/Fields.jsx (100%) rename src/components/{GradesTab => GradesView}/GradebookTable/Fields.test.jsx (100%) rename src/components/{GradesTab => GradesView}/GradebookTable/GradeButton.jsx (100%) rename src/components/{GradesTab => GradesView}/GradebookTable/GradeButton.test.jsx (100%) rename src/components/{GradesTab => GradesView}/GradebookTable/LabelReplacements.jsx (100%) rename src/components/{GradesTab => GradesView}/GradebookTable/LabelReplacements.test.jsx (100%) rename src/components/{GradesTab => GradesView}/GradebookTable/__snapshots__/Fields.test.jsx.snap (100%) rename src/components/{GradesTab => GradesView}/GradebookTable/__snapshots__/GradeButton.test.jsx.snap (100%) rename src/components/{GradesTab => GradesView}/GradebookTable/__snapshots__/LabelReplacements.test.jsx.snap (84%) rename src/components/{GradesTab => GradesView}/GradebookTable/__snapshots__/test.jsx.snap (94%) rename src/components/{GradesTab => GradesView}/GradebookTable/index.jsx (100%) rename src/components/{GradesTab => GradesView}/GradebookTable/messages.js (72%) rename src/components/{GradesTab => GradesView}/GradebookTable/test.jsx (100%) rename src/components/{GradesTab/GradesTab.scss => GradesView/GradesView.scss} (94%) rename src/components/{BulkManagementTab/FileUploadForm.jsx => GradesView/ImportGradesButton.jsx} (73%) create mode 100644 src/components/GradesView/ImportGradesButton.messages.js rename src/components/{BulkManagementTab/FileUploadForm.test.jsx => GradesView/ImportGradesButton.test.jsx} (81%) create mode 100644 src/components/GradesView/ImportSuccessToast.jsx create mode 100644 src/components/GradesView/ImportSuccessToast.messages.js create mode 100644 src/components/GradesView/ImportSuccessToast.test.jsx create mode 100644 src/components/GradesView/InterventionsReport.jsx create mode 100644 src/components/GradesView/InterventionsReport.messages.js create mode 100644 src/components/GradesView/InterventionsReport.test.jsx rename src/components/{GradesTab => GradesView}/PageButtons/PageButtons.test.jsx (100%) rename src/components/{GradesTab => GradesView}/PageButtons/__snapshots__/PageButtons.test.jsx.snap (88%) rename src/components/{GradesTab => GradesView}/PageButtons/index.jsx (100%) rename src/components/{GradesTab => GradesView}/PageButtons/messages.js (76%) rename src/components/{GradesTab => GradesView}/ScoreViewInput.jsx (96%) create mode 100644 src/components/GradesView/ScoreViewInput.messages.js rename src/components/{GradesTab => GradesView}/ScoreViewInput.test.jsx (100%) rename src/components/{GradesTab => GradesView}/SearchControls.jsx (56%) create mode 100644 src/components/GradesView/SearchControls.messages.js rename src/components/{GradesTab => GradesView}/SearchControls.test.jsx (91%) rename src/components/{GradesTab => GradesView}/SpinnerIcon.jsx (100%) rename src/components/{GradesTab => GradesView}/SpinnerIcon.test.jsx (100%) rename src/components/{GradesTab => GradesView}/StatusAlerts.jsx (97%) create mode 100644 src/components/GradesView/StatusAlerts.messages.js rename src/components/{GradesTab => GradesView}/StatusAlerts.test.jsx (98%) rename src/components/{GradesTab => GradesView}/UsersLabel.jsx (100%) rename src/components/{GradesTab => GradesView}/UsersLabel.test.jsx (100%) create mode 100644 src/components/GradesView/__snapshots__/FilterMenuToggle.test.jsx.snap create mode 100644 src/components/GradesView/__snapshots__/FilteredUsersLabel.test.jsx.snap rename src/components/{BulkManagementTab/__snapshots__/FileUploadForm.test.jsx.snap => GradesView/__snapshots__/ImportGradesButton.test.jsx.snap} (53%) create mode 100644 src/components/GradesView/__snapshots__/ImportSuccessToast.test.jsx.snap create mode 100644 src/components/GradesView/__snapshots__/InterventionsReport.test.jsx.snap rename src/components/{GradesTab => GradesView}/__snapshots__/ScoreViewInput.test.jsx.snap (92%) create mode 100644 src/components/GradesView/__snapshots__/SearchControls.test.jsx.snap rename src/components/{GradesTab => GradesView}/__snapshots__/SpinnerIcon.test.jsx.snap (100%) rename src/components/{GradesTab => GradesView}/__snapshots__/StatusAlerts.test.jsx.snap (92%) rename src/components/{GradesTab => GradesView}/__snapshots__/UsersLabel.test.jsx.snap (100%) rename src/components/{GradesTab => GradesView}/__snapshots__/test.jsx.snap (56%) rename src/components/{GradesTab => GradesView}/index.jsx (75%) create mode 100644 src/components/GradesView/messages.js rename src/components/{GradesTab => GradesView}/test.jsx (86%) create mode 100644 src/components/NetworkButton/__snapshots__/test.jsx.snap create mode 100644 src/components/NetworkButton/index.jsx create mode 100644 src/components/NetworkButton/test.jsx diff --git a/documentation/testing/test-plan.md b/documentation/testing/test-plan.md index c7ba3fe..6be0bdd 100644 --- a/documentation/testing/test-plan.md +++ b/documentation/testing/test-plan.md @@ -70,31 +70,38 @@ Confirm the following workflows: - [ ] *Master's (or selectively-enabled) only*: "Bulk Management" allows overriding grades in bulk. - Open a non-masters-track course. - - [ ] Verify that the "Bulk Management" tab does not appear. - [ ] Verify that the "Bulk Management" button does not appear. + - [ ] Verify that the "Download Interventions" interface does not appear. - Open a masters-track course. - - [ ] Verify that the "Bulk Management" tab appears to the right of the "Grades" tab. - - [ ] Verify that the "Bulk Management" button appears. - - Click the "Bulk Management" button. This downloads existing student/assignment info. + - [ ] Verify that the "Bulk Management History" button appears at the right of the header. + - [ ] Verify that the "Download Interventions" interface appears. + - [ ] Verify that the "Download Grades" button appears. + - [ ] Verify that the "Import Grades" button appears. + - Click the "Download Grades" button. This downloads existing student/assignment info. - [ ] Open the downloaded CSV and verify that students and assignments in the file match applied filters/searches. - - Add values in the "new_override-{subsection-short-id}" columns for student grades to be overridden and save the CSV file. - - [ ] Clicking the "Bulk Management" tab shows the Bulk Management page. + - Navigate to Bulk Management History tab. + - [ ] Clicking the "ViewBulk Management History" tab shows the Bulk Management History view. - [ ] The bulk management history table appears with columns: "Gradebook", "Download Summary", "Who", "When". - [ ] Previous bulk management imports (if applicable) appear in the table. + - Add values in the "new_override-{subsection-short-id}" columns for student grades to be overridden and save the CSV file. + - Navigate back to Gradebook view - Click the "Import Grades" button and select the modified CSV file. - [ ] Verify that the "CSV processing" banner appears. - Wait for processing to complete and reload the page. (Can take seconds to minutes depending on environment and size of the override.) - - Navigate back to the "Bulk Management" tab. + - [ ] Verify that Import Grades Success toast appears (and disappears after 5 seconds) + - Navigate back to the "Bulk Management History" view. - [ ] Verify that a new entry appears in the results table indicating how many students were affected by the bulk grade change. - Click the "Download Summary" link to see the summary of changes from the bulk grade changes. - [ ] Verify that students are shown with modified subsections and actions: "No Action" for unchanged users, "Success" for successful overrides. - [ ] *Masters only*: Interventions report shows student activity in the course. - Open a non-masters-track course. - - [ ] Verify that the "Interventions" tab does not appear. - - [ ] Verify that the "Interventions" button does not appear. + - [ ] Verify that the "View Bulk Management History" button does not appear. + - [ ] Verify that the "Interventions" interface does not appear. + - [ ] Verify that the "Download Grades" and "Import Grades" buttons do not appear. - Open a masters-track course. - - [ ] Verify that the "Interventions" tab appears to the right of the "Grades" tab. - - [ ] Verify that the "Interventions" button appears. - - Click on the "Interventions" button to generate a CSV students and activity info. + - [ ] Verify that the "View Bulk Management History" button appears at the right of the header. + - [ ] Verify that the "Interventions" interface appears. + - [ ] Verify that the "Download Grades" and "Import Grades" buttons appear. + - Click on the "Download Interventions" button to generate a CSV students and activity info. - Open the interventions report and verify student info and activity info appear. diff --git a/src/App.scss b/src/App.scss index 6ca7bbb..a392060 100755 --- a/src/App.scss +++ b/src/App.scss @@ -11,6 +11,7 @@ $input-focus-box-shadow: $input-box-shadow; // hack to get upgrade to paragon 4. @import "~@edx/frontend-component-footer/dist/_footer"; -@import "./components/GradesTab/GradesTab"; +@import "./components/GradesView/GradesView"; +@import "./components/BulkManagementHistoryView/BulkManagementHistoryView"; @import "./components/WithSidebar/WithSidebar"; @import "./components/GradebookFilters/GradebookFilters"; diff --git a/src/components/BulkManagementTab/BulkManagementAlerts.jsx b/src/components/BulkManagementHistoryView/BulkManagementAlerts.jsx similarity index 100% rename from src/components/BulkManagementTab/BulkManagementAlerts.jsx rename to src/components/BulkManagementHistoryView/BulkManagementAlerts.jsx diff --git a/src/components/BulkManagementTab/BulkManagementAlerts.test.jsx b/src/components/BulkManagementHistoryView/BulkManagementAlerts.test.jsx similarity index 100% rename from src/components/BulkManagementTab/BulkManagementAlerts.test.jsx rename to src/components/BulkManagementHistoryView/BulkManagementAlerts.test.jsx diff --git a/src/components/BulkManagementHistoryView/BulkManagementHistoryView.scss b/src/components/BulkManagementHistoryView/BulkManagementHistoryView.scss new file mode 100644 index 0000000..f05a962 --- /dev/null +++ b/src/components/BulkManagementHistoryView/BulkManagementHistoryView.scss @@ -0,0 +1,6 @@ +.bulk-management-history-view { + .help-text { + margin-bottom: 40px; + max-width: 70%; + } +} diff --git a/src/components/BulkManagementTab/HistoryTable.jsx b/src/components/BulkManagementHistoryView/HistoryTable.jsx similarity index 88% rename from src/components/BulkManagementTab/HistoryTable.jsx rename to src/components/BulkManagementHistoryView/HistoryTable.jsx index a24e3bf..429f953 100644 --- a/src/components/BulkManagementTab/HistoryTable.jsx +++ b/src/components/BulkManagementHistoryView/HistoryTable.jsx @@ -3,14 +3,12 @@ import React from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; -import { FormattedMessage } from '@edx/frontend-platform/i18n'; import { Table } from '@edx/paragon'; import { bulkManagementColumns } from 'data/constants/app'; import selectors from 'data/selectors'; import ResultsSummary from './ResultsSummary'; -import messages from './messages'; export const mapHistoryRows = ({ resultsSummary, @@ -33,12 +31,6 @@ export const HistoryTable = ({ bulkManagementHistory, }) => ( <> -

- -
- -

- ({ defineMessages: m => m, @@ -58,19 +56,9 @@ describe('HistoryTable', () => { beforeEach(() => { el = shallow(); }); - const snapshotSegments = [ - 'hints display', - 'formatted table', - ]; - test(`snapshot - loads ${snapshotSegments.join(', ')}`, () => { + test('snapshot - loads formatted table', () => { expect(el).toMatchSnapshot(); }); - test('hints with break in between', () => { - const hints = el.find('p'); - expect(hints.childAt(0).getElement()).toEqual(); - expect(hints.childAt(1).is('br')).toEqual(true); - expect(hints.childAt(2).getElement()).toEqual(); - }); describe('history table', () => { let table; beforeEach(() => { diff --git a/src/components/BulkManagementTab/ResultsSummary.jsx b/src/components/BulkManagementHistoryView/ResultsSummary.jsx similarity index 100% rename from src/components/BulkManagementTab/ResultsSummary.jsx rename to src/components/BulkManagementHistoryView/ResultsSummary.jsx diff --git a/src/components/BulkManagementTab/ResultsSummary.test.jsx b/src/components/BulkManagementHistoryView/ResultsSummary.test.jsx similarity index 100% rename from src/components/BulkManagementTab/ResultsSummary.test.jsx rename to src/components/BulkManagementHistoryView/ResultsSummary.test.jsx diff --git a/src/components/BulkManagementTab/__snapshots__/BulkManagementAlerts.test.jsx.snap b/src/components/BulkManagementHistoryView/__snapshots__/BulkManagementAlerts.test.jsx.snap similarity index 90% rename from src/components/BulkManagementTab/__snapshots__/BulkManagementAlerts.test.jsx.snap rename to src/components/BulkManagementHistoryView/__snapshots__/BulkManagementAlerts.test.jsx.snap index e973e0e..6ebce59 100644 --- a/src/components/BulkManagementTab/__snapshots__/BulkManagementAlerts.test.jsx.snap +++ b/src/components/BulkManagementHistoryView/__snapshots__/BulkManagementAlerts.test.jsx.snap @@ -15,7 +15,7 @@ exports[`BulkManagementAlerts component no errer, no upload success snapshot - b @@ -38,7 +38,7 @@ exports[`BulkManagementAlerts component no errer, no upload success snapshot - d diff --git a/src/components/BulkManagementTab/__snapshots__/HistoryTable.test.jsx.snap b/src/components/BulkManagementHistoryView/__snapshots__/HistoryTable.test.jsx.snap similarity index 83% rename from src/components/BulkManagementTab/__snapshots__/HistoryTable.test.jsx.snap rename to src/components/BulkManagementHistoryView/__snapshots__/HistoryTable.test.jsx.snap index bb2da01..7272aa9 100644 --- a/src/components/BulkManagementTab/__snapshots__/HistoryTable.test.jsx.snap +++ b/src/components/BulkManagementHistoryView/__snapshots__/HistoryTable.test.jsx.snap @@ -41,21 +41,8 @@ Array [ ] `; -exports[`HistoryTable component snapshot snapshot - loads hints display, formatted table 1`] = ` +exports[`HistoryTable component snapshot snapshot - loads formatted table 1`] = ` -

- -
- -

, 1`] = ` +
+

+ +

+

+ +

+ + +
+`; diff --git a/src/components/BulkManagementTab/index.jsx b/src/components/BulkManagementHistoryView/index.jsx similarity index 58% rename from src/components/BulkManagementTab/index.jsx rename to src/components/BulkManagementHistoryView/index.jsx index 16491f3..607b7c1 100644 --- a/src/components/BulkManagementTab/index.jsx +++ b/src/components/BulkManagementHistoryView/index.jsx @@ -4,20 +4,21 @@ import { FormattedMessage } from '@edx/frontend-platform/i18n'; import messages from './messages'; import BulkManagementAlerts from './BulkManagementAlerts'; -import FileUploadForm from './FileUploadForm'; import HistoryTable from './HistoryTable'; /** - * + * * top-level view for managing uploads of bulk management override csvs. */ -export const BulkManagementTab = () => ( -
-

+export const BulkManagementHistoryView = () => ( +
+

+

+ +

-
); -export default BulkManagementTab; +export default BulkManagementHistoryView; diff --git a/src/components/BulkManagementTab/index.test.jsx b/src/components/BulkManagementHistoryView/index.test.jsx similarity index 73% rename from src/components/BulkManagementTab/index.test.jsx rename to src/components/BulkManagementHistoryView/index.test.jsx index 0f37525..5ada740 100644 --- a/src/components/BulkManagementTab/index.test.jsx +++ b/src/components/BulkManagementHistoryView/index.test.jsx @@ -3,27 +3,24 @@ import React from 'react'; import { shallow } from 'enzyme'; import { FormattedMessage } from '@edx/frontend-platform/i18n'; -import { BulkManagementTab } from '.'; +import { BulkManagementHistoryView } from '.'; import BulkManagementAlerts from './BulkManagementAlerts'; -import FileUploadForm from './FileUploadForm'; import HistoryTable from './HistoryTable'; import messages from './messages'; jest.mock('./BulkManagementAlerts', () => 'BulkManagementAlerts'); -jest.mock('./FileUploadForm', () => 'FileUploadForm'); jest.mock('./HistoryTable', () => 'HistoryTable'); -describe('BulkManagementTab', () => { +describe('BulkManagementHistoryView', () => { describe('component', () => { let el; beforeEach(() => { - el = shallow(); + el = shallow(); }); describe('snapshot', () => { const snapshotSegments = [ - 'heading from messages.BulkManagementTab.heading', + 'heading from messages.BulkManagementHistoryView.heading', '', - '', '', ]; test(`snapshot - loads ${snapshotSegments.join(', ')}`, () => { @@ -39,8 +36,7 @@ describe('BulkManagementTab', () => { }); test('heading, then alerts, then upload form, then table', () => { expect(el.childAt(0).is('h4')).toEqual(true); - expect(el.childAt(1).is(BulkManagementAlerts)).toEqual(true); - expect(el.childAt(2).is(FileUploadForm)).toEqual(true); + expect(el.childAt(2).is(BulkManagementAlerts)).toEqual(true); expect(el.childAt(3).is(HistoryTable)).toEqual(true); }); }); diff --git a/src/components/BulkManagementHistoryView/messages.js b/src/components/BulkManagementHistoryView/messages.js new file mode 100644 index 0000000..a365ced --- /dev/null +++ b/src/components/BulkManagementHistoryView/messages.js @@ -0,0 +1,21 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + heading: { + id: 'gradebook.BulkManagementHistoryView.heading', + defaultMessage: 'Bulk Management History', + description: 'Heading text for BulkManagement History Tab', + }, + helpText: { + id: 'gradebook.BulkManagementHistoryView', + defaultMessage: 'Below is a log of previous grade imports. To download a CSV of your gradebook and import grades for override, return to the Gradebook. Please note, after importing grades, it may take a few seconds to process the override.', + description: 'Bulk Management History View help text', + }, + successDialog: { + id: 'gradebook.BulkManagementHistoryView.successDialog', + defaultMessage: 'CSV processing. File uploads may take several minutes to complete.', + description: 'Success Dialog message in BulkManagement Tab File Upload Form', + }, +}); + +export default messages; diff --git a/src/components/BulkManagementTab/__snapshots__/index.test.jsx.snap b/src/components/BulkManagementTab/__snapshots__/index.test.jsx.snap deleted file mode 100644 index 08b2424..0000000 --- a/src/components/BulkManagementTab/__snapshots__/index.test.jsx.snap +++ /dev/null @@ -1,16 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`BulkManagementTab component snapshot snapshot - loads heading from messages.BulkManagementTab.heading, , , 1`] = ` -
-

- -

- - - -
-`; diff --git a/src/components/BulkManagementTab/messages.js b/src/components/BulkManagementTab/messages.js deleted file mode 100644 index 4e6517c..0000000 --- a/src/components/BulkManagementTab/messages.js +++ /dev/null @@ -1,36 +0,0 @@ -import { defineMessages } from '@edx/frontend-platform/i18n'; - -const messages = defineMessages({ - csvUploadLabel: { - id: 'gradebook.BulkManagementTab.csvUploadLabel', - defaultMessage: 'Upload Grade CSV', - description: 'Button in BulkManagementTab Alerts', - }, - heading: { - id: 'gradebook.BulkManagementTab.heading', - defaultMessage: 'Use this feature by downloading a CSV for bulk management, overriding grades locally, and coming back here to upload.', - description: 'Heading text for BulkManagement Tab', - }, - hint1: { - id: 'gradebook.BulkManagementTab.hint1', - defaultMessage: 'Results appear in the table below.', - description: 'Hint text on BulkManagement Tab History Table', - }, - hint2: { - id: 'gradebook.BulkManagementTab.hint2', - defaultMessage: 'Grade processing may take a few seconds.', - description: 'Hint text on BulkManagement Tab History Table', - }, - importBtnText: { - id: 'gradebook.BulkManagementTab.importBtnText', - defaultMessage: 'Import Grades', - description: 'Button in BulkManagement Tab File Upload Form', - }, - successDialog: { - id: 'gradebook.BulkManagementTab.successDialog', - defaultMessage: 'CSV processing. File uploads may take several minutes to complete.', - description: 'Success Dialog message in BulkManagement Tab File Upload Form', - }, -}); - -export default messages; diff --git a/src/components/GradebookHeader/__snapshots__/test.jsx.snap b/src/components/GradebookHeader/__snapshots__/test.jsx.snap index 8d27d3e..f37250e 100644 --- a/src/components/GradebookHeader/__snapshots__/test.jsx.snap +++ b/src/components/GradebookHeader/__snapshots__/test.jsx.snap @@ -26,9 +26,13 @@ exports[`GradebookHeader component snapshots default values (grades frozen, cann id="gradebook.GradebookHeader.appLabel" /> -

- fakeID -

+
+

+ fakeID +

+
@@ -98,7 +98,7 @@ exports[`EditMoal Component snapshots gradeOverrideHistoryError is empty and ope , ] @@ -107,7 +107,7 @@ exports[`EditMoal Component snapshots gradeOverrideHistoryError is empty and ope } onClose={[MockFunction this.closeAssignmentModal]} @@ -116,7 +116,7 @@ exports[`EditMoal Component snapshots gradeOverrideHistoryError is empty and ope } /> diff --git a/src/components/GradesTab/EditModal/index.jsx b/src/components/GradesView/EditModal/index.jsx similarity index 100% rename from src/components/GradesTab/EditModal/index.jsx rename to src/components/GradesView/EditModal/index.jsx diff --git a/src/components/GradesTab/EditModal/messages.js b/src/components/GradesView/EditModal/messages.js similarity index 70% rename from src/components/GradesTab/EditModal/messages.js rename to src/components/GradesView/EditModal/messages.js index f59d600..4af11e2 100644 --- a/src/components/GradesTab/EditModal/messages.js +++ b/src/components/GradesView/EditModal/messages.js @@ -2,47 +2,47 @@ import { defineMessages } from '@edx/frontend-platform/i18n'; const messages = defineMessages({ assignmentHeader: { - id: 'gradebook.GradesTab.EditModal.headers.assignment', + id: 'gradebook.GradesView.EditModal.headers.assignment', defaultMessage: 'Assignment', description: 'Edit Modal Assignment header', }, currentGradeHeader: { - id: 'gradebook.GradesTab.EditModal.headers.currentGrade', + id: 'gradebook.GradesView.EditModal.headers.currentGrade', defaultMessage: 'Current Grade', description: 'Edit Modal Current Grade header', }, originalGradeHeader: { - id: 'gradebook.GradesTab.EditModal.headers.originalGrade', + id: 'gradebook.GradesView.EditModal.headers.originalGrade', defaultMessage: 'Original Grade', description: 'Edit Modal Original Grade header', }, studentHeader: { - id: 'gradebook.GradesTab.EditModal.headers.student', + id: 'gradebook.GradesView.EditModal.headers.student', defaultMessage: 'Student', description: 'Edit Modal Student header', }, title: { - id: 'gradebook.GradesTab.EditModal.title', + id: 'gradebook.GradesView.EditModal.title', defaultMessage: 'Edit Grades', description: 'Edit Modal title', }, closeText: { - id: 'gradebook.GradesTab.EditModal.closeText', + id: 'gradebook.GradesView.EditModal.closeText', defaultMessage: 'Cancel', description: 'Edit Modal close button text', }, visibility: { - id: 'gradebook.GradesTab.EditModal.contactSupport', + id: 'gradebook.GradesView.EditModal.contactSupport', defaultMessage: 'Showing most recent actions (max 5). To see more, please contact support', description: 'Edit Modal visibility hint message', }, saveVisibility: { - id: 'gradebook.GradesTab.EditModal.saveVisibility', + id: 'gradebook.GradesView.EditModal.saveVisibility', defaultMessage: 'Note: Once you save, your changes will be visible to students.', description: 'Edit Modal saved changes effect hint', }, saveGrade: { - id: 'gradebook.GradesTab.EditModal.saveGrade', + id: 'gradebook.GradesView.EditModal.saveGrade', defaultMessage: 'Save Grades', description: 'Edit Modal Save button label', }, diff --git a/src/components/GradesTab/EditModal/test.jsx b/src/components/GradesView/EditModal/test.jsx similarity index 100% rename from src/components/GradesTab/EditModal/test.jsx rename to src/components/GradesView/EditModal/test.jsx diff --git a/src/components/GradesTab/FilterBadges/FilterBadge.jsx b/src/components/GradesView/FilterBadges/FilterBadge.jsx similarity index 100% rename from src/components/GradesTab/FilterBadges/FilterBadge.jsx rename to src/components/GradesView/FilterBadges/FilterBadge.jsx diff --git a/src/components/GradesTab/FilterBadges/FilterBadge.test.jsx b/src/components/GradesView/FilterBadges/FilterBadge.test.jsx similarity index 100% rename from src/components/GradesTab/FilterBadges/FilterBadge.test.jsx rename to src/components/GradesView/FilterBadges/FilterBadge.test.jsx diff --git a/src/components/GradesTab/FilterBadges/__snapshots__/FilterBadge.test.jsx.snap b/src/components/GradesView/FilterBadges/__snapshots__/FilterBadge.test.jsx.snap similarity index 100% rename from src/components/GradesTab/FilterBadges/__snapshots__/FilterBadge.test.jsx.snap rename to src/components/GradesView/FilterBadges/__snapshots__/FilterBadge.test.jsx.snap diff --git a/src/components/GradesTab/FilterBadges/__snapshots__/test.jsx.snap b/src/components/GradesView/FilterBadges/__snapshots__/test.jsx.snap similarity index 100% rename from src/components/GradesTab/FilterBadges/__snapshots__/test.jsx.snap rename to src/components/GradesView/FilterBadges/__snapshots__/test.jsx.snap diff --git a/src/components/GradesTab/FilterBadges/index.jsx b/src/components/GradesView/FilterBadges/index.jsx similarity index 100% rename from src/components/GradesTab/FilterBadges/index.jsx rename to src/components/GradesView/FilterBadges/index.jsx diff --git a/src/components/GradesTab/FilterBadges/test.jsx b/src/components/GradesView/FilterBadges/test.jsx similarity index 100% rename from src/components/GradesTab/FilterBadges/test.jsx rename to src/components/GradesView/FilterBadges/test.jsx diff --git a/src/components/GradesView/FilterMenuToggle.jsx b/src/components/GradesView/FilterMenuToggle.jsx new file mode 100644 index 0000000..9261709 --- /dev/null +++ b/src/components/GradesView/FilterMenuToggle.jsx @@ -0,0 +1,37 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; + +import { Button, Icon } from '@edx/paragon'; +import { FormattedMessage } from '@edx/frontend-platform/i18n'; + +import thunkActions from 'data/thunkActions'; + +import messages from './FilterMenuToggle.messages'; + +/** + * Controls for filtering the GradebookTable. Contains the "Edit Filters" button for opening the filter drawer + * as well as the search box for searching by username/email. + */ +export const FilterMenuToggle = ({ toggleFilterDrawer }) => ( + +); + +FilterMenuToggle.propTypes = { + // From Redux + toggleFilterDrawer: PropTypes.func.isRequired, +}; + +export const mapStateToProps = () => ({}); + +export const mapDispatchToProps = { + toggleFilterDrawer: thunkActions.app.filterMenu.toggle, +}; + +export default connect(mapStateToProps, mapDispatchToProps)(FilterMenuToggle); diff --git a/src/components/GradesView/FilterMenuToggle.messages.js b/src/components/GradesView/FilterMenuToggle.messages.js new file mode 100644 index 0000000..42a2e02 --- /dev/null +++ b/src/components/GradesView/FilterMenuToggle.messages.js @@ -0,0 +1,11 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + editFilters: { + id: 'gradebook.GradesView.editFilterLabel', + defaultMessage: 'Edit Filters', + description: 'Button text on Grades tab to open/close the Filters tab', + }, +}); + +export default messages; diff --git a/src/components/GradesView/FilterMenuToggle.test.jsx b/src/components/GradesView/FilterMenuToggle.test.jsx new file mode 100644 index 0000000..4d02fca --- /dev/null +++ b/src/components/GradesView/FilterMenuToggle.test.jsx @@ -0,0 +1,42 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import thunkActions from 'data/thunkActions'; + +import { FilterMenuToggle, mapDispatchToProps, mapStateToProps } from './FilterMenuToggle'; + +jest.mock('@edx/paragon', () => ({ + Button: () => 'Button', + Icon: () => 'Icon', +})); +jest.mock('data/thunkActions', () => ({ + __esModule: true, + default: { + app: { + filterMenu: { toggle: jest.fn() }, + }, + }, +})); + +describe('FilterMenuToggle component', () => { + describe('snapshots', () => { + test('basic snapshot', () => { + const toggleFilterDrawer = jest.fn().mockName('this.props.toggleFilterDrawer'); + expect(shallow(( + + ))).toMatchSnapshot(); + }); + }); + describe('mapStateToProps', () => { + test('does not connect any selectors', () => { + expect(mapStateToProps({ test: 'state' })).toEqual({}); + }); + }); + describe('mapDispatchToProps', () => { + test('toggleFilterDrawer from thunkActions.app.filterMenu.toggle', () => { + expect(mapDispatchToProps.toggleFilterDrawer).toEqual( + thunkActions.app.filterMenu.toggle, + ); + }); + }); +}); diff --git a/src/components/GradesView/FilteredUsersLabel.jsx b/src/components/GradesView/FilteredUsersLabel.jsx new file mode 100644 index 0000000..3752dee --- /dev/null +++ b/src/components/GradesView/FilteredUsersLabel.jsx @@ -0,0 +1,44 @@ +/* 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 { FormattedMessage } from '@edx/frontend-platform/i18n'; + +import selectors from 'data/selectors'; + +/** + * + * Simple label component displaying the filtered and total users shown + */ +export const FilteredUsersLabel = ({ + filteredUsersCount, + totalUsersCount, +}) => { + if (!totalUsersCount) { + return null; + } + const bold = (val) => ({val}); + return ( + + ); +}; +FilteredUsersLabel.propTypes = { + filteredUsersCount: PropTypes.number.isRequired, + totalUsersCount: PropTypes.number.isRequired, +}; + +export const mapStateToProps = (state) => ({ + totalUsersCount: selectors.grades.totalUsersCount(state), + filteredUsersCount: selectors.grades.filteredUsersCount(state), +}); + +export default connect(mapStateToProps)(FilteredUsersLabel); diff --git a/src/components/GradesView/FilteredUsersLabel.test.jsx b/src/components/GradesView/FilteredUsersLabel.test.jsx new file mode 100644 index 0000000..c6ec280 --- /dev/null +++ b/src/components/GradesView/FilteredUsersLabel.test.jsx @@ -0,0 +1,46 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import selectors from 'data/selectors'; +import { FilteredUsersLabel, mapStateToProps } from './FilteredUsersLabel'; + +jest.mock('@edx/paragon', () => ({ + Icon: () => 'Icon', +})); +jest.mock('data/selectors', () => ({ + __esModule: true, + default: { + grades: { + filteredUsersCount: state => ({ filteredUsersCount: state }), + totalUsersCount: state => ({ totalUsersCount: state }), + }, + }, +})); + +describe('FilteredUsersLabel', () => { + describe('component', () => { + const props = { + filteredUsersCount: 23, + totalUsersCount: 140, + }; + it('does not render if totalUsersCount is falsey', () => { + expect(shallow()).toEqual({}); + }); + test('snapshot - displays label with number of filtered users out of total', () => { + expect(shallow()).toMatchSnapshot(); + }); + }); + describe('mapStateToProps', () => { + const testState = { a: 'nice', day: 'for', some: 'rain' }; + let mapped; + beforeEach(() => { + mapped = mapStateToProps(testState); + }); + test('filteredUsersCount from grades.filteredUsersCount', () => { + expect(mapped.filteredUsersCount).toEqual(selectors.grades.filteredUsersCount(testState)); + }); + test('totalUsersCount from grades.totalUsersCount', () => { + expect(mapped.totalUsersCount).toEqual(selectors.grades.totalUsersCount(testState)); + }); + }); +}); diff --git a/src/components/GradesTab/GradebookTable/Fields.jsx b/src/components/GradesView/GradebookTable/Fields.jsx similarity index 100% rename from src/components/GradesTab/GradebookTable/Fields.jsx rename to src/components/GradesView/GradebookTable/Fields.jsx diff --git a/src/components/GradesTab/GradebookTable/Fields.test.jsx b/src/components/GradesView/GradebookTable/Fields.test.jsx similarity index 100% rename from src/components/GradesTab/GradebookTable/Fields.test.jsx rename to src/components/GradesView/GradebookTable/Fields.test.jsx diff --git a/src/components/GradesTab/GradebookTable/GradeButton.jsx b/src/components/GradesView/GradebookTable/GradeButton.jsx similarity index 100% rename from src/components/GradesTab/GradebookTable/GradeButton.jsx rename to src/components/GradesView/GradebookTable/GradeButton.jsx diff --git a/src/components/GradesTab/GradebookTable/GradeButton.test.jsx b/src/components/GradesView/GradebookTable/GradeButton.test.jsx similarity index 100% rename from src/components/GradesTab/GradebookTable/GradeButton.test.jsx rename to src/components/GradesView/GradebookTable/GradeButton.test.jsx diff --git a/src/components/GradesTab/GradebookTable/LabelReplacements.jsx b/src/components/GradesView/GradebookTable/LabelReplacements.jsx similarity index 100% rename from src/components/GradesTab/GradebookTable/LabelReplacements.jsx rename to src/components/GradesView/GradebookTable/LabelReplacements.jsx diff --git a/src/components/GradesTab/GradebookTable/LabelReplacements.test.jsx b/src/components/GradesView/GradebookTable/LabelReplacements.test.jsx similarity index 100% rename from src/components/GradesTab/GradebookTable/LabelReplacements.test.jsx rename to src/components/GradesView/GradebookTable/LabelReplacements.test.jsx diff --git a/src/components/GradesTab/GradebookTable/__snapshots__/Fields.test.jsx.snap b/src/components/GradesView/GradebookTable/__snapshots__/Fields.test.jsx.snap similarity index 100% rename from src/components/GradesTab/GradebookTable/__snapshots__/Fields.test.jsx.snap rename to src/components/GradesView/GradebookTable/__snapshots__/Fields.test.jsx.snap diff --git a/src/components/GradesTab/GradebookTable/__snapshots__/GradeButton.test.jsx.snap b/src/components/GradesView/GradebookTable/__snapshots__/GradeButton.test.jsx.snap similarity index 100% rename from src/components/GradesTab/GradebookTable/__snapshots__/GradeButton.test.jsx.snap rename to src/components/GradesView/GradebookTable/__snapshots__/GradeButton.test.jsx.snap diff --git a/src/components/GradesTab/GradebookTable/__snapshots__/LabelReplacements.test.jsx.snap b/src/components/GradesView/GradebookTable/__snapshots__/LabelReplacements.test.jsx.snap similarity index 84% rename from src/components/GradesTab/GradebookTable/__snapshots__/LabelReplacements.test.jsx.snap rename to src/components/GradesView/GradebookTable/__snapshots__/LabelReplacements.test.jsx.snap index 53a9b63..79db50e 100644 --- a/src/components/GradesTab/GradebookTable/__snapshots__/LabelReplacements.test.jsx.snap +++ b/src/components/GradesView/GradebookTable/__snapshots__/LabelReplacements.test.jsx.snap @@ -7,7 +7,7 @@ exports[`LabelReplacements TotalGradeLabelReplacement displays overlay tooltip 1 `; @@ -23,7 +23,7 @@ exports[`LabelReplacements TotalGradeLabelReplacement snapshot 1`] = ` } @@ -39,7 +39,7 @@ exports[`LabelReplacements TotalGradeLabelReplacement snapshot 1`] = `
} /> @@ -66,7 +66,7 @@ exports[`LabelReplacements UsernameLabelReplacement snapshot 1`] = `
diff --git a/src/components/GradesTab/GradebookTable/__snapshots__/test.jsx.snap b/src/components/GradesView/GradebookTable/__snapshots__/test.jsx.snap similarity index 94% rename from src/components/GradesTab/GradebookTable/__snapshots__/test.jsx.snap rename to src/components/GradesView/GradebookTable/__snapshots__/test.jsx.snap index 160f9bc..702d896 100644 --- a/src/components/GradesTab/GradebookTable/__snapshots__/test.jsx.snap +++ b/src/components/GradesView/GradebookTable/__snapshots__/test.jsx.snap @@ -19,7 +19,7 @@ exports[`GradebookTable component snapshot - fields1 and 2 between email and tot "label": , }, Object { diff --git a/src/components/GradesTab/GradebookTable/index.jsx b/src/components/GradesView/GradebookTable/index.jsx similarity index 100% rename from src/components/GradesTab/GradebookTable/index.jsx rename to src/components/GradesView/GradebookTable/index.jsx diff --git a/src/components/GradesTab/GradebookTable/messages.js b/src/components/GradesView/GradebookTable/messages.js similarity index 72% rename from src/components/GradesTab/GradebookTable/messages.js rename to src/components/GradesView/GradebookTable/messages.js index 07f5613..458e8dd 100644 --- a/src/components/GradesTab/GradebookTable/messages.js +++ b/src/components/GradesView/GradebookTable/messages.js @@ -2,32 +2,32 @@ import { defineMessages } from '@edx/frontend-platform/i18n'; const messages = defineMessages({ emailHeading: { - id: 'gradebook.GradesTab.table.headings.email', + id: 'gradebook.GradesView.table.headings.email', defaultMessage: 'Email', description: 'Gradebook table email column header', }, totalGradeHeading: { - id: 'gradebook.GradesTab.table.headings.totalGrade', + id: 'gradebook.GradesView.table.headings.totalGrade', defaultMessage: 'Total Grade (%)', description: 'Gradebook table total grade column header', }, usernameHeading: { - id: 'gradebook.GradesTab.table.headings.username', + id: 'gradebook.GradesView.table.headings.username', defaultMessage: 'Username', description: 'Gradebook table username column header', }, studentKeyLabel: { - id: 'gradebook.GradesTab.table.labels.studentKey', + id: 'gradebook.GradesView.table.labels.studentKey', defaultMessage: 'Student Key*', description: 'Gradebook table Student Key label', }, usernameLabel: { - id: 'gradebook.GradesTab.table.labels.username', + id: 'gradebook.GradesView.table.labels.username', defaultMessage: 'Username', description: 'Gradebook table username label', }, totalGradePercentage: { - id: 'gradebook.GradesTab.table.totalGradePercentage', + id: 'gradebook.GradesView.table.totalGradePercentage', defaultMessage: 'Total Grade values are always displayed as a percentage', description: 'Gradebook table message that total grades are displayed in percent format', }, diff --git a/src/components/GradesTab/GradebookTable/test.jsx b/src/components/GradesView/GradebookTable/test.jsx similarity index 100% rename from src/components/GradesTab/GradebookTable/test.jsx rename to src/components/GradesView/GradebookTable/test.jsx diff --git a/src/components/GradesTab/GradesTab.scss b/src/components/GradesView/GradesView.scss similarity index 94% rename from src/components/GradesTab/GradesTab.scss rename to src/components/GradesView/GradesView.scss index 2311fa8..2aad4c2 100644 --- a/src/components/GradesTab/GradesTab.scss +++ b/src/components/GradesView/GradesView.scss @@ -26,7 +26,16 @@ } h4 { font-weight: bold; - margin-top: 2rem; + margin-top: 0rem; + } + .import-grades-btn { + margin-left: 20px; + } + .intervention-report-description: { + margin-right: 40px; + } + h4.step-message-1 { + margin-top: 40px; } } diff --git a/src/components/BulkManagementTab/FileUploadForm.jsx b/src/components/GradesView/ImportGradesButton.jsx similarity index 73% rename from src/components/BulkManagementTab/FileUploadForm.jsx rename to src/components/GradesView/ImportGradesButton.jsx index 3f1a42a..377b3dd 100644 --- a/src/components/BulkManagementTab/FileUploadForm.jsx +++ b/src/components/GradesView/ImportGradesButton.jsx @@ -6,7 +6,6 @@ import { connect } from 'react-redux'; import { FormattedMessage } from '@edx/frontend-platform/i18n'; import { - Button, Form, FormControl, FormGroup, @@ -14,14 +13,15 @@ import { import selectors from 'data/selectors'; import thunkActions from 'data/thunkActions'; -import messages from './messages'; +import NetworkButton from 'components/NetworkButton'; +import messages from './ImportGradesButton.messages'; /** - * + * * File-type input wrapped with hidden control such that when a valid file is * added, it is automattically uploaded. */ -export class FileUploadForm extends React.Component { +export class ImportGradesButton extends React.Component { constructor(props) { super(props); this.fileInputRef = React.createRef(); @@ -49,7 +49,7 @@ export class FileUploadForm extends React.Component { handleFileInputChange() { return this.hasFile && ( - this.props.submitFileUploadFormData(this.formData).then( + this.props.submitImportGradesButtonData(this.formData).then( () => { this.fileInput.value = null; }, ) ); @@ -71,17 +71,20 @@ export class FileUploadForm extends React.Component { - + ); } } -FileUploadForm.propTypes = { +ImportGradesButton.propTypes = { // redux gradeExportUrl: PropTypes.string.isRequired, - submitFileUploadFormData: PropTypes.func.isRequired, + submitImportGradesButtonData: PropTypes.func.isRequired, }; export const mapStateToProps = (state) => ({ @@ -89,7 +92,7 @@ export const mapStateToProps = (state) => ({ }); export const mapDispatchToProps = { - submitFileUploadFormData: thunkActions.grades.submitFileUploadFormData, + submitImportGradesButtonData: thunkActions.grades.submitImportGradesButtonData, }; -export default connect(mapStateToProps, mapDispatchToProps)(FileUploadForm); +export default connect(mapStateToProps, mapDispatchToProps)(ImportGradesButton); diff --git a/src/components/GradesView/ImportGradesButton.messages.js b/src/components/GradesView/ImportGradesButton.messages.js new file mode 100644 index 0000000..3e47d57 --- /dev/null +++ b/src/components/GradesView/ImportGradesButton.messages.js @@ -0,0 +1,16 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + csvUploadLabel: { + id: 'gradebook.BulkManagementHistoryView.csvUploadLabel', + defaultMessage: 'Upload Grade CSV', + description: 'Button in BulkManagementHistoryView Alerts', + }, + importGradesBtnText: { + id: 'gradebook.GradesView.importGradesBtnText', + defaultMessage: 'Import Grades', + description: 'Button in BulkManagement Tab File Upload Form', + }, +}); + +export default messages; diff --git a/src/components/BulkManagementTab/FileUploadForm.test.jsx b/src/components/GradesView/ImportGradesButton.test.jsx similarity index 81% rename from src/components/BulkManagementTab/FileUploadForm.test.jsx rename to src/components/GradesView/ImportGradesButton.test.jsx index 2e907dd..0a67205 100644 --- a/src/components/BulkManagementTab/FileUploadForm.test.jsx +++ b/src/components/GradesView/ImportGradesButton.test.jsx @@ -3,23 +3,24 @@ import React from 'react'; import { shallow } from 'enzyme'; import TestRenderer from 'react-test-renderer'; import { - Button, Form, FormControl, FormGroup, } from '@edx/paragon'; -import { FormattedMessage } from '@edx/frontend-platform/i18n'; + +import NetworkButton from 'components/NetworkButton'; import selectors from 'data/selectors'; import thunkActions from 'data/thunkActions'; -import { FileUploadForm, mapStateToProps, mapDispatchToProps } from './FileUploadForm'; +import { ImportGradesButton, mapStateToProps, mapDispatchToProps } from './ImportGradesButton'; -import messages from './messages'; +import messages from './ImportGradesButton.messages'; jest.mock('@edx/frontend-platform/i18n', () => ({ defineMessages: m => m, FormattedMessage: () => 'FormattedMessage', })); +jest.mock('components/NetworkButton', () => 'NetworkButton'); jest.mock('data/selectors', () => ({ __esModule: true, default: { @@ -34,16 +35,14 @@ jest.mock('data/selectors', () => ({ jest.mock('data/thunkActions', () => ({ __esModule: true, default: { - grades: { submitFileUploadFormData: jest.fn() }, + grades: { submitImportGradesButtonData: jest.fn() }, }, })); -jest.mock('./BulkManagementAlerts', () => 'BulkManagementAlerts'); -jest.mock('./ResultsSummary', () => 'ResultsSummary'); const mockRef = { click: jest.fn(), files: [] }; -describe('FileUploadForm', () => { +describe('ImportGradesButton', () => { beforeEach(() => { mockRef.click.mockClear(); }); @@ -54,7 +53,7 @@ describe('FileUploadForm', () => { beforeEach(() => { props = { gradeExportUrl: 'fakeUrl', - submitFileUploadFormData: jest.fn(), + submitImportGradesButtonData: jest.fn(), }; }); describe('snapshot', () => { @@ -64,12 +63,11 @@ describe('FileUploadForm', () => { ]; test(`snapshot - loads ${snapshotSegments.join(', ')}`, () => { jest.mock('@edx/paragon', () => ({ - Button: () => 'Button', Form: () => 'Form', FormControl: () => 'FormControl', FormGroup: () => 'FormGroup', })); - el = shallow(); + el = shallow(); el.instance().handleFileInputChange = jest.fn().mockName('this.handleFileInputChange'); el.instance().fileInputRef = jest.fn().mockName('this.fileInputRef'); el.instance().handleClickImportGrades = jest.fn().mockName('this.handleClickImportGrades'); @@ -80,7 +78,7 @@ describe('FileUploadForm', () => { describe('render', () => { beforeEach(() => { el = TestRenderer.create( - , + , { createNodeMock: () => mockRef }, ); inst = el.root; @@ -119,21 +117,21 @@ describe('FileUploadForm', () => { describe('import button', () => { let btn; beforeEach(() => { - btn = inst.findByType(Button); + btn = inst.findByType(NetworkButton); }); test('handleClickImportGrade on click', () => { expect(btn.props.onClick).toEqual(el.getInstance().handleClickImportGrades); }); - test('text from messages.importBtn', () => { - const messageEl = btn.findByType(FormattedMessage); - expect(messageEl.props).toEqual(messages.importBtnText); + test('label from messages.importGradesBtnText and import true', () => { + expect(btn.props.label).toEqual(messages.importGradesBtnText); + expect(btn.props.import).toEqual(true); }); }); }); describe('fileInput helper', () => { test('links to fileInputRef.current', () => { el = TestRenderer.create( - , + , { createNodeMock: () => mockRef }, ); expect(el.getInstance().fileInput).not.toEqual(undefined); @@ -144,7 +142,7 @@ describe('FileUploadForm', () => { let fileInput; beforeEach(() => { el = TestRenderer.create( - , + , { createNodeMock: () => mockRef }, ); fileInput = jest.spyOn(el.getInstance(), 'fileInput', 'get'); @@ -164,17 +162,17 @@ describe('FileUploadForm', () => { it('does nothing if file input has not loaded with files', () => { fileInput.mockReturnValue(null); el.getInstance().handleFileInputChange(); - expect(props.submitFileUploadFormData).not.toHaveBeenCalled(); + expect(props.submitImportGradesButtonData).not.toHaveBeenCalled(); fileInput.mockReturnValue({ files: [] }); el.getInstance().handleFileInputChange(); - expect(props.submitFileUploadFormData).not.toHaveBeenCalled(); + expect(props.submitImportGradesButtonData).not.toHaveBeenCalled(); }); - it('calls submitFileUploadFormData and then clears fileInput if has files', () => { + it('calls submitImportGradesButtonData and then clears fileInput if has files', () => { fileInput.mockReturnValue({ files: ['some', 'files'], value: 'a value' }); const formData = { fake: 'form data' }; jest.spyOn(el.getInstance(), 'formData', 'get').mockReturnValue(formData); const submit = jest.fn(() => ({ then: (thenCB) => { thenCB(); } })); - el.update(); + el.update(); el.getInstance().handleFileInputChange(); expect(submit).toHaveBeenCalledWith(formData); expect(el.getInstance().fileInput.value).toEqual(null); @@ -205,10 +203,10 @@ describe('FileUploadForm', () => { }); describe('mapDispatchToProps', () => { - test('submitFileUploadFormData from thunkActions.grades', () => { + test('submitImportGradesButtonData from thunkActions.grades', () => { expect( - mapDispatchToProps.submitFileUploadFormData, - ).toEqual(thunkActions.grades.submitFileUploadFormData); + mapDispatchToProps.submitImportGradesButtonData, + ).toEqual(thunkActions.grades.submitImportGradesButtonData); }); }); }); diff --git a/src/components/GradesView/ImportSuccessToast.jsx b/src/components/GradesView/ImportSuccessToast.jsx new file mode 100644 index 0000000..a38a2e0 --- /dev/null +++ b/src/components/GradesView/ImportSuccessToast.jsx @@ -0,0 +1,72 @@ +/* eslint-disable react/sort-comp, react/button-has-type */ +import React from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; + +import { Toast } from '@edx/paragon'; +import { + injectIntl, + intlShape, +} from '@edx/frontend-platform/i18n'; + +import selectors from 'data/selectors'; +import actions from 'data/actions'; +import { views } from 'data/constants/app'; +import messages from './ImportSuccessToast.messages'; + +/** + * + * Toast component triggered by successful grade upload. + * Provides a link to view the Bulk Management History tab. + */ +export class ImportSuccessToast extends React.Component { + constructor(props) { + super(props); + this.onClose = this.onClose.bind(this); + this.handleShowHistoryView = this.handleShowHistoryView.bind(this); + } + + onClose() { + this.props.setShow(false); + } + + handleShowHistoryView() { + this.props.setAppView(views.bulkManagementHistory); + this.onClose(); + } + + render() { + return ( + + {this.props.intl.formatMessage(messages.description)} + + ); + } +} + +ImportSuccessToast.propTypes = { + // injected + intl: intlShape.isRequired, + // redux + show: PropTypes.bool.isRequired, + setAppView: PropTypes.func.isRequired, + setShow: PropTypes.func.isRequired, +}; + +export const mapStateToProps = (state) => ({ + show: selectors.app.showImportSuccessToast(state), +}); + +export const mapDispatchToProps = { + setAppView: actions.app.setView, + setShow: actions.app.setShowImportSuccessToast, +}; + +export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(ImportSuccessToast)); diff --git a/src/components/GradesView/ImportSuccessToast.messages.js b/src/components/GradesView/ImportSuccessToast.messages.js new file mode 100644 index 0000000..f8ddc5a --- /dev/null +++ b/src/components/GradesView/ImportSuccessToast.messages.js @@ -0,0 +1,16 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + description: { + id: 'gradebook.GradesView.ImportSuccessToast.description', + defaultMessage: 'Import Successful! Grades will be updated momentarily.', + description: 'Import Success Toast description', + }, + showHistoryViewBtn: { + id: 'gradebook.GradesView.ImportSuccessToast.showHistoryViewBtn', + defaultMessage: 'View Activity Log', + description: 'Button text for action that loads Bulk Management Activity Log view', + }, +}); + +export default messages; diff --git a/src/components/GradesView/ImportSuccessToast.test.jsx b/src/components/GradesView/ImportSuccessToast.test.jsx new file mode 100644 index 0000000..be13492 --- /dev/null +++ b/src/components/GradesView/ImportSuccessToast.test.jsx @@ -0,0 +1,110 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import selectors from 'data/selectors'; +import actions from 'data/actions'; +import { views } from 'data/constants/app'; + +import { + ImportSuccessToast, + mapStateToProps, + mapDispatchToProps, +} from './ImportSuccessToast'; +import messages from './ImportSuccessToast.messages'; + +jest.mock('@edx/paragon', () => ({ + Toast: () => 'Toast', +})); +jest.mock('data/selectors', () => ({ + __esModule: true, + default: { + app: { + showImportSuccessToast: (state) => ({ showImportSuccessToast: state }), + }, + }, +})); +jest.mock('data/actions', () => ({ + __esModule: true, + default: { + app: { + setView: jest.fn(), + setShow: jest.fn(), + }, + }, +})); + +describe('ImportSuccessToast component', () => { + describe('snapshots', () => { + let el; + let props = { + show: true, + }; + beforeEach(() => { + props = { + ...props, + intl: { formatMessage: (msg) => msg.defaultMessage }, + setAppView: jest.fn(), + setShow: jest.fn(), + }; + el = shallow(); + }); + test('snapshot', () => { + el.instance().handleShowHistoryView = jest.fn().mockName('handleShowHistoryView'); + el.instance().onClose = jest.fn().mockName('onClose'); + expect(el).toMatchSnapshot(); + }); + describe('Toast props', () => { + let toastProps; + beforeEach(() => { + toastProps = el.props(); + }); + test('action has translated label and onClick from this.handleShowHistoryView', () => { + expect(toastProps.action).toEqual({ + label: props.intl.formatMessage(messages.showHistoryViewBtn), + onClick: el.instance().handleShowHistoryView, + }); + }); + test('onClose from this.onClose method', () => { + expect(toastProps.onClose).toEqual(el.instance().onClose); + }); + test('show from show prop', () => { + expect(toastProps.show).toEqual(props.show); + el.setProps({ show: false }); + expect(el.props().show).toEqual(false); + }); + }); + describe('onClose', () => { + it('calls props.setShow(false)', () => { + el.instance().onClose(); + expect(props.setShow).toHaveBeenCalledWith(false); + }); + }); + describe('handleShowHistoryView', () => { + it('calls setAppView with views.bulkManagementHistory and this.onClose', () => { + el.instance().onClose = jest.fn(); + el.instance().handleShowHistoryView(); + expect(props.setAppView).toHaveBeenCalledWith(views.bulkManagementHistory); + expect(el.instance().onClose).toHaveBeenCalled(); + }); + }); + }); + describe('behavior', () => { + }); + describe('mapStateToProps', () => { + const testState = { somewhere: 'over', the: 'rainbow' }; + const mapped = mapStateToProps(testState); + test('show from app showImportSuccessToast selector', () => { + expect(mapped.show).toEqual( + selectors.app.showImportSuccessToast(testState), + ); + }); + }); + describe('mapDispatchToProps', () => { + test('setAppView from actions.app.setView', () => { + expect(mapDispatchToProps.setAppView).toEqual(actions.app.setView); + }); + test('setShow from actions.setShowImportSuccessToast', () => { + expect(mapDispatchToProps.setShow).toEqual(actions.app.setShowImportSuccessToast); + }); + }); +}); diff --git a/src/components/GradesView/InterventionsReport.jsx b/src/components/GradesView/InterventionsReport.jsx new file mode 100644 index 0000000..5a0402f --- /dev/null +++ b/src/components/GradesView/InterventionsReport.jsx @@ -0,0 +1,72 @@ +/* eslint-disable react/sort-comp, react/button-has-type */ +import React from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; + +import { FormattedMessage } from '@edx/frontend-platform/i18n'; + +import actions from 'data/actions'; +import selectors from 'data/selectors'; + +import NetworkButton from 'components/NetworkButton'; +import messages from './InterventionsReport.messages'; + +/** + * + * Provides download buttons for Bulk Management and Intervention reports, only if + * showBulkManagement is set in redus. + */ +export class InterventionsReport extends React.Component { + constructor(props) { + super(props); + this.handleClick = this.handleClick.bind(this); + } + + handleClick() { + this.props.downloadInterventionReport(); + window.location.assign(this.props.interventionExportUrl); + } + + render() { + return this.props.showBulkManagement && ( +
+

+ +

+
+
+ +
+ +
+
+ ); + } +} + +InterventionsReport.defaultProps = { + showBulkManagement: false, +}; + +InterventionsReport.propTypes = { + // redux + downloadInterventionReport: PropTypes.func.isRequired, + interventionExportUrl: PropTypes.string.isRequired, + showBulkManagement: PropTypes.bool, +}; + +export const mapStateToProps = (state) => ({ + interventionExportUrl: selectors.root.interventionExportUrl(state), + showBulkManagement: selectors.root.showBulkManagement(state), +}); + +export const mapDispatchToProps = { + downloadInterventionReport: actions.grades.downloadReport.intervention, +}; + +export default connect(mapStateToProps, mapDispatchToProps)(InterventionsReport); diff --git a/src/components/GradesView/InterventionsReport.messages.js b/src/components/GradesView/InterventionsReport.messages.js new file mode 100644 index 0000000..e3aa8f1 --- /dev/null +++ b/src/components/GradesView/InterventionsReport.messages.js @@ -0,0 +1,21 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + title: { + id: 'gradebook.GradesView.InterventionsReport.title', + defaultMessage: 'Interventions Report', + description: 'Intervention report subsection label', + }, + description: { + id: 'gradebook.GradesView.InterventionsReport.description', + defaultMessage: 'Need to find students who may be falling behind? Download the interventions report to obtain engagement metrics such as section attempts and visits.', + description: 'Intervention report subsection description', + }, + downloadBtn: { + id: 'gradebook.GradesView.InterventionsReport.downloadBtn', + defaultMessage: 'Download Interventions', + description: 'Button text for intervention report download control in GradesView', + }, +}); + +export default messages; diff --git a/src/components/GradesView/InterventionsReport.test.jsx b/src/components/GradesView/InterventionsReport.test.jsx new file mode 100644 index 0000000..3dfe2b5 --- /dev/null +++ b/src/components/GradesView/InterventionsReport.test.jsx @@ -0,0 +1,107 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import selectors from 'data/selectors'; +import actions from 'data/actions'; + +import { + InterventionsReport, + mapStateToProps, + mapDispatchToProps, +} from './InterventionsReport'; + +jest.mock('@edx/paragon', () => ({ + Toast: () => 'Toast', +})); +jest.mock('components/NetworkButton', () => 'NetworkButton'); +jest.mock('data/selectors', () => ({ + __esModule: true, + default: { + root: { + interventionExportUrl: (state) => ({ interventionExportUrl: state }), + showBulkManagement: (state) => ({ showBulkManagement: state }), + }, + }, +})); +jest.mock('data/actions', () => ({ + __esModule: true, + default: { + grades: { + downloadReport: { intervention: jest.fn() }, + }, + }, +})); + +describe('InterventionsReport component', () => { + let el; + let props = { + interventionExportUrl: 'url.for.exporting.interventions', + showBulkManagement: true, + }; + let location; + beforeAll(() => { + location = window.location; + }); + beforeEach(() => { + delete window.location; + window.location = Object.defineProperties( + {}, + { + ...Object.getOwnPropertyDescriptors(location), + assign: { configurable: true, value: jest.fn() }, + }, + ); + props = { + ...props, + downloadInterventionReport: jest.fn(), + }; + }); + afterAll(() => { + window.location = location; + }); + describe('snapshots', () => { + beforeEach(() => { + el = shallow(); + }); + test('snapshot', () => { + el.instance().handleClick = jest.fn().mockName('handleClick'); + expect(el.instance().render()).toMatchSnapshot(); + }); + test('returns empty if props.showBulkManagement is false', () => { + el.setProps({ showBulkManagement: false }); + expect(el.instance().render()).toEqual(false); + }); + }); + describe('behavior', () => { + beforeEach(() => { + el = shallow(); + }); + describe('handleClick', () => { + it('calls props.downloadInterventionReport and navigates to props.interventionExportUrl', () => { + el.instance().handleClick(); + expect(props.downloadInterventionReport).toHaveBeenCalled(); + }); + }); + }); + describe('mapStateToProps', () => { + const testState = { somewhere: 'over', the: 'rainbow' }; + const mapped = mapStateToProps(testState); + test('interventionExportUrl from root interventionExportUrl selector', () => { + expect(mapped.interventionExportUrl).toEqual( + selectors.root.interventionExportUrl(testState), + ); + }); + test('showBulkManagement from root showBulkManagement selector', () => { + expect(mapped.showBulkManagement).toEqual( + selectors.root.showBulkManagement(testState), + ); + }); + }); + describe('mapDispatchToProps', () => { + test('downloadInterventionReport from actions.grades.downloadReport.intervention', () => { + expect(mapDispatchToProps.downloadInterventionReport).toEqual( + actions.grades.downloadReport.intervention, + ); + }); + }); +}); diff --git a/src/components/GradesTab/PageButtons/PageButtons.test.jsx b/src/components/GradesView/PageButtons/PageButtons.test.jsx similarity index 100% rename from src/components/GradesTab/PageButtons/PageButtons.test.jsx rename to src/components/GradesView/PageButtons/PageButtons.test.jsx diff --git a/src/components/GradesTab/PageButtons/__snapshots__/PageButtons.test.jsx.snap b/src/components/GradesView/PageButtons/__snapshots__/PageButtons.test.jsx.snap similarity index 88% rename from src/components/GradesTab/PageButtons/__snapshots__/PageButtons.test.jsx.snap rename to src/components/GradesView/PageButtons/__snapshots__/PageButtons.test.jsx.snap index 2c9c77d..144c7c0 100644 --- a/src/components/GradesTab/PageButtons/__snapshots__/PageButtons.test.jsx.snap +++ b/src/components/GradesView/PageButtons/__snapshots__/PageButtons.test.jsx.snap @@ -22,7 +22,7 @@ exports[`PageButtons component snapshots buttons enabled with both endpoints pro @@ -66,7 +66,7 @@ exports[`PageButtons component snapshots nextPage disabled if not provided 1`] = @@ -110,7 +110,7 @@ exports[`PageButtons component snapshots prevPage disabled if not provided 1`] = diff --git a/src/components/GradesTab/PageButtons/index.jsx b/src/components/GradesView/PageButtons/index.jsx similarity index 100% rename from src/components/GradesTab/PageButtons/index.jsx rename to src/components/GradesView/PageButtons/index.jsx diff --git a/src/components/GradesTab/PageButtons/messages.js b/src/components/GradesView/PageButtons/messages.js similarity index 76% rename from src/components/GradesTab/PageButtons/messages.js rename to src/components/GradesView/PageButtons/messages.js index 5531a1e..5626399 100644 --- a/src/components/GradesTab/PageButtons/messages.js +++ b/src/components/GradesView/PageButtons/messages.js @@ -2,12 +2,12 @@ import { defineMessages } from '@edx/frontend-platform/i18n'; const messages = defineMessages({ prevPage: { - id: 'gradebook.GradesTab.PageButtons.prevPage', + id: 'gradebook.GradesView.PageButtons.prevPage', defaultMessage: 'Previous Page', description: 'Grades tab Previous Page button text', }, nextPage: { - id: 'gradebook.GradesTab.PageButtons.nextPage', + id: 'gradebook.GradesView.PageButtons.nextPage', defaultMessage: 'Next Page', description: 'Grades tab Next Page button text', }, diff --git a/src/components/GradesTab/ScoreViewInput.jsx b/src/components/GradesView/ScoreViewInput.jsx similarity index 96% rename from src/components/GradesTab/ScoreViewInput.jsx rename to src/components/GradesView/ScoreViewInput.jsx index 7412087..6759573 100644 --- a/src/components/GradesTab/ScoreViewInput.jsx +++ b/src/components/GradesView/ScoreViewInput.jsx @@ -7,7 +7,7 @@ import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/ import actions from 'data/actions'; import selectors from 'data/selectors'; -import messages from './messages'; +import messages from './ScoreViewInput.messages'; /** * diff --git a/src/components/GradesView/ScoreViewInput.messages.js b/src/components/GradesView/ScoreViewInput.messages.js new file mode 100644 index 0000000..b4130fb --- /dev/null +++ b/src/components/GradesView/ScoreViewInput.messages.js @@ -0,0 +1,21 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + scoreView: { + id: 'gradebook.GradesView.scoreViewLabel', + defaultMessage: 'Score View', + description: 'Score format select dropdown label', + }, + absolute: { + id: 'gradebook.GradesView.absoluteOption', + defaultMessage: 'Absolute', + description: 'Score format select dropdown option', + }, + percent: { + id: 'gradebook.GradesView.percentOption', + defaultMessage: 'Percent', + description: 'Score format select dropdown option', + }, +}); + +export default messages; diff --git a/src/components/GradesTab/ScoreViewInput.test.jsx b/src/components/GradesView/ScoreViewInput.test.jsx similarity index 100% rename from src/components/GradesTab/ScoreViewInput.test.jsx rename to src/components/GradesView/ScoreViewInput.test.jsx diff --git a/src/components/GradesTab/SearchControls.jsx b/src/components/GradesView/SearchControls.jsx similarity index 56% rename from src/components/GradesTab/SearchControls.jsx rename to src/components/GradesView/SearchControls.jsx index 72a764d..e17823e 100644 --- a/src/components/GradesTab/SearchControls.jsx +++ b/src/components/GradesView/SearchControls.jsx @@ -2,14 +2,14 @@ import React from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; -import { Button, Icon, SearchField } from '@edx/paragon'; +import { SearchField } from '@edx/paragon'; import { FormattedMessage } from '@edx/frontend-platform/i18n'; import actions from 'data/actions'; import selectors from 'data/selectors'; import thunkActions from 'data/thunkActions'; -import messages from './messages'; +import messages from './SearchControls.messages'; /** * Controls for filtering the GradebookTable. Contains the "Edit Filters" button for opening the filter drawer @@ -34,30 +34,18 @@ export class SearchControls extends React.Component { render() { return ( - <> -

-
- -
- } - onChange={this.onChange} - onClear={this.onClear} - value={this.props.searchValue} - /> - - - -
-
- +
+ } + onChange={this.onChange} + onClear={this.onClear} + value={this.props.searchValue} + /> + + + +
); } } @@ -67,7 +55,6 @@ SearchControls.propTypes = { fetchGrades: PropTypes.func.isRequired, searchValue: PropTypes.string.isRequired, setSearchValue: PropTypes.func.isRequired, - toggleFilterDrawer: PropTypes.func.isRequired, }; export const mapStateToProps = (state) => ({ @@ -77,7 +64,6 @@ 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/GradesView/SearchControls.messages.js b/src/components/GradesView/SearchControls.messages.js new file mode 100644 index 0000000..3212d77 --- /dev/null +++ b/src/components/GradesView/SearchControls.messages.js @@ -0,0 +1,16 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + label: { + id: 'gradebook.GradesView.search.label', + defaultMessage: 'Search for a learner', + description: 'Search description label', + }, + hint: { + id: 'gradebook.GradesView.search.hint', + defaultMessage: 'Search by username, email, or student key', + description: 'Search hint label', + }, +}); + +export default messages; diff --git a/src/components/GradesTab/SearchControls.test.jsx b/src/components/GradesView/SearchControls.test.jsx similarity index 91% rename from src/components/GradesTab/SearchControls.test.jsx rename to src/components/GradesView/SearchControls.test.jsx index 05419f1..b9d3621 100644 --- a/src/components/GradesTab/SearchControls.test.jsx +++ b/src/components/GradesView/SearchControls.test.jsx @@ -40,7 +40,6 @@ describe('SearchControls', () => { searchValue: 'alice', setSearchValue: jest.fn(), fetchGrades: jest.fn().mockName('fetchGrades'), - toggleFilterDrawer: jest.fn().mockName('toggleFilterDrawer'), }; }); @@ -92,12 +91,6 @@ describe('SearchControls', () => { test('setSearchValue from actions.app.setSearchValue', () => { expect(mapDispatchToProps.setSearchValue).toEqual(actions.app.setSearchValue); }); - - test('toggleFilterDrawer from thunkActions.app.filterMenu.toggle', () => { - expect( - mapDispatchToProps.toggleFilterDrawer, - ).toEqual(thunkActions.app.filterMenu.toggle); - }); }); }); }); diff --git a/src/components/GradesTab/SpinnerIcon.jsx b/src/components/GradesView/SpinnerIcon.jsx similarity index 100% rename from src/components/GradesTab/SpinnerIcon.jsx rename to src/components/GradesView/SpinnerIcon.jsx diff --git a/src/components/GradesTab/SpinnerIcon.test.jsx b/src/components/GradesView/SpinnerIcon.test.jsx similarity index 100% rename from src/components/GradesTab/SpinnerIcon.test.jsx rename to src/components/GradesView/SpinnerIcon.test.jsx diff --git a/src/components/GradesTab/StatusAlerts.jsx b/src/components/GradesView/StatusAlerts.jsx similarity index 97% rename from src/components/GradesTab/StatusAlerts.jsx rename to src/components/GradesView/StatusAlerts.jsx index 9da3ba3..4f8bc16 100644 --- a/src/components/GradesTab/StatusAlerts.jsx +++ b/src/components/GradesView/StatusAlerts.jsx @@ -7,7 +7,7 @@ import { FormattedMessage } from '@edx/frontend-platform/i18n'; import selectors from 'data/selectors'; import actions from 'data/actions'; -import messages from './messages'; +import messages from './StatusAlerts.messages'; export class StatusAlerts extends React.Component { get isCourseGradeFilterAlertOpen() { diff --git a/src/components/GradesView/StatusAlerts.messages.js b/src/components/GradesView/StatusAlerts.messages.js new file mode 100644 index 0000000..f4defda --- /dev/null +++ b/src/components/GradesView/StatusAlerts.messages.js @@ -0,0 +1,21 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + editSuccessAlert: { + id: 'gradebook.GradesView.editSuccessAlert', + defaultMessage: 'The grade has been successfully edited. You may see a slight delay before updates appear in the Gradebook.', + description: 'Alert text for successful edit action', + }, + maxGradeInvalid: { + id: 'gradebook.GradesView.maxCourseGradeInvalid', + defaultMessage: 'Maximum course grade must be between 0 and 100', + description: 'Alert text for invalid maximum course grade', + }, + minGradeInvalid: { + id: 'gradebook.GradesView.minCourseGradeInvalid', + defaultMessage: 'Minimum course grade must be between 0 and 100', + description: 'Alert text for invalid minimum course grade', + }, +}); + +export default messages; diff --git a/src/components/GradesTab/StatusAlerts.test.jsx b/src/components/GradesView/StatusAlerts.test.jsx similarity index 98% rename from src/components/GradesTab/StatusAlerts.test.jsx rename to src/components/GradesView/StatusAlerts.test.jsx index 0088068..7707465 100644 --- a/src/components/GradesTab/StatusAlerts.test.jsx +++ b/src/components/GradesView/StatusAlerts.test.jsx @@ -5,7 +5,7 @@ import { FormattedMessage } from '@edx/frontend-platform/i18n'; import actions from 'data/actions'; import selectors from 'data/selectors'; -import messages from './messages'; +import messages from './StatusAlerts.messages'; import { StatusAlerts, mapDispatchToProps, diff --git a/src/components/GradesTab/UsersLabel.jsx b/src/components/GradesView/UsersLabel.jsx similarity index 100% rename from src/components/GradesTab/UsersLabel.jsx rename to src/components/GradesView/UsersLabel.jsx diff --git a/src/components/GradesTab/UsersLabel.test.jsx b/src/components/GradesView/UsersLabel.test.jsx similarity index 100% rename from src/components/GradesTab/UsersLabel.test.jsx rename to src/components/GradesView/UsersLabel.test.jsx diff --git a/src/components/GradesView/__snapshots__/FilterMenuToggle.test.jsx.snap b/src/components/GradesView/__snapshots__/FilterMenuToggle.test.jsx.snap new file mode 100644 index 0000000..02ef8a8 --- /dev/null +++ b/src/components/GradesView/__snapshots__/FilterMenuToggle.test.jsx.snap @@ -0,0 +1,19 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`FilterMenuToggle component snapshots basic snapshot 1`] = ` + +`; diff --git a/src/components/GradesView/__snapshots__/FilteredUsersLabel.test.jsx.snap b/src/components/GradesView/__snapshots__/FilteredUsersLabel.test.jsx.snap new file mode 100644 index 0000000..2086de8 --- /dev/null +++ b/src/components/GradesView/__snapshots__/FilteredUsersLabel.test.jsx.snap @@ -0,0 +1,23 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`FilteredUsersLabel component snapshot - displays label with number of filtered users out of total 1`] = ` + + 23 + , + "totalUsers": + 140 + , + } + } +/> +`; diff --git a/src/components/BulkManagementTab/__snapshots__/FileUploadForm.test.jsx.snap b/src/components/GradesView/__snapshots__/ImportGradesButton.test.jsx.snap similarity index 53% rename from src/components/BulkManagementTab/__snapshots__/FileUploadForm.test.jsx.snap rename to src/components/GradesView/__snapshots__/ImportGradesButton.test.jsx.snap index f79ff1e..6470ad6 100644 --- a/src/components/BulkManagementTab/__snapshots__/FileUploadForm.test.jsx.snap +++ b/src/components/GradesView/__snapshots__/ImportGradesButton.test.jsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`FileUploadForm component snapshot snapshot - loads export form w/ alerts and file input, import btn 1`] = ` +exports[`ImportGradesButton component snapshot snapshot - loads export form w/ alerts and file input, import btn 1`] = `
} onChange={[MockFunction this.handleFileInputChange]} @@ -29,17 +29,17 @@ exports[`FileUploadForm component snapshot snapshot - loads export form w/ alert /> - - - + />
`; diff --git a/src/components/GradesView/__snapshots__/ImportSuccessToast.test.jsx.snap b/src/components/GradesView/__snapshots__/ImportSuccessToast.test.jsx.snap new file mode 100644 index 0000000..b42d994 --- /dev/null +++ b/src/components/GradesView/__snapshots__/ImportSuccessToast.test.jsx.snap @@ -0,0 +1,16 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ImportSuccessToast component snapshots snapshot 1`] = ` + + Import Successful! Grades will be updated momentarily. + +`; diff --git a/src/components/GradesView/__snapshots__/InterventionsReport.test.jsx.snap b/src/components/GradesView/__snapshots__/InterventionsReport.test.jsx.snap new file mode 100644 index 0000000..804c90e --- /dev/null +++ b/src/components/GradesView/__snapshots__/InterventionsReport.test.jsx.snap @@ -0,0 +1,38 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`InterventionsReport component snapshots snapshot 1`] = ` +
+

+ +

+
+
+ +
+ +
+
+`; diff --git a/src/components/GradesTab/__snapshots__/ScoreViewInput.test.jsx.snap b/src/components/GradesView/__snapshots__/ScoreViewInput.test.jsx.snap similarity index 92% rename from src/components/GradesTab/__snapshots__/ScoreViewInput.test.jsx.snap rename to src/components/GradesView/__snapshots__/ScoreViewInput.test.jsx.snap index 061bc26..35d417f 100644 --- a/src/components/GradesTab/__snapshots__/ScoreViewInput.test.jsx.snap +++ b/src/components/GradesView/__snapshots__/ScoreViewInput.test.jsx.snap @@ -8,7 +8,7 @@ exports[`ScoreViewInput component snapshot - select box with percent and absolut : diff --git a/src/components/GradesView/__snapshots__/SearchControls.test.jsx.snap b/src/components/GradesView/__snapshots__/SearchControls.test.jsx.snap new file mode 100644 index 0000000..d0515e8 --- /dev/null +++ b/src/components/GradesView/__snapshots__/SearchControls.test.jsx.snap @@ -0,0 +1,28 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SearchControls Component Snapshots basic snapshot 1`] = ` +
+ + } + onChange={[MockFunction onChange]} + onClear={[MockFunction onClear]} + onSubmit={[MockFunction fetchGrades]} + value="alice" + /> + + + +
+`; diff --git a/src/components/GradesTab/__snapshots__/SpinnerIcon.test.jsx.snap b/src/components/GradesView/__snapshots__/SpinnerIcon.test.jsx.snap similarity index 100% rename from src/components/GradesTab/__snapshots__/SpinnerIcon.test.jsx.snap rename to src/components/GradesView/__snapshots__/SpinnerIcon.test.jsx.snap diff --git a/src/components/GradesTab/__snapshots__/StatusAlerts.test.jsx.snap b/src/components/GradesView/__snapshots__/StatusAlerts.test.jsx.snap similarity index 92% rename from src/components/GradesTab/__snapshots__/StatusAlerts.test.jsx.snap rename to src/components/GradesView/__snapshots__/StatusAlerts.test.jsx.snap index 4c54341..3fe5f39 100644 --- a/src/components/GradesTab/__snapshots__/StatusAlerts.test.jsx.snap +++ b/src/components/GradesView/__snapshots__/StatusAlerts.test.jsx.snap @@ -8,7 +8,7 @@ exports[`StatusAlerts snapshots basic snapshot 1`] = ` } onClose={[MockFunction handleCloseSuccessBanner]} diff --git a/src/components/GradesTab/__snapshots__/UsersLabel.test.jsx.snap b/src/components/GradesView/__snapshots__/UsersLabel.test.jsx.snap similarity index 100% rename from src/components/GradesTab/__snapshots__/UsersLabel.test.jsx.snap rename to src/components/GradesView/__snapshots__/UsersLabel.test.jsx.snap diff --git a/src/components/GradesTab/__snapshots__/test.jsx.snap b/src/components/GradesView/__snapshots__/test.jsx.snap similarity index 56% rename from src/components/GradesTab/__snapshots__/test.jsx.snap rename to src/components/GradesView/__snapshots__/test.jsx.snap index dd149a7..912b7ad 100644 --- a/src/components/GradesTab/__snapshots__/test.jsx.snap +++ b/src/components/GradesView/__snapshots__/test.jsx.snap @@ -1,9 +1,24 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`GradesTab Component snapshots basic snapshot 1`] = ` +exports[`GradesView Component snapshots basic snapshot 1`] = ` - + +

+ +

+
+ + +
@@ -12,16 +27,16 @@ exports[`GradesTab Component snapshots basic snapshot 1`] = ` -
+

@@ -29,9 +44,10 @@ exports[`GradesTab Component snapshots basic snapshot 1`] = `

+
`; diff --git a/src/components/GradesTab/index.jsx b/src/components/GradesView/index.jsx similarity index 75% rename from src/components/GradesTab/index.jsx rename to src/components/GradesView/index.jsx index 951dc3f..dfc1ec6 100644 --- a/src/components/GradesTab/index.jsx +++ b/src/components/GradesView/index.jsx @@ -8,20 +8,22 @@ import { FormattedMessage } from '@edx/frontend-platform/i18n'; 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 FilterBadges from './FilterBadges'; +import FilteredUsersLabel from './FilteredUsersLabel'; +import FilterMenuToggle from './FilterMenuToggle'; import GradebookTable from './GradebookTable'; -import SearchControls from './SearchControls'; -import StatusAlerts from './StatusAlerts'; -import SpinnerIcon from './SpinnerIcon'; +import ImportSuccessToast from './ImportSuccessToast'; +import InterventionsReport from './InterventionsReport'; +import PageButtons from './PageButtons'; import ScoreViewInput from './ScoreViewInput'; -import UsersLabel from './UsersLabel'; +import SearchControls from './SearchControls'; +import SpinnerIcon from './SpinnerIcon'; +import StatusAlerts from './StatusAlerts'; import messages from './messages'; -export class GradesTab extends React.Component { +export class GradesView extends React.Component { constructor(props) { super(props); this.handleFilterBadgeClose = this.handleFilterBadgeClose.bind(this); @@ -42,31 +44,44 @@ export class GradesTab extends React.Component { return ( <> - + + +

+ +

+ +
+ + +
+

-
+ +

*

+ + ); } } -GradesTab.defaultProps = {}; +GradesView.defaultProps = {}; -GradesTab.propTypes = { +GradesView.propTypes = { updateQueryParams: PropTypes.func.isRequired, // redux @@ -81,4 +96,4 @@ export const mapDispatchToProps = { resetFilters: actions.filters.reset, }; -export default connect(mapStateToProps, mapDispatchToProps)(GradesTab); +export default connect(mapStateToProps, mapDispatchToProps)(GradesView); diff --git a/src/components/GradesView/messages.js b/src/components/GradesView/messages.js new file mode 100644 index 0000000..2929648 --- /dev/null +++ b/src/components/GradesView/messages.js @@ -0,0 +1,21 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + filterStepHeading: { + id: 'gradebook.GradesView.filterHeading', + defaultMessage: 'Step 1: Filter the Grade Report', + description: 'Filter controls container heading string', + }, + gradebookStepHeading: { + id: 'gradebook.GradesView.gradebookStepHeading', + defaultMessage: 'Step 2: View or Modify Individual Grades', + description: 'Alert text for invalid minimum course grade', + }, + mastersHint: { + id: 'gradebook.GradesView.mastersHint', + defaultMessage: "available for learners in the Master's track only", + description: 'Masters feature availability hint on Grades Tab', + }, +}); + +export default messages; diff --git a/src/components/GradesTab/test.jsx b/src/components/GradesView/test.jsx similarity index 86% rename from src/components/GradesTab/test.jsx rename to src/components/GradesView/test.jsx index 6bb2524..847e989 100644 --- a/src/components/GradesTab/test.jsx +++ b/src/components/GradesView/test.jsx @@ -5,7 +5,7 @@ import actions from 'data/actions'; import thunkActions from 'data/thunkActions'; import { - GradesTab, + GradesView, mapStateToProps, mapDispatchToProps, } from '.'; @@ -13,6 +13,7 @@ import { jest.mock('data/actions', () => ({ __esModule: true, default: { + app: { setView: jest.fn() }, filters: { resetFilters: jest.fn() }, }, })); @@ -23,18 +24,21 @@ jest.mock('data/thunkActions', () => ({ }, })); -jest.mock('./PageButtons', () => 'PageButtons'); -jest.mock('./FilterBadges', () => 'FilterBadges'); jest.mock('./BulkManagementControls', () => 'BulkManagementControls'); jest.mock('./EditModal', () => 'EditModal'); +jest.mock('./FilterBadges', () => 'FilterBadges'); +jest.mock('./FilteredUsersLabel', () => 'FilteredUsersLabel'); +jest.mock('./FilterMenuToggle', () => 'FilterMenuToggle'); jest.mock('./GradebookTable', () => 'GradebookTable'); -jest.mock('./SearchControls', () => 'SearchControls'); -jest.mock('./StatusAlerts', () => 'StatusAlerts'); -jest.mock('./SpinnerIcon', () => 'SpinnerIcon'); +jest.mock('./ImportSuccessToast', () => 'ImportSuccessToast'); +jest.mock('./InterventionsReport', () => 'InterventionsReport'); +jest.mock('./PageButtons', () => 'PageButtons'); jest.mock('./ScoreViewInput', () => 'ScoreViewInput'); -jest.mock('./UsersLabel', () => 'UsersLabel'); +jest.mock('./SearchControls', () => 'SearchControls'); +jest.mock('./SpinnerIcon', () => 'SpinnerIcon'); +jest.mock('./StatusAlerts', () => 'StatusAlerts'); -describe('GradesTab', () => { +describe('GradesView', () => { let props; beforeEach(() => { props = { @@ -49,7 +53,7 @@ describe('GradesTab', () => { describe('behavior', () => { let el; beforeEach(() => { - el = shallow(); + el = shallow(); }); describe('handleFilterBadgeClose', () => { beforeEach(() => { @@ -73,7 +77,7 @@ describe('GradesTab', () => { }); describe('snapshots', () => { test('basic snapshot', () => { - const el = shallow(); + const el = shallow(); el.instance().handleFilterBadgeClose = jest.fn().mockName('this.handleFilterBadgeClose'); expect(el.instance().render()).toMatchSnapshot(); }); diff --git a/src/components/NetworkButton/__snapshots__/test.jsx.snap b/src/components/NetworkButton/__snapshots__/test.jsx.snap new file mode 100644 index 0000000..2b57968 --- /dev/null +++ b/src/components/NetworkButton/__snapshots__/test.jsx.snap @@ -0,0 +1,41 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`NetworkButton component snapshots snapshot 1`] = ` +, + "pending": , + } + } + labels={ + Object { + "default": , + "pending": , + } + } + onClick={[MockFunction]} + state="default" + variant="outline-primary" +/> +`; diff --git a/src/components/NetworkButton/index.jsx b/src/components/NetworkButton/index.jsx new file mode 100644 index 0000000..f0a8889 --- /dev/null +++ b/src/components/NetworkButton/index.jsx @@ -0,0 +1,88 @@ +/* eslint-disable react/sort-comp, react/button-has-type */ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import { connect } from 'react-redux'; + +import { StatefulButton, Icon } from '@edx/paragon'; +import { FormattedMessage } from '@edx/frontend-platform/i18n'; + +import selectors from 'data/selectors'; +import { StrictDict } from 'utils'; + +export const buttonStates = StrictDict({ + pending: 'pending', + default: 'default', +}); + +/** + * + * Simplified Download/Upload button, tied to the app's network status (showSpinner). + * Provides a button with a default `download` icon along with a label, which should + * be a transifex-friendly message object. If the `import` argument is passed, the default + * icon will be an upload icon. + * If the app is busy on a network task (showSpinner === true), the button will be disabled + * and show a spinner/working indicator. + * The buttons can also be passed an optional className for further css customization. + * @param {string} className - optional extra css class(es) + * @param {object} label - transifex-friendly message object + * @param {func} onClick - button on-click action + * @param {bool} import - should show import icon instead of download icon in default state. + */ +export class NetworkButton extends React.Component { + get labels() { + const label = ; + return { default: label, pending: label }; + } + + get icons() { + const iconClass = 'fa mr-2'; + const defaultIcon = this.props.import ? 'fa-upload' : 'fa-download'; + return { + pending: (), + default: (), + }; + } + + get buttonState() { + return this.props.showSpinner ? buttonStates.pending : buttonStates.default; + } + + render() { + return ( + + ); + } +} + +NetworkButton.defaultProps = { + className: '', + showSpinner: false, + import: false, +}; + +NetworkButton.propTypes = { + className: PropTypes.string, + label: PropTypes.shape({ + id: PropTypes.string, + defaultMessage: PropTypes.string, + description: PropTypes.string, + }).isRequired, + onClick: PropTypes.func.isRequired, + import: PropTypes.bool, + // redux + showSpinner: PropTypes.bool, +}; + +export const mapStateToProps = (state) => ({ + showSpinner: selectors.root.shouldShowSpinner(state), +}); +export default connect(mapStateToProps)(NetworkButton); diff --git a/src/components/NetworkButton/test.jsx b/src/components/NetworkButton/test.jsx new file mode 100644 index 0000000..79e5937 --- /dev/null +++ b/src/components/NetworkButton/test.jsx @@ -0,0 +1,89 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import { Icon, StatefulButton } from '@edx/paragon'; +import { FormattedMessage } from '@edx/frontend-platform/i18n'; + +import selectors from 'data/selectors'; +import { NetworkButton, mapStateToProps, buttonStates } from '.'; + +jest.mock('@edx/frontend-platform/i18n', () => ({ + FormattedMessage: () => 'FormattedMessage', +})); +jest.mock('@edx/paragon', () => ({ + Icon: () => 'Icon', + StatefulButton: () => 'StatefulButton', +})); +jest.mock('data/selectors', () => ({ + __esModule: true, + default: { + root: { shouldShowSpinner: (state) => ({ shouldShowSpinner: state }) }, + }, +})); + +describe('NetworkButton component', () => { + describe('snapshots', () => { + let el; + let btnProps; + const props = { + className: 'test-class', + label: { + id: 'label-id', + defaultMessage: 'test button label', + description: 'test button label description', + showSpinner: false, + }, + }; + beforeEach(() => { + props.onClick = jest.fn(); + el = shallow(); + btnProps = el.find(StatefulButton).props(); + }); + test('snapshot', () => { + expect(el).toMatchSnapshot(); + }); + it('sets labels to translated label prop', () => { + expect(btnProps.labels).toEqual({ + default: (), + pending: (), + }); + }); + describe('export icons', () => { + it('sets icons with spinner pending icon and download default', () => { + expect(btnProps.icons).toEqual({ + pending: (), + default: (), + }); + }); + }); + describe('import icons', () => { + it('sets icons with spinner pending icon and upload default', () => { + el.setProps({ import: true }); + expect(el.find(StatefulButton).props().icons).toEqual({ + pending: (), + default: (), + }); + }); + }); + describe('buttonState', () => { + it('is set to pending state if props.showSpinner', () => { + expect(btnProps.state).toEqual(buttonStates.default); + }); + it('is set to pending state if props.showSpinner', () => { + el.setProps({ showSpinner: true }); + expect(el.find(StatefulButton).props().state).toEqual(buttonStates.pending); + expect(btnProps.state).toEqual(buttonStates.default); + }); + }); + }); + describe('mapStateToProps', () => { + const testState = { a: 'wrinkle', in: 'time' }; + let mapped; + beforeEach(() => { + mapped = mapStateToProps(testState); + }); + test('showSpinner from root shouldShowSpinner selector', () => { + expect(mapped.showSpinner).toEqual(selectors.root.shouldShowSpinner(testState)); + }); + }); +}); diff --git a/src/containers/GradebookPage/__snapshots__/test.jsx.snap b/src/containers/GradebookPage/__snapshots__/test.jsx.snap index 5b0d0aa..323b2d6 100644 --- a/src/containers/GradebookPage/__snapshots__/test.jsx.snap +++ b/src/containers/GradebookPage/__snapshots__/test.jsx.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`GradebookPage component snapshot - shows BulkManagementTab if showBulkManagement 1`] = ` +exports[`GradebookPage component snapshot - shows BulkManagementHistoryView if activeView === views.bulkManagementHistory 1`] = ` - - - - - - - - + `; -exports[`GradebookPage component snapshot - shows only GradesTab if showBulkManagement=false 1`] = ` +exports[`GradebookPage component snapshot - shows GradesView if aciveView === views.grades 1`] = ` - - - - - + `; diff --git a/src/containers/GradebookPage/index.jsx b/src/containers/GradebookPage/index.jsx index f1b5001..38e7ed6 100644 --- a/src/containers/GradebookPage/index.jsx +++ b/src/containers/GradebookPage/index.jsx @@ -4,21 +4,20 @@ import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import queryString from 'query-string'; -import { Tab, Tabs } from '@edx/paragon'; - import selectors from 'data/selectors'; import thunkActions from 'data/thunkActions'; +import { views } from 'data/constants/app'; import WithSidebar from 'components/WithSidebar'; import GradebookHeader from 'components/GradebookHeader'; -import GradesTab from 'components/GradesTab'; +import GradesView from 'components/GradesView'; import GradebookFilters from 'components/GradebookFilters'; -import BulkManagementTab from 'components/BulkManagementTab'; +import BulkManagementHistoryView from 'components/BulkManagementHistoryView'; /** * * Top-level view for the Gradebook MFE. - * Organizes a header and a pair of tabs (Grades and BulkManagement) with a toggle-able + * Organizes a header and a pair of views (Grades and BulkManagement) with a toggle-able * filter sidebar. */ export class GradebookPage extends React.Component { @@ -51,23 +50,16 @@ export class GradebookPage extends React.Component { >
- - - - - {this.props.showBulkManagement && ( - - - - )} - + {(this.props.activeView === views.bulkManagementHistory + ? + : + )}
); } } GradebookPage.defaultProps = { - showBulkManagement: false, location: { search: '' }, }; GradebookPage.propTypes = { @@ -75,17 +67,18 @@ GradebookPage.propTypes = { push: PropTypes.func, }).isRequired, location: PropTypes.shape({ search: PropTypes.string }), - initializeApp: PropTypes.func.isRequired, - showBulkManagement: PropTypes.bool, match: PropTypes.shape({ params: PropTypes.shape({ courseId: PropTypes.string, }), }).isRequired, + // redux + activeView: PropTypes.string.isRequired, + initializeApp: PropTypes.func.isRequired, }; export const mapStateToProps = (state) => ({ - showBulkManagement: selectors.root.showBulkManagement(state), + activeView: selectors.app.activeView(state), }); export const mapDispatchToProps = { diff --git a/src/containers/GradebookPage/test.jsx b/src/containers/GradebookPage/test.jsx index c1da285..6a4e41c 100644 --- a/src/containers/GradebookPage/test.jsx +++ b/src/containers/GradebookPage/test.jsx @@ -6,12 +6,11 @@ import queryString from 'query-string'; import selectors from 'data/selectors'; import thunkActions from 'data/thunkActions'; -import { Tab, Tabs } from '@edx/paragon'; - import GradebookFilters from 'components/GradebookFilters'; import GradebookHeader from 'components/GradebookHeader'; -import GradesTab from 'components/GradesTab'; -import BulkManagementTab from 'components/BulkManagementTab'; +import GradesView from 'components/GradesView'; +import BulkManagementHistoryView from 'components/BulkManagementHistoryView'; +import { views } from 'data/constants/app'; import { GradebookPage, mapStateToProps, mapDispatchToProps } from '.'; @@ -27,8 +26,8 @@ jest.mock('@edx/paragon', () => ({ jest.mock('data/selectors', () => ({ __esModule: true, default: { - root: { - showBulkManagement: (state) => ({ showBulkManagement: state }), + app: { + activeView: (state) => ({ activeView: state }), }, }, })); @@ -41,9 +40,9 @@ jest.mock('data/thunkActions', () => ({ jest.mock('components/WithSidebar', () => 'WithSidebar'); jest.mock('components/GradebookHeader', () => 'GradebookHeader'); -jest.mock('components/GradesTab', () => 'GradesTab'); +jest.mock('components/GradesView', () => 'GradesView'); jest.mock('components/GradebookFilters', () => 'GradebookFilters'); -jest.mock('components/BulkManagementTab', () => 'BulkManagementTab'); +jest.mock('components/BulkManagementHistoryView', () => 'BulkManagementHistoryView'); describe('GradebookPage', () => { describe('component', () => { @@ -54,17 +53,18 @@ describe('GradebookPage', () => { search: 'searchString', }, match: { params: { courseId } }, + activeView: views.grades, }; beforeEach(() => { props.initializeApp = jest.fn(); props.history = { push: jest.fn() }; }); - test('snapshot - shows BulkManagementTab if showBulkManagement', () => { - el = shallow(); + test('snapshot - shows BulkManagementHistoryView if activeView === views.bulkManagementHistory', () => { + el = shallow(); el.instance().updateQueryParams = jest.fn().mockName('updateQueryParams'); expect(el.instance().render()).toMatchSnapshot(); }); - test('snapshot - shows only GradesTab if showBulkManagement=false', () => { + test('snapshot - shows GradesView if aciveView === views.grades', () => { el = shallow(); el.instance().updateQueryParams = jest.fn().mockName('updateQueryParams'); expect(el.instance().render()).toMatchSnapshot(); @@ -95,28 +95,17 @@ describe('GradebookPage', () => { it('displays Gradebook header and then tabs', () => { expect(children[0]).toEqual(); }); - it('displays tabs with only GradesTab if not showBulkManagement', () => { - expect(shallow(children[1])).toEqual(shallow( - - - - - , - )); + it('displays GradesView if activeView === views.grades', () => { + expect(shallow(children[1])).toEqual(shallow(( + + ))); }); - it('displays tabs with grades and BulkManagement if showBulkManagement', () => { - el = shallow(); - const tabs = el.props().children.props.children[1]; - expect(tabs).toEqual( - - - - - - - - , - ); + it('displays Bulk Management History View if activeView === views.bulkManagementHistory', () => { + el = shallow(); + const mainView = el.props().children.props.children[1]; + expect(mainView).toEqual(( + + )); }); }); }); @@ -163,8 +152,8 @@ describe('GradebookPage', () => { beforeEach(() => { mapped = mapStateToProps(testState); }); - test('showBulkManagement from root.showBulkManagement', () => { - expect(mapped.showBulkManagement).toEqual(selectors.root.showBulkManagement(testState)); + test('activeView from app.activeView', () => { + expect(mapped.activeView).toEqual(selectors.app.activeView(testState)); }); }); describe('mapDispatchToProps', () => { diff --git a/src/data/actions/app.js b/src/data/actions/app.js index 9ed8807..633351d 100644 --- a/src/data/actions/app.js +++ b/src/data/actions/app.js @@ -54,12 +54,28 @@ const setModalState = createAction('setModalState', (modalState) => ({ payload: Object.keys(modalFieldKeys).reduce(filterReducer(modalState), {}), })); +/** + * setShowImportSuccessToast(shouldShow) + * Set whether or not to show the Import Grades success toast + * @param {bool} sholdShow - should show the toast? + */ +const setShowImportSuccessToast = createAction('setShowImportSuccessToast'); + +/** + * setView(viewId) + * sets the UI to display the tab indcated by the passed view id + * @param {string} viewId - view id as set in app constants + */ +const setView = createAction('setView'); + export default StrictDict({ closeModal, filterMenu, setCourseId, + setLocalFilter, setModalState, setModalStateFromTable, setSearchValue, - setLocalFilter, + setShowImportSuccessToast, + setView, }); diff --git a/src/data/actions/app.test.js b/src/data/actions/app.test.js index cda1fd5..763c9e3 100644 --- a/src/data/actions/app.test.js +++ b/src/data/actions/app.test.js @@ -11,6 +11,8 @@ describe('actions', () => { actions.setSearchValue, actions.setLocalFilter, actions.setModalStateFromTable, + actions.setShowImportSuccessToast, + actions.setView, ].map(action => action.toString()); testActionTypes(actionTypes, dataKey); }); @@ -19,6 +21,10 @@ describe('actions', () => { test('setCourseId action', () => testAction(actions.setCourseId)); test('setModalStateFromTable action', () => testAction(actions.setModalStateFromTable)); test('setSearchValue action', () => testAction(actions.setSearchValue)); + test('setView action', () => testAction(actions.setView)); + test('setShowImportSuccessToast action', () => ( + testAction(actions.setShowImportSuccessToast) + )); describe('setLocalFilter', () => { it('forwards all values with filter field keys and no others', () => { const extra = { diff --git a/src/data/constants/app.js b/src/data/constants/app.js index 97d709b..7631535 100644 --- a/src/data/constants/app.js +++ b/src/data/constants/app.js @@ -3,6 +3,11 @@ import { getConfig } from '@edx/frontend-platform'; export const routePath = `${getConfig().PUBLIC_PATH}:courseId`; +export const views = StrictDict({ + grades: 'grades', + bulkManagementHistory: 'bulkManagementHistory', +}); + export const modalFieldKeys = StrictDict({ adjustedGradePossible: 'adjustedGradePossible', adjustedGradeValue: 'adjustedGradeValue', diff --git a/src/data/reducers/app.js b/src/data/reducers/app.js index 8af1ad4..7179c8c 100644 --- a/src/data/reducers/app.js +++ b/src/data/reducers/app.js @@ -1,10 +1,13 @@ import initialFilters from '../constants/filters'; +import { views } from '../constants/app'; import { formatDateForDisplay } from '../actions/utils'; import actions from '../actions/app'; import filterActions from '../actions/filters'; +import gradesActions from '../actions/grades'; const initialState = { courseId: '', + activeView: views.grades, filters: { assignmentGradeMax: initialFilters.assignmentGradeMax, assignmentGradeMin: initialFilters.assignmentGradeMin, @@ -26,6 +29,7 @@ const initialState = { open: false, transitioning: false, }, + showImportSuccessToast: false, searchValue: '', }; @@ -80,6 +84,10 @@ const app = (state = initialState, { type, payload }) => { } case actions.setSearchValue.toString(): return { ...state, searchValue: payload }; + case actions.setShowImportSuccessToast.toString(): + return { ...state, showImportSuccessToast: payload }; + case actions.setView.toString(): + return { ...state, activeView: payload }; // initialize the filter fields that are locally stored case filterActions.initialize.toString(): return { @@ -103,6 +111,8 @@ const app = (state = initialState, { type, payload }) => { }, }), { ...state }); } + case gradesActions.csvUpload.finished.toString(): + return { ...state, showImportSuccessToast: true }; default: return state; } diff --git a/src/data/reducers/app.test.js b/src/data/reducers/app.test.js index 8f1eeb8..b680d4d 100644 --- a/src/data/reducers/app.test.js +++ b/src/data/reducers/app.test.js @@ -1,5 +1,6 @@ import appActions from 'data/actions/app'; import filterActions from 'data/actions/filters'; +import gradesActions from 'data/actions/grades'; import { formatDateForDisplay } from 'data/actions/utils'; import app, { initialState } from './app'; @@ -177,6 +178,20 @@ describe('app reducer', () => { ).toEqual({ ...testingState, searchValue: testValue }); }); }); + describe('appActions.setShowImportSuccessToast', () => { + it('loads showImportSuccessToast from payload', () => { + expect( + app(testingState, appActions.setShowImportSuccessToast(testValue)), + ).toEqual({ ...testingState, showImportSuccessToast: testValue }); + }); + }); + describe('appActions.setView', () => { + it('loads activeView from payload', () => { + expect( + app(testingState, appActions.setView(testValue)), + ).toEqual({ ...testingState, activeView: testValue }); + }); + }); describe('filterActions.initialize', () => { it('loads relevant filter values', () => { expect( @@ -216,5 +231,12 @@ describe('app reducer', () => { }); }); }); + describe('grade actions csvUpload.finished', () => { + it('sets showImportSuccessToast to true', () => { + expect( + app(testingState, gradesActions.csvUpload.finished()), + ).toEqual({ ...testingState, showImportSuccessToast: true }); + }); + }); }); }); diff --git a/src/data/reducers/grades.js b/src/data/reducers/grades.js index 2d4a856..001c159 100644 --- a/src/data/reducers/grades.js +++ b/src/data/reducers/grades.js @@ -25,6 +25,9 @@ const initialState = { bulkManagement: {}, totalUsersCount: 0, filteredUsersCount: 0, + isImportGradesActive: false, + isDownloadInterventionsActive: false, + isDownloadGradesActive: false, }; const grades = (state = initialState, { type, payload }) => { diff --git a/src/data/selectors/app.js b/src/data/selectors/app.js index 347fa28..6fd32bd 100644 --- a/src/data/selectors/app.js +++ b/src/data/selectors/app.js @@ -97,9 +97,11 @@ const filterMenuSelectors = simpleSelectorFactory( const simpleSelectors = simpleSelectorFactory( ({ app }) => app, [ + 'activeView', 'courseId', 'filters', 'searchValue', + 'showImportSuccessToast', ], ); diff --git a/src/data/selectors/app.test.js b/src/data/selectors/app.test.js index 60418a2..e8d2966 100644 --- a/src/data/selectors/app.test.js +++ b/src/data/selectors/app.test.js @@ -159,8 +159,10 @@ describe('app selectors', () => { expect(exportedSelectors[key]({ app: { [key]: testVal } })).toEqual(testVal); }); }; + testSimpleSelector('activeView'); testSimpleSelector('courseId'); testSimpleSelector('filters'); testSimpleSelector('searchValue'); + testSimpleSelector('showImportSuccessToast'); }); }); diff --git a/src/data/thunkActions/grades.js b/src/data/thunkActions/grades.js index 1087857..357b903 100644 --- a/src/data/thunkActions/grades.js +++ b/src/data/thunkActions/grades.js @@ -124,7 +124,7 @@ export const fetchPrevNextGrades = (endpoint) => ( } ); -export const submitFileUploadFormData = (formData) => ( +export const submitImportGradesButtonData = (formData) => ( (dispatch, getState) => { const courseId = selectors.app.courseId(getState()); dispatch(grades.csvUpload.started()); @@ -167,6 +167,6 @@ export default StrictDict({ fetchGradesIfAssignmentGradeFiltersSet, fetchGradeOverrideHistory, fetchPrevNextGrades, - submitFileUploadFormData, + submitImportGradesButtonData, updateGrades, }); diff --git a/src/data/thunkActions/grades.test.js b/src/data/thunkActions/grades.test.js index ee6f15f..e3d5d82 100644 --- a/src/data/thunkActions/grades.test.js +++ b/src/data/thunkActions/grades.test.js @@ -516,11 +516,11 @@ describe('grades thunkActions', () => { }); }); - describe('submitFileUploadFormData', () => { + describe('submitImportGradesButtonData', () => { const formData = { form: 'data' }; const testFetch = createTestFetcher( lms.api.uploadGradeCsv, - thunkActions.submitFileUploadFormData, + thunkActions.submitImportGradesButtonData, [formData], () => expect(lms.api.uploadGradeCsv).toHaveBeenCalledWith(formData), );