Skip to content

Commit 75f449c

Browse files
committed
feat: context factory for components and BuildEmailFormExtensible with context and BulkEmailTaskManager pluggable
1 parent a3ea4f6 commit 75f449c

File tree

12 files changed

+391
-54
lines changed

12 files changed

+391
-54
lines changed

jest.config.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ module.exports = createConfig('jest', {
1111
'src/i18n',
1212
],
1313
moduleNameMapper: {
14-
'@node_modules/(.*)': '<rootDir>/node_modules/$1'
14+
'@node_modules/(.*)': '<rootDir>/node_modules/$1',
15+
'@communications-app/(.*)': '<rootDir>/$1'
1516
},
1617
});

package-lock.json

Lines changed: 54 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
"@edx/reactifex": "^2.1.1",
8080
"@testing-library/jest-dom": "5.16.5",
8181
"@testing-library/react": "12.1.5",
82+
"@testing-library/react-hooks": "^8.0.1",
8283
"axios-mock-adapter": "1.21.2",
8384
"eslint-import-resolver-alias": "^1.1.2",
8485
"eslint-import-resolver-webpack": "^0.13.8",

src/components/bulk-email-tool/BulkEmailTool.jsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import BuildEmailFormExtensible from './bulk-email-form/BuildEmailFormExtensible
1111
import { CourseMetadataContext } from '../page-container/PageContainer';
1212
import { BulkEmailProvider } from './bulk-email-context';
1313
import BackToInstructor from '../navigation-tabs/BackToInstructor';
14+
import PluggableComponent from '../PluggableComponent';
1415

