Merge pull request #1062 from openedx/sundas/INF-1415
feat: removed app level toggles
This commit is contained in:
@@ -113,6 +113,10 @@ $fa-font-path: "~font-awesome/fonts";
|
||||
border-right: 0px !important;
|
||||
}
|
||||
|
||||
.email-channel {
|
||||
width: 250px !important;
|
||||
}
|
||||
|
||||
.dropdown-item:active,
|
||||
.dropdown-item:focus,
|
||||
.btn-tertiary:not(:disabled):not(.disabled).active {
|
||||
|
||||
@@ -1,41 +1,33 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { Collapsible } from '@openedx/paragon';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import messages from './messages';
|
||||
import { useIsOnMobile } from '../hooks';
|
||||
import ToggleSwitch from './ToggleSwitch';
|
||||
import { LOADING_STATUS } from '../constants';
|
||||
import NotificationTypes from './NotificationTypes';
|
||||
import { notificationChannels } from './data/utils';
|
||||
import { updateAppPreferenceToggle } from './data/thunks';
|
||||
import { notificationChannels, shouldHideAppPreferences } from './data/utils';
|
||||
import NotificationPreferenceColumn from './NotificationPreferenceColumn';
|
||||
import { selectPreferenceAppToggleValue, selectSelectedCourseId, selectUpdatePreferencesStatus } from './data/selectors';
|
||||
import { selectPreferenceAppToggleValue, selectSelectedCourseId, selectAppPreferences } from './data/selectors';
|
||||
|
||||
const NotificationPreferenceApp = ({ appId }) => {
|
||||
const dispatch = useDispatch();
|
||||
const intl = useIntl();
|
||||
const courseId = useSelector(selectSelectedCourseId());
|
||||
const appToggle = useSelector(selectPreferenceAppToggleValue(appId));
|
||||
const updatePreferencesStatus = useSelector(selectUpdatePreferencesStatus());
|
||||
const appPreferences = useSelector(selectAppPreferences(appId));
|
||||
const mobileView = useIsOnMobile();
|
||||
const NOTIFICATION_CHANNELS = notificationChannels();
|
||||
|
||||
const onChangeAppSettings = useCallback((event) => {
|
||||
dispatch(updateAppPreferenceToggle(courseId, appId, event.target.checked));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [appId]);
|
||||
const hideAppPreferences = shouldHideAppPreferences(appPreferences, appId) || false;
|
||||
|
||||
if (!courseId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
!hideAppPreferences && (
|
||||
<Collapsible.Advanced
|
||||
open={appToggle}
|
||||
data-testid={`${appId}-app`}
|
||||
@@ -46,19 +38,10 @@ const NotificationPreferenceApp = ({ appId }) => {
|
||||
<span className="mr-auto preference-app font-weight-bold">
|
||||
{intl.formatMessage(messages.notificationAppTitle, { key: appId })}
|
||||
</span>
|
||||
<span className="d-flex" id={`${appId}-app-toggle`}>
|
||||
<ToggleSwitch
|
||||
name={appId}
|
||||
value={appToggle}
|
||||
onChange={onChangeAppSettings}
|
||||
disabled={updatePreferencesStatus === LOADING_STATUS}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
{!mobileView && <hr className="border-light-400 my-4" />}
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Body>
|
||||
<div className="d-flex flex-row justify-content-between">
|
||||
<div className="d-flex flex-row justify-content-between w-100">
|
||||
<NotificationTypes appId={appId} />
|
||||
{!mobileView && (
|
||||
<div className="d-flex">
|
||||
@@ -71,6 +54,7 @@ const NotificationPreferenceApp = ({ appId }) => {
|
||||
{mobileView && <hr className="border-light-400 my-4.5" />}
|
||||
</Collapsible.Body>
|
||||
</Collapsible.Advanced>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import EmailCadences from './EmailCadences';
|
||||
import { LOADING_STATUS } from '../constants';
|
||||
import { updateChannelPreferenceToggle, updatePreferenceToggle } from './data/thunks';
|
||||
import {
|
||||
selectNonEditablePreferences, selectPreferencesOfApp, selectSelectedCourseId, selectUpdatePreferencesStatus,
|
||||
selectNonEditablePreferences, selectAppPreferences, selectSelectedCourseId, selectUpdatePreferencesStatus,
|
||||
} from './data/selectors';
|
||||
import { notificationChannels, shouldHideAppPreferences } from './data/utils';
|
||||
|
||||
@@ -22,7 +22,7 @@ const NotificationPreferenceColumn = ({ appId, channel, appPreference }) => {
|
||||
const dispatch = useDispatch();
|
||||
const intl = useIntl();
|
||||
const courseId = useSelector(selectSelectedCourseId());
|
||||
const appPreferences = useSelector(selectPreferencesOfApp(appId));
|
||||
const appPreferences = useSelector(selectAppPreferences(appId));
|
||||
const nonEditable = useSelector(selectNonEditablePreferences(appId));
|
||||
const updatePreferencesStatus = useSelector(selectUpdatePreferencesStatus());
|
||||
const mobileView = useIsOnMobile();
|
||||
@@ -62,7 +62,6 @@ const NotificationPreferenceColumn = ({ appId, channel, appPreference }) => {
|
||||
className={classNames(
|
||||
'd-flex align-items-center justify-content-center mb-2 h-4.5 column-padding',
|
||||
{
|
||||
'pr-0': channel === NOTIFICATION_CHANNELS[NOTIFICATION_CHANNELS.length - 1],
|
||||
'pl-0': channel === 'web' && mobileView,
|
||||
},
|
||||
)}
|
||||
@@ -71,7 +70,7 @@ const NotificationPreferenceColumn = ({ appId, channel, appPreference }) => {
|
||||
name={channel}
|
||||
value={preference[channel]}
|
||||
onChange={(event) => onToggle(event, preference.id)}
|
||||
disabled={nonEditable?.[preference.id]?.includes(channel) || updatePreferencesStatus === LOADING_STATUS}
|
||||
disabled={updatePreferencesStatus === LOADING_STATUS}
|
||||
id={`${preference.id}-${channel}`}
|
||||
className="my-1"
|
||||
/>
|
||||
@@ -89,7 +88,7 @@ const NotificationPreferenceColumn = ({ appId, channel, appPreference }) => {
|
||||
|
||||
return (
|
||||
<div className={classNames('d-flex flex-column border-right channel-column')}>
|
||||
{!hideAppPreferences && (
|
||||
{!hideAppPreferences && mobileView && (
|
||||
<NavItem
|
||||
id={channel}
|
||||
key={channel}
|
||||
@@ -97,7 +96,7 @@ const NotificationPreferenceColumn = ({ appId, channel, appPreference }) => {
|
||||
onClick={onChannelToggle}
|
||||
className={classNames('mb-3 header-label column-padding', {
|
||||
'pr-0': channel === NOTIFICATION_CHANNELS[NOTIFICATION_CHANNELS.length - 1],
|
||||
'pl-0': channel === 'web' && mobileView,
|
||||
'pl-0': channel === 'web',
|
||||
})}
|
||||
>
|
||||
{intl.formatMessage(messages.notificationChannel, { text: channel })}
|
||||
|
||||
@@ -2,13 +2,15 @@ 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,
|
||||
Container, Hyperlink, Icon, Spinner, NavItem,
|
||||
} from '@openedx/paragon';
|
||||
|
||||
import { useIsOnMobile } from '../hooks';
|
||||
import messages from './messages';
|
||||
import { NotFoundPage } from '../account-settings';
|
||||
import NotificationPreferenceApp from './NotificationPreferenceApp';
|
||||
@@ -19,6 +21,7 @@ import {
|
||||
import {
|
||||
selectCourse, selectCourseList, selectCourseListStatus, selectNotificationPreferencesStatus, selectPreferenceAppsId,
|
||||
} from './data/selectors';
|
||||
import { notificationChannels } from './data/utils';
|
||||
|
||||
const NotificationPreferences = () => {
|
||||
const { courseId } = useParams();
|
||||
@@ -29,6 +32,8 @@ const NotificationPreferences = () => {
|
||||
const course = useSelector(selectCourse(courseId));
|
||||
const notificationStatus = useSelector(selectNotificationPreferencesStatus());
|
||||
const preferenceAppsIds = useSelector(selectPreferenceAppsId());
|
||||
const mobileView = useIsOnMobile();
|
||||
const NOTIFICATION_CHANNELS = notificationChannels();
|
||||
const isLoading = notificationStatus === LOADING_STATUS || courseStatus === LOADING_STATUS;
|
||||
|
||||
const preferencesList = useMemo(() => (
|
||||
@@ -77,6 +82,28 @@ const NotificationPreferences = () => {
|
||||
{course?.name}
|
||||
</span>
|
||||
</div>
|
||||
{!mobileView && (
|
||||
<div className="d-flex flex-row justify-content-between float-right">
|
||||
<div className="d-flex">
|
||||
{Object.values(NOTIFICATION_CHANNELS).map((channel) => (
|
||||
<div className={classNames('d-flex flex-column channel-column')}>
|
||||
<NavItem
|
||||
id={channel}
|
||||
key={channel}
|
||||
className={classNames('mb-3 header-label column-padding', {
|
||||
'pr-0': channel === NOTIFICATION_CHANNELS[NOTIFICATION_CHANNELS.length - 1],
|
||||
'mr-2': channel === 'web',
|
||||
'email-channel ': channel === 'email',
|
||||
|
||||
})}
|
||||
>
|
||||
{intl.formatMessage(messages.notificationChannel, { text: channel })}
|
||||
</NavItem>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{preferencesList}
|
||||
{isLoading && (
|
||||
<div className="d-flex">
|
||||
|
||||
@@ -5,9 +5,7 @@ import { BrowserRouter as Router } from 'react-router-dom';
|
||||
|
||||
import * as auth from '@edx/frontend-platform/auth';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
act, fireEvent, render, screen, waitFor, within,
|
||||
} from '@testing-library/react';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
|
||||
import { defaultState } from './data/reducers';
|
||||
import NotificationPreferences from './NotificationPreferences';
|
||||
@@ -74,20 +72,6 @@ const defaultPreferences = {
|
||||
},
|
||||
};
|
||||
|
||||
const updateChannelPreferences = (toggleVal = false) => ({
|
||||
preferences: [
|
||||
{
|
||||
id: 'core', appId: 'discussion', web: true, coreNotificationTypes: ['new_comment'],
|
||||
},
|
||||
{
|
||||
id: 'newComment', appId: 'discussion', web: toggleVal, coreNotificationTypes: [],
|
||||
},
|
||||
{
|
||||
id: 'newAssignment', appId: 'coursework', web: toggleVal, coreNotificationTypes: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const setupStore = (override = {}) => {
|
||||
const storeState = defaultState;
|
||||
storeState.courses = {
|
||||
@@ -154,13 +138,6 @@ describe('Notification Preferences', () => {
|
||||
expect(screen.queryAllByTestId('notification-preference')).toHaveLength(4);
|
||||
});
|
||||
|
||||
it('update group on click', async () => {
|
||||
const wrapper = await render(notificationPreferences(store));
|
||||
const element = wrapper.container.querySelector('#discussion-app-toggle');
|
||||
await fireEvent.click(element);
|
||||
expect(mockDispatch).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('update preference on click', async () => {
|
||||
const wrapper = await render(notificationPreferences(store));
|
||||
const element = wrapper.container.querySelector('#core-web');
|
||||
@@ -174,40 +151,4 @@ describe('Notification Preferences', () => {
|
||||
await render(notificationPreferences(store));
|
||||
expect(screen.queryByTestId('not-found-page')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('updates all preferences in the column on web channel click', async () => {
|
||||
store = setupStore(updateChannelPreferences(true));
|
||||
const wrapper = render(notificationPreferences(store));
|
||||
|
||||
const getChannelSwitch = (id) => screen.queryByTestId(`${id}-web`);
|
||||
const notificationTypes = ['newComment', 'newAssignment'];
|
||||
|
||||
const verifyState = (toggleState) => {
|
||||
notificationTypes.forEach((notificationType) => {
|
||||
if (toggleState) {
|
||||
expect(getChannelSwitch(notificationType)).toBeChecked();
|
||||
} else {
|
||||
expect(getChannelSwitch(notificationType)).not.toBeChecked();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
verifyState(true);
|
||||
expect(getChannelSwitch('core')).toBeChecked();
|
||||
|
||||
const discussionApp = screen.queryByTestId('discussion-app');
|
||||
const webChannel = within(discussionApp).queryByText('Web');
|
||||
|
||||
await act(async () => {
|
||||
await fireEvent.click(webChannel);
|
||||
});
|
||||
|
||||
store = setupStore(updateChannelPreferences(false));
|
||||
wrapper.rerender(notificationPreferences(store));
|
||||
|
||||
await waitFor(() => {
|
||||
verifyState(false);
|
||||
expect(getChannelSwitch('core')).toBeChecked();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,21 +9,19 @@ import { Icon, OverlayTrigger, Tooltip } from '@openedx/paragon';
|
||||
|
||||
import messages from './messages';
|
||||
import { useIsOnMobile } from '../hooks';
|
||||
import { notificationChannels, shouldHideAppPreferences } from './data/utils';
|
||||
import { notificationChannels } from './data/utils';
|
||||
|
||||
import { selectPreferencesOfApp } from './data/selectors';
|
||||
import { selectAppPreferences } from './data/selectors';
|
||||
import NotificationPreferenceColumn from './NotificationPreferenceColumn';
|
||||
|
||||
const NotificationTypes = ({ appId }) => {
|
||||
const intl = useIntl();
|
||||
const preferences = useSelector(selectPreferencesOfApp(appId));
|
||||
const preferences = useSelector(selectAppPreferences(appId));
|
||||
const mobileView = useIsOnMobile();
|
||||
const NOTIFICATION_CHANNELS = notificationChannels();
|
||||
const hideAppPreferences = shouldHideAppPreferences(preferences, appId) || false;
|
||||
|
||||
return (
|
||||
<div className="d-flex flex-column mr-auto px-0">
|
||||
{!mobileView && !hideAppPreferences && <span className="mb-3 header-label">{intl.formatMessage(messages.typeLabel)}</span>}
|
||||
{preferences.map(preference => (
|
||||
(preference?.coreNotificationTypes?.length > 0 || preference.id !== 'core') && (
|
||||
<>
|
||||
|
||||
@@ -47,12 +47,3 @@ export const updatePreferenceValue = (appId, preferenceName, notificationChannel
|
||||
value,
|
||||
})
|
||||
);
|
||||
|
||||
export const updateAppToggle = (courseId, appId, value) => dispatch => (
|
||||
dispatch({
|
||||
type: Actions.UPDATE_APP_PREFERENCE,
|
||||
courseId,
|
||||
appId,
|
||||
value,
|
||||
})
|
||||
);
|
||||
|
||||
@@ -28,7 +28,7 @@ export const selectPreferenceAppsId = () => state => (
|
||||
state.notificationPreferences.preferences.apps.map(app => app.id)
|
||||
);
|
||||
|
||||
export const selectPreferencesOfApp = appId => state => (
|
||||
export const selectAppPreferences = appId => state => (
|
||||
selectPreferences()(state).filter(preference => (
|
||||
preference.appId === appId
|
||||
))
|
||||
|
||||
@@ -15,16 +15,6 @@ export const getCourseList = async (page, pageSize) => {
|
||||
return data;
|
||||
};
|
||||
|
||||
export const patchAppPreferenceToggle = async (courseId, appId, value) => {
|
||||
const patchData = snakeCaseObject({
|
||||
notificationApp: appId,
|
||||
value,
|
||||
});
|
||||
const url = `${getConfig().LMS_BASE_URL}/api/notifications/configurations/${courseId}`;
|
||||
const { data } = await getAuthenticatedHttpClient().patch(url, patchData);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const patchPreferenceToggle = async (
|
||||
courseId,
|
||||
notificationApp,
|
||||
|
||||
@@ -7,14 +7,12 @@ import {
|
||||
fetchNotificationPreferenceFailed,
|
||||
fetchNotificationPreferenceFetching,
|
||||
fetchNotificationPreferenceSuccess,
|
||||
updateAppToggle,
|
||||
updatePreferenceValue,
|
||||
updateSelectedCourse,
|
||||
} from './actions';
|
||||
import {
|
||||
getCourseList,
|
||||
getCourseNotificationPreferences,
|
||||
patchAppPreferenceToggle,
|
||||
patchChannelPreferenceToggle,
|
||||
patchPreferenceToggle,
|
||||
} from './service';
|
||||
@@ -103,20 +101,6 @@ export const fetchCourseNotificationPreferences = (courseId) => (
|
||||
}
|
||||
);
|
||||
|
||||
export const updateAppPreferenceToggle = (courseId, appId, value) => (
|
||||
async (dispatch) => {
|
||||
try {
|
||||
dispatch(updateAppToggle(courseId, appId, value));
|
||||
const data = await patchAppPreferenceToggle(courseId, appId, value);
|
||||
const normalizedData = normalizePreferences(camelCaseObject(data));
|
||||
dispatch(fetchNotificationPreferenceSuccess(courseId, normalizedData));
|
||||
} catch (errors) {
|
||||
dispatch(updateAppToggle(courseId, appId, !value));
|
||||
dispatch(fetchNotificationPreferenceFailed());
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const updatePreferenceToggle = (
|
||||
courseId,
|
||||
notificationApp,
|
||||
|
||||
@@ -22,7 +22,7 @@ const messages = defineMessages({
|
||||
id: 'notification.preference.title',
|
||||
defaultMessage: `{
|
||||
text, select,
|
||||
core {Core notifications}
|
||||
core {Activity notifications}
|
||||
newDiscussionPost {New discussion posts}
|
||||
newQuestionPost {New question posts}
|
||||
contentReported {Reported content}
|
||||
|
||||
Reference in New Issue
Block a user