From ecfea5dfec257a78443f8395180aa39b140dcd30 Mon Sep 17 00:00:00 2001 From: Muhammad Afaq Shuaib <78806673+AfaqShuaib09@users.noreply.github.com> Date: Wed, 26 Jul 2023 22:10:07 +0500 Subject: [PATCH] feat: add watchers field to course edit form (#890) --- .../EditCoursePage/EditCourseForm.jsx | 31 ++++++++- .../EditCoursePage/EditCourseForm.test.jsx | 1 + .../EditCoursePage/EditCoursePage.test.jsx | 7 ++ .../EditCourseForm.test.jsx.snap | 47 +++++++++++++ .../EditCoursePage.test.jsx.snap | 68 +++++++++++++++++++ src/components/EditCoursePage/index.jsx | 5 ++ src/data/actions/courseInfo.js | 2 + src/data/actions/courseInfo.test.js | 14 ++-- src/utils/validation.js | 8 +++ src/utils/validation.test.js | 42 ++++++++++++ 10 files changed, 217 insertions(+), 8 deletions(-) diff --git a/src/components/EditCoursePage/EditCourseForm.jsx b/src/components/EditCoursePage/EditCourseForm.jsx index 4b982a10a..5e0279a05 100644 --- a/src/components/EditCoursePage/EditCourseForm.jsx +++ b/src/components/EditCoursePage/EditCourseForm.jsx @@ -28,12 +28,14 @@ import Collapsible from '../Collapsible'; import PriceList from '../PriceList'; import { - PUBLISHED, REVIEWED, EXECUTIVE_EDUCATION_SLUG, COURSE_URL_SLUG_VALIDATION_MESSAGE, + PUBLISHED, REVIEWED, EXECUTIVE_EDUCATION_SLUG, COURSE_URL_SLUG_VALIDATION_MESSAGE, REVIEW_BY_INTERNAL, } from '../../data/constants'; import { titleHelp, typeHelp, getUrlSlugHelp, productSourceHelp, } from '../../helpText'; -import { handleCourseEditFail, editCourseValidate, courseTagValidate } from '../../utils/validation'; +import { + handleCourseEditFail, editCourseValidate, courseTagValidate, emailValidate, +} from '../../utils/validation'; import { formatCollaboratorOptions, getDateWithSlashes, getFormattedUTCTimeString, getOptionsData, isPristine, parseCourseTypeOptions, parseOptions, loadOptions, courseTagObjectsToSelectOptions, getCourseUrlSlugPattern, @@ -370,6 +372,30 @@ export class BaseEditCourseForm extends React.Component { disabled={disabled || !administrator} optional /> + {administrator && ( + + A list of email addresses that will receive + notifications when the course run of the course is published or reviewed. +

+ )} + optional + /> + )} + isMulti + disabled={!(courseInfo?.data?.course_run_statuses?.includes(REVIEW_BY_INTERNAL) && administrator)} + optional + isCreatable + createOptionValidator={emailValidate} + /> + )}
{parsedProductSource}
@@ -1091,6 +1117,7 @@ export class BaseEditCourseForm extends React.Component { disabled={disabled} optional /> + {administrator && ( <> diff --git a/src/components/EditCoursePage/EditCourseForm.test.jsx b/src/components/EditCoursePage/EditCourseForm.test.jsx index 872e2660f..b6747cd96 100644 --- a/src/components/EditCoursePage/EditCourseForm.test.jsx +++ b/src/components/EditCoursePage/EditCourseForm.test.jsx @@ -40,6 +40,7 @@ describe('BaseEditCourseForm', () => { skill_names: [], organization_logo_override: 'http://image.src.small', organization_short_code_override: 'test short code', + watchers: ['test@test.com'], location_restriction: { restriction_type: 'allowlist', countries: ['AF', 'AX'], diff --git a/src/components/EditCoursePage/EditCoursePage.test.jsx b/src/components/EditCoursePage/EditCoursePage.test.jsx index 6ba863cc0..9903bf7cc 100644 --- a/src/components/EditCoursePage/EditCoursePage.test.jsx +++ b/src/components/EditCoursePage/EditCoursePage.test.jsx @@ -26,6 +26,7 @@ describe('EditCoursePage', () => { const defaultPrice = '77'; const defaultEnd = '2019-08-14T00:00:00Z'; const defaultUpgradeDeadlineOverride = '2019-09-14T00:00:00Z'; + const watchers = ['test@test.com']; const courseInfo = { data: { @@ -161,6 +162,8 @@ describe('EditCoursePage', () => { skill_names: [], organization_logo_override_url: 'http://image.src.small', organization_short_code_override: 'test short code', + watchers, + watchers_list: watchers?.length ? watchers.map(w => ({ label: w, value: w })) : null, location_restriction: { restriction_type: 'allowlist', countries: [ @@ -384,6 +387,8 @@ describe('EditCoursePage', () => { }, organization_logo_override_url: 'http://image.src.small', organization_short_code_override: 'test short code', + watchers, + watchers_list: watchers?.length ? watchers.map(w => ({ label: w, value: w })) : null, outcome: '

Stuff

', prerequisites_raw: '', prices: { @@ -426,6 +431,7 @@ describe('EditCoursePage', () => { }, organization_logo_override: 'http://image.src.small', organization_short_code_override: 'test short code', + watchers, outcome: '

Stuff

', prerequisites_raw: '', prices: { @@ -968,6 +974,7 @@ describe('EditCoursePage', () => { }, organization_logo_override: 'http://image.src.small', organization_short_code_override: 'test short code', + watchers, outcome: '

Stuff

', prerequisites_raw: '', prices: { diff --git a/src/components/EditCoursePage/__snapshots__/EditCourseForm.test.jsx.snap b/src/components/EditCoursePage/__snapshots__/EditCourseForm.test.jsx.snap index 3c545dfab..9337ca4cc 100644 --- a/src/components/EditCoursePage/__snapshots__/EditCourseForm.test.jsx.snap +++ b/src/components/EditCoursePage/__snapshots__/EditCourseForm.test.jsx.snap @@ -1700,6 +1700,9 @@ exports[`BaseEditCourseForm override slug format when IS_NEW_SLUG_FORMAT_ENABLED "type": "8a8f30e1-23ce-4ed3-a361-1325c656b67b", "uuid": "11111111-1111-1111-1111-111111111111", "videoSrc": "https://www.video.src/watch?v=fdsafd", + "watchers": Array [ + "test@test.com", + ], } } editable={false} @@ -3521,6 +3524,9 @@ exports[`BaseEditCourseForm renders correctly when submitting for review 1`] = ` "type": "8a8f30e1-23ce-4ed3-a361-1325c656b67b", "uuid": "11111111-1111-1111-1111-111111111111", "videoSrc": "https://www.video.src/watch?v=fdsafd", + "watchers": Array [ + "test@test.com", + ], } } editable={false} @@ -5342,6 +5348,9 @@ exports[`BaseEditCourseForm renders html correctly while submitting 1`] = ` "type": "8a8f30e1-23ce-4ed3-a361-1325c656b67b", "uuid": "11111111-1111-1111-1111-111111111111", "videoSrc": "https://www.video.src/watch?v=fdsafd", + "watchers": Array [ + "test@test.com", + ], } } editable={false} @@ -5396,6 +5405,9 @@ exports[`BaseEditCourseForm renders html correctly while submitting 1`] = ` "type": "8a8f30e1-23ce-4ed3-a361-1325c656b67b", "uuid": "11111111-1111-1111-1111-111111111111", "videoSrc": "https://www.video.src/watch?v=fdsafd", + "watchers": Array [ + "test@test.com", + ], } } isSubmittingForReview={false} @@ -5671,6 +5683,29 @@ exports[`BaseEditCourseForm renders html correctly with administrator being true pattern="^[a-z0-9_]+(?:-[a-z0-9_]+)*$" type="text" /> + + A list of email addresses that will receive notifications when the course run of the course is published or reviewed. +

+ } + id="watchers.label" + optional={true} + text="Watchers" + /> + } + name="watchers_list" + optional={true} + />
watcher.value) : [], outcome: courseData.outcome, prerequisites_raw: courseData.prerequisites_raw, ...priceData, @@ -618,6 +619,7 @@ class EditCoursePage extends React.Component { enterprise_subscription_inclusion, organization_short_code_override, organization_logo_override_url, + watchers, }, }, } = this.props; @@ -660,9 +662,12 @@ class EditCoursePage extends React.Component { enterprise_subscription_inclusion, organization_short_code_override, organization_logo_override_url, + // Adding watchers to initialValues so that it can be used in the form to show and update state of watchers_list + watchers, location_restriction: this.buildLocationRestriction(), in_year_value: this.buildInYearValue(), tags: topics?.length ? topics.map(t => ({ label: t, value: t })) : null, + watchers_list: watchers?.length ? watchers.map(w => ({ label: w, value: w })) : null, }; } diff --git a/src/data/actions/courseInfo.js b/src/data/actions/courseInfo.js index 3c28d84a0..d4ec6e450 100644 --- a/src/data/actions/courseInfo.js +++ b/src/data/actions/courseInfo.js @@ -112,6 +112,7 @@ function updateFormValuesAfterSave(change, currentFormValues, initialValues) { tags, url_slug: urlSlug, imageSrc: initialImageSrc, + watchers: courseWatchers, course_runs: initialCourseRuns, in_year_value: { per_lead_usa: perLeadUSA, @@ -127,6 +128,7 @@ function updateFormValuesAfterSave(change, currentFormValues, initialValues) { change('geoLocationLng', geoLocationLng); change('tags', tags); change('url_slug', urlSlug); + change('watchers', courseWatchers); change('in_year_value.per_lead_usa', perLeadUSA); change('in_year_value.per_lead_international', perLeadInternational); change('in_year_value.per_click_usa', perClickUSA); diff --git a/src/data/actions/courseInfo.test.js b/src/data/actions/courseInfo.test.js index 937650867..81a37d7ee 100644 --- a/src/data/actions/courseInfo.test.js +++ b/src/data/actions/courseInfo.test.js @@ -196,6 +196,7 @@ describe('courseInfo edit course actions', () => { geoLocationLng: '45.0000', geoLocationLat: '50.0000', url_slug: 'test_slug', + watchers: ['test@test.com'], tags: ['tag1', 'tag2'], in_year_value: { per_lead_usa: 10, @@ -218,12 +219,13 @@ describe('courseInfo edit course actions', () => { expect(changeMock).toHaveBeenNthCalledWith(3, 'geoLocationLng', '45.0000'); expect(changeMock).toHaveBeenNthCalledWith(4, 'tags', ['tag1', 'tag2']); expect(changeMock).toHaveBeenNthCalledWith(5, 'url_slug', 'test_slug'); - expect(changeMock).toHaveBeenNthCalledWith(6, 'in_year_value.per_lead_usa', 10); - expect(changeMock).toHaveBeenNthCalledWith(7, 'in_year_value.per_lead_international', 20); - expect(changeMock).toHaveBeenNthCalledWith(8, 'in_year_value.per_click_usa', 25); - expect(changeMock).toHaveBeenNthCalledWith(9, 'in_year_value.per_click_international', 30); - expect(changeMock).toHaveBeenNthCalledWith(10, 'course_runs[0].status', 'published'); - expect(changeMock).toHaveBeenNthCalledWith(11, 'course_runs[0].transcript_languages', ['en-us']); + expect(changeMock).toHaveBeenNthCalledWith(6, 'watchers', ['test@test.com']); + expect(changeMock).toHaveBeenNthCalledWith(7, 'in_year_value.per_lead_usa', 10); + expect(changeMock).toHaveBeenNthCalledWith(8, 'in_year_value.per_lead_international', 20); + expect(changeMock).toHaveBeenNthCalledWith(9, 'in_year_value.per_click_usa', 25); + expect(changeMock).toHaveBeenNthCalledWith(10, 'in_year_value.per_click_international', 30); + expect(changeMock).toHaveBeenNthCalledWith(11, 'course_runs[0].status', 'published'); + expect(changeMock).toHaveBeenNthCalledWith(12, 'course_runs[0].transcript_languages', ['en-us']); }); }); diff --git a/src/utils/validation.js b/src/utils/validation.js index 339309b55..0e6d53c4b 100644 --- a/src/utils/validation.js +++ b/src/utils/validation.js @@ -15,6 +15,13 @@ function courseTagValidate(tagValue, selectValue, options) { return ![...selectValue, ...options].some(x => x.label.toLowerCase() === tagValue.toLowerCase()); } +function emailValidate(emailValue, selectValue, options) { + // emailValue should only contain alphabets, numbers, hyphen, underscore, dot and @ + if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/i.test(emailValue)) { return false; } + // disallow emails that have already been selected or are present in options(dropdown) + return ![...selectValue, ...options].some(x => x.label.toLowerCase() === emailValue.toLowerCase()); +} + /** * Iterates through errors on a form and returns the first field name with an error. * @@ -198,4 +205,5 @@ export { handleStafferOrCreateFormFail, editCourseValidate, courseTagValidate, + emailValidate, }; diff --git a/src/utils/validation.test.js b/src/utils/validation.test.js index 97144c521..7c61649b0 100644 --- a/src/utils/validation.test.js +++ b/src/utils/validation.test.js @@ -3,6 +3,7 @@ import { basicValidate, getFieldName, editCourseValidate, + emailValidate, } from './validation'; import { PUBLISHED, UNPUBLISHED } from '../data/constants'; @@ -301,4 +302,45 @@ describe('editCourseValidate', () => { { ...expectedError, course_runs: [null] }, ); }); + + describe('emailValidate function', () => { + const selectValue = []; + const options = [{ label: 'john@example.com' }, { label: 'jane@example.com' }]; + + it('should return true for a valid email that is not present in selectValue or options', () => { + const emailValue = 'newuser@example.com'; + const isValid = emailValidate(emailValue, selectValue, options); + expect(isValid).toBe(true); + }); + + it('should return false for a valid email that is already present in selectValue', () => { + const emailValue = 'john@example.com'; + const isValid = emailValidate(emailValue, selectValue, options); + expect(isValid).toBe(false); + }); + + it('should return false for a valid email that is already present in options', () => { + const emailValue = 'jane@example.com'; + const isValid = emailValidate(emailValue, selectValue, options); + expect(isValid).toBe(false); + }); + + it('should return false for an invalid email format', () => { + const emailValue = 'invalidemailformat'; + const isValid = emailValidate(emailValue, selectValue, options); + expect(isValid).toBe(false); + }); + + it('should return false for a valid email format with invalid characters', () => { + const emailValue = 'user$example.com'; + const isValid = emailValidate(emailValue, selectValue, options); + expect(isValid).toBe(false); + }); + + it('should return false for an empty email value', () => { + const emailValue = ''; + const isValid = emailValidate(emailValue, selectValue, options); + expect(isValid).toBe(false); + }); + }); });