chore: cleaned up course level preferences apis

This commit is contained in:
eemaanamir
2025-07-30 12:32:37 +05:00
parent f6babc2db9
commit 46acf2a5a4
18 changed files with 60 additions and 439 deletions

1
.env
View File

@@ -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'

View File

@@ -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'

View File

@@ -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'

View File

@@ -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,

View File

@@ -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');
},
},

View File

@@ -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;

View File

@@ -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') && (

View File

@@ -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;

View File

@@ -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();
});
});
});

View File

@@ -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>
)

View File

@@ -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,

View File

@@ -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,

View File

@@ -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: {},

View File

@@ -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
);

View File

@@ -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;
};

View File

@@ -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,

View File

@@ -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(

View File

@@ -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: {