feat: added notification preference ui (#784)
This commit is contained in:
committed by
GitHub
parent
f2f761e8db
commit
192714629c
@@ -819,12 +819,12 @@ class AccountSettingsPage extends React.Component {
|
||||
</h1>
|
||||
<div>
|
||||
<div className="row">
|
||||
<div className="col-md-3">
|
||||
<div className="col-md-2">
|
||||
<JumpNav
|
||||
displayDemographicsLink={this.props.formValues.shouldDisplayDemographicsSection}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-md-9">
|
||||
<div className="col-md-10">
|
||||
{loading ? this.renderLoading() : null}
|
||||
{loaded ? this.renderContent() : null}
|
||||
{loadingError ? this.renderError() : null}
|
||||
|
||||
@@ -565,6 +565,11 @@ const messages = defineMessages({
|
||||
defaultMessage: 'No value set.',
|
||||
description: 'The placeholder for an empty but uneditable field when there is no administrator',
|
||||
},
|
||||
'notification.preferences.notifications.label': {
|
||||
id: 'notification.preferences.notifications.label',
|
||||
defaultMessage: 'Notifications',
|
||||
description: 'Label for Notifications',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { breakpoints, useWindowSize } from '@edx/paragon';
|
||||
import { breakpoints, useWindowSize, Icon } from '@edx/paragon';
|
||||
import { OpenInNew } from '@edx/paragon/icons';
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { NavHashLink } from 'react-router-hash-link';
|
||||
import Scrollspy from 'react-scrollspy';
|
||||
import { Link } from 'react-router-dom';
|
||||
import messages from './AccountSettingsPage.messages';
|
||||
|
||||
const JumpNav = ({
|
||||
@@ -13,8 +15,9 @@ const JumpNav = ({
|
||||
displayDemographicsLink,
|
||||
}) => {
|
||||
const stickToTop = useWindowSize().width > breakpoints.small.minWidth;
|
||||
const showNotificationMenu = false;
|
||||
return (
|
||||
<div className={classNames('jump-nav', { 'jump-nav-sm position-sticky pt-3': stickToTop })}>
|
||||
<div className={classNames('jump-nav px-2.25', { 'jump-nav-sm position-sticky pt-3': stickToTop })}>
|
||||
<Scrollspy
|
||||
items={[
|
||||
'basic-information',
|
||||
@@ -67,6 +70,22 @@ const JumpNav = ({
|
||||
</NavHashLink>
|
||||
</li>
|
||||
</Scrollspy>
|
||||
{showNotificationMenu
|
||||
&& (
|
||||
<>
|
||||
<hr />
|
||||
<Scrollspy
|
||||
className="list-unstyled"
|
||||
>
|
||||
<li>
|
||||
<Link to="/notifications" target="_blank" rel="noopener noreferrer">
|
||||
<span>{intl.formatMessage(messages['notification.preferences.notifications.label'])}</span>
|
||||
<Icon className="d-inline-block align-bottom ml-1" src={OpenInNew} />
|
||||
</Link>
|
||||
</li>
|
||||
</Scrollspy>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
exports[`JumpNav should not render Optional Information link 1`] = `
|
||||
<div
|
||||
className="jump-nav jump-nav-sm position-sticky pt-3"
|
||||
className="jump-nav px-2.25 jump-nav-sm position-sticky pt-3"
|
||||
>
|
||||
<ul
|
||||
className="list-unstyled"
|
||||
@@ -80,7 +80,7 @@ exports[`JumpNav should not render Optional Information link 1`] = `
|
||||
|
||||
exports[`JumpNav should render Optional Information link 1`] = `
|
||||
<div
|
||||
className="jump-nav jump-nav-sm position-sticky pt-3"
|
||||
className="jump-nav px-2.25 jump-nav-sm position-sticky pt-3"
|
||||
>
|
||||
<ul
|
||||
className="list-unstyled"
|
||||
|
||||
@@ -4,8 +4,10 @@ import {
|
||||
reducer as accountSettingsReducer,
|
||||
storeName as accountSettingsStoreName,
|
||||
} from '../account-settings';
|
||||
import notificationPreferencesReducer from '../notification-preferences/data/reducers';
|
||||
|
||||
const createRootReducer = () => combineReducers({
|
||||
[accountSettingsStoreName]: accountSettingsReducer,
|
||||
notificationPreferences: notificationPreferencesReducer,
|
||||
});
|
||||
export default createRootReducer;
|
||||
|
||||
@@ -21,8 +21,11 @@ import messages from './i18n';
|
||||
|
||||
import './index.scss';
|
||||
import Head from './head/Head';
|
||||
import NotificationCourses from './notification-preferences/NotificationCourses';
|
||||
import NotificationPreferences from './notification-preferences/NotificationPreferences';
|
||||
|
||||
subscribe(APP_READY, () => {
|
||||
const allowNotificationRoutes = false;
|
||||
ReactDOM.render(
|
||||
<AppProvider store={configureStore()}>
|
||||
<Head />
|
||||
@@ -32,6 +35,12 @@ subscribe(APP_READY, () => {
|
||||
<Header />
|
||||
<main className="flex-grow-1">
|
||||
<Switch>
|
||||
{allowNotificationRoutes && (
|
||||
<>
|
||||
<Route path="/notifications/:courseId" component={NotificationPreferences} />
|
||||
<Route path="/notifications" component={NotificationCourses} />
|
||||
</>
|
||||
)}
|
||||
<Route path="/id-verification" component={IdVerificationPage} />
|
||||
<Route exact path="/" component={AccountSettingsPage} />
|
||||
<Route path="/notfound" component={NotFoundPage} />
|
||||
|
||||
@@ -62,3 +62,27 @@ $fa-font-path: "~font-awesome/fonts";
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.notification-heading {
|
||||
line-height: 36px;
|
||||
font-weight: 700;
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.notification-course-title {
|
||||
line-height: 28px;
|
||||
font-weight: 700;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.px-2\.25 {
|
||||
padding-left: 0.625rem;
|
||||
}
|
||||
|
||||
.notification-help-text {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 28px;
|
||||
height: 28px;
|
||||
color: #707070;
|
||||
}
|
||||
|
||||
65
src/notification-preferences/NotificationCourses.jsx
Normal file
65
src/notification-preferences/NotificationCourses.jsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Container, Icon, Spinner } from '@edx/paragon';
|
||||
import { ArrowForwardIos } from '@edx/paragon/icons';
|
||||
import { fetchCourseList } from './data/thunks';
|
||||
import { courseListStatus, getCourseList } from './data/selectors';
|
||||
import { IDLE_STATUS, LOADING_STATUS } from '../constants';
|
||||
import { messages } from './messages';
|
||||
|
||||
const NotificationCourses = ({ intl }) => {
|
||||
const dispatch = useDispatch();
|
||||
const courseStatus = useSelector(courseListStatus());
|
||||
const coursesList = useSelector(getCourseList());
|
||||
useEffect(() => {
|
||||
if (courseStatus === IDLE_STATUS || coursesList.length === 0) {
|
||||
dispatch(fetchCourseList());
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [courseStatus]);
|
||||
if (courseStatus === LOADING_STATUS) {
|
||||
return (
|
||||
<div className="d-flex h-100">
|
||||
<Spinner
|
||||
variant="primary"
|
||||
animation="border"
|
||||
className="mx-auto my-auto"
|
||||
style={{ width: '4rem', height: '4rem' }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Container size="md">
|
||||
<h2 className="notification-heading mt-6 mb-5.5">
|
||||
{intl.formatMessage(messages.notificationHeading)}
|
||||
</h2>
|
||||
<div>
|
||||
{
|
||||
coursesList.map(course => (
|
||||
<Link
|
||||
to={`/notifications/${course.id}`}
|
||||
>
|
||||
<div className="mb-4 d-flex text-gray-700">
|
||||
<span className="ml-0 mr-auto">
|
||||
{course.name}
|
||||
</span>
|
||||
<span className="ml-auto mr-0">
|
||||
<Icon src={ArrowForwardIos} />
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
NotificationCourses.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(NotificationCourses);
|
||||
74
src/notification-preferences/NotificationPreferenceGroup.jsx
Normal file
74
src/notification-preferences/NotificationPreferenceGroup.jsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import React, { useCallback, useMemo, useState } 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 { messages } from './messages';
|
||||
import ToggleSwitch from './ToggleSwitch';
|
||||
import {
|
||||
getPreferenceGroup,
|
||||
getSelectedCourse,
|
||||
} from './data/selectors';
|
||||
import NotificationPreferenceRow from './NotificationPreferenceRow';
|
||||
import { updateGroupValue } from './data/actions';
|
||||
|
||||
const NotificationPreferenceGroup = ({ groupId }) => {
|
||||
const dispatch = useDispatch();
|
||||
const intl = useIntl();
|
||||
const courseId = useSelector(getSelectedCourse());
|
||||
const preferenceGroup = useSelector(getPreferenceGroup(groupId));
|
||||
const [groupToggle, setGroupToggle] = useState(true);
|
||||
|
||||
const preferences = useMemo(() => (
|
||||
preferenceGroup.map(preference => (
|
||||
<NotificationPreferenceRow
|
||||
key={preference.id}
|
||||
groupId={groupId}
|
||||
preferenceName={preference.id}
|
||||
/>
|
||||
))), [groupId, preferenceGroup]);
|
||||
|
||||
const onChangeGroupSettings = useCallback((checked) => {
|
||||
setGroupToggle(checked);
|
||||
dispatch(updateGroupValue(courseId, groupId, checked));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [groupId]);
|
||||
|
||||
if (!courseId) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Collapsible.Advanced open={groupToggle}>
|
||||
<Collapsible.Trigger>
|
||||
<div className="d-flex">
|
||||
<span className="ml-0 mr-auto">
|
||||
{intl.formatMessage(messages.notificationGroupTitle, { key: groupId })}
|
||||
</span>
|
||||
<span className="ml-auto mr-0">
|
||||
<ToggleSwitch value={groupToggle} onChange={onChangeGroupSettings} />
|
||||
</span>
|
||||
</div>
|
||||
<hr />
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Body>
|
||||
<div className="d-flex flex-row notification-help-text">
|
||||
<span className="col-8 px-0">{intl.formatMessage(messages.notificationHelpType)}</span>
|
||||
<span className="d-flex col-4 px-0">
|
||||
<span className="ml-0 mr-auto">{intl.formatMessage(messages.notificationHelpWeb)}</span>
|
||||
<span className="mx-auto">{intl.formatMessage(messages.notificationHelpEmail)}</span>
|
||||
<span className="ml-auto mr-0 pr-2.5">{intl.formatMessage(messages.notificationHelpPush)}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-3 pb-5">
|
||||
{ preferences }
|
||||
</div>
|
||||
</Collapsible.Body>
|
||||
</Collapsible.Advanced>
|
||||
);
|
||||
};
|
||||
|
||||
NotificationPreferenceGroup.propTypes = {
|
||||
groupId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default React.memo(NotificationPreferenceGroup);
|
||||
51
src/notification-preferences/NotificationPreferenceRow.jsx
Normal file
51
src/notification-preferences/NotificationPreferenceRow.jsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { messages } from './messages';
|
||||
import ToggleSwitch from './ToggleSwitch';
|
||||
import { getPreferenceAttribute } from './data/selectors';
|
||||
import { updatePreferenceValue } from './data/actions';
|
||||
|
||||
const NotificationPreferenceRow = ({ groupId, preferenceName }) => {
|
||||
const dispatch = useDispatch();
|
||||
const intl = useIntl();
|
||||
const preference = useSelector(getPreferenceAttribute(groupId, preferenceName));
|
||||
const onToggle = useCallback((checked, notificationChannel) => {
|
||||
dispatch(updatePreferenceValue(groupId, preferenceName, notificationChannel, checked));
|
||||
}, [dispatch, groupId, preferenceName]);
|
||||
return (
|
||||
<div className="d-flex flex-row mb-3">
|
||||
<span className="col-8 px-0">
|
||||
{intl.formatMessage(messages.notificationTitle, { text: preferenceName })}
|
||||
</span>
|
||||
<span className="d-flex col-4 px-0">
|
||||
<span className="ml-0 mr-auto">
|
||||
<ToggleSwitch
|
||||
value={preference.web}
|
||||
onChange={(checked) => onToggle(checked, 'web')}
|
||||
/>
|
||||
</span>
|
||||
<span className="mx-auto">
|
||||
<ToggleSwitch
|
||||
value={preference.email}
|
||||
onChange={(checked) => onToggle(checked, 'email')}
|
||||
/>
|
||||
</span>
|
||||
<span className="ml-auto mr-0">
|
||||
<ToggleSwitch
|
||||
value={preference.push}
|
||||
onChange={(checked) => onToggle(checked, 'push')}
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
NotificationPreferenceRow.propTypes = {
|
||||
groupId: PropTypes.string.isRequired,
|
||||
preferenceName: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default React.memo(NotificationPreferenceRow);
|
||||
78
src/notification-preferences/NotificationPreferences.jsx
Normal file
78
src/notification-preferences/NotificationPreferences.jsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useParams } from 'react-router';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Container, Icon, Spinner } from '@edx/paragon';
|
||||
import { ArrowBack } from '@edx/paragon/icons';
|
||||
import {
|
||||
courseListStatus,
|
||||
getCourse,
|
||||
getPreferenceGroupIds,
|
||||
notificationPreferencesStatus,
|
||||
} from './data/selectors';
|
||||
import { fetchCourseList, fetchCourseNotificationPreferences } from './data/thunks';
|
||||
import { messages } from './messages';
|
||||
import NotificationPreferenceGroup from './NotificationPreferenceGroup';
|
||||
import { updateSelectedCourse } from './data/actions';
|
||||
import { LOADING_STATUS, SUCCESS_STATUS } from '../constants';
|
||||
|
||||
const NotificationPreferences = () => {
|
||||
const { courseId } = useParams();
|
||||
const dispatch = useDispatch();
|
||||
const intl = useIntl();
|
||||
const courseStatus = useSelector(courseListStatus());
|
||||
const notificationStatus = useSelector(notificationPreferencesStatus());
|
||||
const course = useSelector(getCourse(courseId));
|
||||
const preferenceGroups = useSelector(getPreferenceGroupIds());
|
||||
|
||||
const preferencesList = useMemo(() => (
|
||||
preferenceGroups.map(key => (
|
||||
<NotificationPreferenceGroup groupId={key} key={key} />
|
||||
))
|
||||
), [preferenceGroups]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(updateSelectedCourse(courseId));
|
||||
if (courseStatus !== SUCCESS_STATUS) {
|
||||
dispatch(fetchCourseList());
|
||||
}
|
||||
if (notificationStatus !== SUCCESS_STATUS) {
|
||||
dispatch(fetchCourseNotificationPreferences(courseId));
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [courseId]);
|
||||
|
||||
if (notificationStatus === LOADING_STATUS) {
|
||||
return (
|
||||
<div className="d-flex h-100">
|
||||
<Spinner
|
||||
variant="primary"
|
||||
animation="border"
|
||||
className="mx-auto my-auto"
|
||||
style={{ width: '4rem', height: '4rem' }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Container size="md">
|
||||
<h2 className="notification-heading mt-6 mb-5.5">
|
||||
{intl.formatMessage(messages.notificationHeading)}
|
||||
</h2>
|
||||
<div>
|
||||
<div className="d-flex mb-4">
|
||||
<Link to="/notifications">
|
||||
<Icon className="d-inline-block align-bottom ml-1" src={ArrowBack} />
|
||||
</Link>
|
||||
<span className="notification-course-title ml-auto mr-auto">
|
||||
{course?.name}
|
||||
</span>
|
||||
</div>
|
||||
{ preferencesList }
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotificationPreferences;
|
||||
18
src/notification-preferences/ToggleSwitch.jsx
Normal file
18
src/notification-preferences/ToggleSwitch.jsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Form } from '@edx/paragon';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const ToggleSwitch = ({ value, onChange }) => (
|
||||
<Form.Switch checked={value} onChange={(event) => onChange(event.target.checked)} />
|
||||
);
|
||||
|
||||
ToggleSwitch.propTypes = {
|
||||
value: PropTypes.bool.isRequired,
|
||||
onChange: PropTypes.func,
|
||||
};
|
||||
|
||||
ToggleSwitch.defaultProps = {
|
||||
onChange: () => null,
|
||||
};
|
||||
|
||||
export default React.memo(ToggleSwitch);
|
||||
58
src/notification-preferences/data/actions.js
Normal file
58
src/notification-preferences/data/actions.js
Normal file
@@ -0,0 +1,58 @@
|
||||
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_GROUP_PREFERENCE: 'updateGroupValue',
|
||||
};
|
||||
|
||||
export const fetchNotificationPreferenceSuccess = (courseId, payload) => dispatch => (
|
||||
dispatch({ type: Actions.FETCHED_PREFERENCES, courseId, payload })
|
||||
);
|
||||
|
||||
export const fetchNotificationPreferenceFetching = () => dispatch => (
|
||||
dispatch({ type: Actions.FETCHING_PREFERENCES })
|
||||
);
|
||||
|
||||
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 = (groupName, preferenceName, notificationChannel, value) => dispatch => (
|
||||
dispatch({
|
||||
type: Actions.UPDATE_PREFERENCE,
|
||||
groupName,
|
||||
preferenceName,
|
||||
notificationChannel,
|
||||
value,
|
||||
})
|
||||
);
|
||||
|
||||
export const updateGroupValue = (courseId, groupName, value) => dispatch => (
|
||||
dispatch({
|
||||
type: Actions.UPDATE_GROUP_PREFERENCE,
|
||||
courseId,
|
||||
groupName,
|
||||
value,
|
||||
})
|
||||
);
|
||||
119
src/notification-preferences/data/reducers.js
Normal file
119
src/notification-preferences/data/reducers.js
Normal file
@@ -0,0 +1,119 @@
|
||||
import { Actions } from './actions';
|
||||
import {
|
||||
IDLE_STATUS,
|
||||
LOADING_STATUS,
|
||||
SUCCESS_STATUS,
|
||||
FAILURE_STATUS,
|
||||
} from '../../constants';
|
||||
|
||||
export const defaultState = {
|
||||
courses: {
|
||||
status: IDLE_STATUS,
|
||||
courses: [],
|
||||
},
|
||||
preferences: {
|
||||
status: IDLE_STATUS,
|
||||
selectedCourse: null,
|
||||
preferences: [],
|
||||
groups: [],
|
||||
},
|
||||
};
|
||||
|
||||
const notificationPreferencesReducer = (state = defaultState, action = {}) => {
|
||||
const {
|
||||
courseId, groupName, notificationChannel, preferenceName, value,
|
||||
} = action;
|
||||
switch (action.type) {
|
||||
case Actions.FETCHING_COURSE_LIST:
|
||||
return {
|
||||
...state,
|
||||
courses: {
|
||||
status: LOADING_STATUS,
|
||||
courses: [],
|
||||
},
|
||||
};
|
||||
case Actions.FETCHED_COURSE_LIST:
|
||||
return {
|
||||
...state,
|
||||
courses: {
|
||||
status: SUCCESS_STATUS,
|
||||
courses: action.payload,
|
||||
},
|
||||
};
|
||||
case Actions.FAILED_COURSE_LIST:
|
||||
return {
|
||||
...state,
|
||||
courses: {
|
||||
status: FAILURE_STATUS,
|
||||
courses: [],
|
||||
},
|
||||
};
|
||||
case Actions.FETCHING_PREFERENCES:
|
||||
return {
|
||||
...state,
|
||||
preferences: {
|
||||
...state.preferences,
|
||||
status: LOADING_STATUS,
|
||||
preferences: {},
|
||||
},
|
||||
};
|
||||
case Actions.FETCHED_PREFERENCES:
|
||||
return {
|
||||
...state,
|
||||
preferences: {
|
||||
...state.preferences,
|
||||
status: SUCCESS_STATUS,
|
||||
...action.payload,
|
||||
},
|
||||
};
|
||||
case Actions.FAILED_PREFERENCES:
|
||||
return {
|
||||
...state,
|
||||
preferences: {
|
||||
status: FAILURE_STATUS,
|
||||
preferences: {},
|
||||
},
|
||||
};
|
||||
case Actions.UPDATE_SELECTED_COURSE:
|
||||
return {
|
||||
...state,
|
||||
preferences: {
|
||||
...state.preferences,
|
||||
selectedCourse: courseId,
|
||||
},
|
||||
};
|
||||
case Actions.UPDATE_PREFERENCE:
|
||||
return {
|
||||
...state,
|
||||
preferences: {
|
||||
...state.preferences,
|
||||
preferences: state.preferences.preferences.map((element) => (
|
||||
element.id === preferenceName
|
||||
? { ...element, [notificationChannel]: value }
|
||||
: element
|
||||
)),
|
||||
},
|
||||
};
|
||||
case Actions.UPDATE_GROUP_PREFERENCE:
|
||||
return {
|
||||
...state,
|
||||
preferences: {
|
||||
...state.preferences,
|
||||
preferences: state.preferences.preferences.map((element) => (
|
||||
element.groupId === groupName
|
||||
? {
|
||||
...element,
|
||||
web: value,
|
||||
email: value,
|
||||
push: value,
|
||||
}
|
||||
: element
|
||||
)),
|
||||
},
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default notificationPreferencesReducer;
|
||||
41
src/notification-preferences/data/selectors.js
Normal file
41
src/notification-preferences/data/selectors.js
Normal file
@@ -0,0 +1,41 @@
|
||||
export const notificationPreferencesStatus = () => state => (
|
||||
state.notificationPreferences.preferences.status
|
||||
);
|
||||
|
||||
export const getPreferences = () => state => (
|
||||
state.notificationPreferences?.preferences?.preferences
|
||||
);
|
||||
|
||||
export const courseListStatus = () => state => (
|
||||
state.notificationPreferences.courses.status
|
||||
);
|
||||
|
||||
export const getCourseList = () => state => (
|
||||
state.notificationPreferences.courses.courses
|
||||
);
|
||||
|
||||
export const getCourse = courseId => state => (
|
||||
getCourseList()(state).find(
|
||||
element => element.id === courseId,
|
||||
)
|
||||
);
|
||||
|
||||
export const getPreferenceGroupIds = () => state => (
|
||||
state.notificationPreferences.preferences.groups
|
||||
);
|
||||
|
||||
export const getPreferenceGroup = group => state => (
|
||||
getPreferences()(state).filter((preference) => (
|
||||
preference.groupId === group
|
||||
))
|
||||
);
|
||||
|
||||
export const getPreferenceAttribute = (group, name) => state => (
|
||||
getPreferenceGroup(group)(state).find((preference) => (
|
||||
preference.id === name
|
||||
))
|
||||
);
|
||||
|
||||
export const getSelectedCourse = () => state => (
|
||||
state.notificationPreferences.preferences.selectedCourse
|
||||
);
|
||||
46
src/notification-preferences/data/service.js
Normal file
46
src/notification-preferences/data/service.js
Normal file
@@ -0,0 +1,46 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
|
||||
// import { getConfig } from '@edx/frontend-platform';
|
||||
// import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
|
||||
export async function getCourseNotificationPreferences(courseId) {
|
||||
// const url = `${getConfig().LMS_BASE_URL}/api/notifications/${courseId}`;
|
||||
// const { data } = await getAuthenticatedHttpClient().get(url);
|
||||
const data = {
|
||||
discussion: {
|
||||
new_post: {
|
||||
web: true,
|
||||
push: false,
|
||||
email: false,
|
||||
},
|
||||
new_comment: {
|
||||
web: true,
|
||||
push: false,
|
||||
email: false,
|
||||
},
|
||||
},
|
||||
coursework: {
|
||||
new_assignment: {
|
||||
web: true,
|
||||
push: false,
|
||||
email: false,
|
||||
},
|
||||
new_grade: {
|
||||
web: true,
|
||||
push: false,
|
||||
email: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function getCourseList() {
|
||||
// const url = `${getConfig().LMS_BASE_URL}/api/notifications/${courseId}`;
|
||||
// const { data } = await getAuthenticatedHttpClient().get(url);
|
||||
return [
|
||||
{ id: 'course-v1:edX+Supply+Demo_Course', name: 'Supply Chain Analytics' },
|
||||
{ id: 'course-v1:edX+Happiness+At+Work_Course', name: 'The Foundation of Happiness At Work' },
|
||||
{ id: 'course-v1:edX+Empathy+At+Work_Course', name: 'Empathy and Emotional Intelligence At Work' },
|
||||
];
|
||||
}
|
||||
60
src/notification-preferences/data/thunks.js
Normal file
60
src/notification-preferences/data/thunks.js
Normal file
@@ -0,0 +1,60 @@
|
||||
import { camelCaseObject } from '@edx/frontend-platform';
|
||||
import {
|
||||
fetchCourseListSuccess,
|
||||
fetchCourseListFetching,
|
||||
fetchCourseListFailed,
|
||||
fetchNotificationPreferenceFailed,
|
||||
fetchNotificationPreferenceFetching,
|
||||
fetchNotificationPreferenceSuccess,
|
||||
} from './actions';
|
||||
import {
|
||||
getCourseList,
|
||||
getCourseNotificationPreferences,
|
||||
} from './service';
|
||||
|
||||
const normalizePreferences = (preferences) => {
|
||||
const groups = Object.keys(preferences);
|
||||
const preferenceList = groups.map(groupId => {
|
||||
const preferencesKeys = Object.keys(preferences[groupId]);
|
||||
const flatPreferences = preferencesKeys.map(preferenceId => (
|
||||
{
|
||||
id: preferenceId,
|
||||
groupId,
|
||||
web: preferences?.[groupId]?.[preferenceId].web,
|
||||
push: preferences?.[groupId]?.[preferenceId].push,
|
||||
mobile: preferences?.[groupId]?.[preferenceId].mobile,
|
||||
}
|
||||
));
|
||||
return flatPreferences;
|
||||
}).flat();
|
||||
const normalizedPreferences = {
|
||||
groups,
|
||||
preferences: preferenceList,
|
||||
};
|
||||
return normalizedPreferences;
|
||||
};
|
||||
|
||||
export const fetchCourseList = () => (
|
||||
async (dispatch) => {
|
||||
try {
|
||||
dispatch(fetchCourseListFetching());
|
||||
const data = await getCourseList();
|
||||
dispatch(fetchCourseListSuccess(data));
|
||||
} catch (errors) {
|
||||
dispatch(fetchCourseListFailed());
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const fetchCourseNotificationPreferences = (courseId) => (
|
||||
async (dispatch) => {
|
||||
try {
|
||||
dispatch(fetchNotificationPreferenceFetching());
|
||||
const data = await getCourseNotificationPreferences(courseId);
|
||||
const normalizedData = normalizePreferences(camelCaseObject(data));
|
||||
dispatch(fetchNotificationPreferenceSuccess(courseId, normalizedData));
|
||||
} catch (errors) {
|
||||
dispatch(fetchNotificationPreferenceFailed());
|
||||
}
|
||||
}
|
||||
);
|
||||
52
src/notification-preferences/messages.js
Normal file
52
src/notification-preferences/messages.js
Normal file
@@ -0,0 +1,52 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const messages = defineMessages({
|
||||
notificationHeading: {
|
||||
id: 'notification.preference.heading',
|
||||
defaultMessage: 'Notifications',
|
||||
description: 'Notification title',
|
||||
},
|
||||
notificationGroupTitle: {
|
||||
id: 'notification.preference.group.title',
|
||||
defaultMessage: `{
|
||||
key, select,
|
||||
discussion {Discussions}
|
||||
coursework {Course Work}
|
||||
other {{key}}
|
||||
}`,
|
||||
description: 'Display text for Notification Types',
|
||||
},
|
||||
notificationTitle: {
|
||||
id: 'notification.preference.title',
|
||||
defaultMessage: `{
|
||||
text, select,
|
||||
newPost {New Post}
|
||||
newComment {New Comment}
|
||||
newAssignment {New Assignment}
|
||||
newGrade {New Grade}
|
||||
other {{text}}
|
||||
}`,
|
||||
description: 'Display text for Notification Types',
|
||||
},
|
||||
notificationHelpType: {
|
||||
id: 'notification.preference.help.type',
|
||||
defaultMessage: 'Type',
|
||||
description: 'Display text for type',
|
||||
},
|
||||
notificationHelpWeb: {
|
||||
id: 'notification.preference.help.web',
|
||||
defaultMessage: 'Web',
|
||||
description: 'Display text for web',
|
||||
},
|
||||
notificationHelpEmail: {
|
||||
id: 'notification.preference.help.email',
|
||||
defaultMessage: 'Email',
|
||||
description: 'Display text for email',
|
||||
},
|
||||
notificationHelpPush: {
|
||||
id: 'notification.preference.help.push',
|
||||
defaultMessage: 'Push',
|
||||
description: 'Display text for push',
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user