From d3e8b36e692bf7450be75a5bc65d800cdde4b0ac Mon Sep 17 00:00:00 2001 From: eemaanamir Date: Mon, 22 Jan 2024 15:01:11 +0500 Subject: [PATCH 1/7] feat: make notification channel headings clickable in notification preferences --- .../NotificationPreferenceApp.jsx | 60 +++++++++++++++++-- .../data/selectors.js | 4 ++ src/notification-preferences/data/service.js | 16 +++++ src/notification-preferences/data/thunks.js | 24 +++++++- 4 files changed, 98 insertions(+), 6 deletions(-) diff --git a/src/notification-preferences/NotificationPreferenceApp.jsx b/src/notification-preferences/NotificationPreferenceApp.jsx index 89e2438f6..f6a84c771 100644 --- a/src/notification-preferences/NotificationPreferenceApp.jsx +++ b/src/notification-preferences/NotificationPreferenceApp.jsx @@ -2,18 +2,20 @@ import React, { useCallback, useMemo } from 'react'; import PropTypes from 'prop-types'; import { useDispatch, useSelector } from 'react-redux'; import { useIntl } from '@edx/frontend-platform/i18n'; -import { Collapsible } from '@edx/paragon'; +import { Collapsible, NavItem } from '@edx/paragon'; +import classNames from 'classnames'; import messages from './messages'; import ToggleSwitch from './ToggleSwitch'; import { - selectPreferenceAppToggleValue, + selectPreferenceAppToggleValue, selectPreferenceNonEditable, selectPreferencesOfApp, selectSelectedCourseId, selectUpdatePreferencesStatus, } from './data/selectors'; import NotificationPreferenceRow from './NotificationPreferenceRow'; -import { updateAppPreferenceToggle } from './data/thunks'; +import { updateAppPreferenceToggle, updateChannelPreferenceToggle } from './data/thunks'; import { LOADING_STATUS } from '../constants'; +import NOTIFICATION_CHANNELS from './data/constants'; const NotificationPreferenceApp = ({ appId }) => { const dispatch = useDispatch(); @@ -22,6 +24,35 @@ const NotificationPreferenceApp = ({ appId }) => { const appPreferences = useSelector(selectPreferencesOfApp(appId)); const appToggle = useSelector(selectPreferenceAppToggleValue(appId)); const updatePreferencesStatus = useSelector(selectUpdatePreferencesStatus()); + const nonEditable = useSelector(selectPreferenceNonEditable(appId)); + + const onColumnToggle = useCallback( + (event) => { + const { + id: notificationChannel, + } = event.target; + const truePreferences = appPreferences.filter((preference) => { + const isPreferenceNonEditable = nonEditable?.[preference.id]?.includes(notificationChannel) || false; + return preference[notificationChannel] === true && !isPreferenceNonEditable; + }); + if (truePreferences.length > 0) { + dispatch(updateChannelPreferenceToggle( + courseId, + appId, + notificationChannel, + false, + )); + } else { + dispatch(updateChannelPreferenceToggle( + courseId, + appId, + notificationChannel, + true, + )); + } + }, + [appId, appPreferences, courseId, dispatch, nonEditable], + ); const preferences = useMemo(() => ( appPreferences.map(preference => ( @@ -36,7 +67,6 @@ const NotificationPreferenceApp = ({ appId }) => { dispatch(updateAppPreferenceToggle(courseId, appId, event.target.checked)); // eslint-disable-next-line react-hooks/exhaustive-deps }, [appId]); - if (!courseId) { return null; } @@ -62,7 +92,27 @@ const NotificationPreferenceApp = ({ appId }) => {
{intl.formatMessage(messages.typeLabel)} - {intl.formatMessage(messages.webLabel)} + {NOTIFICATION_CHANNELS.map((channel) => ( + + { + // eslint-disable-next-line no-nested-ternary + channel === 'web' ? intl.formatMessage(messages.webLabel) + // eslint-disable-next-line no-nested-ternary + : channel === 'email' ? intl.formatMessage(messages.notificationHelpEmail) + : channel === 'push' ? intl.formatMessage(messages.notificationHelpPush) : null + } + + ))}
diff --git a/src/notification-preferences/data/selectors.js b/src/notification-preferences/data/selectors.js index d918705c5..d6f7ee46a 100644 --- a/src/notification-preferences/data/selectors.js +++ b/src/notification-preferences/data/selectors.js @@ -54,6 +54,10 @@ export const selectPreferenceNonEditableChannels = (appId, name) => state => ( state?.notificationPreferences.preferences.nonEditable[appId]?.[name] || [] ); +export const selectPreferenceNonEditable = (appId) => state => ( + state?.notificationPreferences.preferences.nonEditable[appId] || [] +); + export const selectSelectedCourseId = () => state => ( state.notificationPreferences.preferences.selectedCourse ); diff --git a/src/notification-preferences/data/service.js b/src/notification-preferences/data/service.js index c3deb5616..6e357b1c5 100644 --- a/src/notification-preferences/data/service.js +++ b/src/notification-preferences/data/service.js @@ -42,3 +42,19 @@ export const patchPreferenceToggle = async ( const { data } = await getAuthenticatedHttpClient().patch(url, patchData); return data; }; + +export const patchChannelPreferenceToggle = async ( + courseId, + notificationApp, + notificationChannel, + value, +) => { + const patchData = snakeCaseObject({ + notificationApp, + notificationChannel, + value, + }); + const url = `${getConfig().LMS_BASE_URL}/api/notifications/channel/configurations/${courseId}`; + const { data } = await getAuthenticatedHttpClient().patch(url, patchData); + return data; +}; diff --git a/src/notification-preferences/data/thunks.js b/src/notification-preferences/data/thunks.js index 86ff5c0f8..7b257c88f 100644 --- a/src/notification-preferences/data/thunks.js +++ b/src/notification-preferences/data/thunks.js @@ -13,7 +13,7 @@ import { import { getCourseList, getCourseNotificationPreferences, - patchAppPreferenceToggle, + patchAppPreferenceToggle, patchChannelPreferenceToggle, patchPreferenceToggle, } from './service'; @@ -148,3 +148,25 @@ export const updatePreferenceToggle = ( } } ); + +export const updateChannelPreferenceToggle = ( + courseId, + notificationApp, + notificationChannel, + value, +) => ( + async (dispatch) => { + try { + const data = await patchChannelPreferenceToggle( + courseId, + notificationApp, + notificationChannel, + value, + ); + const normalizedData = normalizePreferences(camelCaseObject(data)); + dispatch(fetchNotificationPreferenceSuccess(courseId, normalizedData)); + } catch (errors) { + dispatch(fetchNotificationPreferenceFailed()); + } + } +); From a3497adb6d5a5b33dd9869ce393ef2579b95daba Mon Sep 17 00:00:00 2001 From: eemaanamir Date: Tue, 23 Jan 2024 15:07:40 +0500 Subject: [PATCH 2/7] chore: refactoring the code for readability according to ESLint --- .../NotificationPreferenceApp.jsx | 50 +++++++------------ .../data/selectors.js | 2 +- src/notification-preferences/data/service.js | 13 +---- src/notification-preferences/data/thunks.js | 17 ++----- src/notification-preferences/messages.js | 11 ++++ 5 files changed, 35 insertions(+), 58 deletions(-) diff --git a/src/notification-preferences/NotificationPreferenceApp.jsx b/src/notification-preferences/NotificationPreferenceApp.jsx index f6a84c771..97f025267 100644 --- a/src/notification-preferences/NotificationPreferenceApp.jsx +++ b/src/notification-preferences/NotificationPreferenceApp.jsx @@ -7,7 +7,8 @@ import classNames from 'classnames'; import messages from './messages'; import ToggleSwitch from './ToggleSwitch'; import { - selectPreferenceAppToggleValue, selectPreferenceNonEditable, + selectPreferenceAppToggleValue, + selectNonEditablePreferences, selectPreferencesOfApp, selectSelectedCourseId, selectUpdatePreferencesStatus, @@ -24,32 +25,19 @@ const NotificationPreferenceApp = ({ appId }) => { const appPreferences = useSelector(selectPreferencesOfApp(appId)); const appToggle = useSelector(selectPreferenceAppToggleValue(appId)); const updatePreferencesStatus = useSelector(selectUpdatePreferencesStatus()); - const nonEditable = useSelector(selectPreferenceNonEditable(appId)); + const nonEditable = useSelector(selectNonEditablePreferences(appId)); - const onColumnToggle = useCallback( + const onChannelToggle = useCallback( (event) => { - const { - id: notificationChannel, - } = event.target; - const truePreferences = appPreferences.filter((preference) => { - const isPreferenceNonEditable = nonEditable?.[preference.id]?.includes(notificationChannel) || false; - return preference[notificationChannel] === true && !isPreferenceNonEditable; - }); - if (truePreferences.length > 0) { - dispatch(updateChannelPreferenceToggle( - courseId, - appId, - notificationChannel, - false, - )); - } else { - dispatch(updateChannelPreferenceToggle( - courseId, - appId, - notificationChannel, - true, - )); - } + const { id: notificationChannel } = event.target; + const activePreferences = appPreferences.filter((preference) => preference[notificationChannel] === true + && !nonEditable?.[preference.id]?.includes(notificationChannel)); + dispatch(updateChannelPreferenceToggle( + courseId, + appId, + notificationChannel, + !(activePreferences.length > 0), + )); }, [appId, appPreferences, courseId, dispatch, nonEditable], ); @@ -70,6 +58,7 @@ const NotificationPreferenceApp = ({ appId }) => { if (!courseId) { return null; } + return ( @@ -95,6 +84,7 @@ const NotificationPreferenceApp = ({ appId }) => { {NOTIFICATION_CHANNELS.map((channel) => ( { { 'ml-auto mr-0': channel === 'push' }, )} role="button" - onClick={onColumnToggle} + onClick={onChannelToggle} > - { - // eslint-disable-next-line no-nested-ternary - channel === 'web' ? intl.formatMessage(messages.webLabel) - // eslint-disable-next-line no-nested-ternary - : channel === 'email' ? intl.formatMessage(messages.notificationHelpEmail) - : channel === 'push' ? intl.formatMessage(messages.notificationHelpPush) : null - } + {intl.formatMessage(messages.notificationChannel, { text: channel })} ))} diff --git a/src/notification-preferences/data/selectors.js b/src/notification-preferences/data/selectors.js index d6f7ee46a..ffbc5d91f 100644 --- a/src/notification-preferences/data/selectors.js +++ b/src/notification-preferences/data/selectors.js @@ -54,7 +54,7 @@ export const selectPreferenceNonEditableChannels = (appId, name) => state => ( state?.notificationPreferences.preferences.nonEditable[appId]?.[name] || [] ); -export const selectPreferenceNonEditable = (appId) => state => ( +export const selectNonEditablePreferences = appId => state => ( state?.notificationPreferences.preferences.nonEditable[appId] || [] ); diff --git a/src/notification-preferences/data/service.js b/src/notification-preferences/data/service.js index 6e357b1c5..50a1e04c0 100644 --- a/src/notification-preferences/data/service.js +++ b/src/notification-preferences/data/service.js @@ -43,17 +43,8 @@ export const patchPreferenceToggle = async ( return data; }; -export const patchChannelPreferenceToggle = async ( - courseId, - notificationApp, - notificationChannel, - value, -) => { - const patchData = snakeCaseObject({ - notificationApp, - notificationChannel, - value, - }); +export const patchChannelPreferenceToggle = async (courseId, notificationApp, notificationChannel, value) => { + const patchData = snakeCaseObject({ notificationApp, notificationChannel, value }); const url = `${getConfig().LMS_BASE_URL}/api/notifications/channel/configurations/${courseId}`; const { data } = await getAuthenticatedHttpClient().patch(url, patchData); return data; diff --git a/src/notification-preferences/data/thunks.js b/src/notification-preferences/data/thunks.js index 7b257c88f..222201b52 100644 --- a/src/notification-preferences/data/thunks.js +++ b/src/notification-preferences/data/thunks.js @@ -13,7 +13,8 @@ import { import { getCourseList, getCourseNotificationPreferences, - patchAppPreferenceToggle, patchChannelPreferenceToggle, + patchAppPreferenceToggle, + patchChannelPreferenceToggle, patchPreferenceToggle, } from './service'; @@ -149,20 +150,10 @@ export const updatePreferenceToggle = ( } ); -export const updateChannelPreferenceToggle = ( - courseId, - notificationApp, - notificationChannel, - value, -) => ( +export const updateChannelPreferenceToggle = (courseId, notificationApp, notificationChannel, value) => ( async (dispatch) => { try { - const data = await patchChannelPreferenceToggle( - courseId, - notificationApp, - notificationChannel, - value, - ); + const data = await patchChannelPreferenceToggle(courseId, notificationApp, notificationChannel, value); const normalizedData = normalizePreferences(camelCaseObject(data)); dispatch(fetchNotificationPreferenceSuccess(courseId, normalizedData)); } catch (errors) { diff --git a/src/notification-preferences/messages.js b/src/notification-preferences/messages.js index 91780fbb6..1ba173316 100644 --- a/src/notification-preferences/messages.js +++ b/src/notification-preferences/messages.js @@ -27,6 +27,17 @@ const messages = defineMessages({ }`, description: 'Display text for Notification Types', }, + notificationChannel: { + id: 'notification.preference.channel', + defaultMessage: `{ + text, select, + web {Web} + email {Email} + push {Push} + other {{text}} + }`, + description: 'Display text for Notification Channel', + }, typeLabel: { id: 'notification.preference.type.label', defaultMessage: 'Type', From a6699f94c9d0ff9d45c1e8d4d21f446e47b4fd03 Mon Sep 17 00:00:00 2001 From: eemaanamir Date: Tue, 23 Jan 2024 19:47:59 +0500 Subject: [PATCH 3/7] refactor: onChannelToggle updated for readability --- .../NotificationPreferenceApp.jsx | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/src/notification-preferences/NotificationPreferenceApp.jsx b/src/notification-preferences/NotificationPreferenceApp.jsx index 97f025267..7e37a1be1 100644 --- a/src/notification-preferences/NotificationPreferenceApp.jsx +++ b/src/notification-preferences/NotificationPreferenceApp.jsx @@ -27,20 +27,16 @@ const NotificationPreferenceApp = ({ appId }) => { const updatePreferencesStatus = useSelector(selectUpdatePreferencesStatus()); const nonEditable = useSelector(selectNonEditablePreferences(appId)); - const onChannelToggle = useCallback( - (event) => { - const { id: notificationChannel } = event.target; - const activePreferences = appPreferences.filter((preference) => preference[notificationChannel] === true - && !nonEditable?.[preference.id]?.includes(notificationChannel)); - dispatch(updateChannelPreferenceToggle( - courseId, - appId, - notificationChannel, - !(activePreferences.length > 0), - )); - }, - [appId, appPreferences, courseId, dispatch, nonEditable], - ); + const onChannelToggle = useCallback((event) => { + const { id: notificationChannel } = event.target; + const isPreferenceNonEditable = (preference) => nonEditable?.[preference.id]?.includes(notificationChannel) + || false; + + const truePreferences = appPreferences.filter((preference) => preference[notificationChannel] === true + && !isPreferenceNonEditable(preference)); + + dispatch(updateChannelPreferenceToggle(courseId, appId, notificationChannel, truePreferences.length === 0)); + }, [appId, appPreferences, courseId, dispatch, nonEditable]); const preferences = useMemo(() => ( appPreferences.map(preference => ( From aeb8109d9471171ad708c3acdd81adef35783987 Mon Sep 17 00:00:00 2001 From: eemaanamir Date: Wed, 24 Jan 2024 13:26:30 +0500 Subject: [PATCH 4/7] refactor: onChannelToggle updated --- src/notification-preferences/NotificationPreferenceApp.jsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/notification-preferences/NotificationPreferenceApp.jsx b/src/notification-preferences/NotificationPreferenceApp.jsx index 7e37a1be1..6eaddfb98 100644 --- a/src/notification-preferences/NotificationPreferenceApp.jsx +++ b/src/notification-preferences/NotificationPreferenceApp.jsx @@ -32,10 +32,11 @@ const NotificationPreferenceApp = ({ appId }) => { const isPreferenceNonEditable = (preference) => nonEditable?.[preference.id]?.includes(notificationChannel) || false; - const truePreferences = appPreferences.filter((preference) => preference[notificationChannel] === true - && !isPreferenceNonEditable(preference)); + const activePreferences = appPreferences.filter( + (preference) => preference[notificationChannel] && !isPreferenceNonEditable(preference), + ); - dispatch(updateChannelPreferenceToggle(courseId, appId, notificationChannel, truePreferences.length === 0)); + dispatch(updateChannelPreferenceToggle(courseId, appId, notificationChannel, activePreferences.length === 0)); }, [appId, appPreferences, courseId, dispatch, nonEditable]); const preferences = useMemo(() => ( From 36f7c939e5f2b2e4a4c1245a0d73ac0afe49eab1 Mon Sep 17 00:00:00 2001 From: eemaanamir Date: Wed, 24 Jan 2024 15:53:09 +0500 Subject: [PATCH 5/7] refactor: further simplified onChannelToggle --- src/notification-preferences/NotificationPreferenceApp.jsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/notification-preferences/NotificationPreferenceApp.jsx b/src/notification-preferences/NotificationPreferenceApp.jsx index 6eaddfb98..3ca9f98a2 100644 --- a/src/notification-preferences/NotificationPreferenceApp.jsx +++ b/src/notification-preferences/NotificationPreferenceApp.jsx @@ -29,8 +29,7 @@ const NotificationPreferenceApp = ({ appId }) => { const onChannelToggle = useCallback((event) => { const { id: notificationChannel } = event.target; - const isPreferenceNonEditable = (preference) => nonEditable?.[preference.id]?.includes(notificationChannel) - || false; + const isPreferenceNonEditable = (preference) => nonEditable?.[preference.id]?.includes(notificationChannel); const activePreferences = appPreferences.filter( (preference) => preference[notificationChannel] && !isPreferenceNonEditable(preference), From 09c02cb367d450d35a11d001b8edc95463c4a01f Mon Sep 17 00:00:00 2001 From: eemaanamir Date: Wed, 24 Jan 2024 16:35:19 +0500 Subject: [PATCH 6/7] perf: updated onChannelToggle to improve performance --- src/notification-preferences/NotificationPreferenceApp.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/notification-preferences/NotificationPreferenceApp.jsx b/src/notification-preferences/NotificationPreferenceApp.jsx index 3ca9f98a2..480ddd919 100644 --- a/src/notification-preferences/NotificationPreferenceApp.jsx +++ b/src/notification-preferences/NotificationPreferenceApp.jsx @@ -31,11 +31,11 @@ const NotificationPreferenceApp = ({ appId }) => { const { id: notificationChannel } = event.target; const isPreferenceNonEditable = (preference) => nonEditable?.[preference.id]?.includes(notificationChannel); - const activePreferences = appPreferences.filter( + const hasActivePreferences = appPreferences.some( (preference) => preference[notificationChannel] && !isPreferenceNonEditable(preference), ); - dispatch(updateChannelPreferenceToggle(courseId, appId, notificationChannel, activePreferences.length === 0)); + dispatch(updateChannelPreferenceToggle(courseId, appId, notificationChannel, !hasActivePreferences)); }, [appId, appPreferences, courseId, dispatch, nonEditable]); const preferences = useMemo(() => ( From 6573b593285d99817c4c988f2d0d97eddc9d658b Mon Sep 17 00:00:00 2001 From: ayeshoali Date: Wed, 7 Feb 2024 15:26:14 +0500 Subject: [PATCH 7/7] fix: fixed lint error --- src/notification-preferences/NotificationPreferenceApp.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/notification-preferences/NotificationPreferenceApp.jsx b/src/notification-preferences/NotificationPreferenceApp.jsx index 480ddd919..e0d462c79 100644 --- a/src/notification-preferences/NotificationPreferenceApp.jsx +++ b/src/notification-preferences/NotificationPreferenceApp.jsx @@ -2,7 +2,7 @@ import React, { useCallback, useMemo } from 'react'; import PropTypes from 'prop-types'; import { useDispatch, useSelector } from 'react-redux'; import { useIntl } from '@edx/frontend-platform/i18n'; -import { Collapsible, NavItem } from '@edx/paragon'; +import { Collapsible, NavItem } from '@openedx/paragon'; import classNames from 'classnames'; import messages from './messages'; import ToggleSwitch from './ToggleSwitch';