diff --git a/src/account-settings/AccountSettingsPage.jsx b/src/account-settings/AccountSettingsPage.jsx index 1d1b813da..a6b335225 100644 --- a/src/account-settings/AccountSettingsPage.jsx +++ b/src/account-settings/AccountSettingsPage.jsx @@ -50,6 +50,7 @@ import { } from './data/constants'; import { fetchSiteLanguages } from './site-language'; import { fetchCourseList } from '../notification-preferences/data/thunks'; +import NotificationSettings from '../notification-preferences/NotificationSettings'; import { withLocation, withNavigate } from './hoc'; class AccountSettingsPage extends React.Component { @@ -732,7 +733,7 @@ class AccountSettingsPage extends React.Component { {...editableFieldProps} /> -
+

{this.props.intl.formatMessage(messages['account.settings.section.social.media'])}

@@ -768,8 +769,9 @@ class AccountSettingsPage extends React.Component { {...editableFieldProps} />
- -
+
+ +

{this.props.intl.formatMessage(messages['account.settings.section.site.preferences'])}

diff --git a/src/account-settings/JumpNav.jsx b/src/account-settings/JumpNav.jsx index 7251fcdae..fda2973ce 100644 --- a/src/account-settings/JumpNav.jsx +++ b/src/account-settings/JumpNav.jsx @@ -1,21 +1,16 @@ import { getConfig } from '@edx/frontend-platform'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; -import { breakpoints, useWindowSize, Icon } from '@openedx/paragon'; -import { OpenInNew } from '@openedx/paragon/icons'; +import { breakpoints, useWindowSize } from '@openedx/paragon'; import classNames from 'classnames'; import React from 'react'; -import { useSelector } from 'react-redux'; import { NavHashLink } from 'react-router-hash-link'; import Scrollspy from 'react-scrollspy'; -import { Link } from 'react-router-dom'; import messages from './AccountSettingsPage.messages'; -import { selectShowPreferences } from '../notification-preferences/data/selectors'; const JumpNav = ({ intl, }) => { const stickToTop = useWindowSize().width > breakpoints.small.minWidth; - const showPreferences = useSelector(selectShowPreferences()); return (
@@ -65,21 +60,6 @@ const JumpNav = ({ )} - {showPreferences && ( - <> -
- -
  • - - {intl.formatMessage(messages['notification.preferences.notifications.label'])} - - -
  • -
    - - )}
    ); }; diff --git a/src/account-settings/test/AccountSettingsPage.test.jsx b/src/account-settings/test/AccountSettingsPage.test.jsx index 5acd4c563..385a27d17 100644 --- a/src/account-settings/test/AccountSettingsPage.test.jsx +++ b/src/account-settings/test/AccountSettingsPage.test.jsx @@ -71,6 +71,16 @@ describe('AccountSettingsPage', () => { afterEach(() => jest.clearAllMocks()); + beforeAll(() => { + global.lightningjs = { + require: jest.fn().mockImplementation((module, url) => ({ moduleName: module, url })), + }; + }); + + afterAll(() => { + delete global.lightningjs; + }); + it('renders AccountSettingsPage correctly with editing enabled', async () => { const { getByText, rerender, getByLabelText } = render(reduxWrapper()); diff --git a/src/account-settings/test/mockData.js b/src/account-settings/test/mockData.js index 6c102fe08..cccfb7ce5 100644 --- a/src/account-settings/test/mockData.js +++ b/src/account-settings/test/mockData.js @@ -84,7 +84,7 @@ const mockData = { profileDataManager: null, }, notificationPreferences: { - showPreferences: false, + showPreferences: true, courses: { status: 'success', courses: [], @@ -98,7 +98,7 @@ const mockData = { preferences: { status: 'idle', updatePreferenceStatus: 'idle', - selectedCourse: null, + selectedCourse: 'account', preferences: [], apps: [], nonEditable: {}, diff --git a/src/divider/Divider.jsx b/src/divider/Divider.jsx new file mode 100644 index 000000000..6b75eff3d --- /dev/null +++ b/src/divider/Divider.jsx @@ -0,0 +1,16 @@ +import PropTypes from 'prop-types'; +import classNames from 'classnames'; + +const Divider = ({ className, ...props }) => ( +
    +); + +Divider.propTypes = { + className: PropTypes.string, +}; + +Divider.defaultProps = { + className: undefined, +}; + +export default Divider; diff --git a/src/divider/index.jsx b/src/divider/index.jsx new file mode 100644 index 000000000..ca4fc1636 --- /dev/null +++ b/src/divider/index.jsx @@ -0,0 +1,2 @@ +// eslint-disable-next-line import/prefer-default-export +export { default as Divider } from './Divider'; diff --git a/src/index.jsx b/src/index.jsx index 4c23e2a82..7b4c4e82a 100755 --- a/src/index.jsx +++ b/src/index.jsx @@ -20,8 +20,6 @@ import messages from './i18n'; import './index.scss'; import Head from './head/Head'; -import NotificationCourses from './notification-preferences/NotificationCourses'; -import NotificationPreferences from './notification-preferences/NotificationPreferences'; subscribe(APP_READY, () => { ReactDOM.render( @@ -38,8 +36,6 @@ subscribe(APP_READY, () => {
    )} > - } /> - } /> } diff --git a/src/index.scss b/src/index.scss index 1518f5680..7a4d985c0 100755 --- a/src/index.scss +++ b/src/index.scss @@ -118,7 +118,7 @@ $fa-font-path: "~font-awesome/fonts"; } .dropdown-item:active, - .dropdown-item:focus, + .dropdown-item:focus, .btn-tertiary:not(:disabled):not(.disabled).active { background-color: $light-300 !important; } @@ -131,6 +131,20 @@ $fa-font-path: "~font-awesome/fonts"; .h-4\.5 { height: 36px; } + + .course-dropdown{ + #course-dropdown-btn { + width: 100%; + font-size: 14px !important; + padding-top: 10px !important; + padding-bottom: 10px !important; + border: 1px solid $light-500 !important; + } + + .dropdown-item { + font-size: 14px !important; + } + } } .usabilla_live_button_container { diff --git a/src/notification-preferences/NotificationCourses.jsx b/src/notification-preferences/NotificationCourses.jsx deleted file mode 100644 index 2020d5fff..000000000 --- a/src/notification-preferences/NotificationCourses.jsx +++ /dev/null @@ -1,85 +0,0 @@ -import React, { useCallback, useEffect } from 'react'; - -import { Link } from 'react-router-dom'; -import { useDispatch, useSelector } from 'react-redux'; - -import { ArrowForwardIos } from '@openedx/paragon/icons'; -import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; -import { - Button, Container, Icon, Spinner, -} from '@openedx/paragon'; - -import messages from './messages'; -import { useFeedbackWrapper } from '../hooks'; -import { fetchCourseList } from './data/thunks'; -import { NotFoundPage } from '../account-settings'; -import { IDLE_STATUS, LOADING_STATUS, SUCCESS_STATUS } from '../constants'; -import { selectCourseList, selectCourseListStatus, selectPagination } from './data/selectors'; - -const NotificationCourses = ({ intl }) => { - useFeedbackWrapper(); - const dispatch = useDispatch(); - const coursesList = useSelector(selectCourseList()); - const courseListStatus = useSelector(selectCourseListStatus()); - const { hasMore, currentPage } = useSelector(selectPagination()); - - const loadMore = useCallback((page = 1, pageSize = 10) => { - dispatch(fetchCourseList(page, pageSize)); - }, [dispatch]); - - useEffect(() => { - if (courseListStatus === IDLE_STATUS) { loadMore(); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - if (courseListStatus === SUCCESS_STATUS && coursesList.length === 0) { - return ; - } - - return ( - -

    - {intl.formatMessage(messages.notificationHeading)} -

    -
    - {coursesList.map(course => ( - -
    - - {course.name} - - - - -
    - - ))} -
    - {courseListStatus === LOADING_STATUS ? ( -
    - -
    - ) : hasMore && ( - - )} -
    - ); -}; - -NotificationCourses.propTypes = { - intl: intlShape.isRequired, -}; - -export default injectIntl(NotificationCourses); diff --git a/src/notification-preferences/NotificationCourses.test.jsx b/src/notification-preferences/NotificationCourses.test.jsx deleted file mode 100644 index d5cf32641..000000000 --- a/src/notification-preferences/NotificationCourses.test.jsx +++ /dev/null @@ -1,97 +0,0 @@ -/* eslint-disable no-import-assign */ -import { Provider } from 'react-redux'; -import configureStore from 'redux-mock-store'; -import { BrowserRouter as Router } from 'react-router-dom'; - -import * as auth from '@edx/frontend-platform/auth'; -import { render, screen } from '@testing-library/react'; -import { IntlProvider } from '@edx/frontend-platform/i18n'; - -import { defaultState } from './data/reducers'; -import NotificationCourses from './NotificationCourses'; -import { LOADING_STATUS, SUCCESS_STATUS } from '../constants'; - -const mockStore = configureStore(); - -jest.mock('@edx/frontend-platform/auth'); - -const courseList = [ - { id: 'course-id-1', name: 'Course Name 1' }, - { id: 'course-id-2', name: 'Course Name 2' }, - { id: 'course-id-3', name: 'Course Name 3' }, -]; - -const setupStore = (override = {}) => { - const storeState = defaultState; - storeState.courses = { - ...storeState.courses, - ...override, - }; - const store = mockStore({ - notificationPreferences: storeState, - }); - return store; -}; - -const renderComponent = (store = {}) => ( - render( - - - - - - - , - ) -); - -describe('Notification Courses', () => { - let store; - beforeEach(() => { - store = setupStore({ - courses: courseList, - status: SUCCESS_STATUS, - pagination: { - count: 3, - currentPage: 1, - hasMore: false, - totalPages: 1, - }, - }); - - auth.getAuthenticatedHttpClient = jest.fn(() => ({ - patch: async () => ({ - data: { status: 200 }, - catch: () => {}, - }), - })); - auth.getAuthenticatedUser = jest.fn(() => ({ userId: 3 })); - window.lightningjs = null; - }); - - afterEach(() => jest.clearAllMocks()); - - it('tests if all courses are available', async () => { - await renderComponent(store); - expect(screen.queryByTestId('courses-list').children).toHaveLength(3); - }); - - it('show spinner if api call is in progress', async () => { - store = setupStore({ status: LOADING_STATUS }); - await renderComponent(store); - expect(screen.queryByTestId('loading-spinner')).toBeInTheDocument(); - }); - - it('show not found page if course list is empty', async () => { - store = setupStore({ status: SUCCESS_STATUS, courses: [] }); - await renderComponent(store); - expect(screen.queryByTestId('not-found-page')).toBeInTheDocument(); - }); - - it('show load more courses button when hasMore True', async () => { - store = setupStore({ status: SUCCESS_STATUS, pagination: { ...store.pagination, hasMore: true, totalPages: 2 } }); - await renderComponent(store); - - expect(screen.queryByText('Load more courses')).toBeInTheDocument(); - }); -}); diff --git a/src/notification-preferences/NotificationCoursesDropdown.jsx b/src/notification-preferences/NotificationCoursesDropdown.jsx new file mode 100644 index 000000000..22f375650 --- /dev/null +++ b/src/notification-preferences/NotificationCoursesDropdown.jsx @@ -0,0 +1,73 @@ +import React, { useCallback, useEffect, useMemo } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; + +import { useIntl } from '@edx/frontend-platform/i18n'; +import { Dropdown } from '@openedx/paragon'; + +import { IDLE_STATUS, SUCCESS_STATUS } from '../constants'; +import { selectCourseList, selectCourseListStatus, selectSelectedCourseId } from './data/selectors'; +import { fetchCourseList, setSelectedCourse } from './data/thunks'; +import messages from './messages'; + +const NotificationCoursesDropdown = () => { + const intl = useIntl(); + const dispatch = useDispatch(); + const coursesList = useSelector(selectCourseList()); + const courseListStatus = useSelector(selectCourseListStatus()); + const selectedCourseId = useSelector(selectSelectedCourseId()); + const selectedCourse = useMemo( + () => coursesList.find((course) => course.id === selectedCourseId), + [coursesList, selectedCourseId], + ); + + const handleCourseSelection = useCallback((courseId) => { + dispatch(setSelectedCourse(courseId)); + }, [dispatch]); + + const fetchCourses = useCallback((page = 1, pageSize = 99999) => { + dispatch(fetchCourseList(page, pageSize)); + }, [dispatch]); + + useEffect(() => { + if (courseListStatus === IDLE_STATUS) { + fetchCourses(); + } + }, [courseListStatus, fetchCourses]); + + return ( + courseListStatus === SUCCESS_STATUS && ( +
    +
    {intl.formatMessage(messages.notificationDropdownlabel)}
    + + + {selectedCourse?.name} + + + {coursesList.map((course) => ( + + {course.name} + + ))} + + + + {selectedCourse?.name === 'Account' + ? intl.formatMessage(messages.notificationDropdownApplies) + : intl.formatMessage(messages.notificationCourseDropdownApplies)} + +
    + ) + ); +}; + +export default NotificationCoursesDropdown; diff --git a/src/notification-preferences/NotificationPreferenceApp.jsx b/src/notification-preferences/NotificationPreferenceApp.jsx index 2796232d8..52d7d217a 100644 --- a/src/notification-preferences/NotificationPreferenceApp.jsx +++ b/src/notification-preferences/NotificationPreferenceApp.jsx @@ -11,27 +11,22 @@ import { useIsOnMobile } from '../hooks'; import NotificationTypes from './NotificationTypes'; import { notificationChannels, shouldHideAppPreferences } from './data/utils'; import NotificationPreferenceColumn from './NotificationPreferenceColumn'; -import { selectPreferenceAppToggleValue, selectSelectedCourseId, selectAppPreferences } from './data/selectors'; +import { selectPreferenceAppToggleValue, selectAppPreferences } from './data/selectors'; const NotificationPreferenceApp = ({ appId }) => { const intl = useIntl(); - const courseId = useSelector(selectSelectedCourseId()); const appToggle = useSelector(selectPreferenceAppToggleValue(appId)); const appPreferences = useSelector(selectAppPreferences(appId)); const mobileView = useIsOnMobile(); const NOTIFICATION_CHANNELS = notificationChannels(); const hideAppPreferences = shouldHideAppPreferences(appPreferences, appId) || false; - if (!courseId) { - return null; - } - return ( !hideAppPreferences && (
    diff --git a/src/notification-preferences/NotificationPreferenceColumn.jsx b/src/notification-preferences/NotificationPreferenceColumn.jsx index 32a3d7fd7..7607ee632 100644 --- a/src/notification-preferences/NotificationPreferenceColumn.jsx +++ b/src/notification-preferences/NotificationPreferenceColumn.jsx @@ -28,7 +28,9 @@ const NotificationPreferenceColumn = ({ appId, channel, appPreference }) => { const onToggle = useCallback((event, notificationType) => { const { name: notificationChannel } = event.target; - const value = notificationChannel === 'email_cadence' ? event.target.innerText : event.target.checked; + const appNotificationPreference = appPreferences.find(preference => preference.id === notificationType); + const value = notificationChannel === 'email_cadence' && courseId ? event.target.innerText : event.target.checked; + const emailCadence = notificationChannel === 'email_cadence' ? event.target.innerText : appNotificationPreference.emailCadence; dispatch(updatePreferenceToggle( courseId, @@ -36,9 +38,10 @@ const NotificationPreferenceColumn = ({ appId, channel, appPreference }) => { notificationType, notificationChannel, value, + emailCadence !== 'Mixed' ? emailCadence : undefined, )); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [appId]); + }, [appId, appPreferences]); const renderPreference = (preference) => ( (preference?.coreNotificationTypes?.length > 0 || preference.id !== 'core') && ( diff --git a/src/notification-preferences/NotificationPreferences.jsx b/src/notification-preferences/NotificationPreferences.jsx index 9402cd3a9..46cbc87a4 100644 --- a/src/notification-preferences/NotificationPreferences.jsx +++ b/src/notification-preferences/NotificationPreferences.jsx @@ -1,35 +1,26 @@ import React, { useEffect, useMemo } from 'react'; -import { Link, useParams } from 'react-router-dom'; import { useDispatch, useSelector } from 'react-redux'; import classNames from 'classnames'; -import { ArrowBack } from '@openedx/paragon/icons'; import { useIntl } from '@edx/frontend-platform/i18n'; -import { - Container, Hyperlink, Icon, Spinner, NavItem, -} from '@openedx/paragon'; +import { Spinner, NavItem } from '@openedx/paragon'; import { useIsOnMobile } from '../hooks'; import messages from './messages'; -import { NotFoundPage } from '../account-settings'; import NotificationPreferenceApp from './NotificationPreferenceApp'; -import { fetchCourseList, fetchCourseNotificationPreferences } from './data/thunks'; -import { - FAILURE_STATUS, IDLE_STATUS, LOADING_STATUS, SUCCESS_STATUS, -} from '../constants'; +import { fetchCourseNotificationPreferences } from './data/thunks'; +import { LOADING_STATUS } from '../constants'; import { - selectCourse, selectCourseList, selectCourseListStatus, selectNotificationPreferencesStatus, selectPreferenceAppsId, + selectCourseListStatus, selectNotificationPreferencesStatus, selectPreferenceAppsId, selectSelectedCourseId, } from './data/selectors'; import { notificationChannels } from './data/utils'; const NotificationPreferences = () => { - const { courseId } = useParams(); const dispatch = useDispatch(); const intl = useIntl(); const courseStatus = useSelector(selectCourseListStatus()); - const coursesList = useSelector(selectCourseList()); - const course = useSelector(selectCourse(courseId)); + const courseId = useSelector(selectSelectedCourseId()); const notificationStatus = useSelector(selectNotificationPreferencesStatus()); const preferenceAppsIds = useSelector(selectPreferenceAppsId()); const mobileView = useIsOnMobile(); @@ -43,46 +34,16 @@ const NotificationPreferences = () => { ), [preferenceAppsIds]); useEffect(() => { - if ([IDLE_STATUS, FAILURE_STATUS].includes(courseStatus)) { - dispatch(fetchCourseList()); - } dispatch(fetchCourseNotificationPreferences(courseId)); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [courseId]); + }, [courseId, dispatch]); - if ( - (courseStatus === SUCCESS_STATUS && coursesList.length === 0) - || (notificationStatus === FAILURE_STATUS && coursesList.length !== 0) - ) { - return ; + if (preferenceAppsIds.length === 0) { + return null; } return ( - -

    - {intl.formatMessage(messages.notificationHeading)} -

    -
    - {intl.formatMessage(messages.notificationPreferenceGuideBody)} - - {intl.formatMessage(messages.notificationPreferenceGuideLink)} - -
    -
    -
    - - - - - {course?.name} - -
    - {!mobileView && !isLoading && ( +
    + {!mobileView && !isLoading && (
    {Object.values(NOTIFICATION_CHANNELS).map((channel) => ( @@ -103,21 +64,20 @@ const NotificationPreferences = () => { ))}
    - )} - {preferencesList} - {isLoading && ( -
    - -
    - )} -
    - + )} + {preferencesList} + {isLoading && ( +
    + +
    + )} +
    ); }; diff --git a/src/notification-preferences/NotificationPreferences.test.jsx b/src/notification-preferences/NotificationPreferences.test.jsx index f70591e9c..bf1bab8e4 100644 --- a/src/notification-preferences/NotificationPreferences.test.jsx +++ b/src/notification-preferences/NotificationPreferences.test.jsx @@ -9,7 +9,7 @@ import { fireEvent, render, screen } from '@testing-library/react'; import { defaultState } from './data/reducers'; import NotificationPreferences from './NotificationPreferences'; -import { FAILURE_STATUS, LOADING_STATUS, SUCCESS_STATUS } from '../constants'; +import { LOADING_STATUS, SUCCESS_STATUS } from '../constants'; const courseId = 'selected-course-id'; @@ -77,6 +77,7 @@ const setupStore = (override = {}) => { storeState.courses = { status: SUCCESS_STATUS, courses: [ + { id: '', name: 'Account' }, { id: 'selected-course-id', name: 'Selected Course' }, ], }; @@ -146,9 +147,15 @@ describe('Notification Preferences', () => { expect(mockDispatch).toHaveBeenCalled(); }); - it('show not found page if invalid course id is entered in url', async () => { - store = setupStore({ status: FAILURE_STATUS, selectedCourse: 'invalid-course-id' }); + it('update account preference on click', async () => { + store = setupStore({ + ...defaultPreferences, + status: SUCCESS_STATUS, + selectedCourse: '', + }); await render(notificationPreferences(store)); - expect(screen.queryByTestId('not-found-page')).toBeInTheDocument(); + const element = screen.getByTestId('core-web'); + await fireEvent.click(element); + expect(mockDispatch).toHaveBeenCalled(); }); }); diff --git a/src/notification-preferences/NotificationSettings.jsx b/src/notification-preferences/NotificationSettings.jsx new file mode 100644 index 000000000..d437f898b --- /dev/null +++ b/src/notification-preferences/NotificationSettings.jsx @@ -0,0 +1,53 @@ +import React from 'react'; +import { useSelector } from 'react-redux'; + +import { useIntl } from '@edx/frontend-platform/i18n'; +import { Container, Hyperlink } from '@openedx/paragon'; + +import { selectSelectedCourseId, selectShowPreferences } from './data/selectors'; +import messages from './messages'; +import NotificationCoursesDropdown from './NotificationCoursesDropdown'; +import NotificationPreferences from './NotificationPreferences'; +import { useFeedbackWrapper } from '../hooks'; + +const NotificationSettings = () => { + useFeedbackWrapper(); + const intl = useIntl(); + const showPreferences = useSelector(selectShowPreferences()); + const courseId = useSelector(selectSelectedCourseId()); + + return ( + showPreferences && ( + +

    + {intl.formatMessage(messages.notificationHeading)} +

    +
    + {intl.formatMessage(messages.accountNotificationDescription)} +
    +
    + {intl.formatMessage(messages.notificationCadenceDescription, { + dailyTime: '22:00 UTC', + weeklyTime: '22:00 UTC Every Sunday', + })} +
    +
    + {intl.formatMessage(messages.notificationPreferenceGuideBody)} + + {intl.formatMessage(messages.notificationPreferenceGuideLink)} + +
    + + +
    + + ) + ); +}; + +export default NotificationSettings; diff --git a/src/notification-preferences/data/actions.js b/src/notification-preferences/data/actions.js index a8e5420ff..e9f900d78 100644 --- a/src/notification-preferences/data/actions.js +++ b/src/notification-preferences/data/actions.js @@ -10,8 +10,10 @@ export const Actions = { UPDATE_APP_PREFERENCE: 'updateAppValue', }; -export const fetchNotificationPreferenceSuccess = (courseId, payload) => dispatch => ( - dispatch({ type: Actions.FETCHED_PREFERENCES, courseId, payload }) +export const fetchNotificationPreferenceSuccess = (courseId, payload, isAccountPreference) => dispatch => ( + dispatch({ + type: Actions.FETCHED_PREFERENCES, courseId, payload, isAccountPreference, + }) ); export const fetchNotificationPreferenceFetching = () => dispatch => ( diff --git a/src/notification-preferences/data/reducers.js b/src/notification-preferences/data/reducers.js index 4820c3043..1683b2009 100644 --- a/src/notification-preferences/data/reducers.js +++ b/src/notification-preferences/data/reducers.js @@ -5,18 +5,19 @@ import { SUCCESS_STATUS, FAILURE_STATUS, } from '../../constants'; +import { normalizeAccountPreferences } from './thunks'; export const defaultState = { showPreferences: false, courses: { status: IDLE_STATUS, - courses: [], + courses: [{ id: '', name: 'Account' }], pagination: {}, }, preferences: { status: IDLE_STATUS, updatePreferenceStatus: IDLE_STATUS, - selectedCourse: null, + selectedCourse: '', preferences: [], apps: [], nonEditable: {}, @@ -66,15 +67,22 @@ const notificationPreferencesReducer = (state = defaultState, action = {}) => { }, }; case Actions.FETCHED_PREFERENCES: + { + const { preferences } = state; + if (action.isAccountPreference) { + normalizeAccountPreferences(preferences, action.payload); + } + return { ...state, preferences: { - ...state.preferences, + ...preferences, status: SUCCESS_STATUS, updatePreferenceStatus: SUCCESS_STATUS, ...action.payload, }, }; + } case Actions.FAILED_PREFERENCES: return { ...state, diff --git a/src/notification-preferences/data/reducers.test.js b/src/notification-preferences/data/reducers.test.js index 919bbfb2d..697b7fe1d 100644 --- a/src/notification-preferences/data/reducers.test.js +++ b/src/notification-preferences/data/reducers.test.js @@ -36,9 +36,7 @@ describe('notification-preferences reducer', () => { hasMore: false, totalPages: 1, }, - courseList: [ - { id: selectedCourseId, name: 'Selected Course' }, - ], + courseList: [], }; const result = reducer( state, @@ -46,7 +44,7 @@ describe('notification-preferences reducer', () => { ); expect(result.courses).toEqual({ status: SUCCESS_STATUS, - courses: data.courseList, + courses: [{ id: '', name: 'Account' }], pagination: data.pagination, }); }); @@ -61,7 +59,10 @@ describe('notification-preferences reducer', () => { ); expect(result.courses).toEqual({ status, - courses: [], + courses: [{ + id: '', + name: 'Account', + }], pagination: {}, }); }); @@ -82,7 +83,7 @@ describe('notification-preferences reducer', () => { expect(result.preferences).toEqual({ status: SUCCESS_STATUS, updatePreferenceStatus: SUCCESS_STATUS, - selectedCourse: null, + selectedCourse: '', ...preferenceData, }); }); @@ -97,7 +98,7 @@ describe('notification-preferences reducer', () => { ); expect(result.preferences).toEqual({ status, - selectedCourse: null, + selectedCourse: '', preferences: [], apps: [], nonEditable: {}, diff --git a/src/notification-preferences/data/service.js b/src/notification-preferences/data/service.js index 4296bbea7..98b18fe98 100644 --- a/src/notification-preferences/data/service.js +++ b/src/notification-preferences/data/service.js @@ -32,3 +32,22 @@ export const patchPreferenceToggle = async ( const { data } = await getAuthenticatedHttpClient().patch(url, patchData); return data; }; + +export const postPreferenceToggle = async ( + notificationApp, + notificationType, + notificationChannel, + value, + emailCadence, +) => { + const patchData = snakeCaseObject({ + notificationApp, + notificationType: snakeCase(notificationType), + notificationChannel, + value, + emailCadence, + }); + const url = `${getConfig().LMS_BASE_URL}/api/notifications/preferences/update-all/`; + const { data } = await getAuthenticatedHttpClient().post(url, patchData); + return data; +}; diff --git a/src/notification-preferences/data/thunks.js b/src/notification-preferences/data/thunks.js index 7008a8127..1b21a6d1a 100644 --- a/src/notification-preferences/data/thunks.js +++ b/src/notification-preferences/data/thunks.js @@ -1,4 +1,5 @@ import { camelCaseObject } from '@edx/frontend-platform'; +import camelCase from 'lodash.camelcase'; import EMAIL_CADENCE from './constants'; import { fetchCourseListSuccess, @@ -14,6 +15,7 @@ import { getCourseList, getCourseNotificationPreferences, patchPreferenceToggle, + postPreferenceToggle, } from './service'; const normalizeCourses = (responseData) => { @@ -36,8 +38,29 @@ const normalizeCourses = (responseData) => { }; }; -const normalizePreferences = (responseData) => { - const preferences = responseData.notificationPreferenceConfig; +export const normalizeAccountPreferences = (originalData, updateInfo) => { + const { + app, notificationType, channel, updatedValue, + } = updateInfo.data; + + const preferenceToUpdate = originalData.preferences.find( + (preference) => preference.appId === app && preference.id === camelCase(notificationType), + ); + + if (preferenceToUpdate) { + preferenceToUpdate[camelCase(channel)] = updatedValue; + } + + return originalData; +}; + +const normalizePreferences = (responseData, courseId) => { + let preferences; + if (courseId) { + preferences = responseData.notificationPreferenceConfig; + } else { + preferences = responseData.data; + } const appKeys = Object.keys(preferences); const apps = appKeys.map((appId) => ({ @@ -92,7 +115,7 @@ export const fetchCourseNotificationPreferences = (courseId) => ( dispatch(updateSelectedCourse(courseId)); dispatch(fetchNotificationPreferenceFetching()); const data = await getCourseNotificationPreferences(courseId); - const normalizedData = normalizePreferences(camelCaseObject(data)); + const normalizedData = normalizePreferences(camelCaseObject(data), courseId); dispatch(fetchNotificationPreferenceSuccess(courseId, normalizedData)); } catch (errors) { dispatch(fetchNotificationPreferenceFailed()); @@ -100,12 +123,19 @@ export const fetchCourseNotificationPreferences = (courseId) => ( } ); +export const setSelectedCourse = courseId => ( + async (dispatch) => { + dispatch(updateSelectedCourse(courseId)); + } +); + export const updatePreferenceToggle = ( courseId, notificationApp, notificationType, notificationChannel, value, + emailCadence, ) => ( async (dispatch) => { try { @@ -115,15 +145,27 @@ export const updatePreferenceToggle = ( notificationChannel, !value, )); - const data = await patchPreferenceToggle( - courseId, - notificationApp, - notificationType, - notificationChannel, - value, - ); - const normalizedData = normalizePreferences(camelCaseObject(data)); - dispatch(fetchNotificationPreferenceSuccess(courseId, normalizedData)); + let data = null; + if (courseId) { + data = await patchPreferenceToggle( + courseId, + notificationApp, + notificationType, + notificationChannel, + value, + ); + const normalizedData = normalizePreferences(camelCaseObject(data), courseId); + dispatch(fetchNotificationPreferenceSuccess(courseId, normalizedData)); + } else { + data = await postPreferenceToggle( + notificationApp, + notificationType, + notificationChannel, + value, + emailCadence, + ); + dispatch(fetchNotificationPreferenceSuccess(courseId, camelCaseObject(data), true)); + } } catch (errors) { dispatch(updatePreferenceValue( notificationApp, diff --git a/src/notification-preferences/messages.js b/src/notification-preferences/messages.js index 49ffaea53..1832faa0d 100644 --- a/src/notification-preferences/messages.js +++ b/src/notification-preferences/messages.js @@ -90,6 +90,36 @@ const messages = defineMessages({ defaultMessage: 'Notifications for certain activities are enabled by default,', description: 'Body of the notification preferences for learner guide', }, + accountNotificationDescription: { + id: 'account.notification.description', + defaultMessage: 'Account-level settings apply to all courses. Notifications for individual courses can be changed within each course and will override account-level settings.', + description: 'Account notification description', + }, + notificationCadenceDescription: { + id: 'notification.cadence.description', + defaultMessage: 'Daily notifications are delivered at {dailyTime}. Weekly notifications are delivered at {weeklyTime}.', + description: 'Notification cadence description', + }, + notificationDefaultInfo: { + id: 'notification.default.info', + defaultMessage: 'Notifications for certain activities are enabled by default, as detailed here', + description: 'Default notification info', + }, + notificationDropdownlabel: { + id: 'notification.dropdown.label', + defaultMessage: 'Select notifications for', + description: 'Dropdown label', + }, + notificationDropdownApplies: { + id: 'notification.dropdown.applies', + defaultMessage: 'Applies to all courses', + description: 'Dropdown applies to all courses', + }, + notificationCourseDropdownApplies: { + id: 'notification.dropdown.course.applies', + defaultMessage: 'Overrides account-wide settings', + description: 'Dropdown applies to specific course', + }, }); export default messages;