1516
export default function BulkEmailTool() {
1617
const { courseId } = useParams();
@@ -36,7 +37,13 @@ export default function BulkEmailTool() {
3637
<BuildEmailFormExtensible courseId={courseId} cohorts={courseMetadata.cohorts} />
3738
</div>
3839
<div className="row py-5">
39-
<BulkEmailTaskManager courseId={courseId} />
40+
<PluggableComponent
41+
id="build-email-task-manager"
42+
as="communications-app-build-email-task-manager"
43+
courseId={courseId}
44+
>
45+
<BulkEmailTaskManager />
46+
</PluggableComponent>
4047
</div>
4148
</Container>
4249
</BulkEmailProvider>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import contextFactory from '@communications-app/src/utils/contextFactory';
2+
3+
import { INITIAL_STATE, reducer } from './reducer';
4+
5+
export const {
6+
useSelector,
7+
withContextProvider,
8+
useDispatch,
9+
DispatchContext,
10+
StateContext,
11+
} = contextFactory(reducer, INITIAL_STATE);
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { renderHook } from '@testing-library/react-hooks';
2+
import { useContext } from 'react';
3+
4+
import { INITIAL_STATE } from './reducer';
5+
6+
import {
7+
useSelector, useDispatch, StateContext, DispatchContext,
8+
} from '.';
9+
10+
describe('BuildEmailFormExtensible stateContext', () => {
11+
test('useSelector returns the state', () => {
12+
const { result } = renderHook(() => useSelector((state) => state));
13+
expect(result.current).toEqual(INITIAL_STATE);
14+
});
15+
16+
test('Context contains the initial value', () => {
17+
const {
18+
result: { current: stateContextValue },
19+
} = renderHook(() => useContext(StateContext));
20+
const {
21+
result: { current: dispatchContextValue },
22+
} = renderHook(() => useContext(DispatchContext));
23+
expect(stateContextValue).toEqual(INITIAL_STATE);
24+
expect(dispatchContextValue).toBeTruthy();
25+
});
26+
27+
test("useDispatch returns the context's dispatch", () => {
28+
const {
29+
result: { current: hookDispatch },
30+
} = renderHook(() => useDispatch());
31+
const {
32+
result: { current: dispatchContextValue },
33+
} = renderHook(() => useContext(DispatchContext));
34+
expect(hookDispatch).toEqual(dispatchContextValue);
35+
});
36+
});
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { produce } from 'immer';
2+
3+
export const INITIAL_STATE = {
4+
form: {
5+
isFormValid: true,
6+
isFormSubmitted: false,
7+
scheduleValid: true,
8+
isScheduled: false,
9+
isEditMode: false,
10+
formStatus: 'default',
11+
isScheduleButtonClicked: false,
12+
courseId: '',
13+
cohorts: '',
14+
scheduleDate: '',
15+
scheduleTime: '',
16+
isScheduledSubmitted: false,
17+
emailId: '',
18+
schedulingId: '',
19+
emailRecipients: [],
20+
subject: '',
21+
body: '',
22+
},
23+
};
24+
const ActionTypes = {
25+
UPDATE_FORM: 'UPDATE_FORM',
26+
RESET_FORM: 'RESET_FORM',
27+
};
28+
29+
export const actionCreators = {
30+
updateForm: (updates) => ({ type: ActionTypes.UPDATE_FORM, updates }),
31+
resetForm: () => ({ type: ActionTypes.RESET_FORM }),
32+
};
33+
34+
// eslint-disable-next-line consistent-return
35+
export const reducer = produce((draft, action) => {
36+
switch (action.type) {
37+
case ActionTypes.UPDATE_FORM: {
38+
Object.assign(draft.form, action.updates);
39+
break; // No explicit return needed due to 'produce' creating a draft
40+
}
41+
case ActionTypes.RESET_FORM: {
42+
// Resets to initial form state
43+
return INITIAL_STATE;
44+
}
45+
// Add other case handlers if needed
46+
default:
47+
// No changes, return the current state
48+
break;
49+
}
50+
}, INITIAL_STATE);
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,36 @@
1-
import { useState, useEffect, useContext } from 'react';
1+
import { useContext } from 'react';
22
import classNames from 'classnames';
33
import PropTypes from 'prop-types';
4+
import useDeepCompareEffect from 'use-deep-compare-effect';
45
import {
56
Form,
67
Spinner,
78
useToggle,
89
} from '@edx/paragon';
9-
import { BulkEmailContext } from '../bulk-email-context';
10-
import useMobileResponsive from '../../../utils/useMobileResponsive';
11-
import PluggableComponent from '../../PluggableComponent';
10+
import { BulkEmailContext } from '../../bulk-email-context';
11+
import useMobileResponsive from '../../../../utils/useMobileResponsive';
12+
import PluggableComponent from '../../../PluggableComponent';
1213

13-
function BuildEmailFormExtensible({ courseId, cohorts }) {
14+
import { withContextProvider, useDispatch } from './context';
15+
import { actionCreators as formActions } from './context/reducer';
16+
17+
const BuildEmailFormExtensible = ({ courseId, cohorts }) => {
1418
const isMobile = useMobileResponsive();
1519
const [{ editor }] = useContext(BulkEmailContext);
1620
const [isTaskAlertOpen, openTaskAlert, closeTaskAlert] = useToggle(false);
17-
const [formState, setFormState] = useState({
18-
isFormValid: true,
19-
isFormSubmitted: false,
20-
scheduleValid: true,
21-
isScheduled: false,
22-
isEditMode: false,
23-
formStatus: 'default',
24-
isScheduleButtonClicked: false,
25-
courseId,
26-
cohorts,
27-
scheduleDate: '',
28-
scheduleTime: '',
29-
isScheduledSubmitted: false,
30-
emailId: '',
31-
schedulingId: '',
32-
emailRecipients: { value: [], isLoaded: false },
33-
subject: { value: '', isLoaded: false },
34-
body: { value: '', isLoaded: false },
35-
});
21+
const dispatch = useDispatch();
3622

37-
useEffect(() => {
23+
useDeepCompareEffect(() => {
3824
if (editor.editMode) {
39-
const { emailRecipients, subject, body } = formState;
40-
const newRecipientsValue = { ...emailRecipients, value: editor.emailRecipients };
41-
const newSubjectValue = { ...subject, value: editor.emailSubject };
42-
const newBodyValue = { ...body, value: editor.emailBody };
25+
const newRecipientsValue = editor.emailRecipients;
26+
const newSubjectValue = editor.emailSubject;
27+
const newBodyValue = editor.emailBody;
4328
const newScheduleDate = editor.scheduleDate;
4429
const newScheduleTime = editor.scheduleTime;
4530
const newEmailId = editor.emailId;
4631
const newSchedulingId = editor.schedulingId;
4732

48-
setFormState({
49-
...formState,
33+
dispatch(formActions.updateForm({
5034
isEditMode: true,
5135
formStatus: 'reschedule',
5236
isScheduled: true,
@@ -57,10 +41,9 @@ function BuildEmailFormExtensible({ courseId, cohorts }) {
5741
emailRecipients: newRecipientsValue,
5842
subject: newSubjectValue,
5943
body: newBodyValue,
60-
});
44+
}));
6145
}
62-
// eslint-disable-next-line react-hooks/exhaustive-deps
63-
}, [editor.editMode, editor.emailRecipients, editor.emailSubject, editor.emailBody]);
46+
}, [editor, dispatch]);
6447

6548
return (
6649
<div className={classNames('w-100 m-auto', !isMobile && 'p-4 border border-primary-200')}>
@@ -69,23 +52,19 @@ function BuildEmailFormExtensible({ courseId, cohorts }) {
6952
<PluggableComponent
7053
id="build-email-form-tasks-alert-modal"
7154
as="communications-app-task-alert-modal"
72-
formState={formState}
73-
setFormState={setFormState}
55+
courseId={courseId}
7456
{...{ isTaskAlertOpen, openTaskAlert, closeTaskAlert }}
7557
/>
7658

7759
<PluggableComponent
7860
id="build-email-form-recipients-field"
7961
as="communications-app-recipients-checks"
80-
formState={formState}
81-
setFormState={setFormState}
62+
cohorts={cohorts}
8263
/>
8364

8465
<PluggableComponent
8566
id="build-email-form-subject-field"
8667
as="communications-app-subject-form"
87-
formState={formState}
88-
setFormState={setFormState}
8968
/>
9069

9170
<PluggableComponent
@@ -99,29 +78,23 @@ function BuildEmailFormExtensible({ courseId, cohorts }) {
9978
screenReaderText="loading"
10079
/>
10180
)}
102-
formState={formState}
103-
setFormState={setFormState}
10481
/>
10582

10683
<PluggableComponent
10784
id="build-email-form-instructions-form"
10885
as="communications-app-instructions-pro-freading"
109-
formState={formState}
110-
setFormState={setFormState}
11186
/>
11287

11388
<PluggableComponent
11489
id="build-email-form-schedule-section"
11590
as="communications-app-schedule-section"
116-
formState={formState}
117-
setFormState={setFormState}
11891
openTaskAlert={openTaskAlert}
11992
/>
12093

12194
</Form>
12295
</div>
12396
);
124-
}
97+
};
12598

12699
BuildEmailFormExtensible.defaultProps = {
127100
cohorts: [],
@@ -132,4 +105,4 @@ BuildEmailFormExtensible.propTypes = {
132105
cohorts: PropTypes.arrayOf(PropTypes.string),
133106
};
134107

135-
export default BuildEmailFormExtensible;
108+
export default withContextProvider(BuildEmailFormExtensible);

0 commit comments

Comments
 (0)