diff --git a/src/components/bulk-email-tool/data/BulkEmailProvider.jsx b/src/components/bulk-email-tool/data/BulkEmailProvider.jsx
new file mode 100644
index 0000000..465f0ab
--- /dev/null
+++ b/src/components/bulk-email-tool/data/BulkEmailProvider.jsx
@@ -0,0 +1,24 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import useAsyncReducer from '../../../utils/useAsyncReducer';
+import BulkEmailReducer from './reducer';
+
+export const BulkEmailContext = React.createContext();
+
+export function BulkEmailProvider({ children }) {
+ const initialState = {
+ editor: {
+ emailBody: {},
+ emailSubject: '',
+ emailRecipient: [],
+ emailSchedule: '',
+ },
+ scheduledEmails: [],
+ };
+ const [state, dispatch] = useAsyncReducer(BulkEmailReducer, initialState);
+ return
body
', + text_message: 'body', + course_id: 'course-v1:edX+DemoX+Demo_Course', + to_option: '', + sender: 'edx', + targets: ['learners'], + }); + +export default Factory.define('courseEmailFactory') + .sequence('id') + .attr('course_email', Factory.build('emailDataFactory')) + .sequence('task') + .attr('task_due', '2022-04-27T17:00:00Z'); diff --git a/src/components/bulk-email-tool/data/test/__factories__/scheduledEmails.factory.js b/src/components/bulk-email-tool/data/test/__factories__/scheduledEmails.factory.js new file mode 100644 index 0000000..260a7eb --- /dev/null +++ b/src/components/bulk-email-tool/data/test/__factories__/scheduledEmails.factory.js @@ -0,0 +1,19 @@ +import { Factory } from 'rosie'; // eslint-disable-line import/no-extraneous-dependencies +import './courseEmail.factory'; + +export default Factory.define('scheduledEmailFactory') + .attr( + 'next', + 'http://localhost:18000/api/instructor_task/v1/schedules/course-v1:edX+DemoX+Demo_Course/bulk_email/?page=$2', + ) + .attr('previous', 'null') + .option('count', '1') + .attr('current_page', 1) + .attr('start', 0) + .attr('results', ['count'], (count) => { + const emails = []; + for (let i = 1; i <= count; i++) { + emails.push(Factory.build('courseEmailFactory')); + } + return emails; + }); diff --git a/src/components/bulk-email-tool/data/test/reducer.test.js b/src/components/bulk-email-tool/data/test/reducer.test.js new file mode 100644 index 0000000..6fd3c39 --- /dev/null +++ b/src/components/bulk-email-tool/data/test/reducer.test.js @@ -0,0 +1,67 @@ +import { initializeMockApp } from '../../../../setupTest'; +import BulkEmailReducer from '../reducer'; + +describe('BulkEmailReducer', () => { + const testState = { + editor: { + emailBody: {}, + emailSubject: '', + emailRecipient: [], + emailSchedule: '', + }, + scheduledEmails: [], + isLoading: false, + errorRetrievingData: false, + }; + beforeAll(async () => { + await initializeMockApp(); + }); + + it('does not change state on FETCH_SCHEDULED_EMAILS', () => { + expect(BulkEmailReducer(testState, { type: 'FETCH_SCHEDULED_EMAILS' })).toEqual(testState); + }); + it('sets loading state on FETCH_START', () => { + const finalState = { + ...testState, + isLoading: true, + }; + const returnedState = BulkEmailReducer(testState, { type: 'FETCH_START' }); + expect(returnedState).toEqual(finalState); + }); + it('adds payload on FETCH_COMPLETE', () => { + const finalState = { + ...testState, + additionalField: true, + isLoading: false, + }; + const returnedState = BulkEmailReducer(testState, { type: 'FETCH_COMPLETE', payload: { additionalField: true } }); + expect(returnedState).toEqual(finalState); + expect(returnedState.isLoading).toEqual(false); + expect(returnedState.errorRetrievingData).toEqual(false); + }); + it('sets Error to true when FETCH_FAILURE action dispatched', () => { + const finalState = { + ...testState, + isLoading: false, + errorRetrievingData: true, + }; + const returnedState = BulkEmailReducer(testState, { type: 'FETCH_FAILURE' }); + expect(returnedState).toEqual(finalState); + }); + it('it copies full editor state when COPY_TO_EDITOR action dispatched', () => { + const newEditorState = { + emailBody: 'test', + emailSubject: 'test', + emailRecipient: ['test'], + emailSchedule: 'test', + }; + const finalState = { + ...testState, + editor: { + ...newEditorState, + }, + }; + const returnedState = BulkEmailReducer(testState, { type: 'COPY_TO_EDITOR', payload: newEditorState }); + expect(returnedState).toEqual(finalState); + }); +}); diff --git a/src/components/bulk-email-tool/data/thunks.js b/src/components/bulk-email-tool/data/thunks.js new file mode 100644 index 0000000..77a787f --- /dev/null +++ b/src/components/bulk-email-tool/data/thunks.js @@ -0,0 +1,29 @@ +import { + copyToEditor, + fetchScheduledEmails, + fetchScheduledEmailsComplete, + fetchScheduledEmailsError, + fetchScheduledEmailsStart, +} from './actions'; +import { getScheduledBulkEmailData } from './api'; + +export function getScheduledBulkEmailThunk(courseId) { + return async (dispatch) => { + dispatch(fetchScheduledEmails()); + dispatch(fetchScheduledEmailsStart()); + try { + const data = await getScheduledBulkEmailData(courseId); + if (!!data && data.results) { + dispatch(fetchScheduledEmailsComplete(data.results)); + } + } catch (error) { + dispatch(fetchScheduledEmailsError()); + } + }; +} + +export function copyTextToEditorThunk(body, subject) { + return async (dispatch) => { + dispatch(copyToEditor(body, subject)); + }; +} diff --git a/src/utils/useAsyncReducer.js b/src/utils/useAsyncReducer.js new file mode 100644 index 0000000..d658f19 --- /dev/null +++ b/src/utils/useAsyncReducer.js @@ -0,0 +1,33 @@ +import { useMemo, useReducer } from 'react'; + +/** + * This helper function wraps the useReducer dispatch function to allow for invoking function calls + * when a state change is dispatched. + * @param {*} dispatch useReducer's dispatch function. + * @returns a wrapped dispatch that execututes function actions. + */ +function wrapAsync(dispatch) { + return (action) => { + if (typeof action === 'function') { + return action(dispatch); + } + return dispatch(action); + }; +} +/** + * By default, the useReducer hook does not allow for async dispatches. This small + * hook takes the dispatch function from useReducer and wraps it to allow for the execution + * of functions that are invoked with the dispatch object. This makes it easier for us to perform + * async operations, or to execute multiple dispatches in a row using a single thunk. + * @param {Function} reducer a reducer function for the context state. + * @param {Object} initialState an initial state for the context store. + * @returns [state, asyncDispatch ] + */ +const useAsyncReducer = (reducer, initialState = null) => { + const [state, dispatch] = useReducer(reducer, initialState); + const asyncDispatch = useMemo(() => wrapAsync(dispatch), [dispatch]); + + return [state, asyncDispatch]; +}; + +export default useAsyncReducer;