+
+
+
{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;
{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 (-
- {intl.formatMessage(messages.notificationHeading)} -
-{intl.formatMessage(messages.notificationDropdownlabel)}
+- {intl.formatMessage(messages.notificationHeading)} -
-+ {intl.formatMessage(messages.notificationHeading)} +
+