chore: cleaned up course level preferences apis
This commit is contained in:
1
.env
1
.env
@@ -37,4 +37,3 @@ SUPPORT_URL_TO_UNLINK_SOCIAL_MEDIA_ACCOUNT='https://help.edx.org/edxlearner/s/ar
|
||||
COUNTRIES_WITH_DELETE_ACCOUNT_DISABLED='[]'
|
||||
# Fallback in local style files
|
||||
PARAGON_THEME_URLS={}
|
||||
ENABLE_PREFERENCES_V2='false'
|
||||
|
||||
@@ -38,4 +38,3 @@ SUPPORT_URL_TO_UNLINK_SOCIAL_MEDIA_ACCOUNT='https://help.edx.org/edxlearner/s/ar
|
||||
COUNTRIES_WITH_DELETE_ACCOUNT_DISABLED='[]'
|
||||
# Fallback in local style files
|
||||
PARAGON_THEME_URLS={}
|
||||
ENABLE_PREFERENCES_V2='false'
|
||||
|
||||
@@ -34,4 +34,3 @@ LEARNER_FEEDBACK_URL=''
|
||||
SUPPORT_URL_TO_UNLINK_SOCIAL_MEDIA_ACCOUNT='https://help.edx.org/edxlearner/s/article/How-do-I-link-or-unlink-my-edX-account-to-a-social-media-account'
|
||||
COUNTRIES_WITH_DELETE_ACCOUNT_DISABLED='[]'
|
||||
PARAGON_THEME_URLS={}
|
||||
ENABLE_PREFERENCES_V2='false'
|
||||
|
||||
@@ -50,7 +50,7 @@ import {
|
||||
FIELD_LABELS,
|
||||
} from './data/constants';
|
||||
import { fetchSiteLanguages } from './site-language';
|
||||
import { fetchCourseList } from '../notification-preferences/data/thunks';
|
||||
import { fetchNotificationPreferences } from '../notification-preferences/data/thunks';
|
||||
import NotificationSettings from '../notification-preferences/NotificationSettings';
|
||||
import { withLocation, withNavigate } from './hoc';
|
||||
|
||||
@@ -75,7 +75,7 @@ class AccountSettingsPage extends React.Component {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.fetchCourseList();
|
||||
this.props.fetchNotificationPreferences();
|
||||
this.props.fetchSettings();
|
||||
this.props.fetchSiteLanguages(this.props.navigate);
|
||||
sendTrackingLogEvent('edx.user.settings.viewed', {
|
||||
@@ -945,7 +945,7 @@ AccountSettingsPage.propTypes = {
|
||||
saveSettings: PropTypes.func.isRequired,
|
||||
fetchSettings: PropTypes.func.isRequired,
|
||||
beginNameChange: PropTypes.func.isRequired,
|
||||
fetchCourseList: PropTypes.func.isRequired,
|
||||
fetchNotificationPreferences: PropTypes.func.isRequired,
|
||||
tpaProviders: PropTypes.arrayOf(PropTypes.shape({
|
||||
connected: PropTypes.bool,
|
||||
})),
|
||||
@@ -1010,7 +1010,7 @@ AccountSettingsPage.defaultProps = {
|
||||
};
|
||||
|
||||
export default withLocation(withNavigate(connect(accountSettingsPageSelector, {
|
||||
fetchCourseList,
|
||||
fetchNotificationPreferences,
|
||||
fetchSettings,
|
||||
saveSettings,
|
||||
saveMultipleSettings,
|
||||
|
||||
@@ -74,7 +74,6 @@ initialize({
|
||||
MARKETING_EMAILS_OPT_IN: (process.env.MARKETING_EMAILS_OPT_IN || false),
|
||||
PASSWORD_RESET_SUPPORT_LINK: process.env.PASSWORD_RESET_SUPPORT_LINK,
|
||||
LEARNER_FEEDBACK_URL: process.env.LEARNER_FEEDBACK_URL,
|
||||
ENABLE_PREFERENCES_V2: process.env.ENABLE_PREFERENCES_V2 || false,
|
||||
}, 'App loadConfig override handler');
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
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 && (
|
||||
<div className="mb-5">
|
||||
<h5 className="text-primary-500 mb-3">{intl.formatMessage(messages.notificationDropdownlabel)}</h5>
|
||||
<Dropdown className="course-dropdown">
|
||||
<Dropdown.Toggle
|
||||
variant="outline-primary"
|
||||
id="course-dropdown-btn"
|
||||
className="w-100 justify-content-between small"
|
||||
disabled
|
||||
>
|
||||
{selectedCourse?.name}
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu className="w-100">
|
||||
{coursesList.map((course) => (
|
||||
<Dropdown.Item
|
||||
className="w-100"
|
||||
key={course.id}
|
||||
active={course.id === selectedCourse?.id}
|
||||
eventKey={course.id}
|
||||
onSelect={handleCourseSelection}
|
||||
>
|
||||
{course.name}
|
||||
</Dropdown.Item>
|
||||
))}
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
<span className="x-small text-gray-500">
|
||||
{selectedCourse?.name === 'Account'
|
||||
? intl.formatMessage(messages.notificationDropdownApplies)
|
||||
: intl.formatMessage(messages.notificationCourseDropdownApplies)}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export default NotificationCoursesDropdown;
|
||||
@@ -15,7 +15,7 @@ import { LOADING_STATUS } from '../constants';
|
||||
import { updatePreferenceToggle } from './data/thunks';
|
||||
import {
|
||||
selectAppNonEditableChannels, selectAppPreferences,
|
||||
selectSelectedCourseId, selectUpdatePreferencesStatus,
|
||||
selectUpdatePreferencesStatus,
|
||||
} from './data/selectors';
|
||||
import { notificationChannels, shouldHideAppPreferences } from './data/utils';
|
||||
import {
|
||||
@@ -25,7 +25,6 @@ import {
|
||||
const NotificationPreferenceColumn = ({ appId, channel, appPreference }) => {
|
||||
const dispatch = useDispatch();
|
||||
const intl = useIntl();
|
||||
const courseId = useSelector(selectSelectedCourseId());
|
||||
const appPreferences = useSelector(selectAppPreferences(appId));
|
||||
const updatePreferencesStatus = useSelector(selectUpdatePreferencesStatus());
|
||||
const nonEditable = useSelector(selectAppNonEditableChannels(appId));
|
||||
@@ -34,11 +33,11 @@ const NotificationPreferenceColumn = ({ appId, channel, appPreference }) => {
|
||||
const hideAppPreferences = shouldHideAppPreferences(appPreferences, appId) || false;
|
||||
|
||||
const getValue = useCallback((notificationChannel, innerText, checked) => {
|
||||
if (notificationChannel === EMAIL_CADENCE && courseId) {
|
||||
if (notificationChannel === EMAIL_CADENCE) {
|
||||
return innerText;
|
||||
}
|
||||
return checked;
|
||||
}, [courseId]);
|
||||
}, []);
|
||||
|
||||
const getEmailCadence = useCallback((notificationChannel, checked, innerText, emailCadence) => {
|
||||
if (notificationChannel === EMAIL_CADENCE) {
|
||||
@@ -63,14 +62,13 @@ const NotificationPreferenceColumn = ({ appId, channel, appPreference }) => {
|
||||
);
|
||||
|
||||
dispatch(updatePreferenceToggle(
|
||||
courseId,
|
||||
appId,
|
||||
notificationType,
|
||||
notificationChannel,
|
||||
value,
|
||||
emailCadence !== MIXED ? emailCadence : undefined,
|
||||
));
|
||||
}, [appPreferences, getValue, getEmailCadence, dispatch, courseId, appId]);
|
||||
}, [appPreferences, getValue, getEmailCadence, dispatch, appId]);
|
||||
|
||||
const renderPreference = (preference) => (
|
||||
(preference?.coreNotificationTypes?.length > 0 || preference.id !== 'core') && (
|
||||
|
||||
@@ -9,23 +9,21 @@ import { Spinner, NavItem } from '@openedx/paragon';
|
||||
import { useIsOnMobile } from '../hooks';
|
||||
import messages from './messages';
|
||||
import NotificationPreferenceApp from './NotificationPreferenceApp';
|
||||
import { fetchCourseNotificationPreferences } from './data/thunks';
|
||||
import { fetchNotificationPreferences } from './data/thunks';
|
||||
import { LOADING_STATUS } from '../constants';
|
||||
import {
|
||||
selectCourseListStatus, selectNotificationPreferencesStatus, selectPreferenceAppsId, selectSelectedCourseId,
|
||||
selectNotificationPreferencesStatus, selectPreferenceAppsId,
|
||||
} from './data/selectors';
|
||||
import { notificationChannels } from './data/utils';
|
||||
|
||||
const NotificationPreferences = () => {
|
||||
const dispatch = useDispatch();
|
||||
const intl = useIntl();
|
||||
const courseStatus = useSelector(selectCourseListStatus());
|
||||
const courseId = useSelector(selectSelectedCourseId());
|
||||
const notificationStatus = useSelector(selectNotificationPreferencesStatus());
|
||||
const preferenceAppsIds = useSelector(selectPreferenceAppsId());
|
||||
const mobileView = useIsOnMobile();
|
||||
const NOTIFICATION_CHANNELS = notificationChannels();
|
||||
const isLoading = notificationStatus === LOADING_STATUS || courseStatus === LOADING_STATUS;
|
||||
const isLoading = notificationStatus === LOADING_STATUS;
|
||||
|
||||
const preferencesList = useMemo(() => (
|
||||
preferenceAppsIds.map(appId => (
|
||||
@@ -34,8 +32,8 @@ const NotificationPreferences = () => {
|
||||
), [preferenceAppsIds]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchCourseNotificationPreferences(courseId));
|
||||
}, [courseId, dispatch]);
|
||||
dispatch(fetchNotificationPreferences());
|
||||
}, [dispatch]);
|
||||
|
||||
if (preferenceAppsIds.length === 0) {
|
||||
return null;
|
||||
|
||||
@@ -12,7 +12,7 @@ import { defaultState } from './data/reducers';
|
||||
import NotificationPreferences from './NotificationPreferences';
|
||||
import { LOADING_STATUS, SUCCESS_STATUS } from '../constants';
|
||||
import {
|
||||
getCourseNotificationPreferences,
|
||||
getNotificationPreferences,
|
||||
postPreferenceToggle,
|
||||
} from './data/service';
|
||||
|
||||
@@ -117,7 +117,6 @@ describe('Notification Preferences', () => {
|
||||
store = setupStore({
|
||||
...defaultPreferences,
|
||||
status: SUCCESS_STATUS,
|
||||
selectedCourse: courseId,
|
||||
});
|
||||
|
||||
auth.getAuthenticatedHttpClient = jest.fn(() => ({
|
||||
@@ -148,19 +147,10 @@ describe('Notification Preferences', () => {
|
||||
expect(screen.queryAllByTestId('notification-preference')).toHaveLength(4);
|
||||
});
|
||||
|
||||
it('update preference on click', async () => {
|
||||
const wrapper = await render(notificationPreferences(store));
|
||||
const element = wrapper.container.querySelector('#core-web');
|
||||
expect(element).not.toBeChecked();
|
||||
await fireEvent.click(element);
|
||||
expect(mockDispatch).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('update account preference on click', async () => {
|
||||
store = setupStore({
|
||||
...defaultPreferences,
|
||||
status: SUCCESS_STATUS,
|
||||
selectedCourse: '',
|
||||
});
|
||||
await render(notificationPreferences(store));
|
||||
const element = screen.getByTestId('toggle-core-web');
|
||||
@@ -203,22 +193,11 @@ describe('Notification Preferences API v2 Logic', () => {
|
||||
setConfig({ LMS_BASE_URL });
|
||||
});
|
||||
|
||||
describe('getCourseNotificationPreferences', () => {
|
||||
it('should call the v2 configurations URL when ENABLE_PREFERENCES_V2 is true', async () => {
|
||||
setConfig({ LMS_BASE_URL, ENABLE_PREFERENCES_V2: 'true' });
|
||||
describe('getNotificationPreferences', () => {
|
||||
it('should call the v2 configurations URL', async () => {
|
||||
const expectedUrl = `${LMS_BASE_URL}/api/notifications/v2/configurations/`;
|
||||
|
||||
await getCourseNotificationPreferences('any-course-id');
|
||||
|
||||
expect(mockHttpClient.get).toHaveBeenCalledWith(expectedUrl);
|
||||
expect(mockHttpClient.get).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should call the original (v1) configurations URL when ENABLE_PREFERENCES_V2 is not true', async () => {
|
||||
setConfig({ LMS_BASE_URL, ENABLE_PREFERENCES_V2: 'false' });
|
||||
const expectedUrl = `${LMS_BASE_URL}/api/notifications/configurations/${courseId}`;
|
||||
|
||||
await getCourseNotificationPreferences(courseId);
|
||||
await getNotificationPreferences();
|
||||
|
||||
expect(mockHttpClient.get).toHaveBeenCalledWith(expectedUrl);
|
||||
expect(mockHttpClient.get).toHaveBeenCalledTimes(1);
|
||||
@@ -226,8 +205,7 @@ describe('Notification Preferences API v2 Logic', () => {
|
||||
});
|
||||
|
||||
describe('postPreferenceToggle', () => {
|
||||
it('should call the v2 configurations URL with PUT method when ENABLE_PREFERENCES_V2 is true', async () => {
|
||||
setConfig({ LMS_BASE_URL, ENABLE_PREFERENCES_V2: 'true' });
|
||||
it('should call the v2 configurations URL with PUT method', async () => {
|
||||
const expectedUrl = `${LMS_BASE_URL}/api/notifications/v2/configurations/`;
|
||||
const testArgs = ['app_name', 'notification_type', 'web', true, 'daily'];
|
||||
|
||||
@@ -237,17 +215,5 @@ describe('Notification Preferences API v2 Logic', () => {
|
||||
expect(mockHttpClient.put).toHaveBeenCalledTimes(1);
|
||||
expect(mockHttpClient.post).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call the original (v1) update-all URL with POST method when ENABLE_PREFERENCES_V2 is not true', async () => {
|
||||
setConfig({ LMS_BASE_URL, ENABLE_PREFERENCES_V2: 'false' });
|
||||
const expectedUrl = `${LMS_BASE_URL}/api/notifications/preferences/update-all/`;
|
||||
const testArgs = ['app_name', 'notification_type', 'web', true, 'daily'];
|
||||
|
||||
await postPreferenceToggle(...testArgs);
|
||||
|
||||
expect(mockHttpClient.post).toHaveBeenCalledWith(expectedUrl, expect.any(Object));
|
||||
expect(mockHttpClient.post).toHaveBeenCalledTimes(1);
|
||||
expect(mockHttpClient.put).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,9 +4,8 @@ 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 { selectShowPreferences } from './data/selectors';
|
||||
import messages from './messages';
|
||||
import NotificationCoursesDropdown from './NotificationCoursesDropdown';
|
||||
import NotificationPreferences from './NotificationPreferences';
|
||||
import { useFeedbackWrapper } from '../hooks';
|
||||
|
||||
@@ -14,7 +13,6 @@ const NotificationSettings = () => {
|
||||
useFeedbackWrapper();
|
||||
const intl = useIntl();
|
||||
const showPreferences = useSelector(selectShowPreferences());
|
||||
const courseId = useSelector(selectSelectedCourseId());
|
||||
|
||||
return (
|
||||
showPreferences && (
|
||||
@@ -42,8 +40,7 @@ const NotificationSettings = () => {
|
||||
{intl.formatMessage(messages.notificationPreferenceGuideLink)}
|
||||
</Hyperlink>
|
||||
</div>
|
||||
<NotificationCoursesDropdown />
|
||||
<NotificationPreferences courseId={courseId} />
|
||||
<NotificationPreferences />
|
||||
<div className="border border-light-700 my-6" />
|
||||
</Container>
|
||||
)
|
||||
|
||||
@@ -2,19 +2,15 @@ export const Actions = {
|
||||
FETCHED_PREFERENCES: 'fetchedPreferences',
|
||||
FETCHING_PREFERENCES: 'fetchingPreferences',
|
||||
FAILED_PREFERENCES: 'failedPreferences',
|
||||
FETCHING_COURSE_LIST: 'fetchingCourseList',
|
||||
FETCHED_COURSE_LIST: 'fetchedCourseList',
|
||||
FAILED_COURSE_LIST: 'failedCourseList',
|
||||
UPDATE_SELECTED_COURSE: 'updateSelectedCourse',
|
||||
UPDATE_PREFERENCE: 'updatePreference',
|
||||
UPDATE_APP_PREFERENCE: 'updateAppValue',
|
||||
};
|
||||
|
||||
export const fetchNotificationPreferenceSuccess = (courseId, payload, isAccountPreference) => dispatch => (
|
||||
export const fetchNotificationPreferenceSuccess = (payload, showPreferences, isPreferenceUpdate) => dispatch => {
|
||||
dispatch({
|
||||
type: Actions.FETCHED_PREFERENCES, courseId, payload, isAccountPreference,
|
||||
})
|
||||
);
|
||||
type: Actions.FETCHED_PREFERENCES, payload, showPreferences, isPreferenceUpdate,
|
||||
});
|
||||
};
|
||||
|
||||
export const fetchNotificationPreferenceFetching = () => dispatch => (
|
||||
dispatch({ type: Actions.FETCHING_PREFERENCES })
|
||||
@@ -24,22 +20,6 @@ export const fetchNotificationPreferenceFailed = () => dispatch => (
|
||||
dispatch({ type: Actions.FAILED_PREFERENCES })
|
||||
);
|
||||
|
||||
export const fetchCourseListSuccess = payload => dispatch => (
|
||||
dispatch({ type: Actions.FETCHED_COURSE_LIST, payload })
|
||||
);
|
||||
|
||||
export const fetchCourseListFetching = () => dispatch => (
|
||||
dispatch({ type: Actions.FETCHING_COURSE_LIST })
|
||||
);
|
||||
|
||||
export const fetchCourseListFailed = () => dispatch => (
|
||||
dispatch({ type: Actions.FAILED_COURSE_LIST })
|
||||
);
|
||||
|
||||
export const updateSelectedCourse = courseId => dispatch => (
|
||||
dispatch({ type: Actions.UPDATE_SELECTED_COURSE, courseId })
|
||||
);
|
||||
|
||||
export const updatePreferenceValue = (appId, preferenceName, notificationChannel, value) => dispatch => (
|
||||
dispatch({
|
||||
type: Actions.UPDATE_PREFERENCE,
|
||||
|
||||
@@ -9,15 +9,9 @@ import { normalizeAccountPreferences } from './thunks';
|
||||
|
||||
export const defaultState = {
|
||||
showPreferences: false,
|
||||
courses: {
|
||||
status: IDLE_STATUS,
|
||||
courses: [{ id: '', name: 'Account' }],
|
||||
pagination: {},
|
||||
},
|
||||
preferences: {
|
||||
status: IDLE_STATUS,
|
||||
updatePreferenceStatus: IDLE_STATUS,
|
||||
selectedCourse: '',
|
||||
preferences: [],
|
||||
apps: [],
|
||||
nonEditable: {},
|
||||
@@ -26,35 +20,9 @@ export const defaultState = {
|
||||
|
||||
const notificationPreferencesReducer = (state = defaultState, action = {}) => {
|
||||
const {
|
||||
courseId, appId, notificationChannel, preferenceName, value,
|
||||
appId, notificationChannel, preferenceName, value,
|
||||
} = action;
|
||||
switch (action.type) {
|
||||
case Actions.FETCHING_COURSE_LIST:
|
||||
return {
|
||||
...state,
|
||||
courses: {
|
||||
...state.courses,
|
||||
status: LOADING_STATUS,
|
||||
},
|
||||
};
|
||||
case Actions.FETCHED_COURSE_LIST:
|
||||
return {
|
||||
...state,
|
||||
courses: {
|
||||
status: SUCCESS_STATUS,
|
||||
courses: [...state.courses.courses, ...action.payload.courseList],
|
||||
pagination: action.payload.pagination,
|
||||
},
|
||||
showPreferences: action.payload.showPreferences,
|
||||
};
|
||||
case Actions.FAILED_COURSE_LIST:
|
||||
return {
|
||||
...state,
|
||||
courses: {
|
||||
...state.courses,
|
||||
status: FAILURE_STATUS,
|
||||
},
|
||||
};
|
||||
case Actions.FETCHING_PREFERENCES:
|
||||
return {
|
||||
...state,
|
||||
@@ -69,7 +37,7 @@ const notificationPreferencesReducer = (state = defaultState, action = {}) => {
|
||||
case Actions.FETCHED_PREFERENCES:
|
||||
{
|
||||
const { preferences } = state;
|
||||
if (action.isAccountPreference) {
|
||||
if (action.isPreferenceUpdate) {
|
||||
normalizeAccountPreferences(preferences, action.payload);
|
||||
}
|
||||
|
||||
@@ -81,6 +49,7 @@ const notificationPreferencesReducer = (state = defaultState, action = {}) => {
|
||||
updatePreferenceStatus: SUCCESS_STATUS,
|
||||
...action.payload,
|
||||
},
|
||||
showPreferences: action.showPreferences,
|
||||
};
|
||||
}
|
||||
case Actions.FAILED_PREFERENCES:
|
||||
@@ -95,14 +64,6 @@ const notificationPreferencesReducer = (state = defaultState, action = {}) => {
|
||||
nonEditable: {},
|
||||
},
|
||||
};
|
||||
case Actions.UPDATE_SELECTED_COURSE:
|
||||
return {
|
||||
...state,
|
||||
preferences: {
|
||||
...state.preferences,
|
||||
selectedCourse: courseId,
|
||||
},
|
||||
};
|
||||
case Actions.UPDATE_PREFERENCE:
|
||||
return {
|
||||
...state,
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
|
||||
describe('notification-preferences reducer', () => {
|
||||
let state = null;
|
||||
const selectedCourseId = 'selected-course-id';
|
||||
|
||||
const preferenceData = {
|
||||
apps: [{ id: 'discussion', enabled: true }],
|
||||
@@ -28,53 +27,6 @@ describe('notification-preferences reducer', () => {
|
||||
state = reducer();
|
||||
});
|
||||
|
||||
it('updates course list when api call is successful', () => {
|
||||
const data = {
|
||||
pagination: {
|
||||
count: 1,
|
||||
currentPage: 1,
|
||||
hasMore: false,
|
||||
totalPages: 1,
|
||||
},
|
||||
courseList: [],
|
||||
};
|
||||
const result = reducer(
|
||||
state,
|
||||
{ type: Actions.FETCHED_COURSE_LIST, payload: data },
|
||||
);
|
||||
expect(result.courses).toEqual({
|
||||
status: SUCCESS_STATUS,
|
||||
courses: [{ id: '', name: 'Account' }],
|
||||
pagination: data.pagination,
|
||||
});
|
||||
});
|
||||
|
||||
test.each([
|
||||
{ action: Actions.FETCHING_COURSE_LIST, status: LOADING_STATUS },
|
||||
{ action: Actions.FAILED_COURSE_LIST, status: FAILURE_STATUS },
|
||||
])('course list is empty when api call is %s', ({ action, status }) => {
|
||||
const result = reducer(
|
||||
state,
|
||||
{ type: action },
|
||||
);
|
||||
expect(result.courses).toEqual({
|
||||
status,
|
||||
courses: [{
|
||||
id: '',
|
||||
name: 'Account',
|
||||
}],
|
||||
pagination: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('updates selected course id', () => {
|
||||
const result = reducer(
|
||||
state,
|
||||
{ type: Actions.UPDATE_SELECTED_COURSE, courseId: selectedCourseId },
|
||||
);
|
||||
expect(result.preferences.selectedCourse).toEqual(selectedCourseId);
|
||||
});
|
||||
|
||||
it('updates preferences when api call is successful', () => {
|
||||
const result = reducer(
|
||||
state,
|
||||
@@ -83,7 +35,6 @@ describe('notification-preferences reducer', () => {
|
||||
expect(result.preferences).toEqual({
|
||||
status: SUCCESS_STATUS,
|
||||
updatePreferenceStatus: SUCCESS_STATUS,
|
||||
selectedCourse: '',
|
||||
...preferenceData,
|
||||
});
|
||||
});
|
||||
@@ -98,7 +49,6 @@ describe('notification-preferences reducer', () => {
|
||||
);
|
||||
expect(result.preferences).toEqual({
|
||||
status,
|
||||
selectedCourse: '',
|
||||
preferences: [],
|
||||
apps: [],
|
||||
nonEditable: {},
|
||||
|
||||
@@ -13,20 +13,6 @@ export const selectPreferences = () => state => (
|
||||
state.notificationPreferences.preferences?.preferences
|
||||
);
|
||||
|
||||
export const selectCourseListStatus = () => state => (
|
||||
state.notificationPreferences.courses.status
|
||||
);
|
||||
|
||||
export const selectCourseList = () => state => (
|
||||
state.notificationPreferences.courses.courses
|
||||
);
|
||||
|
||||
export const selectCourse = courseId => state => (
|
||||
selectCourseList()(state).find(
|
||||
course => course.id === courseId,
|
||||
)
|
||||
);
|
||||
|
||||
export const selectPreferenceAppsId = () => state => (
|
||||
state.notificationPreferences.preferences.apps.map(app => app.id)
|
||||
);
|
||||
@@ -57,14 +43,6 @@ export const selectPreferenceNonEditableChannels = (appId, name) => state => (
|
||||
state?.notificationPreferences.preferences.nonEditable[appId]?.[name] || []
|
||||
);
|
||||
|
||||
export const selectSelectedCourseId = () => state => (
|
||||
state.notificationPreferences.preferences.selectedCourse
|
||||
);
|
||||
|
||||
export const selectPagination = () => state => (
|
||||
state.notificationPreferences.courses.pagination
|
||||
);
|
||||
|
||||
export const selectShowPreferences = () => state => (
|
||||
state.notificationPreferences.showPreferences
|
||||
);
|
||||
|
||||
@@ -2,40 +2,12 @@ import { getConfig, snakeCaseObject } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import snakeCase from 'lodash.snakecase';
|
||||
|
||||
export const getCourseNotificationPreferences = async (courseId) => {
|
||||
let url = `${getConfig().LMS_BASE_URL}/api/notifications/configurations/${courseId}`;
|
||||
if (getConfig().ENABLE_PREFERENCES_V2 === 'true') {
|
||||
url = `${getConfig().LMS_BASE_URL}/api/notifications/v2/configurations/`;
|
||||
}
|
||||
export const getNotificationPreferences = async () => {
|
||||
const url = `${getConfig().LMS_BASE_URL}/api/notifications/v2/configurations/`;
|
||||
const { data } = await getAuthenticatedHttpClient().get(url);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getCourseList = async (page, pageSize) => {
|
||||
const params = snakeCaseObject({ page, pageSize });
|
||||
const url = `${getConfig().LMS_BASE_URL}/api/notifications/enrollments/`;
|
||||
const { data } = await getAuthenticatedHttpClient().get(url, { params });
|
||||
return data;
|
||||
};
|
||||
|
||||
export const patchPreferenceToggle = async (
|
||||
courseId,
|
||||
notificationApp,
|
||||
notificationType,
|
||||
notificationChannel,
|
||||
value,
|
||||
) => {
|
||||
const patchData = snakeCaseObject({
|
||||
notificationApp,
|
||||
notificationType: snakeCase(notificationType),
|
||||
notificationChannel,
|
||||
value,
|
||||
});
|
||||
const url = `${getConfig().LMS_BASE_URL}/api/notifications/configurations/${courseId}`;
|
||||
const { data } = await getAuthenticatedHttpClient().patch(url, patchData);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const postPreferenceToggle = async (
|
||||
notificationApp,
|
||||
notificationType,
|
||||
@@ -50,13 +22,7 @@ export const postPreferenceToggle = async (
|
||||
value,
|
||||
emailCadence,
|
||||
});
|
||||
if (getConfig().ENABLE_PREFERENCES_V2 === 'true') {
|
||||
const url = `${getConfig().LMS_BASE_URL}/api/notifications/v2/configurations/`;
|
||||
const { data } = await getAuthenticatedHttpClient().put(url, patchData);
|
||||
return data;
|
||||
}
|
||||
|
||||
const url = `${getConfig().LMS_BASE_URL}/api/notifications/preferences/update-all/`;
|
||||
const { data } = await getAuthenticatedHttpClient().post(url, patchData);
|
||||
const url = `${getConfig().LMS_BASE_URL}/api/notifications/v2/configurations/`;
|
||||
const { data } = await getAuthenticatedHttpClient().put(url, patchData);
|
||||
return data;
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
fetchNotificationPreferenceSuccess,
|
||||
fetchNotificationPreferenceFailed,
|
||||
} from './actions';
|
||||
import { patchPreferenceToggle, postPreferenceToggle } from './service';
|
||||
import { postPreferenceToggle } from './service';
|
||||
import { EMAIL } from './constants';
|
||||
|
||||
jest.mock('./service', () => ({
|
||||
@@ -60,37 +60,9 @@ describe('updatePreferenceToggle', () => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should update preference for a course-specific notification', async () => {
|
||||
patchPreferenceToggle.mockResolvedValue({ data: mockData });
|
||||
await updatePreferenceToggle(
|
||||
courseId,
|
||||
notificationApp,
|
||||
notificationType,
|
||||
notificationChannel,
|
||||
value,
|
||||
emailCadence,
|
||||
)(dispatch);
|
||||
|
||||
expect(dispatch).toHaveBeenCalledWith(updatePreferenceValue(
|
||||
notificationApp,
|
||||
notificationType,
|
||||
notificationChannel,
|
||||
!value,
|
||||
));
|
||||
expect(patchPreferenceToggle).toHaveBeenCalledWith(
|
||||
courseId,
|
||||
notificationApp,
|
||||
notificationType,
|
||||
notificationChannel,
|
||||
value,
|
||||
);
|
||||
expect(dispatch).toHaveBeenCalledWith(fetchNotificationPreferenceSuccess(courseId, { data: mockData }, false));
|
||||
});
|
||||
|
||||
it('should update preference globally when courseId is not provided', async () => {
|
||||
it('should update preference globally', async () => {
|
||||
postPreferenceToggle.mockResolvedValue({ data: mockData });
|
||||
await updatePreferenceToggle(
|
||||
null,
|
||||
notificationApp,
|
||||
notificationType,
|
||||
notificationChannel,
|
||||
@@ -115,23 +87,22 @@ describe('updatePreferenceToggle', () => {
|
||||
});
|
||||
|
||||
it('should handle email preferences separately', async () => {
|
||||
patchPreferenceToggle.mockResolvedValue({ data: mockData });
|
||||
await updatePreferenceToggle(courseId, notificationApp, notificationType, EMAIL, value, emailCadence)(dispatch);
|
||||
postPreferenceToggle.mockResolvedValue({ data: mockData });
|
||||
await updatePreferenceToggle(notificationApp, notificationType, EMAIL, value, emailCadence)(dispatch);
|
||||
|
||||
expect(patchPreferenceToggle).toHaveBeenCalledWith(
|
||||
courseId,
|
||||
expect(postPreferenceToggle).toHaveBeenCalledWith(
|
||||
notificationApp,
|
||||
notificationType,
|
||||
EMAIL,
|
||||
true,
|
||||
emailCadence,
|
||||
);
|
||||
expect(dispatch).toHaveBeenCalledWith(fetchNotificationPreferenceSuccess(courseId, { data: mockData }, false));
|
||||
});
|
||||
|
||||
it('should dispatch fetchNotificationPreferenceFailed on error', async () => {
|
||||
patchPreferenceToggle.mockRejectedValue(new Error('Network Error'));
|
||||
postPreferenceToggle.mockRejectedValue(new Error('Network Error'));
|
||||
await updatePreferenceToggle(
|
||||
courseId,
|
||||
notificationApp,
|
||||
notificationType,
|
||||
notificationChannel,
|
||||
|
||||
@@ -2,42 +2,16 @@ import { camelCaseObject } from '@edx/frontend-platform';
|
||||
import camelCase from 'lodash.camelcase';
|
||||
import { EMAIL, EMAIL_CADENCE, EMAIL_CADENCE_PREFERENCES } from './constants';
|
||||
import {
|
||||
fetchCourseListSuccess,
|
||||
fetchCourseListFetching,
|
||||
fetchCourseListFailed,
|
||||
fetchNotificationPreferenceFailed,
|
||||
fetchNotificationPreferenceFetching,
|
||||
fetchNotificationPreferenceSuccess,
|
||||
updatePreferenceValue,
|
||||
updateSelectedCourse,
|
||||
} from './actions';
|
||||
import {
|
||||
getCourseList,
|
||||
getCourseNotificationPreferences,
|
||||
patchPreferenceToggle,
|
||||
getNotificationPreferences,
|
||||
postPreferenceToggle,
|
||||
} from './service';
|
||||
|
||||
const normalizeCourses = (responseData) => {
|
||||
const courseList = responseData.results?.map((enrollment) => ({
|
||||
id: enrollment.course.id,
|
||||
name: enrollment.course.displayName,
|
||||
})) || [];
|
||||
|
||||
const pagination = {
|
||||
count: responseData.count,
|
||||
currentPage: responseData.currentPage,
|
||||
hasMore: Boolean(responseData.next),
|
||||
totalPages: responseData.numPages,
|
||||
};
|
||||
|
||||
return {
|
||||
courseList,
|
||||
pagination,
|
||||
showPreferences: responseData.showPreferences,
|
||||
};
|
||||
};
|
||||
|
||||
export const normalizeAccountPreferences = (originalData, updateInfo) => {
|
||||
const {
|
||||
app, notificationType, channel, updatedValue,
|
||||
@@ -54,13 +28,8 @@ export const normalizeAccountPreferences = (originalData, updateInfo) => {
|
||||
return originalData;
|
||||
};
|
||||
|
||||
const normalizePreferences = (responseData, courseId) => {
|
||||
let preferences;
|
||||
if (courseId) {
|
||||
preferences = responseData.notificationPreferenceConfig;
|
||||
} else {
|
||||
preferences = responseData.data;
|
||||
}
|
||||
const normalizePreferences = (responseData) => {
|
||||
const preferences = responseData.data;
|
||||
|
||||
const appKeys = Object.keys(preferences);
|
||||
const apps = appKeys.map((appId) => ({
|
||||
@@ -97,41 +66,20 @@ const normalizePreferences = (responseData, courseId) => {
|
||||
return normalizedPreferences;
|
||||
};
|
||||
|
||||
export const fetchCourseList = (page, pageSize) => (
|
||||
export const fetchNotificationPreferences = () => (
|
||||
async (dispatch) => {
|
||||
try {
|
||||
dispatch(fetchCourseListFetching());
|
||||
const data = await getCourseList(page, pageSize);
|
||||
const normalizedData = normalizeCourses(camelCaseObject(data));
|
||||
dispatch(fetchCourseListSuccess(normalizedData));
|
||||
} catch (errors) {
|
||||
dispatch(fetchCourseListFailed());
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const fetchCourseNotificationPreferences = (courseId) => (
|
||||
async (dispatch) => {
|
||||
try {
|
||||
dispatch(updateSelectedCourse(courseId));
|
||||
dispatch(fetchNotificationPreferenceFetching());
|
||||
const data = await getCourseNotificationPreferences(courseId);
|
||||
const normalizedData = normalizePreferences(camelCaseObject(data), courseId);
|
||||
dispatch(fetchNotificationPreferenceSuccess(courseId, normalizedData));
|
||||
const data = camelCaseObject(await getNotificationPreferences());
|
||||
const normalizedData = normalizePreferences(data);
|
||||
dispatch(fetchNotificationPreferenceSuccess(normalizedData, data.showPreferences));
|
||||
} catch (errors) {
|
||||
dispatch(fetchNotificationPreferenceFailed());
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const setSelectedCourse = courseId => (
|
||||
async (dispatch) => {
|
||||
dispatch(updateSelectedCourse(courseId));
|
||||
}
|
||||
);
|
||||
|
||||
export const updatePreferenceToggle = (
|
||||
courseId,
|
||||
notificationApp,
|
||||
notificationType,
|
||||
notificationChannel,
|
||||
@@ -149,49 +97,35 @@ export const updatePreferenceToggle = (
|
||||
));
|
||||
|
||||
// Function to handle data normalization and dispatching success
|
||||
const handleSuccessResponse = (data, isGlobal = false) => {
|
||||
const processedData = courseId
|
||||
? normalizePreferences(camelCaseObject(data), courseId)
|
||||
: camelCaseObject(data);
|
||||
const handleSuccessResponse = (data) => {
|
||||
const processedData = camelCaseObject(data);
|
||||
|
||||
dispatch(fetchNotificationPreferenceSuccess(courseId, processedData, isGlobal));
|
||||
dispatch(fetchNotificationPreferenceSuccess(processedData, processedData.showPreferences, true));
|
||||
return processedData;
|
||||
};
|
||||
|
||||
// Function to toggle preference based on context (course-specific or global)
|
||||
const togglePreference = async (channel, toggleValue, cadence) => {
|
||||
if (courseId) {
|
||||
return patchPreferenceToggle(
|
||||
courseId,
|
||||
notificationApp,
|
||||
notificationType,
|
||||
channel,
|
||||
channel === EMAIL_CADENCE ? cadence : toggleValue,
|
||||
);
|
||||
}
|
||||
|
||||
return postPreferenceToggle(
|
||||
notificationApp,
|
||||
notificationType,
|
||||
channel,
|
||||
channel === EMAIL_CADENCE ? undefined : toggleValue,
|
||||
cadence,
|
||||
);
|
||||
};
|
||||
// Function to toggle preference based on context
|
||||
const togglePreference = async (channel, toggleValue, cadence) => postPreferenceToggle(
|
||||
notificationApp,
|
||||
notificationType,
|
||||
channel,
|
||||
channel === EMAIL_CADENCE ? undefined : toggleValue,
|
||||
cadence,
|
||||
);
|
||||
|
||||
// Execute the main preference toggle
|
||||
const data = await togglePreference(notificationChannel, value, emailCadence);
|
||||
handleSuccessResponse(data, !courseId);
|
||||
handleSuccessResponse(data);
|
||||
|
||||
// Handle special case for email notifications
|
||||
if (notificationChannel === EMAIL && value) {
|
||||
const emailCadenceData = await togglePreference(
|
||||
EMAIL_CADENCE,
|
||||
courseId ? undefined : value,
|
||||
value,
|
||||
EMAIL_CADENCE_PREFERENCES.DAILY,
|
||||
);
|
||||
|
||||
handleSuccessResponse(emailCadenceData, !courseId);
|
||||
handleSuccessResponse(emailCadenceData);
|
||||
}
|
||||
} catch (errors) {
|
||||
dispatch(updatePreferenceValue(
|
||||
|
||||
@@ -93,7 +93,7 @@ const messages = defineMessages({
|
||||
},
|
||||
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.',
|
||||
defaultMessage: 'Account-level settings apply to all courses.',
|
||||
description: 'Account notification description',
|
||||
},
|
||||
notificationCadenceDescription: {
|
||||
|
||||
Reference in New Issue
Block a user