-
Notifications
You must be signed in to change notification settings - Fork 134
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'master' of https://github.com/openedx/frontend-componen…
…t-header into bilalqamar95/react-upgrade-to-v17
- Loading branch information
Showing
40 changed files
with
2,390 additions
and
436 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
import React, { useCallback } from 'react'; | ||
import { useDispatch } from 'react-redux'; | ||
import { useIntl } from '@edx/frontend-platform/i18n'; | ||
import PropTypes from 'prop-types'; | ||
import { Icon } from '@edx/paragon'; | ||
import { Link } from 'react-router-dom'; | ||
import * as timeago from 'timeago.js'; | ||
import { getIconByType } from './utils'; | ||
import { markNotificationsAsRead } from './data/thunks'; | ||
import messages from './messages'; | ||
import timeLocale from '../common/time-locale'; | ||
|
||
const NotificationRowItem = ({ | ||
id, type, contentUrl, content, courseName, createdAt, lastRead, | ||
}) => { | ||
timeago.register('time-locale', timeLocale); | ||
const intl = useIntl(); | ||
const dispatch = useDispatch(); | ||
|
||
const handleMarkAsRead = useCallback(() => { | ||
dispatch(markNotificationsAsRead(id)); | ||
}, [dispatch, id]); | ||
|
||
const { icon: iconComponent, class: iconClass } = getIconByType(type); | ||
|
||
return ( | ||
<Link | ||
target="_blank" | ||
className="d-flex mb-2 align-items-center text-decoration-none" | ||
to={contentUrl} | ||
onClick={handleMarkAsRead} | ||
> | ||
<Icon src={iconComponent} className={`${iconClass} mr-4 notification-icon`} /> | ||
<div className="d-flex w-100"> | ||
<div className="d-flex align-items-center w-100"> | ||
<div className="py-10px w-100 px-0 cursor-pointer"> | ||
<span | ||
className="line-height-24 text-gray-700 mb-2 notification-item-content overflow-hidden content" | ||
// eslint-disable-next-line react/no-danger | ||
dangerouslySetInnerHTML={{ __html: content }} | ||
/> | ||
<div className="py-0 d-flex"> | ||
<span className="font-size-12 text-gray-500 line-height-20"> | ||
<span>{courseName}</span> | ||
<span className="text-light-700 px-1.5">{intl.formatMessage(messages.fullStop)}</span> | ||
<span>{timeago.format(createdAt, 'time-locale')}</span> | ||
</span> | ||
</div> | ||
</div> | ||
{!lastRead && ( | ||
<div className="d-flex py-1.5 px-1.5 ml-2 cursor-pointer"> | ||
<span className="bg-brand-500 rounded unread" /> | ||
</div> | ||
)} | ||
</div> | ||
</div> | ||
</Link> | ||
); | ||
}; | ||
|
||
NotificationRowItem.propTypes = { | ||
id: PropTypes.string.isRequired, | ||
type: PropTypes.string.isRequired, | ||
contentUrl: PropTypes.string.isRequired, | ||
content: PropTypes.node.isRequired, | ||
courseName: PropTypes.string.isRequired, | ||
createdAt: PropTypes.string.isRequired, | ||
lastRead: PropTypes.string.isRequired, | ||
}; | ||
|
||
export default React.memo(NotificationRowItem); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
import React, { useCallback, useMemo } from 'react'; | ||
import { Button } from '@edx/paragon'; | ||
import { useDispatch, useSelector } from 'react-redux'; | ||
import { useIntl } from '@edx/frontend-platform/i18n'; | ||
import isEmpty from 'lodash/isEmpty'; | ||
import messages from './messages'; | ||
import NotificationRowItem from './NotificationRowItem'; | ||
import { markAllNotificationsAsRead } from './data/thunks'; | ||
import { selectNotificationsByIds, selectPaginationData, selectSelectedAppName } from './data/selectors'; | ||
import { splitNotificationsByTime } from './utils'; | ||
import { updatePaginationRequest } from './data/slice'; | ||
|
||
const NotificationSections = () => { | ||
const intl = useIntl(); | ||
const dispatch = useDispatch(); | ||
const selectedAppName = useSelector(selectSelectedAppName()); | ||
const notifications = useSelector(selectNotificationsByIds(selectedAppName)); | ||
const { currentPage, numPages } = useSelector(selectPaginationData()); | ||
const { today = [], earlier = [] } = useMemo( | ||
() => splitNotificationsByTime(notifications), | ||
[notifications], | ||
); | ||
|
||
const handleMarkAllAsRead = useCallback(() => { | ||
dispatch(markAllNotificationsAsRead(selectedAppName)); | ||
}, [dispatch, selectedAppName]); | ||
|
||
const updatePagination = useCallback(() => { | ||
dispatch(updatePaginationRequest()); | ||
}, [dispatch]); | ||
|
||
const renderNotificationSection = (section, items) => { | ||
if (isEmpty(items)) { return null; } | ||
|
||
return ( | ||
<div className="pb-2"> | ||
<div className="d-flex justify-content-between align-items-center py-10px mb-2"> | ||
<span className="text-gray-500 line-height-10"> | ||
{section === 'today' && intl.formatMessage(messages.notificationTodayHeading)} | ||
{section === 'earlier' && intl.formatMessage(messages.notificationEarlierHeading)} | ||
</span> | ||
{notifications?.length > 0 && (section === 'earlier' ? today.length === 0 : true) && ( | ||
<Button | ||
variant="link" | ||
className="text-info-500 font-size-14 line-height-10 text-decoration-none p-0 border-0" | ||
onClick={handleMarkAllAsRead} | ||
> | ||
{intl.formatMessage(messages.notificationMarkAsRead)} | ||
</Button> | ||
)} | ||
</div> | ||
{items.map((notification) => ( | ||
<NotificationRowItem | ||
key={notification.id} | ||
id={notification.id} | ||
type={notification.type} | ||
contentUrl={notification.contentUrl} | ||
content={notification.content} | ||
courseName={notification.courseName} | ||
createdAt={notification.createdAt} | ||
lastRead={notification.lastRead} | ||
/> | ||
))} | ||
</div> | ||
); | ||
}; | ||
|
||
return ( | ||
<div className="mt-4 px-4"> | ||
{renderNotificationSection('today', today)} | ||
{renderNotificationSection('earlier', earlier)} | ||
{currentPage < numPages && ( | ||
<Button variant="primary" className="w-100 bg-primary-500" onClick={updatePagination}> | ||
{intl.formatMessage(messages.loadMoreNotifications)} | ||
</Button> | ||
)} | ||
</div> | ||
); | ||
}; | ||
|
||
export default React.memo(NotificationSections); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
/* eslint-disable react-hooks/exhaustive-deps */ | ||
import React, { useCallback, useEffect, useMemo } from 'react'; | ||
import { useDispatch, useSelector } from 'react-redux'; | ||
import { Tab, Tabs } from '@edx/paragon'; | ||
import NotificationSections from './NotificationSections'; | ||
import { fetchNotificationList, markNotificationsAsSeen } from './data/thunks'; | ||
import { | ||
selectNotificationTabs, selectNotificationTabsCount, selectPaginationData, selectSelectedAppName, | ||
} from './data/selectors'; | ||
import { updateAppNameRequest } from './data/slice'; | ||
|
||
const NotificationTabs = () => { | ||
const dispatch = useDispatch(); | ||
const selectedAppName = useSelector(selectSelectedAppName()); | ||
const notificationUnseenCounts = useSelector(selectNotificationTabsCount()); | ||
const notificationTabs = useSelector(selectNotificationTabs()); | ||
const { currentPage } = useSelector(selectPaginationData()); | ||
|
||
useEffect(() => { | ||
dispatch(fetchNotificationList({ appName: selectedAppName, page: currentPage, pageSize: 10 })); | ||
if (selectedAppName) { dispatch(markNotificationsAsSeen(selectedAppName)); } | ||
}, [currentPage, selectedAppName]); | ||
|
||
const handleActiveTab = useCallback((appName) => { | ||
dispatch(updateAppNameRequest({ appName })); | ||
}, []); | ||
|
||
const tabArray = useMemo(() => notificationTabs?.map((appName) => ( | ||
<Tab | ||
key={appName} | ||
eventKey={appName} | ||
title={appName} | ||
notification={notificationUnseenCounts[appName]} | ||
tabClassName="pt-0 pb-10px px-2.5 d-flex border-top-0 mb-0 align-items-center line-height-24 text-capitalize" | ||
> | ||
{appName === selectedAppName && (<NotificationSections />)} | ||
</Tab> | ||
)), [notificationUnseenCounts, selectedAppName, notificationTabs]); | ||
|
||
return ( | ||
<Tabs | ||
variant="tabs" | ||
defaultActiveKey={selectedAppName} | ||
onSelect={handleActiveTab} | ||
className="px-2.5 text-primary-500" | ||
> | ||
{tabArray} | ||
</Tabs> | ||
); | ||
}; | ||
|
||
export default React.memo(NotificationTabs); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
import './notifications.factory'; |
22 changes: 22 additions & 0 deletions
22
src/Notifications/data/__factories__/notifications.factory.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import { Factory } from 'rosie'; | ||
|
||
Factory.define('notificationsCount') | ||
.attr('count', 45) | ||
.attr('countByAppName', { | ||
reminders: 10, | ||
discussions: 20, | ||
grades: 10, | ||
authoring: 5, | ||
}) | ||
.attr('showNotificationsTray', true); | ||
|
||
Factory.define('notification') | ||
.sequence('id') | ||
.attr('type', 'post') | ||
.sequence('content', ['id'], (idx, notificationId) => `<p><b>User ${idx}</b> posts <b>Hello and welcome to SC0x | ||
${notificationId}!</b></p>`) | ||
.attr('course_name', 'Supply Chain Analytics') | ||
.sequence('content_url', (idx) => `https://example.com/${idx}`) | ||
.attr('last_read', null) | ||
.attr('last_seen', null) | ||
.sequence('created_at', ['createdDate'], (idx, date) => date); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import { getConfig, snakeCaseObject } from '@edx/frontend-platform'; | ||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; | ||
|
||
export const getNotificationsCountApiUrl = () => `${getConfig().LMS_BASE_URL}/api/notifications/count/`; | ||
export const getNotificationsApiUrl = () => `${getConfig().LMS_BASE_URL}/api/notifications/`; | ||
export const markNotificationsSeenApiUrl = (appName) => `${getConfig().LMS_BASE_URL}/api/notifications/mark-notifications-unseen/${appName}/`; | ||
export const markNotificationAsReadApiUrl = () => `${getConfig().LMS_BASE_URL}/api/notifications/read/`; | ||
|
||
export async function getNotifications(appName, page, pageSize) { | ||
const params = snakeCaseObject({ page, pageSize }); | ||
const { data } = await getAuthenticatedHttpClient().get(getNotificationsApiUrl(), { params }); | ||
|
||
const startIndex = (page - 1) * pageSize; | ||
const endIndex = startIndex + pageSize; | ||
|
||
const notifications = data.slice(startIndex, endIndex); | ||
return { notifications, numPages: 2, currentPage: page }; | ||
} | ||
|
||
export async function getNotificationCounts() { | ||
const { data } = await getAuthenticatedHttpClient().get(getNotificationsCountApiUrl()); | ||
|
||
return data; | ||
} | ||
|
||
export async function markNotificationSeen(appName) { | ||
const { data } = await getAuthenticatedHttpClient().put(`${markNotificationsSeenApiUrl(appName)}`); | ||
|
||
return data; | ||
} | ||
|
||
export async function markAllNotificationRead(appName) { | ||
const params = snakeCaseObject({ appName }); | ||
const { data } = await getAuthenticatedHttpClient().put(markNotificationAsReadApiUrl(), { params }); | ||
|
||
return data; | ||
} | ||
|
||
export async function markNotificationRead(notificationId) { | ||
const params = snakeCaseObject({ notificationId }); | ||
const { data } = await getAuthenticatedHttpClient().put(markNotificationAsReadApiUrl(), { params }); | ||
|
||
return { data, id: notificationId }; | ||
} |
Oops, something went wrong.