diff --git a/plugins/communications-app/BodyForm/index.jsx b/plugins/communications-app/BodyForm/index.jsx index ace9eb49..ad7f3027 100644 --- a/plugins/communications-app/BodyForm/index.jsx +++ b/plugins/communications-app/BodyForm/index.jsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Form } from '@edx/paragon'; +import { Form } from '@openedx/paragon'; import { useIntl } from '@edx/frontend-platform/i18n'; import TextEditor from '@communications-app/src/components/bulk-email-tool/text-editor/TextEditor'; import { useSelector, useDispatch } from '@communications-app/src/components/bulk-email-tool/bulk-email-form/BuildEmailFormExtensible/context'; diff --git a/plugins/communications-app/BodyForm/package.json b/plugins/communications-app/BodyForm/package.json index 0ea35414..21b5a755 100644 --- a/plugins/communications-app/BodyForm/package.json +++ b/plugins/communications-app/BodyForm/package.json @@ -8,7 +8,7 @@ "peerDependencies": { "@edx/frontend-app-communications": "*", "@edx/frontend-platform": "*", - "@edx/paragon": "*", + "@openedx/paragon": "*", "prop-types": "*", "react": "*" }, diff --git a/plugins/communications-app/CheckBoxForm/index.jsx b/plugins/communications-app/CheckBoxForm/index.jsx index 2ff8087c..c1918ca8 100644 --- a/plugins/communications-app/CheckBoxForm/index.jsx +++ b/plugins/communications-app/CheckBoxForm/index.jsx @@ -1,5 +1,5 @@ import PropTypes from 'prop-types'; -import { Form, Container } from '@edx/paragon'; +import { Form, Container } from '@openedx/paragon'; const CheckBoxForm = ({ isChecked, handleChange, label }) => ( diff --git a/plugins/communications-app/CheckBoxForm/package.json b/plugins/communications-app/CheckBoxForm/package.json index c975cc54..95f49097 100644 --- a/plugins/communications-app/CheckBoxForm/package.json +++ b/plugins/communications-app/CheckBoxForm/package.json @@ -8,7 +8,7 @@ "peerDependencies": { "@edx/frontend-app-communications": "*", "@edx/frontend-platform": "*", - "@edx/paragon": "*", + "@openedx/paragon": "*", "prop-types": "*", "react": "*" }, diff --git a/plugins/communications-app/InputForm/index.jsx b/plugins/communications-app/InputForm/index.jsx index d3ea0052..15a9f471 100644 --- a/plugins/communications-app/InputForm/index.jsx +++ b/plugins/communications-app/InputForm/index.jsx @@ -1,5 +1,5 @@ import PropTypes from 'prop-types'; -import { Form, Container } from '@edx/paragon'; +import { Form, Container } from '@openedx/paragon'; const InputForm = ({ isValid, diff --git a/plugins/communications-app/InputForm/package.json b/plugins/communications-app/InputForm/package.json index f2c24b38..93e5cd74 100644 --- a/plugins/communications-app/InputForm/package.json +++ b/plugins/communications-app/InputForm/package.json @@ -8,7 +8,7 @@ "peerDependencies": { "@edx/frontend-app-communications": "*", "@edx/frontend-platform": "*", - "@edx/paragon": "*", + "@openedx/paragon": "*", "prop-types": "*", "react": "*" }, diff --git a/plugins/communications-app/InstructionsProfreading/package.json b/plugins/communications-app/InstructionsProfreading/package.json index 128df89c..42c579fb 100644 --- a/plugins/communications-app/InstructionsProfreading/package.json +++ b/plugins/communications-app/InstructionsProfreading/package.json @@ -8,7 +8,7 @@ "peerDependencies": { "@edx/frontend-app-communications": "*", "@edx/frontend-platform": "*", - "@edx/paragon": "*", + "@openedx/paragon": "*", "prop-types": "*", "react": "*" }, diff --git a/plugins/communications-app/RecipientsForm/index.jsx b/plugins/communications-app/RecipientsForm/index.jsx index 4644cc94..6a9463d8 100644 --- a/plugins/communications-app/RecipientsForm/index.jsx +++ b/plugins/communications-app/RecipientsForm/index.jsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react'; import PropTypes from 'prop-types'; -import { Form } from '@edx/paragon'; +import { Form } from '@openedx/paragon'; import { FormattedMessage } from '@edx/frontend-platform/i18n'; import { useSelector, useDispatch } from '@communications-app/src/components/bulk-email-tool/bulk-email-form/BuildEmailFormExtensible/context'; import { actionCreators as formActions } from '@communications-app/src/components/bulk-email-tool/bulk-email-form/BuildEmailFormExtensible/context/reducer'; @@ -10,10 +10,11 @@ import './styles.scss'; const disableIsHasLearners = ['track', 'cohort']; const recipientsFormDescription = 'A selectable choice from a list of potential email recipients'; -const RecipientsForm = ({ cohorts: additionalCohorts }) => { +const RecipientsForm = ({ cohorts: additionalCohorts, courseModes }) => { const formData = useSelector((state) => state.form); const dispatch = useDispatch(); const { isEditMode, emailRecipients, isFormSubmitted } = formData; + const hasCourseModes = courseModes.length > 1; const [selectedGroups, setSelectedGroups] = useState([]); const hasAllLearnersSelected = selectedGroups.some((group) => group === 'learners'); @@ -91,6 +92,24 @@ const RecipientsForm = ({ cohorts: additionalCohorts }) => { description={recipientsFormDescription} /> + { + // additional modes + hasCourseModes + && courseModes.map((courseMode) => ( + + + + )) + } { // additional cohorts additionalCohorts @@ -152,10 +171,17 @@ const RecipientsForm = ({ cohorts: additionalCohorts }) => { RecipientsForm.defaultProps = { cohorts: [], + courseModes: [], }; RecipientsForm.propTypes = { cohorts: PropTypes.arrayOf(PropTypes.string), + courseModes: PropTypes.arrayOf( + PropTypes.shape({ + slug: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + }), + ), }; export default RecipientsForm; diff --git a/plugins/communications-app/RecipientsForm/package.json b/plugins/communications-app/RecipientsForm/package.json index 2b033cca..50f961e9 100644 --- a/plugins/communications-app/RecipientsForm/package.json +++ b/plugins/communications-app/RecipientsForm/package.json @@ -8,7 +8,7 @@ "peerDependencies": { "@edx/frontend-app-communications": "*", "@edx/frontend-platform": "*", - "@edx/paragon": "*", + "@openedx/paragon": "*", "prop-types": "*", "react": "*" }, diff --git a/plugins/communications-app/ScheduleSection/index.jsx b/plugins/communications-app/ScheduleSection/index.jsx index 05f87883..428b0305 100644 --- a/plugins/communications-app/ScheduleSection/index.jsx +++ b/plugins/communications-app/ScheduleSection/index.jsx @@ -7,14 +7,14 @@ import { Form, Icon, Toast, -} from '@edx/paragon'; +} from '@openedx/paragon'; import { SpinnerSimple, Cancel, Send, Event, Check, -} from '@edx/paragon/icons'; +} from '@openedx/paragon/icons'; import { getConfig } from '@edx/frontend-platform'; import { useIntl } from '@edx/frontend-platform/i18n'; import ScheduleEmailForm from '@communications-app/src/components/bulk-email-tool/bulk-email-form/ScheduleEmailForm'; diff --git a/plugins/communications-app/ScheduleSection/package.json b/plugins/communications-app/ScheduleSection/package.json index 01e75963..10593432 100644 --- a/plugins/communications-app/ScheduleSection/package.json +++ b/plugins/communications-app/ScheduleSection/package.json @@ -8,7 +8,7 @@ "peerDependencies": { "@edx/frontend-app-communications": "*", "@edx/frontend-platform": "*", - "@edx/paragon": "*", + "@openedx/paragon": "*", "prop-types": "*", "react": "*" }, diff --git a/plugins/communications-app/SubjectForm/index.jsx b/plugins/communications-app/SubjectForm/index.jsx index 36be7894..c7512ced 100644 --- a/plugins/communications-app/SubjectForm/index.jsx +++ b/plugins/communications-app/SubjectForm/index.jsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Form } from '@edx/paragon'; +import { Form } from '@openedx/paragon'; import { useIntl } from '@edx/frontend-platform/i18n'; import { useSelector, useDispatch } from '@communications-app/src/components/bulk-email-tool/bulk-email-form/BuildEmailFormExtensible/context'; import { actionCreators as formActions } from '@communications-app/src/components/bulk-email-tool/bulk-email-form/BuildEmailFormExtensible/context/reducer'; diff --git a/plugins/communications-app/SubjectForm/package.json b/plugins/communications-app/SubjectForm/package.json index a172927c..f72bc5eb 100644 --- a/plugins/communications-app/SubjectForm/package.json +++ b/plugins/communications-app/SubjectForm/package.json @@ -8,7 +8,7 @@ "peerDependencies": { "@edx/frontend-app-communications": "*", "@edx/frontend-platform": "*", - "@edx/paragon": "*", + "@openedx/paragon": "*", "prop-types": "*", "react": "*" }, diff --git a/plugins/communications-app/TaskAlertModalForm/package.json b/plugins/communications-app/TaskAlertModalForm/package.json index 9640863e..acbe905d 100644 --- a/plugins/communications-app/TaskAlertModalForm/package.json +++ b/plugins/communications-app/TaskAlertModalForm/package.json @@ -8,7 +8,7 @@ "peerDependencies": { "@edx/frontend-app-communications": "*", "@edx/frontend-platform": "*", - "@edx/paragon": "*", + "@openedx/paragon": "*", "prop-types": "*", "react": "*" }, diff --git a/plugins/communications-app/TestComponent/package.json b/plugins/communications-app/TestComponent/package.json index 185c4fa3..eec40ff0 100644 --- a/plugins/communications-app/TestComponent/package.json +++ b/plugins/communications-app/TestComponent/package.json @@ -8,7 +8,7 @@ "peerDependencies": { "@edx/frontend-app-communications": "*", "@edx/frontend-platform": "*", - "@edx/paragon": "*", + "@openedx/paragon": "*", "prop-types": "*", "react": "*" }, diff --git a/src/components/bulk-email-tool/BulkEmailTool.jsx b/src/components/bulk-email-tool/BulkEmailTool.jsx index a6fc44d5..ca9f15c7 100644 --- a/src/components/bulk-email-tool/BulkEmailTool.jsx +++ b/src/components/bulk-email-tool/BulkEmailTool.jsx @@ -34,9 +34,9 @@ export default function BulkEmailTool() {
-
diff --git a/src/components/bulk-email-tool/bulk-email-form/BuildEmailFormExtensible/index.jsx b/src/components/bulk-email-tool/bulk-email-form/BuildEmailFormExtensible/index.jsx index cac27fd3..c28b5cff 100644 --- a/src/components/bulk-email-tool/bulk-email-form/BuildEmailFormExtensible/index.jsx +++ b/src/components/bulk-email-tool/bulk-email-form/BuildEmailFormExtensible/index.jsx @@ -6,7 +6,7 @@ import { Form, Spinner, useToggle, -} from '@edx/paragon'; +} from '@openedx/paragon'; import { BulkEmailContext } from '../../bulk-email-context'; import useMobileResponsive from '../../../../utils/useMobileResponsive'; import PluggableComponent from '../../../PluggableComponent'; @@ -14,13 +14,14 @@ import PluggableComponent from '../../../PluggableComponent'; import { withContextProvider, useDispatch } from './context'; import { actionCreators as formActions } from './context/reducer'; -const BuildEmailFormExtensible = ({ courseId, cohorts }) => { +const BuildEmailFormExtensible = ({ courseId, cohorts, courseModes }) => { const isMobile = useMobileResponsive(); const [{ editor }] = useContext(BulkEmailContext); const [isTaskAlertOpen, openTaskAlert, closeTaskAlert] = useToggle(false); const dispatch = useDispatch(); useDeepCompareEffect(() => { + /* istanbul ignore next */ if (editor.editMode) { const newRecipientsValue = editor.emailRecipients; const newSubjectValue = editor.emailSubject; @@ -61,6 +62,7 @@ const BuildEmailFormExtensible = ({ courseId, cohorts }) => { as="communications-app-recipients-checks" cohorts={cohorts} courseId={courseId} + courseModes={courseModes} /> { BuildEmailFormExtensible.defaultProps = { cohorts: [], + courseModes: [], }; BuildEmailFormExtensible.propTypes = { courseId: PropTypes.string.isRequired, cohorts: PropTypes.arrayOf(PropTypes.string), + courseModes: PropTypes.arrayOf( + PropTypes.shape({ + slug: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + }), + ), }; export default withContextProvider(BuildEmailFormExtensible); diff --git a/src/components/bulk-email-tool/bulk-email-form/BulkEmailForm.jsx b/src/components/bulk-email-tool/bulk-email-form/BulkEmailForm.jsx deleted file mode 100644 index 0f8a77fe..00000000 --- a/src/components/bulk-email-tool/bulk-email-form/BulkEmailForm.jsx +++ /dev/null @@ -1,444 +0,0 @@ -/* eslint-disable react/no-unstable-nested-components */ -import React, { useContext, useEffect, useState } from 'react'; -import PropTypes from 'prop-types'; -import { - Button, - Form, Icon, StatefulButton, Toast, useToggle, - Card, -} from '@openedx/paragon'; -import { - SpinnerSimple, Cancel, Send, Event, Check, -} from '@openedx/paragon/icons'; -import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; -import classNames from 'classnames'; -import { getConfig } from '@edx/frontend-platform'; -import TextEditor from '../text-editor/TextEditor'; -import BulkEmailRecipient from './bulk-email-recipient'; -import TaskAlertModal from '../task-alert-modal'; -import useTimeout from '../../../utils/useTimeout'; -import PluggableComponent from '../../PluggableComponent'; -import useMobileResponsive from '../../../utils/useMobileResponsive'; -import ScheduleEmailForm from './ScheduleEmailForm'; -import messages from './messages'; -import { BulkEmailContext } from '../bulk-email-context'; -import { - addRecipient, - clearEditor, - clearErrorState, - handleEditorChange, - removeRecipient, -} from './data/actions'; -import { editScheduledEmailThunk, postBulkEmailThunk } from './data/thunks'; -import { getScheduledBulkEmailThunk } from '../bulk-email-task-manager/bulk-email-scheduled-emails-table/data/thunks'; - -import './bulkEmailForm.scss'; - -export const FORM_SUBMIT_STATES = { - DEFAULT: 'default', - PENDING: 'pending', - COMPLETE: 'complete', - COMPLETE_SCHEDULE: 'completed_schedule', - SCHEDULE: 'schedule', - RESCHEDULE: 'reschedule', - ERROR: 'error', -}; - -const FORM_ACTIONS = { - POST: 'POST', - PATCH: 'PATCH', -}; - -function BulkEmailForm(props) { - const { - courseId, - cohorts, - courseModes, - intl, - } = props; - const [{ editor }, dispatch] = useContext(BulkEmailContext); - const [emailFormStatus, setEmailFormStatus] = useState(FORM_SUBMIT_STATES.DEFAULT); - const [emailFormValidation, setEmailFormValidation] = useState({ - // set these as true on initialization, to prevent invalid messages from prematurely showing - subject: true, - body: true, - recipients: true, - schedule: true, - }); - const [isTaskAlertOpen, openTaskAlert, closeTaskAlert] = useToggle(false); - const [isScheduled, toggleScheduled] = useState(false); - const isMobile = useMobileResponsive(); - - /** - * Since we are working with both an old and new API endpoint, the body for the POST - * and the PATCH have different signatures. Therefore, based on the action required, we need to - * format the data properly to be accepted on the back end. - * @param {*} action "POST" or "PATCH" of the FORM_ACTIONS constant - * @returns formatted Data - */ - const formatDataForFormAction = (action) => { - if (action === FORM_ACTIONS.POST) { - const emailData = new FormData(); - emailData.append('action', 'send'); - emailData.append('send_to', JSON.stringify(editor.emailRecipients)); - emailData.append('subject', editor.emailSubject); - emailData.append('message', editor.emailBody); - if (isScheduled) { - emailData.append('schedule', new Date(`${editor.scheduleDate} ${editor.scheduleTime}`).toISOString()); - } - return emailData; - } - if (action === FORM_ACTIONS.PATCH) { - return { - email: { - targets: editor.emailRecipients, - subject: editor.emailSubject, - message: editor.emailBody, - id: editor.emailId, - }, - schedule: isScheduled ? new Date(`${editor.scheduleDate} ${editor.scheduleTime}`).toISOString() : null, - }; - } - return {}; - }; - - /** - * This function resets the form based on what state the form is currently in. Used after - * successfully sending or scheduling and email, or on error. - * - * @param {Boolean} error If true, resets just the state of the form, and not the editor. - * if false, reset the form completely, and wipe all email data form the form. - */ - const resetEmailForm = (error) => { - if (error) { - dispatch(clearErrorState()); - } else { - dispatch(clearEditor()); - } - }; - - /** - * Allows for a delayed form reset, to give the user time to process completion and error - * states before reseting the form. - */ - const delayedEmailFormReset = useTimeout( - () => resetEmailForm(editor.errorRetrievingData), - 3000, - ); - - const onFormChange = (event) => dispatch(handleEditorChange(event.target.name, event.target.value)); - - const onRecipientChange = (event) => { - if (event.target.checked) { - dispatch(addRecipient(event.target.value)); - // if "All Learners" is checked then we want to remove any cohorts, verified learners, and audit learners - if (event.target.value === 'learners') { - editor.emailRecipients.forEach(recipient => { - if (/^cohort/.test(recipient) || /^track/.test(recipient)) { - dispatch(removeRecipient(recipient)); - } - }); - } - } else { - dispatch(removeRecipient(event.target.value)); - } - }; - - const validateDateTime = (date, time) => { - if (isScheduled) { - const now = new Date(); - const newSchedule = new Date(`${editor.scheduleDate} ${editor.scheduleTime}`); - return !!date && !!time && newSchedule > now; - } - return true; - }; - - const validateEmailForm = () => { - const subjectValid = editor.emailSubject.length !== 0; - const bodyValid = editor.emailBody.length !== 0; - const recipientsValid = editor.emailRecipients.length !== 0; - const scheduleValid = validateDateTime(editor.scheduleDate, editor.scheduleTime); - setEmailFormValidation({ - subject: subjectValid, - recipients: recipientsValid, - body: bodyValid, - schedule: scheduleValid, - }); - return subjectValid && bodyValid && recipientsValid && scheduleValid; - }; - - const createEmailTask = async () => { - if (validateEmailForm()) { - if (editor.editMode) { - const editedEmail = formatDataForFormAction(FORM_ACTIONS.PATCH); - await dispatch(editScheduledEmailThunk(editedEmail, courseId, editor.schedulingId)); - } else { - const emailData = formatDataForFormAction(FORM_ACTIONS.POST); - await dispatch(postBulkEmailThunk(emailData, courseId)); - } - dispatch(getScheduledBulkEmailThunk(courseId, 1)); - } - }; - - /** - * State manager for the various states the form can be in at any given time. - * The states of the form are based off various pieces of the editor store, and - * calculates what state and whether to reset the form based on these booleans. - * Any time the form needs to change state, the conditions for that state change should - * placed here to prevent unecessary rerenders and implicit/flakey state update batching. - */ - useEffect(() => { - if (editor.isLoading) { - setEmailFormStatus(FORM_SUBMIT_STATES.PENDING); - return; - } - if (editor.errorRetrievingData) { - setEmailFormStatus(FORM_SUBMIT_STATES.ERROR); - delayedEmailFormReset(); - return; - } - if (editor.formComplete) { - if (isScheduled) { - setEmailFormStatus(FORM_SUBMIT_STATES.COMPLETE_SCHEDULE); - } else { - setEmailFormStatus(FORM_SUBMIT_STATES.COMPLETE); - } - delayedEmailFormReset(); - return; - } - if (editor.editMode === true) { - toggleScheduled(true); - setEmailFormStatus(FORM_SUBMIT_STATES.RESCHEDULE); - } else if (isScheduled) { - setEmailFormStatus(FORM_SUBMIT_STATES.SCHEDULE); - } else { - setEmailFormStatus(FORM_SUBMIT_STATES.DEFAULT); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isScheduled, editor.editMode, editor.isLoading, editor.errorRetrievingData, editor.formComplete]); - - const AlertMessage = () => ( - <> -

{intl.formatMessage(messages.bulkEmailTaskAlertRecipients, { subject: editor.emailSubject })}

- - {!isScheduled && ( -

- {intl.formatMessage(messages.bulkEmailInstructionsCaution)} - {intl.formatMessage(messages.bulkEmailInstructionsCautionMessage)} -

- )} - - ); - - const EditMessage = () => ( - <> -

- {intl.formatMessage(messages.bulkEmailTaskAlertEditingDate, { - dateTime: new Date(`${editor.scheduleDate} ${editor.scheduleTime}`).toLocaleString(), - })} -

-

- {intl.formatMessage(messages.bulkEmailTaskAlertEditingSubject, { - subject: editor.emailSubject, - })} -

-

{intl.formatMessage(messages.bulkEmailTaskAlertEditingTo)}

- -

{intl.formatMessage(messages.bulkEmailTaskAlertEditingWarning)}

- {!isScheduled && ( -

- {intl.formatMessage(messages.bulkEmailInstructionsCaution)} - {intl.formatMessage(messages.bulkEmailInstructionsCautionMessage)} -

- )} - - ); - - return ( -
- { - closeTaskAlert(); - if (event.target.name === 'continue') { - createEmailTask(); - } - }} - /> -
- {/* this will be pluggable */} -
-

Pluggable components

- -

Checkbox -default

-
- - -

Input -default

-
- {/* this will return default child if the plugin has not been installed */} - - - - -

Default Card

-
-

@openedx-plugins/communications-app-card

-
-
-
- - - - {intl.formatMessage(messages.bulkEmailSubjectLabel)} - - - {intl.formatMessage(messages.bulkEmailFormSubjectTip)} - - {!emailFormValidation.subject && ( - - {intl.formatMessage(messages.bulkEmailFormSubjectError)} - - )} - - - {intl.formatMessage(messages.bulkEmailBodyLabel)} - dispatch(handleEditorChange('emailBody', value))} value={editor.emailBody} /> - {!emailFormValidation.body && ( - - {intl.formatMessage(messages.bulkEmailFormBodyError)} - - )} - -
-

{intl.formatMessage(messages.bulkEmailInstructionsProofreading)}

-
- - {getConfig().SCHEDULE_EMAIL_SECTION && ( -
- toggleScheduled((prev) => !prev)} - disabled={emailFormStatus === FORM_SUBMIT_STATES.PENDING} - > - {intl.formatMessage(messages.bulkEmailFormScheduleBox)} - -
- )} - {isScheduled && ( - - )} -
- {editor.editMode && } - { - event.preventDefault(); - openTaskAlert(); - }} - state={emailFormStatus} - icons={{ - [FORM_SUBMIT_STATES.DEFAULT]: , - [FORM_SUBMIT_STATES.SCHEDULE]: , - [FORM_SUBMIT_STATES.RESCHEDULE]: , - [FORM_SUBMIT_STATES.PENDING]: , - [FORM_SUBMIT_STATES.COMPLETE]: , - [FORM_SUBMIT_STATES.COMPLETE_SCHEDULE]: , - [FORM_SUBMIT_STATES.ERROR]: , - }} - labels={{ - [FORM_SUBMIT_STATES.DEFAULT]: intl.formatMessage(messages.bulkEmailSubmitButtonDefault), - [FORM_SUBMIT_STATES.SCHEDULE]: intl.formatMessage(messages.bulkEmailSubmitButtonSchedule), - [FORM_SUBMIT_STATES.RESCHEDULE]: intl.formatMessage(messages.bulkEmailSubmitButtonReschedule), - [FORM_SUBMIT_STATES.PENDING]: intl.formatMessage(messages.bulkEmailSubmitButtonPending), - [FORM_SUBMIT_STATES.COMPLETE]: intl.formatMessage(messages.bulkEmailSubmitButtonComplete), - [FORM_SUBMIT_STATES.COMPLETE_SCHEDULE]: intl.formatMessage( - messages.bulkEmailSubmitButtonCompleteSchedule, - ), - [FORM_SUBMIT_STATES.ERROR]: intl.formatMessage(messages.bulkEmailSubmitButtonError), - }} - disabledStates={[ - FORM_SUBMIT_STATES.PENDING, - FORM_SUBMIT_STATES.COMPLETE, - FORM_SUBMIT_STATES.COMPLETE_SCHEDULE, - ]} - /> - resetEmailForm(emailFormStatus === FORM_SUBMIT_STATES.ERROR)} - > - {emailFormStatus === FORM_SUBMIT_STATES.ERROR && intl.formatMessage(messages.bulkEmailFormError)} - {emailFormStatus === FORM_SUBMIT_STATES.COMPLETE && intl.formatMessage(messages.bulkEmailFormSuccess)} - {emailFormStatus === FORM_SUBMIT_STATES.COMPLETE_SCHEDULE - && intl.formatMessage(messages.bulkEmailFormScheduledSuccess)} - -
-
- -
- ); -} - -BulkEmailForm.defaultProps = { - cohorts: [], -}; - -BulkEmailForm.propTypes = { - courseId: PropTypes.string.isRequired, - cohorts: PropTypes.arrayOf(PropTypes.string), - intl: intlShape.isRequired, - courseModes: PropTypes.arrayOf( - PropTypes.shape({ - slug: PropTypes.string.isRequired, - name: PropTypes.string.isRequired, - }), - ).isRequired, -}; - -export default injectIntl(BulkEmailForm); diff --git a/src/components/bulk-email-tool/bulk-email-form/bulk-email-recipient/BulkEmailRecipient.jsx b/src/components/bulk-email-tool/bulk-email-form/bulk-email-recipient/BulkEmailRecipient.jsx deleted file mode 100644 index d60b316b..00000000 --- a/src/components/bulk-email-tool/bulk-email-form/bulk-email-recipient/BulkEmailRecipient.jsx +++ /dev/null @@ -1,136 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { Form } from '@openedx/paragon'; -import { FormattedMessage } from '@edx/frontend-platform/i18n'; - -import './bulkEmailRecepient.scss'; - -const DEFAULT_GROUPS = { - SELF: 'myself', - STAFF: 'staff', - ALL_LEARNERS: 'learners', - VERIFIED: 'track:verified', - AUDIT: 'track:audit', -}; - -export default function BulkEmailRecipient(props) { - const { - handleCheckboxes, - selectedGroups, - additionalCohorts, - courseModes, - } = props; - const hasCourseModes = courseModes && courseModes.length > 1; - return ( - - - - - - - - - - - - - - { - // additional modes - hasCourseModes - && courseModes.map((courseMode) => ( - group === DEFAULT_GROUPS.ALL_LEARNERS)} - className="col col-lg-4 col-sm-6 col-12" - > - - - )) - } - { - // additional cohorts - additionalCohorts - && additionalCohorts.map((cohort) => ( - group === DEFAULT_GROUPS.ALL_LEARNERS)} - className="col col-lg-4 col-sm-6 col-12" - > - - - )) - } - - - - - {!props.isValid && ( - - - - )} - - ); -} - -BulkEmailRecipient.defaultProps = { - isValid: true, - additionalCohorts: [], -}; - -BulkEmailRecipient.propTypes = { - selectedGroups: PropTypes.arrayOf(PropTypes.string).isRequired, - handleCheckboxes: PropTypes.func.isRequired, - isValid: PropTypes.bool, - additionalCohorts: PropTypes.arrayOf(PropTypes.string), - courseModes: PropTypes.arrayOf( - PropTypes.shape({ - slug: PropTypes.string.isRequired, - name: PropTypes.string.isRequired, - }), - ).isRequired, -}; diff --git a/src/components/bulk-email-tool/bulk-email-form/bulk-email-recipient/bulkEmailRecepient.scss b/src/components/bulk-email-tool/bulk-email-form/bulk-email-recipient/bulkEmailRecepient.scss deleted file mode 100644 index 29206d35..00000000 --- a/src/components/bulk-email-tool/bulk-email-form/bulk-email-recipient/bulkEmailRecepient.scss +++ /dev/null @@ -1,8 +0,0 @@ -.recipient-groups { - > div { - padding-right: 0.5rem; - input { - padding: 0.5rem !important; - } - } -} diff --git a/src/components/bulk-email-tool/bulk-email-form/bulk-email-recipient/index.js b/src/components/bulk-email-tool/bulk-email-form/bulk-email-recipient/index.js deleted file mode 100644 index 788c581d..00000000 --- a/src/components/bulk-email-tool/bulk-email-form/bulk-email-recipient/index.js +++ /dev/null @@ -1,2 +0,0 @@ -// eslint-disable-next-line no-restricted-exports -export { default } from './BulkEmailRecipient'; diff --git a/src/components/bulk-email-tool/bulk-email-form/bulkEmailForm.scss b/src/components/bulk-email-tool/bulk-email-form/bulkEmailForm.scss deleted file mode 100644 index 24d868c2..00000000 --- a/src/components/bulk-email-tool/bulk-email-form/bulkEmailForm.scss +++ /dev/null @@ -1,10 +0,0 @@ -// Flip a leading icon to be a trailing icon -.send-email-btn { - > span { - flex-direction: row-reverse; - gap: 0.5rem; - > span { - margin: 0; - } - } -} diff --git a/src/components/bulk-email-tool/bulk-email-form/index.js b/src/components/bulk-email-tool/bulk-email-form/index.js deleted file mode 100644 index e91ac619..00000000 --- a/src/components/bulk-email-tool/bulk-email-form/index.js +++ /dev/null @@ -1,2 +0,0 @@ -// eslint-disable-next-line no-restricted-exports -export { default } from './BulkEmailForm'; diff --git a/src/components/bulk-email-tool/bulk-email-form/messages.js b/src/components/bulk-email-tool/bulk-email-form/messages.js deleted file mode 100644 index 0d0194a1..00000000 --- a/src/components/bulk-email-tool/bulk-email-form/messages.js +++ /dev/null @@ -1,124 +0,0 @@ -import { defineMessages } from '@edx/frontend-platform/i18n'; - -const messages = defineMessages({ - /* BulkEmailForm.jsx Messages */ - bulkEmailSubmitButtonDefault: { - id: 'bulk.email.submit.button.default', - defaultMessage: 'Send email', - }, - bulkEmailSubmitButtonSchedule: { - id: 'bulk.email.submit.button.schedule', - defaultMessage: 'Schedule Email', - }, - bulkEmailSubmitButtonPending: { - id: 'bulk.email.submit.button.pending', - defaultMessage: 'Submitting', - }, - bulkEmailSubmitButtonComplete: { - id: 'bulk.email.submit.button.send.complete', - defaultMessage: 'Email Created', - }, - bulkEmailSubmitButtonError: { - id: 'bulk.email.submit.button.error', - defaultMessage: 'Error', - }, - bulkEmailSubmitButtonCompleteSchedule: { - id: 'bulk.email.submit.button.schedule.complete', - defaultMessage: 'Scheduling Done', - }, - bulkEmailTaskAlertRecipients: { - id: 'bulk.email.task.alert.recipients', - defaultMessage: 'You are sending an email message with the subject {subject} to the following recipients:', - description: 'A warning shown to the user after submitting the email, to confirm the email recipients.', - }, - bulkEmailToolLabel: { - id: 'bulk.email.tool.label', - defaultMessage: 'Email', - description: 'Tool label. Describes the function of the tool (to send email).', - }, - bulkEmailSubjectLabel: { - id: 'bulk.email.subject.label', - defaultMessage: 'Subject', - description: 'Email subject line input label. Meant to have colon or equivilant punctuation.', - }, - bulkEmailFormSubjectTip: { - id: 'bulk.email.form.subject.tip', - defaultMessage: '(Maximum 128 characters)', - description: 'Default Subject tip', - }, - bulkEmailFormSubjectError: { - id: 'bulk.email.form.subject.error', - defaultMessage: 'A subject is required', - description: 'An Error message located under the subject line. Visible only on failure.', - }, - bulkEmailBodyLabel: { - id: 'bulk.email.body.label', - defaultMessage: 'Body', - description: 'Email Body label. Meant to have colon or equivilant punctuation.', - }, - bulkEmailFormBodyError: { - id: 'bulk.email.form.body.error', - defaultMessage: 'The message cannot be blank', - description: 'An error message located under the body editor. Visible only on failure.', - }, - bulkEmailInstructionsProofreading: { - id: 'bulk.email.instructions.proofreading', - defaultMessage: 'We recommend sending learners no more than one email message per week. Before you send your email, review the text carefully and send it to yourself first, so that you can preview the formatting and make sure embedded images and links work correctly.', - description: 'A set of instructions to give users a heads up about the formatting of the email they are about to send', - }, - bulkEmailInstructionsCaution: { id: 'bulk.email.instructions.caution', defaultMessage: 'Caution!' }, - - bulkEmailInstructionsCautionMessage: { - id: 'bulk.email.instructions.caution.message.new.email', - defaultMessage: - ' When you select Send Email, you are creating a new email message that is added to the queue for sending, and cannot be cancelled.', - description: 'A warning about how emails are sent out to users', - }, - bulkEmailFormScheduleBox: { - id: 'bulk.email.form.scheduleBox', - defaultMessage: 'Schedule this email for a future date', - description: 'Checkbox to schedule sending the email at a later date', - }, - bulkEmailSendEmailButton: { - id: 'bulk.email.send.email.button', - defaultMessage: 'Send Email', - description: 'Schedule/Send email button', - }, - bulkEmailFormError: { - id: 'bulk.email.form.error', - defaultMessage: 'An error occured while attempting to send the email.', - description: 'An Error message located under the submit button for the email form. Visible only on a failure.', - }, - bulkEmailFormSuccess: { - id: 'bilk.email.form.success', - defaultMessage: 'Email successfully created', - }, - bulkEmailFormScheduledSuccess: { - id: 'bulk.email.form.scheduled.success', - defaultMessage: 'Email successfully scheduled', - }, - bulkEmailSubmitButtonReschedule: { - id: 'bulk.email.submit.button.reschedule', - defaultMessage: 'Reschedule Email', - }, - bulkEmailTaskAlertEditingDate: { - id: 'bulk.email.task.alert.editing', - defaultMessage: 'You are editing a scheduled email to be sent on: {dateTime}', - description: 'This alert pops up before submitting when editing an email that has already been scheduled', - }, - bulkEmailTaskAlertEditingSubject: { - id: 'bulk.email.task.alert.subject', - defaultMessage: 'with the subject: {subject}', - }, - bulkEmailTaskAlertEditingTo: { - id: 'bulk.email.task.alert.to', - defaultMessage: 'to recipients:', - }, - bulkEmailTaskAlertEditingWarning: { - id: 'bulk.email.task.alert.warning', - defaultMessage: 'This will not create a new scheduled email task and instead overwrite the one currently selected. Do you want to overwrite this scheduled email?', - description: 'This alert pops up before submitting when editing an email that has already been scheduled', - }, -}); - -export default messages; diff --git a/src/components/bulk-email-tool/bulk-email-form/test/BulkEmailForm.test.jsx b/src/components/bulk-email-tool/bulk-email-form/test/BulkEmailForm.test.jsx deleted file mode 100644 index 2bf89a3a..00000000 --- a/src/components/bulk-email-tool/bulk-email-form/test/BulkEmailForm.test.jsx +++ /dev/null @@ -1,180 +0,0 @@ -/** - * @jest-environment jsdom - */ -import React from 'react'; -import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; -import MockAdapter from 'axios-mock-adapter'; -import { - render, screen, cleanup, fireEvent, initializeMockApp, getConfig, -} from '../../../../setupTest'; -import BulkEmailForm from '..'; -import * as bulkEmailFormApi from '../data/api'; -import { BulkEmailContext, BulkEmailProvider } from '../../bulk-email-context'; -import { formatDate } from '../../../../utils/formatDateAndTime'; -import cohortFactory from '../data/__factories__/bulkEmailFormCohort.factory'; -import courseModeFactory from '../data/__factories__/bulkEmailFormCourseMode.factory'; - -jest.mock('../../text-editor/TextEditor'); -jest.mock('../../../PluggableComponent', () => () => null); - -const appendMock = jest.spyOn(FormData.prototype, 'append'); -const dispatchMock = jest.fn(); - -const tomorrow = new Date(); -tomorrow.setDate(new Date().getDate() + 1); -const courseMode = courseModeFactory(); - -function renderBulkEmailForm() { - const { cohorts } = cohortFactory.build(); - return ( - - - - ); -} - -function renderBulkEmailFormContext(value) { - return ( - - - - ); -} - -describe('bulk-email-form', () => { - beforeAll(async () => { - await initializeMockApp(); - }); - beforeEach(() => jest.resetModules()); - afterEach(() => cleanup()); - test('it renders', () => { - render(renderBulkEmailForm()); - expect(screen.getByText('Send email')).toBeTruthy(); - }); - test('it shows a warning when clicking submit', async () => { - render(renderBulkEmailForm()); - fireEvent.click(screen.getByText('Send email')); - const warning = await screen.findByText('CAUTION!', { exact: false }); - expect(warning).toBeTruthy(); - }); - test('Prevent form POST if invalid', async () => { - render(renderBulkEmailForm()); - fireEvent.click(screen.getByText('Send email')); - expect(await screen.findByRole('button', { name: /continue/i })).toBeInTheDocument(); - fireEvent.click(screen.getByRole('button', { name: /continue/i })); - expect(await screen.findByText('At least one recipient is required', { exact: false })).toBeInTheDocument(); - expect(await screen.findByText('A subject is required')).toBeInTheDocument(); - }); - test('Shows complete message on completed POST', async () => { - const axiosMock = new MockAdapter(getAuthenticatedHttpClient()); - axiosMock.onPost().reply(200, { - course_id: 'test', - success: true, - }); - render(renderBulkEmailForm()); - fireEvent.click(screen.getByRole('checkbox', { name: 'Myself' })); - expect(screen.getByRole('checkbox', { name: 'Myself' })).toBeChecked(); - fireEvent.change(screen.getByRole('textbox', { name: 'Subject' }), { target: { value: 'test subject' } }); - fireEvent.change(screen.getByTestId('textEditor'), { target: { value: 'test body' } }); - fireEvent.click(screen.getByText('Send email')); - expect(await screen.findByRole('button', { name: /continue/i })).toBeInTheDocument(); - fireEvent.click(screen.getByRole('button', { name: /continue/i })); - expect(await screen.findByText('Submitting')).toBeInTheDocument(); - expect(await screen.findByText('Email Created')).toBeInTheDocument(); - }); - test('Shows Error on failed POST', async () => { - const axiosMock = new MockAdapter(getAuthenticatedHttpClient()); - axiosMock.onPost(`${getConfig().LMS_BASE_URL}/courses/test/instructor/api/send_email`).reply(500); - render(renderBulkEmailForm()); - const subjectLine = screen.getByRole('textbox', { name: 'Subject' }); - const recipient = screen.getByRole('checkbox', { name: 'Myself' }); - fireEvent.click(recipient); - fireEvent.change(subjectLine, { target: { value: 'test subject' } }); - fireEvent.change(screen.getByTestId('textEditor'), { target: { value: 'test body' } }); - fireEvent.click(screen.getByText('Send email')); - expect(await screen.findByRole('button', { name: /continue/i })).toBeInTheDocument(); - fireEvent.click(await screen.findByRole('button', { name: /continue/i })); - expect(await screen.findByText('An error occured while attempting to send the email.')).toBeInTheDocument(); - }); - test('Checking "All Learners" disables each learner group', async () => { - render(renderBulkEmailForm()); - fireEvent.click(screen.getByRole('checkbox', { name: 'All Learners' })); - const verifiedLearners = screen.getByRole('checkbox', { name: 'Learners in the Verified Certificate Track' }); - const auditLearners = screen.getByRole('checkbox', { name: 'Learners in the Audit Track' }); - const { cohorts } = cohortFactory.build(); - cohorts.forEach(cohort => expect(screen.getByRole('checkbox', { name: `Cohort: ${cohort}` })).toBeDisabled()); - expect(verifiedLearners).toBeDisabled(); - expect(auditLearners).toBeDisabled(); - }); - test('Shows scheduling form when checkbox is checked and submit is changed', async () => { - render(renderBulkEmailForm()); - const scheduleCheckbox = screen.getByText('Schedule this email for a future date'); - fireEvent.click(scheduleCheckbox); - expect(screen.getByText('Send time')); - expect(screen.getByText('Send date')); - expect(screen.getByText('Schedule Email')); - }); - test('Prevents sending email when scheduling inputs are empty', async () => { - render(renderBulkEmailForm()); - const scheduleCheckbox = screen.getByText('Schedule this email for a future date'); - fireEvent.click(scheduleCheckbox); - const submitButton = await screen.findByText('Schedule Email'); - fireEvent.click(submitButton); - const continueButton = await screen.findByRole('button', { name: /continue/i }); - fireEvent.click(continueButton); - expect(screen.getByText('Date and time cannot be blank, and must be a date in the future')); - }); - test('Adds scheduling data to POST requests when schedule is selected', async () => { - const postBulkEmailInstructorTask = jest.spyOn(bulkEmailFormApi, 'postBulkEmailInstructorTask'); - render(renderBulkEmailForm()); - fireEvent.click(screen.getByRole('checkbox', { name: 'Myself' })); - fireEvent.change(screen.getByRole('textbox', { name: 'Subject' }), { target: { value: 'test subject' } }); - fireEvent.change(screen.getByTestId('textEditor'), { target: { value: 'test body' } }); - const scheduleCheckbox = screen.getByText('Schedule this email for a future date'); - fireEvent.click(scheduleCheckbox); - const submitButton = screen.getByText('Schedule Email'); - const scheduleDate = screen.getByTestId('scheduleDate'); - const scheduleTime = screen.getByTestId('scheduleTime'); - fireEvent.change(scheduleDate, { target: { value: formatDate(tomorrow) } }); - fireEvent.change(scheduleTime, { target: { value: '10:00' } }); - fireEvent.click(submitButton); - const continueButton = await screen.findByRole('button', { name: /continue/i }); - fireEvent.click(continueButton); - expect(appendMock).toHaveBeenCalledWith('schedule', expect.stringContaining(formatDate(tomorrow))); - expect(postBulkEmailInstructorTask).toHaveBeenCalledWith(expect.any(FormData), expect.stringContaining('test')); - }); - test('will PATCH instead of POST when in edit mode', async () => { - const axiosMock = new MockAdapter(getAuthenticatedHttpClient()); - axiosMock.onPatch().reply(200); - render( - renderBulkEmailFormContext({ - editor: { - editMode: true, - emailBody: 'test', - emailSubject: 'test', - emailRecipients: ['test'], - scheduleDate: formatDate(tomorrow), - scheduleTime: '10:00', - schedulingId: 1, - emailId: 1, - isLoading: false, - errorRetrievingData: false, - }, - }), - ); - const submitButton = screen.getByText('Reschedule Email'); - fireEvent.click(submitButton); - expect( - await screen.findByText( - 'This will not create a new scheduled email task and instead overwrite the one currently selected. Do you want to overwrite this scheduled email?', - ), - ).toBeInTheDocument(); - const continueButton = await screen.findByRole('button', { name: /continue/i }); - fireEvent.click(continueButton); - expect(dispatchMock).toHaveBeenCalled(); - }); -});