feat: [MICROBA-1506] bulkEmailTool data context (#39)

Currently, our bulk email tool in concept looks something like this:
- BulkEmailTool
	- BulkEmailForm
	- BulkEmailTaskManager

Right now, the two components under the parent BulkEmailTool dont really
need to communicate with each other. For scheduled email, these two
components are going to be relying on the same data, and there need to
be provided that data by the parent. In order to make things more
manageable, this PR sets up some boilerplate and patterning for this
data. What this PR will include:
- Documentation around the pattern
- Necessary boilerplate to leverage the context store for the
  BulkEmailTool
- Tests around said store

What this PR will not include:
- Changes to the UI or form functionality
This commit is contained in:
Thomas Tracy
2022-05-16 11:43:04 -04:00
committed by GitHub
parent e09bb55544
commit ffa0361c22
9 changed files with 280 additions and 0 deletions

View File

@@ -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 <BulkEmailContext.Provider value={[state, dispatch]}>{children}</BulkEmailContext.Provider>;
}
BulkEmailProvider.propTypes = {
children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]).isRequired,
};

View File

@@ -0,0 +1,31 @@
export const fetchScheduledEmails = () => ({
type: 'FETCH_SCHEDULED_EMAILS',
});
export const fetchScheduledEmailsStart = () => ({
type: 'FETCH_START',
});
export const fetchScheduledEmailsComplete = (scheduledEmails) => ({
type: 'FETCH_COMPLETE',
payload: { scheduledEmails },
});
export const fetchScheduledEmailsError = () => ({
type: 'FETCH_ERROR',
});
export const copyToEditor = (body, subject) => ({
type: 'COPY_TO_EDITOR',
payload: {
emailBody: body,
emailSubject: subject,
},
});
export const handleEditorChange = (fieldName, fieldValue) => ({
type: 'EDITOR_ON_CHANGE',
payload: {
[fieldName]: fieldValue,
},
});

View File

@@ -0,0 +1,9 @@
import { camelCaseObject, getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
// eslint-disable-next-line import/prefer-default-export
export async function getScheduledBulkEmailData(courseId) {
const endpointUrl = `${getConfig().LMS_BASE_URL}/api/instructor_task/v1/schedules/${courseId}/bulk_email/`;
const { data } = await getAuthenticatedHttpClient().get(endpointUrl);
return camelCaseObject(data);
}

View File

@@ -0,0 +1,49 @@
import { logError, logInfo } from '@edx/frontend-platform/logging';
export default function BulkEmailReducer(state, action) {
switch (action.type) {
case 'FETCH_SCHEDULED_EMAILS':
logInfo(action.type, state);
return state;
case 'FETCH_START':
logInfo(action.type, state);
return {
...state,
isLoading: true,
};
case 'FETCH_COMPLETE':
logInfo(action.type, state);
return {
...state,
isLoading: false,
errorRetrievingData: false,
...action.payload,
};
case 'FETCH_FAILURE':
logError(action.type, state);
return {
...state,
isLoading: false,
errorRetrievingData: true,
};
case 'COPY_TO_EDITOR':
logInfo(action.type, state);
return {
...state,
editor: {
...action.payload,
},
};
case 'EDITOR_ON_CHANGE':
logInfo(action.type, state);
return {
...state,
editor: {
...state.editor,
...action.payload,
},
};
default:
throw new Error();
}
}

View File

@@ -0,0 +1,19 @@
import { Factory } from 'rosie'; // eslint-disable-line import/no-extraneous-dependencies
Factory.define('emailDataFactory')
.sequence('id')
.attrs({
subject: 'subject',
html_message: '<p>body</p>',
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');

View File

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

View File

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

View File

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