Merge pull request #1062 from openedx/sundas/INF-1415

feat: removed app level toggles
This commit is contained in:
sundasnoreen12
2024-06-14 03:07:36 -07:00
committed by GitHub
11 changed files with 52 additions and 134 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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