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
This commit is contained in:
Ben Warzeski
2021-08-26 10:37:35 -04:00
committed by GitHub
parent 4e26247ac3
commit a1de3a8612
129 changed files with 1796 additions and 702 deletions

View File

@@ -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,
});

View File

@@ -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 = {

View File

@@ -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',

View File

@@ -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;
}

View File

@@ -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 });
});
});
});
});

View File

@@ -25,6 +25,9 @@ const initialState = {
bulkManagement: {},
totalUsersCount: 0,
filteredUsersCount: 0,
isImportGradesActive: false,
isDownloadInterventionsActive: false,
isDownloadGradesActive: false,
};
const grades = (state = initialState, { type, payload }) => {

View File

@@ -97,9 +97,11 @@ const filterMenuSelectors = simpleSelectorFactory(
const simpleSelectors = simpleSelectorFactory(
({ app }) => app,
[
'activeView',
'courseId',
'filters',
'searchValue',
'showImportSuccessToast',
],
);

View File

@@ -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');
});
});

View File

@@ -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,
});

View File

@@ -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),
);