Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
…t-header into bilalqamar95/react-upgrade-to-v17
  • Loading branch information
mashal-m committed Jun 20, 2023
2 parents f19c34f + 022515d commit 97bbae7
Show file tree
Hide file tree
Showing 40 changed files with 2,390 additions and 436 deletions.
3 changes: 2 additions & 1 deletion example/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import React from 'react';
import ReactDOM from 'react-dom';
import { initialize, getConfig, subscribe, APP_READY } from '@edx/frontend-platform';
import { AppContext, AppProvider } from '@edx/frontend-platform/react';
import Header from '@edx/frontend-component-header';
// import Header from '@edx/frontend-component-header';
import { LearningHeader as Header } from '@edx/frontend-component-header';

import './index.scss';

Expand Down
1,098 changes: 708 additions & 390 deletions package-lock.json

Large diffs are not rendered by default.

14 changes: 11 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,10 @@
"devDependencies": {
"@edx/brand": "npm:@edx/[email protected]",
"@edx/browserslist-config": "^1.1.1",
"@edx/frontend-build": "12.8.40",
"@edx/frontend-build": "12.8.54",
"@edx/frontend-platform": "4.5.1",
"@edx/reactifex": "^2.1.1",
"@testing-library/dom": "9.3.0",
"@testing-library/dom": "9.3.1",
"@testing-library/jest-dom": "5.16.5",
"@testing-library/react": "10.4.9",
"@wojtekmaj/enzyme-adapter-react-17": "0.8.0",
Expand All @@ -62,9 +62,17 @@
"@fortawesome/free-regular-svg-icons": "6.3.0",
"@fortawesome/free-solid-svg-icons": "6.3.0",
"@fortawesome/react-fontawesome": "^0.2.0",
"@reduxjs/toolkit": "1.9.5",
"axios-mock-adapter": "1.21.4",
"babel-polyfill": "6.26.0",
"classnames": "2.3.2",
"lodash": "4.17.21",
"react-redux": "7.2.9",
"react-responsive": "8.2.0",
"react-transition-group": "4.4.5"
"react-router-dom": "5.3.4",
"react-transition-group": "4.4.5",
"rosie": "2.1.0",
"timeago.js": "4.0.2"
},
"peerDependencies": {
"@edx/frontend-platform": "^4.0.0",
Expand Down
26 changes: 20 additions & 6 deletions src/Header.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,38 @@
import React from 'react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import TestRenderer from 'react-test-renderer';
import { AppContext } from '@edx/frontend-platform/react';
import { AppContext, AppProvider } from '@edx/frontend-platform/react';
import { Context as ResponsiveContext } from 'react-responsive';
import { initializeMockApp } from '@edx/frontend-platform';
import store from './store';

import Header from './index';

const HeaderComponent = ({ width, contextValue }) => (
<ResponsiveContext.Provider value={width}>
<IntlProvider locale="en" messages={{}}>
<AppContext.Provider
value={contextValue}
>
<Header />
</AppContext.Provider>
<AppProvider store={store}>
<AppContext.Provider
value={contextValue}
>
<Header />
</AppContext.Provider>
</AppProvider>
</IntlProvider>
</ResponsiveContext.Provider>
);

describe('<Header />', () => {
beforeEach(async () => {
initializeMockApp({
authenticatedUser: {
userId: '123abc',
username: 'testuser',
administrator: false,
roles: [],
},
});
});
it('renders correctly for anonymous desktop', () => {
const contextValue = {
authenticatedUser: null,
Expand Down
71 changes: 71 additions & 0 deletions src/Notifications/NotificationRowItem.jsx
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);
81 changes: 81 additions & 0 deletions src/Notifications/NotificationSections.jsx
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);
52 changes: 52 additions & 0 deletions src/Notifications/NotificationTabs.jsx
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);
1 change: 1 addition & 0 deletions src/Notifications/data/__factories__/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import './notifications.factory';
22 changes: 22 additions & 0 deletions src/Notifications/data/__factories__/notifications.factory.js
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);
44 changes: 44 additions & 0 deletions src/Notifications/data/api.js
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 };
}
Loading

0 comments on commit 97bbae7

Please sign in to comment.